| /* |
| * 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 |
| * <http://creativecommons.org/publicdomain/zero/1.0/>. |
| */ |
| |
| #![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<String>, |
| #[structopt(name = "committer", long)] |
| /// committer to sort by |
| flag_committer: Option<String>, |
| #[structopt(name = "pat", long = "grep")] |
| /// pattern to filter commit messages by |
| flag_grep: Option<String>, |
| #[structopt(name = "dir", long = "git-dir")] |
| /// alternative git directory to use |
| flag_git_dir: Option<String>, |
| #[structopt(name = "skip", long)] |
| /// number of commits to skip |
| flag_skip: Option<usize>, |
| #[structopt(name = "max-count", short = "n", long)] |
| /// maximum number of commits to show |
| flag_max_count: Option<usize>, |
| #[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<usize>, |
| #[structopt(name = "min-parents")] |
| /// specify a minimum number of parents for a commit |
| flag_min_parents: Option<usize>, |
| #[structopt(name = "patch", long, short)] |
| /// show commit diff |
| flag_patch: bool, |
| #[structopt(name = "commit")] |
| arg_commit: Vec<String>, |
| #[structopt(name = "spec", last = true)] |
| arg_spec: Vec<String>, |
| } |
| |
| 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<String>) -> 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<String>) -> 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<bool, Error> { |
| 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<usize> { |
| 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), |
| } |
| } |