1#![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 flag_topo_order: bool,
27 #[structopt(name = "date-order", long)]
28 flag_date_order: bool,
30 #[structopt(name = "reverse", long)]
31 flag_reverse: bool,
33 #[structopt(name = "author", long)]
34 flag_author: Option<String>,
36 #[structopt(name = "committer", long)]
37 flag_committer: Option<String>,
39 #[structopt(name = "pat", long = "grep")]
40 flag_grep: Option<String>,
42 #[structopt(name = "dir", long = "git-dir")]
43 flag_git_dir: Option<String>,
45 #[structopt(name = "skip", long)]
46 flag_skip: Option<usize>,
48 #[structopt(name = "max-count", short = "n", long)]
49 flag_max_count: Option<usize>,
51 #[structopt(name = "merges", long)]
52 flag_merges: bool,
54 #[structopt(name = "no-merges", long)]
55 flag_no_merges: bool,
57 #[structopt(name = "no-min-parents", long)]
58 flag_no_min_parents: bool,
60 #[structopt(name = "no-max-parents", long)]
61 flag_no_max_parents: bool,
63 #[structopt(name = "max-parents")]
64 flag_max_parents: Option<usize>,
66 #[structopt(name = "min-parents")]
67 flag_min_parents: Option<usize>,
69 #[structopt(name = "patch", long, short)]
70 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 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 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 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 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}