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