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 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 215 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 225 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 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 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 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 { 287 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 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 304 fn main() { 305 let args = Args::from_args(); 306 match run(&args) { 307 Ok(()) => {} 308 Err(e) => println!("error: {}", e), 309 } 310 } 311