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  * <https://creativecommons.org/publicdomain/zero/1.0/>.
13  */
14 /**
15  * This example demonstrates the libgit2 rev walker APIs to roughly
16  * simulate the output of `git log` and a few of command line arguments.
17  * `git log` has many many options and this only shows a few of them.
18  *
19  * This does not have:
20  *
21  * - Robust error handling
22  * - Colorized or paginated output formatting
23  * - Most of the `git log` options
24  *
25  * This does have:
26  *
27  * - Examples of translating command line arguments to equivalent libgit2
28  *   revwalker configuration calls
29  * - Simplified options to apply pathspec limits and to show basic diffs
30  *
31  * License: $(LINK2 https://creativecommons.org/publicdomain/zero/1.0/, CC0 1.0 Universal)
32  */
33 module libgit2.example.log;
34 
35 
36 private static import core.stdc.stdio;
37 private static import core.stdc.stdlib;
38 private static import core.stdc.string;
39 private static import core.stdc.time;
40 private static import libgit2.commit;
41 private static import libgit2.diff;
42 private static import libgit2.example.args;
43 private static import libgit2.example.common;
44 private static import libgit2.merge;
45 private static import libgit2.object;
46 private static import libgit2.oid;
47 private static import libgit2.pathspec;
48 private static import libgit2.repository;
49 private static import libgit2.revparse;
50 private static import libgit2.revwalk;
51 private static import libgit2.tree;
52 private static import libgit2.types;
53 
54 /**
55  * log_state represents walker being configured while handling options
56  */
57 extern (C)
58 public struct log_state
59 {
60 	libgit2.types.git_repository* repo;
61 	const (char)* repodir;
62 	libgit2.types.git_revwalk* walker;
63 	int hide;
64 	int sorting;
65 	int revisions;
66 }
67 
68 /*
69  * utility functions that are called to configure the walker
70  *
71  * - set_sorting()
72  * - push_rev()
73  * - add_revision()
74  */
75 
76 /**
77  * log_options holds other command line options that affect log output
78  */
79 extern (C)
80 public struct log_options
81 {
82 	int show_diff;
83 	int show_log_size;
84 	int skip;
85 	int limit;
86 	int min_parents;
87 	int max_parents;
88 	libgit2.types.git_time_t before;
89 	libgit2.types.git_time_t after;
90 	const (char)* author;
91 	const (char)* committer;
92 	const (char)* grep;
93 }
94 
95 /*
96  * utility functions that parse options and help with log output
97  *
98  * - parse_options()
99  * - print_time()
100  * - print_commit()
101  * - match_with_parent()
102  */
103 
104 /*
105  * utility functions for filtering
106  *
107  * - signature_matches()
108  * - log_message_matches()
109  */
110 
111 extern (C)
112 nothrow @nogc
113 public int lg2_log(libgit2.types.git_repository* repo, int argc, char** argv)
114 
115 	in
116 	{
117 	}
118 
119 	do
120 	{
121 		/** Parse arguments and set up revwalker. */
122 		.log_state s;
123 		.log_options opt;
124 		int last_arg = .parse_options(&s, &opt, argc, argv);
125 		s.repo = repo;
126 
127 		libgit2.diff.git_diff_options diffopts = libgit2.diff.GIT_DIFF_OPTIONS_INIT();
128 		diffopts.pathspec.strings = &argv[last_arg];
129 		diffopts.pathspec.count = argc - last_arg;
130 
131 		libgit2.pathspec.git_pathspec* ps = null;
132 
133 		if (diffopts.pathspec.count > 0) {
134 			libgit2.example.common.check_lg2(libgit2.pathspec.git_pathspec_new(&ps, &diffopts.pathspec), "Building pathspec", null);
135 		}
136 
137 		if (!s.revisions) {
138 			.add_revision(&s, null);
139 		}
140 
141 		/** Use the revwalker to traverse the history. */
142 
143 		int count = 0;
144 		int printed = 0;
145 		libgit2.oid.git_oid oid;
146 		libgit2.types.git_commit* commit = null;
147 		libgit2.types.git_tree* tree;
148 		libgit2.types.git_commit* parent;
149 
150 		for (; !libgit2.revwalk.git_revwalk_next(&oid, s.walker); libgit2.commit.git_commit_free(commit)) {
151 			libgit2.example.common.check_lg2(libgit2.commit.git_commit_lookup(&commit, s.repo, &oid), "Failed to look up commit", null);
152 
153 			int parents = cast(int)(libgit2.commit.git_commit_parentcount(commit));
154 
155 			if (parents < opt.min_parents) {
156 				continue;
157 			}
158 
159 			if ((opt.max_parents > 0) && (parents > opt.max_parents)) {
160 				continue;
161 			}
162 
163 			if (diffopts.pathspec.count > 0) {
164 				int unmatched = parents;
165 
166 				if (parents == 0) {
167 					libgit2.example.common.check_lg2(libgit2.commit.git_commit_tree(&tree, commit), "Get tree", null);
168 
169 					if (libgit2.pathspec.git_pathspec_match_tree(null, tree, libgit2.pathspec.git_pathspec_flag_t.GIT_PATHSPEC_NO_MATCH_ERROR, ps) != 0) {
170 						unmatched = 1;
171 					}
172 
173 					libgit2.tree.git_tree_free(tree);
174 				} else if (parents == 1) {
175 					unmatched = (.match_with_parent(commit, 0, &diffopts)) ? (0) : (1);
176 				} else {
177 					for (int i = 0; i < parents; ++i) {
178 						if (.match_with_parent(commit, i, &diffopts)) {
179 							unmatched--;
180 						}
181 					}
182 				}
183 
184 				if (unmatched > 0) {
185 					continue;
186 				}
187 			}
188 
189 			if (!.signature_matches(libgit2.commit.git_commit_author(commit), opt.author)) {
190 				continue;
191 			}
192 
193 			if (!.signature_matches(libgit2.commit.git_commit_committer(commit), opt.committer)) {
194 				continue;
195 			}
196 
197 			if (!.log_message_matches(commit, opt.grep)) {
198 				continue;
199 			}
200 
201 			if (count++ < opt.skip) {
202 				continue;
203 			}
204 
205 			if ((opt.limit != -1) && (printed++ >= opt.limit)) {
206 				libgit2.commit.git_commit_free(commit);
207 
208 				break;
209 			}
210 
211 			.print_commit(commit, &opt);
212 
213 			if (opt.show_diff) {
214 				libgit2.types.git_tree* a = null;
215 				libgit2.types.git_tree* b = null;
216 				libgit2.diff.git_diff* diff = null;
217 
218 				if (parents > 1) {
219 					continue;
220 				}
221 
222 				libgit2.example.common.check_lg2(libgit2.commit.git_commit_tree(&b, commit), "Get tree", null);
223 
224 				if (parents == 1) {
225 					libgit2.example.common.check_lg2(libgit2.commit.git_commit_parent(&parent, commit, 0), "Get parent", null);
226 					libgit2.example.common.check_lg2(libgit2.commit.git_commit_tree(&a, parent), "Tree for parent", null);
227 					libgit2.commit.git_commit_free(parent);
228 				}
229 
230 				libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_tree(&diff, libgit2.commit.git_commit_owner(commit), a, b, &diffopts), "Diff commit with parent", null);
231 				libgit2.example.common.check_lg2(libgit2.diff.git_diff_print(diff, libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_PATCH, &libgit2.example.common.diff_output, null), "Displaying diff", null);
232 
233 				libgit2.diff.git_diff_free(diff);
234 				libgit2.tree.git_tree_free(a);
235 				libgit2.tree.git_tree_free(b);
236 			}
237 		}
238 
239 		libgit2.pathspec.git_pathspec_free(ps);
240 		libgit2.revwalk.git_revwalk_free(s.walker);
241 
242 		return 0;
243 	}
244 
245 /**
246  * Determine if the given libgit2.types.git_signature does not contain the filter text.
247  */
248 nothrow @nogc
249 private int signature_matches(const (libgit2.types.git_signature)* sig, const (char)* filter)
250 
251 	in
252 	{
253 	}
254 
255 	do
256 	{
257 		if (filter == null) {
258 			return 1;
259 		}
260 
261 		if ((sig != null) && ((core.stdc..string.strstr(sig.name, filter) != null) || (core.stdc..string.strstr(sig.email, filter) != null))) {
262 			return 1;
263 		}
264 
265 		return 0;
266 	}
267 
268 nothrow @nogc
269 private int log_message_matches(const (libgit2.types.git_commit)* commit, const (char)* filter)
270 
271 	in
272 	{
273 	}
274 
275 	do
276 	{
277 		const (char)* message = null;
278 
279 		if (filter == null) {
280 			return 1;
281 		}
282 
283 		message = libgit2.commit.git_commit_message(commit);
284 
285 		if ((message != null) && (core.stdc..string.strstr(message, filter) != null)) {
286 			return 1;
287 		}
288 
289 		return 0;
290 	}
291 
292 /**
293  * Push object (for hide or show) onto revwalker.
294  */
295 nothrow @nogc
296 private void push_rev(.log_state* s, libgit2.types.git_object* obj, int hide)
297 
298 	in
299 	{
300 	}
301 
302 	do
303 	{
304 		hide = s.hide ^ hide;
305 
306 		/** Create revwalker on demand if it doesn't already exist. */
307 		if (s.walker == null) {
308 			libgit2.example.common.check_lg2(libgit2.revwalk.git_revwalk_new(&s.walker, s.repo), "Could not create revision walker", null);
309 			libgit2.revwalk.git_revwalk_sorting(s.walker, s.sorting);
310 		}
311 
312 		if (obj == null) {
313 			libgit2.example.common.check_lg2(libgit2.revwalk.git_revwalk_push_head(s.walker), "Could not find repository HEAD", null);
314 		} else if (hide) {
315 			libgit2.example.common.check_lg2(libgit2.revwalk.git_revwalk_hide(s.walker, libgit2.object.git_object_id(obj)), "Reference does not refer to a commit", null);
316 		} else {
317 			libgit2.example.common.check_lg2(libgit2.revwalk.git_revwalk_push(s.walker, libgit2.object.git_object_id(obj)), "Reference does not refer to a commit", null);
318 		}
319 
320 		libgit2.object.git_object_free(obj);
321 	}
322 
323 /**
324  * Parse revision string and add revs to walker.
325  */
326 nothrow @nogc
327 private int add_revision(.log_state* s, const (char)* revstr)
328 
329 	in
330 	{
331 	}
332 
333 	do
334 	{
335 		libgit2.revparse.git_revspec revs;
336 		int hide = 0;
337 
338 		if (revstr == null) {
339 			.push_rev(s, null, hide);
340 
341 			return 0;
342 		}
343 
344 		if (*revstr == '^') {
345 			revs.flags = libgit2.revparse.git_revspec_t.GIT_REVSPEC_SINGLE;
346 			hide = !hide;
347 
348 			if (libgit2.revparse.git_revparse_single(&revs.from, s.repo, revstr + 1) < 0) {
349 				return -1;
350 			}
351 		} else if (libgit2.revparse.git_revparse(&revs, s.repo, revstr) < 0) {
352 			return -1;
353 		}
354 
355 		if ((revs.flags & libgit2.revparse.git_revspec_t.GIT_REVSPEC_SINGLE) != 0) {
356 			.push_rev(s, revs.from, hide);
357 		} else {
358 			.push_rev(s, revs.to, hide);
359 
360 			if ((revs.flags & libgit2.revparse.git_revspec_t.GIT_REVSPEC_MERGE_BASE) != 0) {
361 				libgit2.oid.git_oid base;
362 				libgit2.example.common.check_lg2(libgit2.merge.git_merge_base(&base, s.repo, libgit2.object.git_object_id(revs.from), libgit2.object.git_object_id(revs.to)), "Could not find merge base", revstr);
363 				libgit2.example.common.check_lg2(libgit2.object.git_object_lookup(&revs.to, s.repo, &base, libgit2.types.git_object_t.GIT_OBJECT_COMMIT), "Could not find merge base commit", null);
364 
365 				.push_rev(s, revs.to, hide);
366 			}
367 
368 			.push_rev(s, revs.from, !hide);
369 		}
370 
371 		return 0;
372 	}
373 
374 /**
375  * Update revwalker with sorting mode.
376  */
377 nothrow @nogc
378 private void set_sorting(.log_state* s, uint sort_mode)
379 
380 	in
381 	{
382 	}
383 
384 	do
385 	{
386 		/** Open repo on demand if it isn't already open. */
387 		if (s.repo == null) {
388 			if (s.repodir == null) {
389 				s.repodir = ".";
390 			}
391 
392 			libgit2.example.common.check_lg2(libgit2.repository.git_repository_open_ext(&s.repo, s.repodir, 0, null), "Could not open repository", s.repodir);
393 		}
394 
395 		/** Create revwalker on demand if it doesn't already exist. */
396 		if (s.walker == null) {
397 			libgit2.example.common.check_lg2(libgit2.revwalk.git_revwalk_new(&s.walker, s.repo), "Could not create revision walker", null);
398 		}
399 
400 		if (sort_mode == libgit2.revwalk.git_sort_t.GIT_SORT_REVERSE) {
401 			s.sorting = s.sorting ^ libgit2.revwalk.git_sort_t.GIT_SORT_REVERSE;
402 		} else {
403 			s.sorting = sort_mode | (s.sorting & libgit2.revwalk.git_sort_t.GIT_SORT_REVERSE);
404 		}
405 
406 		libgit2.revwalk.git_revwalk_sorting(s.walker, s.sorting);
407 	}
408 
409 /**
410  * Helper to format a libgit2.types.git_time value like Git.
411  */
412 nothrow @nogc
413 private void print_time(const (libgit2.types.git_time)* intime, const (char)* prefix)
414 
415 	in
416 	{
417 	}
418 
419 	do
420 	{
421 		char sign;
422 		int offset = intime.offset;
423 
424 		if (offset < 0) {
425 			sign = '-';
426 			offset = -offset;
427 		} else {
428 			sign = '+';
429 		}
430 
431 		int hours = offset / 60;
432 		int minutes = offset % 60;
433 
434 		core.stdc.time.time_t t = cast(core.stdc.time.time_t)(intime.time) + (intime.offset * 60);
435 
436 		core.stdc.time.tm* intm = core.stdc.time.gmtime(&t);
437 		char[32] out_;
438 		core.stdc.time.strftime(&(out_[0]), out_.length, "%a %b %e %T %Y", intm);
439 
440 		core.stdc.stdio.printf("%s%s %c%02d%02d\n", prefix, &(out_[0]), sign, hours, minutes);
441 	}
442 
443 /**
444  * Helper to print a commit object.
445  */
446 nothrow @nogc
447 private void print_commit(libgit2.types.git_commit* commit, .log_options* opts)
448 
449 	in
450 	{
451 	}
452 
453 	do
454 	{
455 		char[libgit2.oid.GIT_OID_SHA1_HEXSIZE + 1] buf;
456 		libgit2.oid.git_oid_tostr(&(buf[0]), buf.length, libgit2.commit.git_commit_id(commit));
457 		core.stdc.stdio.printf("commit %s\n", &(buf[0]));
458 
459 		if (opts.show_log_size) {
460 			core.stdc.stdio.printf("log size %d\n", cast(int)(core.stdc..string.strlen(libgit2.commit.git_commit_message(commit))));
461 		}
462 
463 		int count = cast(int)(libgit2.commit.git_commit_parentcount(commit));
464 
465 		if (count > 1) {
466 			core.stdc.stdio.printf("Merge:");
467 
468 			for (int i = 0; i < count; ++i) {
469 				libgit2.oid.git_oid_tostr(&(buf[0]), 8, libgit2.commit.git_commit_parent_id(commit, i));
470 				core.stdc.stdio.printf(" %s", &(buf[0]));
471 			}
472 
473 			core.stdc.stdio.printf("\n");
474 		}
475 
476 		const (libgit2.types.git_signature)* sig = libgit2.commit.git_commit_author(commit);
477 
478 		if (sig != null) {
479 			core.stdc.stdio.printf("Author: %s <%s>\n", sig.name, sig.email);
480 			.print_time(&sig.when, "Date:   ");
481 		}
482 
483 		core.stdc.stdio.printf("\n");
484 		const (char)* scan;
485 		const (char)* eol;
486 
487 		for (scan = libgit2.commit.git_commit_message(commit); (scan) && (*scan); ) {
488 			for (eol = scan; (*eol) && (*eol != '\n'); ++eol) {/* find eol */
489 			}
490 
491 			core.stdc.stdio.printf("    %.*s\n", cast(int)(eol - scan), scan);
492 			scan = (*eol) ? (eol + 1) : (null);
493 		}
494 
495 		core.stdc.stdio.printf("\n");
496 	}
497 
498 /**
499  * Helper to find how many files in a commit changed from its nth parent.
500  */
501 nothrow @nogc
502 private int match_with_parent(libgit2.types.git_commit* commit, int i, libgit2.diff.git_diff_options* opts)
503 
504 	in
505 	{
506 	}
507 
508 	do
509 	{
510 		libgit2.types.git_commit* parent;
511 		libgit2.types.git_tree* a;
512 		libgit2.types.git_tree* b;
513 		libgit2.diff.git_diff* diff;
514 
515 		libgit2.example.common.check_lg2(libgit2.commit.git_commit_parent(&parent, commit, cast(size_t)(i)), "Get parent", null);
516 		libgit2.example.common.check_lg2(libgit2.commit.git_commit_tree(&a, parent), "Tree for parent", null);
517 		libgit2.example.common.check_lg2(libgit2.commit.git_commit_tree(&b, commit), "Tree for commit", null);
518 		libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_tree(&diff, libgit2.commit.git_commit_owner(commit), a, b, opts), "Checking diff between parent and commit", null);
519 
520 		int ndeltas = cast(int)(libgit2.diff.git_diff_num_deltas(diff));
521 
522 		libgit2.diff.git_diff_free(diff);
523 		libgit2.tree.git_tree_free(a);
524 		libgit2.tree.git_tree_free(b);
525 		libgit2.commit.git_commit_free(parent);
526 
527 		return ndeltas > 0;
528 	}
529 
530 /**
531  * Print a usage message for the program.
532  */
533 nothrow @nogc
534 private void usage(const (char)* message, const (char)* arg)
535 
536 	in
537 	{
538 	}
539 
540 	do
541 	{
542 		if ((message != null) && (arg != null)) {
543 			core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s: %s\n", message, arg);
544 		} else if (message != null) {
545 			core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s\n", message);
546 		}
547 
548 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "usage: log [<options>]\n");
549 		core.stdc.stdlib.exit(1);
550 	}
551 
552 /**
553  * Parse some log command line options.
554  */
555 nothrow @nogc
556 private int parse_options(.log_state* s, .log_options* opt, int argc, char** argv)
557 
558 	in
559 	{
560 	}
561 
562 	do
563 	{
564 		libgit2.example.args.args_info args = libgit2.example.args.ARGS_INFO_INIT(argc, argv);
565 
566 		core.stdc..string.memset(s, 0, (*s).sizeof);
567 		s.sorting = libgit2.revwalk.git_sort_t.GIT_SORT_TIME;
568 
569 		core.stdc..string.memset(opt, 0, (*opt).sizeof);
570 		opt.max_parents = -1;
571 		opt.limit = -1;
572 
573 		for (args.pos = 1; args.pos < argc; ++args.pos) {
574 			const (char)* a = argv[args.pos];
575 
576 			if (a[0] != '-') {
577 				if (!.add_revision(s, a)) {
578 					s.revisions++;
579 				} else {
580 					/** Try failed revision parse as filename. */
581 					break;
582 				}
583 			} else if (!libgit2.example.args.match_arg_separator(&args)) {
584 				break;
585 			} else if (!core.stdc..string.strcmp(a, "--date-order")) {
586 				.set_sorting(s, libgit2.revwalk.git_sort_t.GIT_SORT_TIME);
587 			} else if (!core.stdc..string.strcmp(a, "--topo-order")) {
588 				.set_sorting(s, libgit2.revwalk.git_sort_t.GIT_SORT_TOPOLOGICAL);
589 			} else if (!core.stdc..string.strcmp(a, "--reverse")) {
590 				.set_sorting(s, libgit2.revwalk.git_sort_t.GIT_SORT_REVERSE);
591 			} else if (libgit2.example.args.match_str_arg(&opt.author, &args, "--author")) {
592 				/** Found valid --author */
593 			} else if (libgit2.example.args.match_str_arg(&opt.committer, &args, "--committer")) {
594 				/** Found valid --committer */
595 			} else if (libgit2.example.args.match_str_arg(&opt.grep, &args, "--grep")) {
596 				/** Found valid --grep */
597 			} else if (libgit2.example.args.match_str_arg(&s.repodir, &args, "--git-dir")) {
598 				/** Found git-dir. */
599 			} else if (libgit2.example.args.match_int_arg(&opt.skip, &args, "--skip", 0)) {
600 				/** Found valid --skip. */
601 			} else if (libgit2.example.args.match_int_arg(&opt.limit, &args, "--max-count", 0)) {
602 				/** Found valid --max-count. */
603 			} else if ((a[1] >= '0') && (a[1] <= '9')) {
604 				libgit2.example.args.is_integer(&opt.limit, a + 1, 0);
605 			} else if (libgit2.example.args.match_int_arg(&opt.limit, &args, "-n", 0)) {
606 				/** Found valid -n. */
607 			} else if (!core.stdc..string.strcmp(a, "--merges")) {
608 				opt.min_parents = 2;
609 			} else if (!core.stdc..string.strcmp(a, "--no-merges")) {
610 				opt.max_parents = 1;
611 			} else if (!core.stdc..string.strcmp(a, "--no-min-parents")) {
612 				opt.min_parents = 0;
613 			} else if (!core.stdc..string.strcmp(a, "--no-max-parents")) {
614 				opt.max_parents = -1;
615 			} else if (libgit2.example.args.match_int_arg(&opt.max_parents, &args, "--max-parents=", 1)) {
616 				/** Found valid --max-parents. */
617 			} else if (libgit2.example.args.match_int_arg(&opt.min_parents, &args, "--min-parents=", 0)) {
618 				/** Found valid --min_parents. */
619 			} else if ((!core.stdc..string.strcmp(a, "-p")) || (!core.stdc..string.strcmp(a, "-u")) || (!core.stdc..string.strcmp(a, "--patch"))) {
620 				opt.show_diff = 1;
621 			} else if (!core.stdc..string.strcmp(a, "--log-size")) {
622 				opt.show_log_size = 1;
623 			} else {
624 				.usage("Unsupported argument", a);
625 			}
626 		}
627 
628 		return args.pos;
629 	}