/* * libgit2 "log" example - shows how to walk history and get commit info * * Written by the libgit2 contributors * * To the extent possible under law, the author(s) have dedicated all copyright * and related and neighboring rights to this software to the public domain * worldwide. This software is distributed without any warranty. * * You should have received a copy of the CC0 Public Domain Dedication along * with this software. If not, see * . */ #![deny(warnings)] use git2::{Commit, DiffOptions, ObjectType, Repository, Signature, Time}; use git2::{DiffFormat, Error, Pathspec}; use std::str; use structopt::StructOpt; #[derive(StructOpt)] struct Args { #[structopt(name = "topo-order", long)] /// sort commits in topological order flag_topo_order: bool, #[structopt(name = "date-order", long)] /// sort commits in date order flag_date_order: bool, #[structopt(name = "reverse", long)] /// sort commits in reverse flag_reverse: bool, #[structopt(name = "author", long)] /// author to sort by flag_author: Option, #[structopt(name = "committer", long)] /// committer to sort by flag_committer: Option, #[structopt(name = "pat", long = "grep")] /// pattern to filter commit messages by flag_grep: Option, #[structopt(name = "dir", long = "git-dir")] /// alternative git directory to use flag_git_dir: Option, #[structopt(name = "skip", long)] /// number of commits to skip flag_skip: Option, #[structopt(name = "max-count", short = "n", long)] /// maximum number of commits to show flag_max_count: Option, #[structopt(name = "merges", long)] /// only show merge commits flag_merges: bool, #[structopt(name = "no-merges", long)] /// don't show merge commits flag_no_merges: bool, #[structopt(name = "no-min-parents", long)] /// don't require a minimum number of parents flag_no_min_parents: bool, #[structopt(name = "no-max-parents", long)] /// don't require a maximum number of parents flag_no_max_parents: bool, #[structopt(name = "max-parents")] /// specify a maximum number of parents for a commit flag_max_parents: Option, #[structopt(name = "min-parents")] /// specify a minimum number of parents for a commit flag_min_parents: Option, #[structopt(name = "patch", long, short)] /// show commit diff flag_patch: bool, #[structopt(name = "commit")] arg_commit: Vec, #[structopt(name = "spec", last = true)] arg_spec: Vec, } fn run(args: &Args) -> Result<(), Error> { let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or("."); let repo = Repository::open(path)?; let mut revwalk = repo.revwalk()?; // Prepare the revwalk based on CLI parameters let base = if args.flag_reverse { git2::Sort::REVERSE } else { git2::Sort::NONE }; revwalk.set_sorting( base | if args.flag_topo_order { git2::Sort::TOPOLOGICAL } else if args.flag_date_order { git2::Sort::TIME } else { git2::Sort::NONE }, )?; for commit in &args.arg_commit { if commit.starts_with('^') { let obj = repo.revparse_single(&commit[1..])?; revwalk.hide(obj.id())?; continue; } let revspec = repo.revparse(commit)?; if revspec.mode().contains(git2::RevparseMode::SINGLE) { revwalk.push(revspec.from().unwrap().id())?; } else { let from = revspec.from().unwrap().id(); let to = revspec.to().unwrap().id(); revwalk.push(to)?; if revspec.mode().contains(git2::RevparseMode::MERGE_BASE) { let base = repo.merge_base(from, to)?; let o = repo.find_object(base, Some(ObjectType::Commit))?; revwalk.push(o.id())?; } revwalk.hide(from)?; } } if args.arg_commit.is_empty() { revwalk.push_head()?; } // Prepare our diff options and pathspec matcher let (mut diffopts, mut diffopts2) = (DiffOptions::new(), DiffOptions::new()); for spec in &args.arg_spec { diffopts.pathspec(spec); diffopts2.pathspec(spec); } let ps = Pathspec::new(args.arg_spec.iter())?; // Filter our revwalk based on the CLI parameters macro_rules! filter_try { ($e:expr) => { match $e { Ok(t) => t, Err(e) => return Some(Err(e)), } }; } let revwalk = revwalk .filter_map(|id| { let id = filter_try!(id); let commit = filter_try!(repo.find_commit(id)); let parents = commit.parents().len(); if parents < args.min_parents() { return None; } if let Some(n) = args.max_parents() { if parents >= n { return None; } } if !args.arg_spec.is_empty() { match commit.parents().len() { 0 => { let tree = filter_try!(commit.tree()); let flags = git2::PathspecFlags::NO_MATCH_ERROR; if ps.match_tree(&tree, flags).is_err() { return None; } } _ => { let m = commit .parents() .all(|parent| match_with_parent(&repo, &commit, &parent, &mut diffopts).unwrap_or(false)); if !m { return None; } } } } if !sig_matches(&commit.author(), &args.flag_author) { return None; } if !sig_matches(&commit.committer(), &args.flag_committer) { return None; } if !log_message_matches(commit.message(), &args.flag_grep) { return None; } Some(Ok(commit)) }) .skip(args.flag_skip.unwrap_or(0)) .take(args.flag_max_count.unwrap_or(!0)); // print! for commit in revwalk { let commit = commit?; print_commit(&commit); if !args.flag_patch || commit.parents().len() > 1 { continue; } let a = if commit.parents().len() == 1 { let parent = commit.parent(0)?; Some(parent.tree()?) } else { None }; let b = commit.tree()?; let diff = repo.diff_tree_to_tree(a.as_ref(), Some(&b), Some(&mut diffopts2))?; diff.print(DiffFormat::Patch, |_delta, _hunk, line| { match line.origin() { ' ' | '+' | '-' => print!("{}", line.origin()), _ => {} } print!("{}", str::from_utf8(line.content()).unwrap()); true })?; } Ok(()) } fn sig_matches(sig: &Signature, arg: &Option) -> bool { match *arg { Some(ref s) => sig.name().map(|n| n.contains(s)).unwrap_or(false) || sig.email().map(|n| n.contains(s)).unwrap_or(false), None => true, } } fn log_message_matches(msg: Option<&str>, grep: &Option) -> bool { match (grep, msg) { (&None, _) => true, (&Some(_), None) => false, (&Some(ref s), Some(msg)) => msg.contains(s), } } fn print_commit(commit: &Commit) { println!("commit {}", commit.id()); if commit.parents().len() > 1 { print!("Merge:"); for id in commit.parent_ids() { print!(" {:.8}", id); } println!(); } let author = commit.author(); println!("Author: {}", author); print_time(&author.when(), "Date: "); println!(); for line in String::from_utf8_lossy(commit.message_bytes()).lines() { println!(" {}", line); } println!(); } fn print_time(time: &Time, prefix: &str) { let (offset, sign) = match time.offset_minutes() { n if n < 0 => (-n, '-'), n => (n, '+'), }; let (hours, minutes) = (offset / 60, offset % 60); let ts = time::Timespec::new(time.seconds() + (time.offset_minutes() as i64) * 60, 0); let time = time::at(ts); println!( "{}{} {}{:02}{:02}", prefix, time.strftime("%a %b %e %T %Y").unwrap(), sign, hours, minutes ); } fn match_with_parent(repo: &Repository, commit: &Commit, parent: &Commit, opts: &mut DiffOptions) -> Result { let a = parent.tree()?; let b = commit.tree()?; let diff = repo.diff_tree_to_tree(Some(&a), Some(&b), Some(opts))?; Ok(diff.deltas().len() > 0) } impl Args { fn min_parents(&self) -> usize { if self.flag_no_min_parents { return 0; } self.flag_min_parents.unwrap_or(if self.flag_merges { 2 } else { 0 }) } fn max_parents(&self) -> Option { if self.flag_no_max_parents { return None; } self.flag_max_parents.or(if self.flag_no_merges { Some(1) } else { None }) } } fn main() { let args = Args::from_args(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), } }