1 /*
2  * libgit2 "log" example - shows how to walk history and get commit info
3  *
4  * Written by the libgit2 contributors
5  *
6  * To the extent possible under law, the author(s) have dedicated all copyright
7  * and related and neighboring rights to this software to the public domain
8  * worldwide. This software is distributed without any warranty.
9  *
10  * You should have received a copy of the CC0 Public Domain Dedication along
11  * with this software. If not, see
12  * <http://creativecommons.org/publicdomain/zero/1.0/>.
13  */
14 
15 #![deny(warnings)]
16 
17 use git2::{Commit, DiffOptions, ObjectType, Repository, Signature, Time};
18 use git2::{DiffFormat, Error, Pathspec};
19 use std::str;
20 use structopt::StructOpt;
21 
22 #[derive(StructOpt)]
23 struct Args {
24     #[structopt(name = "topo-order", long)]
25     /// sort commits in topological order
26     flag_topo_order: bool,
27     #[structopt(name = "date-order", long)]
28     /// sort commits in date order
29     flag_date_order: bool,
30     #[structopt(name = "reverse", long)]
31     /// sort commits in reverse
32     flag_reverse: bool,
33     #[structopt(name = "author", long)]
34     /// author to sort by
35     flag_author: Option<String>,
36     #[structopt(name = "committer", long)]
37     /// committer to sort by
38     flag_committer: Option<String>,
39     #[structopt(name = "pat", long = "grep")]
40     /// pattern to filter commit messages by
41     flag_grep: Option<String>,
42     #[structopt(name = "dir", long = "git-dir")]
43     /// alternative git directory to use
44     flag_git_dir: Option<String>,
45     #[structopt(name = "skip", long)]
46     /// number of commits to skip
47     flag_skip: Option<usize>,
48     #[structopt(name = "max-count", short = "n", long)]
49     /// maximum number of commits to show
50     flag_max_count: Option<usize>,
51     #[structopt(name = "merges", long)]
52     /// only show merge commits
53     flag_merges: bool,
54     #[structopt(name = "no-merges", long)]
55     /// don't show merge commits
56     flag_no_merges: bool,
57     #[structopt(name = "no-min-parents", long)]
58     /// don't require a minimum number of parents
59     flag_no_min_parents: bool,
60     #[structopt(name = "no-max-parents", long)]
61     /// don't require a maximum number of parents
62     flag_no_max_parents: bool,
63     #[structopt(name = "max-parents")]
64     /// specify a maximum number of parents for a commit
65     flag_max_parents: Option<usize>,
66     #[structopt(name = "min-parents")]
67     /// specify a minimum number of parents for a commit
68     flag_min_parents: Option<usize>,
69     #[structopt(name = "patch", long, short)]
70     /// show commit diff
71     flag_patch: bool,
72     #[structopt(name = "commit")]
73     arg_commit: Vec<String>,
74     #[structopt(name = "spec", last = true)]
75     arg_spec: Vec<String>,
76 }
77 
run(args: &Args) -> Result<(), Error>78 fn run(args: &Args) -> Result<(), Error> {
79     let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or(".");
80     let repo = Repository::open(path)?;
81     let mut revwalk = repo.revwalk()?;
82 
83     // Prepare the revwalk based on CLI parameters
84     let base = if args.flag_reverse {
85         git2::Sort::REVERSE
86     } else {
87         git2::Sort::NONE
88     };
89     revwalk.set_sorting(
90         base | if args.flag_topo_order {
91             git2::Sort::TOPOLOGICAL
92         } else if args.flag_date_order {
93             git2::Sort::TIME
94         } else {
95             git2::Sort::NONE
96         },
97     )?;
98     for commit in &args.arg_commit {
99         if commit.starts_with('^') {
100             let obj = repo.revparse_single(&commit[1..])?;
101             revwalk.hide(obj.id())?;
102             continue;
103         }
104         let revspec = repo.revparse(commit)?;
105         if revspec.mode().contains(git2::RevparseMode::SINGLE) {
106             revwalk.push(revspec.from().unwrap().id())?;
107         } else {
108             let from = revspec.from().unwrap().id();
109             let to = revspec.to().unwrap().id();
110             revwalk.push(to)?;
111             if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) {
112                 let base = repo.merge_base(from, to)?;
113                 let o = repo.find_object(base, Some(ObjectType::Commit))?;
114                 revwalk.push(o.id())?;
115             }
116             revwalk.hide(from)?;
117         }
118     }
119     if args.arg_commit.is_empty() {
120         revwalk.push_head()?;
121     }
122 
123     // Prepare our diff options and pathspec matcher
124     let (mut diffopts, mut diffopts2) = (DiffOptions::new(), DiffOptions::new());
125     for spec in &args.arg_spec {
126         diffopts.pathspec(spec);
127         diffopts2.pathspec(spec);
128     }
129     let ps = Pathspec::new(args.arg_spec.iter())?;
130 
131     // Filter our revwalk based on the CLI parameters
132     macro_rules! filter_try {
133         ($e:expr) => {
134             match $e {
135                 Ok(t) => t,
136                 Err(e) => return Some(Err(e)),
137             }
138         };
139     }
140     let revwalk = revwalk
141         .filter_map(|id| {
142             let id = filter_try!(id);
143             let commit = filter_try!(repo.find_commit(id));
144             let parents = commit.parents().len();
145             if parents < args.min_parents() {
146                 return None;
147             }
148             if let Some(n) = args.max_parents() {
149                 if parents >= n {
150                     return None;
151                 }
152             }
153             if !args.arg_spec.is_empty() {
154                 match commit.parents().len() {
155                     0 => {
156                         let tree = filter_try!(commit.tree());
157                         let flags = git2::PathspecFlags::NO_MATCH_ERROR;
158                         if ps.match_tree(&tree, flags).is_err() {
159                             return None;
160                         }
161                     }
162                     _ => {
163                         let m = commit.parents().all(|parent| {
164                             match_with_parent(&repo, &commit, &parent, &mut diffopts)
165                                 .unwrap_or(false)
166                         });
167                         if !m {
168                             return None;
169                         }
170                     }
171                 }
172             }
173             if !sig_matches(&commit.author(), &args.flag_author) {
174                 return None;
175             }
176             if !sig_matches(&commit.committer(), &args.flag_committer) {
177                 return None;
178             }
179             if !log_message_matches(commit.message(), &args.flag_grep) {
180                 return None;
181             }
182             Some(Ok(commit))
183         })
184         .skip(args.flag_skip.unwrap_or(0))
185         .take(args.flag_max_count.unwrap_or(!0));
186 
187     // print!
188     for commit in revwalk {
189         let commit = commit?;
190         print_commit(&commit);
191         if !args.flag_patch || commit.parents().len() > 1 {
192             continue;
193         }
194         let a = if commit.parents().len() == 1 {
195             let parent = commit.parent(0)?;
196             Some(parent.tree()?)
197         } else {
198             None
199         };
200         let b = commit.tree()?;
201         let diff = repo.diff_tree_to_tree(a.as_ref(), Some(&b), Some(&mut diffopts2))?;
202         diff.print(DiffFormat::Patch, |_delta, _hunk, line| {
203             match line.origin() {
204                 ' ' | '+' | '-' => print!("{}", line.origin()),
205                 _ => {}
206             }
207             print!("{}", str::from_utf8(line.content()).unwrap());
208             true
209         })?;
210     }
211 
212     Ok(())
213 }
214 
sig_matches(sig: &Signature, arg: &Option<String>) -> bool215 fn sig_matches(sig: &Signature, arg: &Option<String>) -> bool {
216     match *arg {
217         Some(ref s) => {
218             sig.name().map(|n| n.contains(s)).unwrap_or(false)
219                 || sig.email().map(|n| n.contains(s)).unwrap_or(false)
220         }
221         None => true,
222     }
223 }
224 
log_message_matches(msg: Option<&str>, grep: &Option<String>) -> bool225 fn log_message_matches(msg: Option<&str>, grep: &Option<String>) -> bool {
226     match (grep, msg) {
227         (&None, _) => true,
228         (&Some(_), None) => false,
229         (&Some(ref s), Some(msg)) => msg.contains(s),
230     }
231 }
232 
print_commit(commit: &Commit)233 fn print_commit(commit: &Commit) {
234     println!("commit {}", commit.id());
235 
236     if commit.parents().len() > 1 {
237         print!("Merge:");
238         for id in commit.parent_ids() {
239             print!(" {:.8}", id);
240         }
241         println!();
242     }
243 
244     let author = commit.author();
245     println!("Author: {}", author);
246     print_time(&author.when(), "Date:   ");
247     println!();
248 
249     for line in String::from_utf8_lossy(commit.message_bytes()).lines() {
250         println!("    {}", line);
251     }
252     println!();
253 }
254 
print_time(time: &Time, prefix: &str)255 fn print_time(time: &Time, prefix: &str) {
256     let (offset, sign) = match time.offset_minutes() {
257         n if n < 0 => (-n, '-'),
258         n => (n, '+'),
259     };
260     let (hours, minutes) = (offset / 60, offset % 60);
261     let ts = time::Timespec::new(time.seconds() + (time.offset_minutes() as i64) * 60, 0);
262     let time = time::at(ts);
263 
264     println!(
265         "{}{} {}{:02}{:02}",
266         prefix,
267         time.strftime("%a %b %e %T %Y").unwrap(),
268         sign,
269         hours,
270         minutes
271     );
272 }
273 
match_with_parent( repo: &Repository, commit: &Commit, parent: &Commit, opts: &mut DiffOptions, ) -> Result<bool, Error>274 fn match_with_parent(
275     repo: &Repository,
276     commit: &Commit,
277     parent: &Commit,
278     opts: &mut DiffOptions,
279 ) -> Result<bool, Error> {
280     let a = parent.tree()?;
281     let b = commit.tree()?;
282     let diff = repo.diff_tree_to_tree(Some(&a), Some(&b), Some(opts))?;
283     Ok(diff.deltas().len() > 0)
284 }
285 
286 impl Args {
min_parents(&self) -> usize287     fn min_parents(&self) -> usize {
288         if self.flag_no_min_parents {
289             return 0;
290         }
291         self.flag_min_parents
292             .unwrap_or(if self.flag_merges { 2 } else { 0 })
293     }
294 
max_parents(&self) -> Option<usize>295     fn max_parents(&self) -> Option<usize> {
296         if self.flag_no_max_parents {
297             return None;
298         }
299         self.flag_max_parents
300             .or(if self.flag_no_merges { Some(1) } else { None })
301     }
302 }
303 
main()304 fn main() {
305     let args = Args::from_args();
306     match run(&args) {
307         Ok(()) => {}
308         Err(e) => println!("error: {}", e),
309     }
310 }
311