| /* |
| * libgit2 "log" example - shows how to walk history and get commit info |
| * |
| * Written by the libgit2 contributors |
| * |
| * To the extent possible under law, the author(s) have dedicated all copyright |
| * and related and neighboring rights to this software to the public domain |
| * worldwide. This software is distributed without any warranty. |
| * |
| * You should have received a copy of the CC0 Public Domain Dedication along |
| * with this software. If not, see |
| * <http://creativecommons.org/publicdomain/zero/1.0/>. |
| */ |
| |
| #include "common.h" |
| |
| /** |
| * This example demonstrates the libgit2 rev walker APIs to roughly |
| * simulate the output of `git log` and a few of command line arguments. |
| * `git log` has many many options and this only shows a few of them. |
| * |
| * This does not have: |
| * |
| * - Robust error handling |
| * - Colorized or paginated output formatting |
| * - Most of the `git log` options |
| * |
| * This does have: |
| * |
| * - Examples of translating command line arguments to equivalent libgit2 |
| * revwalker configuration calls |
| * - Simplified options to apply pathspec limits and to show basic diffs |
| */ |
| |
| /** log_state represents walker being configured while handling options */ |
| struct log_state { |
| git_repository *repo; |
| const char *repodir; |
| git_revwalk *walker; |
| int hide; |
| int sorting; |
| int revisions; |
| }; |
| |
| /** utility functions that are called to configure the walker */ |
| static void set_sorting(struct log_state *s, unsigned int sort_mode); |
| static void push_rev(struct log_state *s, git_object *obj, int hide); |
| static int add_revision(struct log_state *s, const char *revstr); |
| |
| /** log_options holds other command line options that affect log output */ |
| struct log_options { |
| int show_diff; |
| int skip, limit; |
| int min_parents, max_parents; |
| git_time_t before; |
| git_time_t after; |
| const char *author; |
| const char *committer; |
| const char *grep; |
| }; |
| |
| /** utility functions that parse options and help with log output */ |
| static int parse_options( |
| struct log_state *s, struct log_options *opt, int argc, char **argv); |
| static void print_time(const git_time *intime, const char *prefix); |
| static void print_commit(git_commit *commit); |
| static int match_with_parent(git_commit *commit, int i, git_diff_options *); |
| |
| /** utility functions for filtering */ |
| static int signature_matches(const git_signature *sig, const char *filter); |
| static int log_message_matches(const git_commit *commit, const char *filter); |
| |
| int main(int argc, char *argv[]) |
| { |
| int i, count = 0, printed = 0, parents, last_arg; |
| struct log_state s; |
| struct log_options opt; |
| git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT; |
| git_oid oid; |
| git_commit *commit = NULL; |
| git_pathspec *ps = NULL; |
| |
| git_libgit2_init(); |
| |
| /** Parse arguments and set up revwalker. */ |
| |
| last_arg = parse_options(&s, &opt, argc, argv); |
| |
| diffopts.pathspec.strings = &argv[last_arg]; |
| diffopts.pathspec.count = argc - last_arg; |
| if (diffopts.pathspec.count > 0) |
| check_lg2(git_pathspec_new(&ps, &diffopts.pathspec), |
| "Building pathspec", NULL); |
| |
| if (!s.revisions) |
| add_revision(&s, NULL); |
| |
| /** Use the revwalker to traverse the history. */ |
| |
| printed = count = 0; |
| |
| for (; !git_revwalk_next(&oid, s.walker); git_commit_free(commit)) { |
| check_lg2(git_commit_lookup(&commit, s.repo, &oid), |
| "Failed to look up commit", NULL); |
| |
| parents = (int)git_commit_parentcount(commit); |
| if (parents < opt.min_parents) |
| continue; |
| if (opt.max_parents > 0 && parents > opt.max_parents) |
| continue; |
| |
| if (diffopts.pathspec.count > 0) { |
| int unmatched = parents; |
| |
| if (parents == 0) { |
| git_tree *tree; |
| check_lg2(git_commit_tree(&tree, commit), "Get tree", NULL); |
| if (git_pathspec_match_tree( |
| NULL, tree, GIT_PATHSPEC_NO_MATCH_ERROR, ps) != 0) |
| unmatched = 1; |
| git_tree_free(tree); |
| } else if (parents == 1) { |
| unmatched = match_with_parent(commit, 0, &diffopts) ? 0 : 1; |
| } else { |
| for (i = 0; i < parents; ++i) { |
| if (match_with_parent(commit, i, &diffopts)) |
| unmatched--; |
| } |
| } |
| |
| if (unmatched > 0) |
| continue; |
| } |
| |
| if (!signature_matches(git_commit_author(commit), opt.author)) |
| continue; |
| |
| if (!signature_matches(git_commit_committer(commit), opt.committer)) |
| continue; |
| |
| if (!log_message_matches(commit, opt.grep)) |
| continue; |
| |
| if (count++ < opt.skip) |
| continue; |
| if (opt.limit != -1 && printed++ >= opt.limit) { |
| git_commit_free(commit); |
| break; |
| } |
| |
| print_commit(commit); |
| |
| if (opt.show_diff) { |
| git_tree *a = NULL, *b = NULL; |
| git_diff *diff = NULL; |
| |
| if (parents > 1) |
| continue; |
| check_lg2(git_commit_tree(&b, commit), "Get tree", NULL); |
| if (parents == 1) { |
| git_commit *parent; |
| check_lg2(git_commit_parent(&parent, commit, 0), "Get parent", NULL); |
| check_lg2(git_commit_tree(&a, parent), "Tree for parent", NULL); |
| git_commit_free(parent); |
| } |
| |
| check_lg2(git_diff_tree_to_tree( |
| &diff, git_commit_owner(commit), a, b, &diffopts), |
| "Diff commit with parent", NULL); |
| check_lg2( |
| git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, diff_output, NULL), |
| "Displaying diff", NULL); |
| |
| git_diff_free(diff); |
| git_tree_free(a); |
| git_tree_free(b); |
| } |
| } |
| |
| git_pathspec_free(ps); |
| git_revwalk_free(s.walker); |
| git_repository_free(s.repo); |
| git_libgit2_shutdown(); |
| |
| return 0; |
| } |
| |
| /** Determine if the given git_signature does not contain the filter text. */ |
| static int signature_matches(const git_signature *sig, const char *filter) { |
| if (filter == NULL) |
| return 1; |
| |
| if (sig != NULL && |
| (strstr(sig->name, filter) != NULL || |
| strstr(sig->email, filter) != NULL)) |
| return 1; |
| |
| return 0; |
| } |
| |
| static int log_message_matches(const git_commit *commit, const char *filter) { |
| const char *message = NULL; |
| |
| if (filter == NULL) |
| return 1; |
| |
| if ((message = git_commit_message(commit)) != NULL && |
| strstr(message, filter) != NULL) |
| return 1; |
| |
| return 0; |
| } |
| |
| /** Push object (for hide or show) onto revwalker. */ |
| static void push_rev(struct log_state *s, git_object *obj, int hide) |
| { |
| hide = s->hide ^ hide; |
| |
| /** Create revwalker on demand if it doesn't already exist. */ |
| if (!s->walker) { |
| check_lg2(git_revwalk_new(&s->walker, s->repo), |
| "Could not create revision walker", NULL); |
| git_revwalk_sorting(s->walker, s->sorting); |
| } |
| |
| if (!obj) |
| check_lg2(git_revwalk_push_head(s->walker), |
| "Could not find repository HEAD", NULL); |
| else if (hide) |
| check_lg2(git_revwalk_hide(s->walker, git_object_id(obj)), |
| "Reference does not refer to a commit", NULL); |
| else |
| check_lg2(git_revwalk_push(s->walker, git_object_id(obj)), |
| "Reference does not refer to a commit", NULL); |
| |
| git_object_free(obj); |
| } |
| |
| /** Parse revision string and add revs to walker. */ |
| static int add_revision(struct log_state *s, const char *revstr) |
| { |
| git_revspec revs; |
| int hide = 0; |
| |
| /** Open repo on demand if it isn't already open. */ |
| if (!s->repo) { |
| if (!s->repodir) s->repodir = "."; |
| check_lg2(git_repository_open_ext(&s->repo, s->repodir, 0, NULL), |
| "Could not open repository", s->repodir); |
| } |
| |
| if (!revstr) { |
| push_rev(s, NULL, hide); |
| return 0; |
| } |
| |
| if (*revstr == '^') { |
| revs.flags = GIT_REVPARSE_SINGLE; |
| hide = !hide; |
| |
| if (git_revparse_single(&revs.from, s->repo, revstr + 1) < 0) |
| return -1; |
| } else if (git_revparse(&revs, s->repo, revstr) < 0) |
| return -1; |
| |
| if ((revs.flags & GIT_REVPARSE_SINGLE) != 0) |
| push_rev(s, revs.from, hide); |
| else { |
| push_rev(s, revs.to, hide); |
| |
| if ((revs.flags & GIT_REVPARSE_MERGE_BASE) != 0) { |
| git_oid base; |
| check_lg2(git_merge_base(&base, s->repo, |
| git_object_id(revs.from), git_object_id(revs.to)), |
| "Could not find merge base", revstr); |
| check_lg2( |
| git_object_lookup(&revs.to, s->repo, &base, GIT_OBJ_COMMIT), |
| "Could not find merge base commit", NULL); |
| |
| push_rev(s, revs.to, hide); |
| } |
| |
| push_rev(s, revs.from, !hide); |
| } |
| |
| return 0; |
| } |
| |
| /** Update revwalker with sorting mode. */ |
| static void set_sorting(struct log_state *s, unsigned int sort_mode) |
| { |
| /** Open repo on demand if it isn't already open. */ |
| if (!s->repo) { |
| if (!s->repodir) s->repodir = "."; |
| check_lg2(git_repository_open_ext(&s->repo, s->repodir, 0, NULL), |
| "Could not open repository", s->repodir); |
| } |
| |
| /** Create revwalker on demand if it doesn't already exist. */ |
| if (!s->walker) |
| check_lg2(git_revwalk_new(&s->walker, s->repo), |
| "Could not create revision walker", NULL); |
| |
| if (sort_mode == GIT_SORT_REVERSE) |
| s->sorting = s->sorting ^ GIT_SORT_REVERSE; |
| else |
| s->sorting = sort_mode | (s->sorting & GIT_SORT_REVERSE); |
| |
| git_revwalk_sorting(s->walker, s->sorting); |
| } |
| |
| /** Helper to format a git_time value like Git. */ |
| static void print_time(const git_time *intime, const char *prefix) |
| { |
| char sign, out[32]; |
| struct tm *intm; |
| int offset, hours, minutes; |
| time_t t; |
| |
| offset = intime->offset; |
| if (offset < 0) { |
| sign = '-'; |
| offset = -offset; |
| } else { |
| sign = '+'; |
| } |
| |
| hours = offset / 60; |
| minutes = offset % 60; |
| |
| t = (time_t)intime->time + (intime->offset * 60); |
| |
| intm = gmtime(&t); |
| strftime(out, sizeof(out), "%a %b %e %T %Y", intm); |
| |
| printf("%s%s %c%02d%02d\n", prefix, out, sign, hours, minutes); |
| } |
| |
| /** Helper to print a commit object. */ |
| static void print_commit(git_commit *commit) |
| { |
| char buf[GIT_OID_HEXSZ + 1]; |
| int i, count; |
| const git_signature *sig; |
| const char *scan, *eol; |
| |
| git_oid_tostr(buf, sizeof(buf), git_commit_id(commit)); |
| printf("commit %s\n", buf); |
| |
| if ((count = (int)git_commit_parentcount(commit)) > 1) { |
| printf("Merge:"); |
| for (i = 0; i < count; ++i) { |
| git_oid_tostr(buf, 8, git_commit_parent_id(commit, i)); |
| printf(" %s", buf); |
| } |
| printf("\n"); |
| } |
| |
| if ((sig = git_commit_author(commit)) != NULL) { |
| printf("Author: %s <%s>\n", sig->name, sig->email); |
| print_time(&sig->when, "Date: "); |
| } |
| printf("\n"); |
| |
| for (scan = git_commit_message(commit); scan && *scan; ) { |
| for (eol = scan; *eol && *eol != '\n'; ++eol) /* find eol */; |
| |
| printf(" %.*s\n", (int)(eol - scan), scan); |
| scan = *eol ? eol + 1 : NULL; |
| } |
| printf("\n"); |
| } |
| |
| /** Helper to find how many files in a commit changed from its nth parent. */ |
| static int match_with_parent(git_commit *commit, int i, git_diff_options *opts) |
| { |
| git_commit *parent; |
| git_tree *a, *b; |
| git_diff *diff; |
| int ndeltas; |
| |
| check_lg2( |
| git_commit_parent(&parent, commit, (size_t)i), "Get parent", NULL); |
| check_lg2(git_commit_tree(&a, parent), "Tree for parent", NULL); |
| check_lg2(git_commit_tree(&b, commit), "Tree for commit", NULL); |
| check_lg2( |
| git_diff_tree_to_tree(&diff, git_commit_owner(commit), a, b, opts), |
| "Checking diff between parent and commit", NULL); |
| |
| ndeltas = (int)git_diff_num_deltas(diff); |
| |
| git_diff_free(diff); |
| git_tree_free(a); |
| git_tree_free(b); |
| git_commit_free(parent); |
| |
| return ndeltas > 0; |
| } |
| |
| /** Print a usage message for the program. */ |
| static void usage(const char *message, const char *arg) |
| { |
| if (message && arg) |
| fprintf(stderr, "%s: %s\n", message, arg); |
| else if (message) |
| fprintf(stderr, "%s\n", message); |
| fprintf(stderr, "usage: log [<options>]\n"); |
| exit(1); |
| } |
| |
| /** Parse some log command line options. */ |
| static int parse_options( |
| struct log_state *s, struct log_options *opt, int argc, char **argv) |
| { |
| struct args_info args = ARGS_INFO_INIT; |
| |
| memset(s, 0, sizeof(*s)); |
| s->sorting = GIT_SORT_TIME; |
| |
| memset(opt, 0, sizeof(*opt)); |
| opt->max_parents = -1; |
| opt->limit = -1; |
| |
| for (args.pos = 1; args.pos < argc; ++args.pos) { |
| const char *a = argv[args.pos]; |
| |
| if (a[0] != '-') { |
| if (!add_revision(s, a)) |
| s->revisions++; |
| else |
| /** Try failed revision parse as filename. */ |
| break; |
| } else if (!strcmp(a, "--")) { |
| ++args.pos; |
| break; |
| } |
| else if (!strcmp(a, "--date-order")) |
| set_sorting(s, GIT_SORT_TIME); |
| else if (!strcmp(a, "--topo-order")) |
| set_sorting(s, GIT_SORT_TOPOLOGICAL); |
| else if (!strcmp(a, "--reverse")) |
| set_sorting(s, GIT_SORT_REVERSE); |
| else if (match_str_arg(&opt->author, &args, "--author")) |
| /** Found valid --author */; |
| else if (match_str_arg(&opt->committer, &args, "--committer")) |
| /** Found valid --committer */; |
| else if (match_str_arg(&opt->grep, &args, "--grep")) |
| /** Found valid --grep */; |
| else if (match_str_arg(&s->repodir, &args, "--git-dir")) |
| /** Found git-dir. */; |
| else if (match_int_arg(&opt->skip, &args, "--skip", 0)) |
| /** Found valid --skip. */; |
| else if (match_int_arg(&opt->limit, &args, "--max-count", 0)) |
| /** Found valid --max-count. */; |
| else if (a[1] >= '0' && a[1] <= '9') |
| is_integer(&opt->limit, a + 1, 0); |
| else if (match_int_arg(&opt->limit, &args, "-n", 0)) |
| /** Found valid -n. */; |
| else if (!strcmp(a, "--merges")) |
| opt->min_parents = 2; |
| else if (!strcmp(a, "--no-merges")) |
| opt->max_parents = 1; |
| else if (!strcmp(a, "--no-min-parents")) |
| opt->min_parents = 0; |
| else if (!strcmp(a, "--no-max-parents")) |
| opt->max_parents = -1; |
| else if (match_int_arg(&opt->max_parents, &args, "--max-parents=", 1)) |
| /** Found valid --max-parents. */; |
| else if (match_int_arg(&opt->min_parents, &args, "--min-parents=", 0)) |
| /** Found valid --min_parents. */; |
| else if (!strcmp(a, "-p") || !strcmp(a, "-u") || !strcmp(a, "--patch")) |
| opt->show_diff = 1; |
| else |
| usage("Unsupported argument", a); |
| } |
| |
| return args.pos; |
| } |
| |