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 }