diff options
Diffstat (limited to 'examples/blame.c')
-rw-r--r-- | examples/blame.c | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/examples/blame.c b/examples/blame.c new file mode 100644 index 0000000..77087a5 --- /dev/null +++ b/examples/blame.c @@ -0,0 +1,198 @@ +/* + * libgit2 "blame" example - shows how to use the blame API + * + * 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 how to invoke the libgit2 blame API to roughly + * simulate the output of `git blame` and a few of its command line arguments. + */ + +struct blame_opts { + char *path; + char *commitspec; + int C; + int M; + int start_line; + int end_line; + int F; +}; +static void parse_opts(struct blame_opts *o, int argc, char *argv[]); + +int lg2_blame(git_repository *repo, int argc, char *argv[]) +{ + int line, break_on_null_hunk; + git_object_size_t i, rawsize; + char spec[1024] = {0}; + struct blame_opts o = {0}; + const char *rawdata; + git_revspec revspec = {0}; + git_blame_options blameopts = GIT_BLAME_OPTIONS_INIT; + git_blame *blame = NULL; + git_blob *blob; + git_object *obj; + + parse_opts(&o, argc, argv); + if (o.M) blameopts.flags |= GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES; + if (o.C) blameopts.flags |= GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES; + if (o.F) blameopts.flags |= GIT_BLAME_FIRST_PARENT; + + /** + * The commit range comes in "committish" form. Use the rev-parse API to + * nail down the end points. + */ + if (o.commitspec) { + check_lg2(git_revparse(&revspec, repo, o.commitspec), "Couldn't parse commit spec", NULL); + if (revspec.flags & GIT_REVSPEC_SINGLE) { + git_oid_cpy(&blameopts.newest_commit, git_object_id(revspec.from)); + git_object_free(revspec.from); + } else { + git_oid_cpy(&blameopts.oldest_commit, git_object_id(revspec.from)); + git_oid_cpy(&blameopts.newest_commit, git_object_id(revspec.to)); + git_object_free(revspec.from); + git_object_free(revspec.to); + } + } + + /** Run the blame. */ + check_lg2(git_blame_file(&blame, repo, o.path, &blameopts), "Blame error", NULL); + + /** + * Get the raw data inside the blob for output. We use the + * `committish:path/to/file.txt` format to find it. + */ + if (git_oid_is_zero(&blameopts.newest_commit)) + strcpy(spec, "HEAD"); + else + git_oid_tostr(spec, sizeof(spec), &blameopts.newest_commit); + strcat(spec, ":"); + strcat(spec, o.path); + + check_lg2(git_revparse_single(&obj, repo, spec), "Object lookup error", NULL); + check_lg2(git_blob_lookup(&blob, repo, git_object_id(obj)), "Blob lookup error", NULL); + git_object_free(obj); + + rawdata = git_blob_rawcontent(blob); + rawsize = git_blob_rawsize(blob); + + /** Produce the output. */ + line = 1; + i = 0; + break_on_null_hunk = 0; + while (i < rawsize) { + const char *eol = memchr(rawdata + i, '\n', (size_t)(rawsize - i)); + char oid[10] = {0}; + const git_blame_hunk *hunk = git_blame_get_hunk_byline(blame, line); + + if (break_on_null_hunk && !hunk) + break; + + if (hunk) { + char sig[128] = {0}; + break_on_null_hunk = 1; + + git_oid_tostr(oid, 10, &hunk->final_commit_id); + snprintf(sig, 30, "%s <%s>", hunk->final_signature->name, hunk->final_signature->email); + + printf("%s ( %-30s %3d) %.*s\n", + oid, + sig, + line, + (int)(eol - rawdata - i), + rawdata + i); + } + + i = (int)(eol - rawdata + 1); + line++; + } + + /** Cleanup. */ + git_blob_free(blob); + git_blame_free(blame); + + return 0; +} + +/** Tell the user how to make this thing work. */ +static void usage(const char *msg, const char *arg) +{ + if (msg && arg) + fprintf(stderr, "%s: %s\n", msg, arg); + else if (msg) + fprintf(stderr, "%s\n", msg); + fprintf(stderr, "usage: blame [options] [<commit range>] <path>\n"); + fprintf(stderr, "\n"); + fprintf(stderr, " <commit range> example: `HEAD~10..HEAD`, or `1234abcd`\n"); + fprintf(stderr, " -L <n,m> process only line range n-m, counting from 1\n"); + fprintf(stderr, " -M find line moves within and across files\n"); + fprintf(stderr, " -C find line copies within and across files\n"); + fprintf(stderr, " -F follow only the first parent commits\n"); + fprintf(stderr, "\n"); + exit(1); +} + +/** Parse the arguments. */ +static void parse_opts(struct blame_opts *o, int argc, char *argv[]) +{ + int i; + char *bare_args[3] = {0}; + + if (argc < 2) usage(NULL, NULL); + + for (i=1; i<argc; i++) { + char *a = argv[i]; + + if (a[0] != '-') { + int i=0; + while (bare_args[i] && i < 3) ++i; + if (i >= 3) + usage("Invalid argument set", NULL); + bare_args[i] = a; + } + else if (!strcmp(a, "--")) + continue; + else if (!strcasecmp(a, "-M")) + o->M = 1; + else if (!strcasecmp(a, "-C")) + o->C = 1; + else if (!strcasecmp(a, "-F")) + o->F = 1; + else if (!strcasecmp(a, "-L")) { + i++; a = argv[i]; + if (i >= argc) fatal("Not enough arguments to -L", NULL); + check_lg2(sscanf(a, "%d,%d", &o->start_line, &o->end_line)-2, "-L format error", NULL); + } + else { + /* commit range */ + if (o->commitspec) fatal("Only one commit spec allowed", NULL); + o->commitspec = a; + } + } + + /* Handle the bare arguments */ + if (!bare_args[0]) usage("Please specify a path", NULL); + o->path = bare_args[0]; + if (bare_args[1]) { + /* <commitspec> <path> */ + o->path = bare_args[1]; + o->commitspec = bare_args[0]; + } + if (bare_args[2]) { + /* <oldcommit> <newcommit> <path> */ + char spec[128] = {0}; + o->path = bare_args[2]; + sprintf(spec, "%s..%s", bare_args[0], bare_args[1]); + o->commitspec = spec; + } +} |