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 }