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 }