1 /*
2  * libgit2 "blame" example - shows how to use the blame API
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 
15 #include "common.h"
16 
17 /**
18  * This example demonstrates how to invoke the libgit2 blame API to roughly
19  * simulate the output of `git blame` and a few of its command line arguments.
20  */
21 
22 struct blame_opts {
23 	char *path;
24 	char *commitspec;
25 	int C;
26 	int M;
27 	int start_line;
28 	int end_line;
29 	int F;
30 };
31 static void parse_opts(struct blame_opts *o, int argc, char *argv[]);
32 
lg2_blame(git_repository * repo,int argc,char * argv[])33 int lg2_blame(git_repository *repo, int argc, char *argv[])
34 {
35 	int line, break_on_null_hunk;
36 	git_object_size_t i, rawsize;
37 	char spec[1024] = {0};
38 	struct blame_opts o = {0};
39 	const char *rawdata;
40 	git_revspec revspec = {0};
41 	git_blame_options blameopts = GIT_BLAME_OPTIONS_INIT;
42 	git_blame *blame = NULL;
43 	git_blob *blob;
44 	git_object *obj;
45 
46 	parse_opts(&o, argc, argv);
47 	if (o.M) blameopts.flags |= GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES;
48 	if (o.C) blameopts.flags |= GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES;
49 	if (o.F) blameopts.flags |= GIT_BLAME_FIRST_PARENT;
50 
51 	/**
52 	 * The commit range comes in "commitish" form. Use the rev-parse API to
53 	 * nail down the end points.
54 	 */
55 	if (o.commitspec) {
56 		check_lg2(git_revparse(&revspec, repo, o.commitspec), "Couldn't parse commit spec", NULL);
57 		if (revspec.flags & GIT_REVPARSE_SINGLE) {
58 			git_oid_cpy(&blameopts.newest_commit, git_object_id(revspec.from));
59 			git_object_free(revspec.from);
60 		} else {
61 			git_oid_cpy(&blameopts.oldest_commit, git_object_id(revspec.from));
62 			git_oid_cpy(&blameopts.newest_commit, git_object_id(revspec.to));
63 			git_object_free(revspec.from);
64 			git_object_free(revspec.to);
65 		}
66 	}
67 
68 	/** Run the blame. */
69 	check_lg2(git_blame_file(&blame, repo, o.path, &blameopts), "Blame error", NULL);
70 
71 	/**
72 	 * Get the raw data inside the blob for output. We use the
73 	 * `commitish:path/to/file.txt` format to find it.
74 	 */
75 	if (git_oid_is_zero(&blameopts.newest_commit))
76 		strcpy(spec, "HEAD");
77 	else
78 		git_oid_tostr(spec, sizeof(spec), &blameopts.newest_commit);
79 	strcat(spec, ":");
80 	strcat(spec, o.path);
81 
82 	check_lg2(git_revparse_single(&obj, repo, spec), "Object lookup error", NULL);
83 	check_lg2(git_blob_lookup(&blob, repo, git_object_id(obj)), "Blob lookup error", NULL);
84 	git_object_free(obj);
85 
86 	rawdata = git_blob_rawcontent(blob);
87 	rawsize = git_blob_rawsize(blob);
88 
89 	/** Produce the output. */
90 	line = 1;
91 	i = 0;
92 	break_on_null_hunk = 0;
93 	while (i < rawsize) {
94 		const char *eol = memchr(rawdata + i, '\n', (size_t)(rawsize - i));
95 		char oid[10] = {0};
96 		const git_blame_hunk *hunk = git_blame_get_hunk_byline(blame, line);
97 
98 		if (break_on_null_hunk && !hunk)
99 			break;
100 
101 		if (hunk) {
102 			char sig[128] = {0};
103 			break_on_null_hunk = 1;
104 
105 			git_oid_tostr(oid, 10, &hunk->final_commit_id);
106 			snprintf(sig, 30, "%s <%s>", hunk->final_signature->name, hunk->final_signature->email);
107 
108 			printf("%s ( %-30s %3d) %.*s\n",
109 					oid,
110 					sig,
111 					line,
112 					(int)(eol - rawdata - i),
113 					rawdata + i);
114 		}
115 
116 		i = (int)(eol - rawdata + 1);
117 		line++;
118 	}
119 
120 	/** Cleanup. */
121 	git_blob_free(blob);
122 	git_blame_free(blame);
123 
124 	return 0;
125 }
126 
127 /** Tell the user how to make this thing work. */
usage(const char * msg,const char * arg)128 static void usage(const char *msg, const char *arg)
129 {
130 	if (msg && arg)
131 		fprintf(stderr, "%s: %s\n", msg, arg);
132 	else if (msg)
133 		fprintf(stderr, "%s\n", msg);
134 	fprintf(stderr, "usage: blame [options] [<commit range>] <path>\n");
135 	fprintf(stderr, "\n");
136 	fprintf(stderr, "   <commit range>      example: `HEAD~10..HEAD`, or `1234abcd`\n");
137 	fprintf(stderr, "   -L <n,m>            process only line range n-m, counting from 1\n");
138 	fprintf(stderr, "   -M                  find line moves within and across files\n");
139 	fprintf(stderr, "   -C                  find line copies within and across files\n");
140 	fprintf(stderr, "   -F                  follow only the first parent commits\n");
141 	fprintf(stderr, "\n");
142 	exit(1);
143 }
144 
145 /** Parse the arguments. */
parse_opts(struct blame_opts * o,int argc,char * argv[])146 static void parse_opts(struct blame_opts *o, int argc, char *argv[])
147 {
148 	int i;
149 	char *bare_args[3] = {0};
150 
151 	if (argc < 2) usage(NULL, NULL);
152 
153 	for (i=1; i<argc; i++) {
154 		char *a = argv[i];
155 
156 		if (a[0] != '-') {
157 			int i=0;
158 			while (bare_args[i] && i < 3) ++i;
159 			if (i >= 3)
160 				usage("Invalid argument set", NULL);
161 			bare_args[i] = a;
162 		}
163 		else if (!strcmp(a, "--"))
164 			continue;
165 		else if (!strcasecmp(a, "-M"))
166 			o->M = 1;
167 		else if (!strcasecmp(a, "-C"))
168 			o->C = 1;
169 		else if (!strcasecmp(a, "-F"))
170 			o->F = 1;
171 		else if (!strcasecmp(a, "-L")) {
172 			i++; a = argv[i];
173 			if (i >= argc) fatal("Not enough arguments to -L", NULL);
174 			check_lg2(sscanf(a, "%d,%d", &o->start_line, &o->end_line)-2, "-L format error", NULL);
175 		}
176 		else {
177 			/* commit range */
178 			if (o->commitspec) fatal("Only one commit spec allowed", NULL);
179 			o->commitspec = a;
180 		}
181 	}
182 
183 	/* Handle the bare arguments */
184 	if (!bare_args[0]) usage("Please specify a path", NULL);
185 	o->path = bare_args[0];
186 	if (bare_args[1]) {
187 		/* <commitspec> <path> */
188 		o->path = bare_args[1];
189 		o->commitspec = bare_args[0];
190 	}
191 	if (bare_args[2]) {
192 		/* <oldcommit> <newcommit> <path> */
193 		char spec[128] = {0};
194 		o->path = bare_args[2];
195 		sprintf(spec, "%s..%s", bare_args[0], bare_args[1]);
196 		o->commitspec = spec;
197 	}
198 }
199