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 #[macro_use]
18 extern crate serde_derive;
19 extern crate docopt;
20 extern crate git2;
21 extern crate time;
22 
23 use std::str;
24 use docopt::Docopt;
25 use git2::{Repository, Signature, Commit, ObjectType, Time, DiffOptions};
26 use git2::{Pathspec, Error, DiffFormat};
27 
28 #[derive(Deserialize)]
29 struct Args {
30     arg_commit: Vec<String>,
31     arg_spec: Vec<String>,
32     flag_topo_order: bool,
33     flag_date_order: bool,
34     flag_reverse: bool,
35     flag_author: Option<String>,
36     flag_committer: Option<String>,
37     flag_grep: Option<String>,
38     flag_git_dir: Option<String>,
39     flag_skip: Option<usize>,
40     flag_max_count: Option<usize>,
41     flag_merges: bool,
42     flag_no_merges: bool,
43     flag_no_min_parents: bool,
44     flag_no_max_parents: bool,
45     flag_max_parents: Option<usize>,
46     flag_min_parents: Option<usize>,
47     flag_patch: bool,
48 }
49 
run(args: &Args) -> Result<(), Error>50 fn run(args: &Args) -> Result<(), Error> {
51     let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or(".");
52     let repo = try!(Repository::open(path));
53     let mut revwalk = try!(repo.revwalk());
54 
55     // Prepare the revwalk based on CLI parameters
56     let base = if args.flag_reverse {git2::Sort::REVERSE} else {git2::Sort::NONE};
57     revwalk.set_sorting(base | if args.flag_topo_order {
58         git2::Sort::TOPOLOGICAL
59     } else if args.flag_date_order {
60         git2::Sort::TIME
61     } else {
62         git2::Sort::NONE
63     });
64     for commit in &args.arg_commit {
65         if commit.starts_with('^') {
66             let obj = try!(repo.revparse_single(&commit[1..]));
67             try!(revwalk.hide(obj.id()));
68             continue
69         }
70         let revspec = try!(repo.revparse(commit));
71         if revspec.mode().contains(git2::RevparseMode::SINGLE) {
72             try!(revwalk.push(revspec.from().unwrap().id()));
73         } else {
74             let from = revspec.from().unwrap().id();
75             let to = revspec.to().unwrap().id();
76             try!(revwalk.push(to));
77             if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) {
78                 let base = try!(repo.merge_base(from, to));
79                 let o = try!(repo.find_object(base, Some(ObjectType::Commit)));
80                 try!(revwalk.push(o.id()));
81             }
82             try!(revwalk.hide(from));
83         }
84     }
85     if args.arg_commit.is_empty() {
86         try!(revwalk.push_head());
87     }
88 
89     // Prepare our diff options and pathspec matcher
90     let (mut diffopts, mut diffopts2) = (DiffOptions::new(), DiffOptions::new());
91     for spec in &args.arg_spec {
92         diffopts.pathspec(spec);
93         diffopts2.pathspec(spec);
94     }
95     let ps = try!(Pathspec::new(args.arg_spec.iter()));
96 
97     // Filter our revwalk based on the CLI parameters
98     macro_rules! filter_try {
99         ($e:expr) => (match $e { Ok(t) => t, Err(e) => return Some(Err(e)) })
100     }
101     let revwalk = revwalk.filter_map(|id| {
102         let id = filter_try!(id);
103         let commit = filter_try!(repo.find_commit(id));
104         let parents = commit.parents().len();
105         if parents < args.min_parents() { return None }
106         if let Some(n) = args.max_parents() {
107             if parents >= n { return None }
108         }
109         if !args.arg_spec.is_empty() {
110             match commit.parents().len() {
111                 0 => {
112                     let tree = filter_try!(commit.tree());
113                     let flags = git2::PathspecFlags::NO_MATCH_ERROR;
114                     if ps.match_tree(&tree, flags).is_err() { return None }
115                 }
116                 _ => {
117                     let m = commit.parents().all(|parent| {
118                         match_with_parent(&repo, &commit, &parent, &mut diffopts)
119                                         .unwrap_or(false)
120                     });
121                     if !m { return None }
122                 }
123             }
124         }
125         if !sig_matches(&commit.author(), &args.flag_author) { return None }
126         if !sig_matches(&commit.committer(), &args.flag_committer) { return None }
127         if !log_message_matches(commit.message(), &args.flag_grep) { return None }
128         Some(Ok(commit))
129     }).skip(args.flag_skip.unwrap_or(0)).take(args.flag_max_count.unwrap_or(!0));
130 
131     // print!
132     for commit in revwalk {
133         let commit = try!(commit);
134         print_commit(&commit);
135         if !args.flag_patch || commit.parents().len() > 1 { continue }
136         let a = if commit.parents().len() == 1 {
137             let parent = try!(commit.parent(0));
138             Some(try!(parent.tree()))
139         } else {
140             None
141         };
142         let b = try!(commit.tree());
143         let diff = try!(repo.diff_tree_to_tree(a.as_ref(), Some(&b),
144                                                Some(&mut diffopts2)));
145         try!(diff.print(DiffFormat::Patch, |_delta, _hunk, line| {
146             match line.origin() {
147                 ' ' | '+' | '-' => print!("{}", line.origin()),
148                 _ => {}
149             }
150             print!("{}", str::from_utf8(line.content()).unwrap());
151             true
152         }));
153     }
154 
155     Ok(())
156 }
157 
sig_matches(sig: &Signature, arg: &Option<String>) -> bool158 fn sig_matches(sig: &Signature, arg: &Option<String>) -> bool {
159     match *arg {
160         Some(ref s) => {
161             sig.name().map(|n| n.contains(s)).unwrap_or(false) ||
162                 sig.email().map(|n| n.contains(s)).unwrap_or(false)
163         }
164         None => true
165     }
166 }
167 
log_message_matches(msg: Option<&str>, grep: &Option<String>) -> bool168 fn log_message_matches(msg: Option<&str>, grep: &Option<String>) -> bool {
169     match (grep, msg) {
170         (&None, _) => true,
171         (&Some(_), None) => false,
172         (&Some(ref s), Some(msg)) => msg.contains(s),
173     }
174 }
175 
print_commit(commit: &Commit)176 fn print_commit(commit: &Commit) {
177     println!("commit {}", commit.id());
178 
179     if commit.parents().len() > 1 {
180         print!("Merge:");
181         for id in commit.parent_ids() {
182             print!(" {:.8}", id);
183         }
184         println!("");
185     }
186 
187     let author = commit.author();
188     println!("Author: {}", author);
189     print_time(&author.when(), "Date:   ");
190     println!("");
191 
192     for line in String::from_utf8_lossy(commit.message_bytes()).lines() {
193         println!("    {}", line);
194     }
195     println!("");
196 }
197 
print_time(time: &Time, prefix: &str)198 fn print_time(time: &Time, prefix: &str) {
199     let (offset, sign) = match time.offset_minutes() {
200         n if n < 0 => (-n, '-'),
201         n => (n, '+'),
202     };
203     let (hours, minutes) = (offset / 60, offset % 60);
204     let ts = time::Timespec::new(time.seconds() +
205                                  (time.offset_minutes() as i64) * 60, 0);
206     let time = time::at(ts);
207 
208     println!("{}{} {}{:02}{:02}", prefix,
209              time.strftime("%a %b %e %T %Y").unwrap(), sign, hours, minutes);
210 
211 }
212 
match_with_parent(repo: &Repository, commit: &Commit, parent: &Commit, opts: &mut DiffOptions) -> Result<bool, Error>213 fn match_with_parent(repo: &Repository, commit: &Commit, parent: &Commit,
214                      opts: &mut DiffOptions) -> Result<bool, Error> {
215     let a = try!(parent.tree());
216     let b = try!(commit.tree());
217     let diff = try!(repo.diff_tree_to_tree(Some(&a), Some(&b), Some(opts)));
218     Ok(diff.deltas().len() > 0)
219 }
220 
221 impl Args {
min_parents(&self) -> usize222     fn min_parents(&self) -> usize {
223         if self.flag_no_min_parents { return 0 }
224         self.flag_min_parents.unwrap_or(if self.flag_merges {2} else {0})
225     }
226 
max_parents(&self) -> Option<usize>227     fn max_parents(&self) -> Option<usize> {
228         if self.flag_no_max_parents { return None }
229         self.flag_max_parents.or(if self.flag_no_merges {Some(1)} else {None})
230     }
231 }
232 
main()233 fn main() {
234     const USAGE: &'static str = "
235 usage: log [options] [<commit>..] [--] [<spec>..]
236 
237 Options:
238     --topo-order            sort commits in topological order
239     --date-order            sort commits in date order
240     --reverse               sort commits in reverse
241     --author <user>         author to sort by
242     --committer <user>      committer to sort by
243     --grep <pat>            pattern to filter commit messages by
244     --git-dir <dir>         alternative git directory to use
245     --skip <n>              number of commits to skip
246     -n, --max-count <n>     maximum number of commits to show
247     --merges                only show merge commits
248     --no-merges             don't show merge commits
249     --no-min-parents        don't require a minimum number of parents
250     --no-max-parents        don't require a maximum number of parents
251     --max-parents <n>       specify a maximum number of parents for a commit
252     --min-parents <n>       specify a minimum number of parents for a commit
253     -p, --patch             show commit diff
254     -h, --help              show this message
255 ";
256 
257     let args = Docopt::new(USAGE).and_then(|d| d.deserialize())
258                                  .unwrap_or_else(|e| e.exit());
259     match run(&args) {
260         Ok(()) => {}
261         Err(e) => println!("error: {}", e),
262     }
263 }
264