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
run(args: &Args) -> Result<(), Error>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
sig_matches(sig: &Signature, arg: &Option<String>) -> bool215 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
log_message_matches(msg: Option<&str>, grep: &Option<String>) -> bool225 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
print_commit(commit: &Commit)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
print_time(time: &Time, prefix: &str)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
match_with_parent( repo: &Repository, commit: &Commit, parent: &Commit, opts: &mut DiffOptions, ) -> Result<bool, Error>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 {
min_parents(&self) -> usize287 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
max_parents(&self) -> Option<usize>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
main()304 fn main() {
305 let args = Args::from_args();
306 match run(&args) {
307 Ok(()) => {}
308 Err(e) => println!("error: {}", e),
309 }
310 }
311