Skip to main content

git_log/
git-log.rs

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
17use git2::{Commit, DiffOptions, ObjectType, Repository, Signature, Time};
18use git2::{DiffFormat, Error, Pathspec};
19use std::str;
20use structopt::StructOpt;
21
22#[derive(StructOpt)]
23struct 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
78fn 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 { git2::Sort::REVERSE } else { git2::Sort::NONE };
85    revwalk.set_sorting(
86        base | if args.flag_topo_order {
87            git2::Sort::TOPOLOGICAL
88        } else if args.flag_date_order {
89            git2::Sort::TIME
90        } else {
91            git2::Sort::NONE
92        },
93    )?;
94    for commit in &args.arg_commit {
95        if commit.starts_with('^') {
96            let obj = repo.revparse_single(&commit[1..])?;
97            revwalk.hide(obj.id())?;
98            continue;
99        }
100        let revspec = repo.revparse(commit)?;
101        if revspec.mode().contains(git2::RevparseMode::SINGLE) {
102            revwalk.push(revspec.from().unwrap().id())?;
103        } else {
104            let from = revspec.from().unwrap().id();
105            let to = revspec.to().unwrap().id();
106            revwalk.push(to)?;
107            if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) {
108                let base = repo.merge_base(from, to)?;
109                let o = repo.find_object(base, Some(ObjectType::Commit))?;
110                revwalk.push(o.id())?;
111            }
112            revwalk.hide(from)?;
113        }
114    }
115    if args.arg_commit.is_empty() {
116        revwalk.push_head()?;
117    }
118
119    // Prepare our diff options and pathspec matcher
120    let (mut diffopts, mut diffopts2) = (DiffOptions::new(), DiffOptions::new());
121    for spec in &args.arg_spec {
122        diffopts.pathspec(spec);
123        diffopts2.pathspec(spec);
124    }
125    let ps = Pathspec::new(args.arg_spec.iter())?;
126
127    // Filter our revwalk based on the CLI parameters
128    macro_rules! filter_try {
129        ($e:expr) => {
130            match $e {
131                Ok(t) => t,
132                Err(e) => return Some(Err(e)),
133            }
134        };
135    }
136    let revwalk = revwalk
137        .filter_map(|id| {
138            let id = filter_try!(id);
139            let commit = filter_try!(repo.find_commit(id));
140            let parents = commit.parents().len();
141            if parents < args.min_parents() {
142                return None;
143            }
144            if let Some(n) = args.max_parents() {
145                if parents >= n {
146                    return None;
147                }
148            }
149            if !args.arg_spec.is_empty() {
150                match commit.parents().len() {
151                    0 => {
152                        let tree = filter_try!(commit.tree());
153                        let flags = git2::PathspecFlags::NO_MATCH_ERROR;
154                        if ps.match_tree(&tree, flags).is_err() {
155                            return None;
156                        }
157                    }
158                    _ => {
159                        let m = commit
160                            .parents()
161                            .all(|parent| match_with_parent(&repo, &commit, &parent, &mut diffopts).unwrap_or(false));
162                        if !m {
163                            return None;
164                        }
165                    }
166                }
167            }
168            if !sig_matches(&commit.author(), &args.flag_author) {
169                return None;
170            }
171            if !sig_matches(&commit.committer(), &args.flag_committer) {
172                return None;
173            }
174            if !log_message_matches(commit.message(), &args.flag_grep) {
175                return None;
176            }
177            Some(Ok(commit))
178        })
179        .skip(args.flag_skip.unwrap_or(0))
180        .take(args.flag_max_count.unwrap_or(!0));
181
182    // print!
183    for commit in revwalk {
184        let commit = commit?;
185        print_commit(&commit);
186        if !args.flag_patch || commit.parents().len() > 1 {
187            continue;
188        }
189        let a = if commit.parents().len() == 1 {
190            let parent = commit.parent(0)?;
191            Some(parent.tree()?)
192        } else {
193            None
194        };
195        let b = commit.tree()?;
196        let diff = repo.diff_tree_to_tree(a.as_ref(), Some(&b), Some(&mut diffopts2))?;
197        diff.print(DiffFormat::Patch, |_delta, _hunk, line| {
198            match line.origin() {
199                ' ' | '+' | '-' => print!("{}", line.origin()),
200                _ => {}
201            }
202            print!("{}", str::from_utf8(line.content()).unwrap());
203            true
204        })?;
205    }
206
207    Ok(())
208}
209
210fn sig_matches(sig: &Signature, arg: &Option<String>) -> bool {
211    match *arg {
212        Some(ref s) => sig.name().map(|n| n.contains(s)).unwrap_or(false) || sig.email().map(|n| n.contains(s)).unwrap_or(false),
213        None => true,
214    }
215}
216
217fn log_message_matches(msg: Option<&str>, grep: &Option<String>) -> bool {
218    match (grep, msg) {
219        (&None, _) => true,
220        (&Some(_), None) => false,
221        (&Some(ref s), Some(msg)) => msg.contains(s),
222    }
223}
224
225fn print_commit(commit: &Commit) {
226    println!("commit {}", commit.id());
227
228    if commit.parents().len() > 1 {
229        print!("Merge:");
230        for id in commit.parent_ids() {
231            print!(" {:.8}", id);
232        }
233        println!();
234    }
235
236    let author = commit.author();
237    println!("Author: {}", author);
238    print_time(&author.when(), "Date:   ");
239    println!();
240
241    for line in String::from_utf8_lossy(commit.message_bytes()).lines() {
242        println!("    {}", line);
243    }
244    println!();
245}
246
247fn print_time(time: &Time, prefix: &str) {
248    let (offset, sign) = match time.offset_minutes() {
249        n if n < 0 => (-n, '-'),
250        n => (n, '+'),
251    };
252    let (hours, minutes) = (offset / 60, offset % 60);
253    let ts = time::Timespec::new(time.seconds() + (time.offset_minutes() as i64) * 60, 0);
254    let time = time::at(ts);
255
256    println!(
257        "{}{} {}{:02}{:02}",
258        prefix,
259        time.strftime("%a %b %e %T %Y").unwrap(),
260        sign,
261        hours,
262        minutes
263    );
264}
265
266fn match_with_parent(repo: &Repository, commit: &Commit, parent: &Commit, opts: &mut DiffOptions) -> Result<bool, Error> {
267    let a = parent.tree()?;
268    let b = commit.tree()?;
269    let diff = repo.diff_tree_to_tree(Some(&a), Some(&b), Some(opts))?;
270    Ok(diff.deltas().len() > 0)
271}
272
273impl Args {
274    fn min_parents(&self) -> usize {
275        if self.flag_no_min_parents {
276            return 0;
277        }
278        self.flag_min_parents.unwrap_or(if self.flag_merges { 2 } else { 0 })
279    }
280
281    fn max_parents(&self) -> Option<usize> {
282        if self.flag_no_max_parents {
283            return None;
284        }
285        self.flag_max_parents.or(if self.flag_no_merges { Some(1) } else { None })
286    }
287}
288
289fn main() {
290    let args = Args::from_args();
291    match run(&args) {
292        Ok(()) => {}
293        Err(e) => println!("error: {}", e),
294    }
295}