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  * <https://creativecommons.org/publicdomain/zero/1.0/>.
13  */
14 /**
15  * This example demonstrates how to invoke the libgit2 blame API to roughly
16  * simulate the output of `git blame` and a few of its command line arguments.
17  *
18  * License: $(LINK2 https://creativecommons.org/publicdomain/zero/1.0/, CC0 1.0 Universal)
19  */
20 module libgit2.example.blame;
21 
22 
23 private static import core.stdc.stdio;
24 private static import core.stdc.stdlib;
25 private static import core.stdc.string;
26 private static import libgit2.blame;
27 private static import libgit2.blob;
28 private static import libgit2.example.common;
29 private static import libgit2.object;
30 private static import libgit2.oid;
31 private static import libgit2.revparse;
32 private static import libgit2.types;
33 private static import std.ascii;
34 
35 extern (C)
36 public struct blame_opts
37 {
38 	char* path;
39 	char* commitspec;
40 	int C;
41 	int M;
42 	int start_line;
43 	int end_line;
44 	int F;
45 }
46 
47 extern (C)
48 nothrow @nogc
49 public int lg2_blame(libgit2.types.git_repository* repo, int argc, char** argv)
50 
51 	in
52 	{
53 	}
54 
55 	do
56 	{
57 		.blame_opts o = .blame_opts.init;
58 		.parse_opts(&o, argc, argv);
59 
60 		libgit2.blame.git_blame_options blameopts = libgit2.blame.GIT_BLAME_OPTIONS_INIT();
61 
62 		if (o.M) {
63 			blameopts.flags |= libgit2.blame.git_blame_flag_t.GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES;
64 		}
65 
66 		if (o.C) {
67 			blameopts.flags |= libgit2.blame.git_blame_flag_t.GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES;
68 		}
69 
70 		if (o.F) {
71 			blameopts.flags |= libgit2.blame.git_blame_flag_t.GIT_BLAME_FIRST_PARENT;
72 		}
73 
74 		libgit2.revparse.git_revspec revspec = libgit2.revparse.git_revspec.init;
75 
76 		/**
77 		 * The commit range comes in "committish" form. Use the rev-parse API to
78 		 * nail down the end points.
79 		 */
80 		if (o.commitspec != null) {
81 			libgit2.example.common.check_lg2(libgit2.revparse.git_revparse(&revspec, repo, o.commitspec), "Couldn't parse commit spec", null);
82 
83 			if (revspec.flags & libgit2.revparse.git_revspec_t.GIT_REVSPEC_SINGLE) {
84 				libgit2.oid.git_oid_cpy(&blameopts.newest_commit, libgit2.object.git_object_id(revspec.from));
85 				libgit2.object.git_object_free(revspec.from);
86 			} else {
87 				libgit2.oid.git_oid_cpy(&blameopts.oldest_commit, libgit2.object.git_object_id(revspec.from));
88 				libgit2.oid.git_oid_cpy(&blameopts.newest_commit, libgit2.object.git_object_id(revspec.to));
89 				libgit2.object.git_object_free(revspec.from);
90 				libgit2.object.git_object_free(revspec.to);
91 			}
92 		}
93 
94 		/** Run the blame. */
95 		libgit2.blame.git_blame* blame = null;
96 		libgit2.example.common.check_lg2(libgit2.blame.git_blame_file(&blame, repo, o.path, &blameopts), "Blame error", null);
97 
98 		char[1024] spec  = '\0';
99 
100 		/**
101 		 * Get the raw data inside the blob for output. We use the
102 		 * `committish:path/to/file.txt` format to find it.
103 		 */
104 		if (libgit2.oid.git_oid_is_zero(&blameopts.newest_commit)) {
105 			core.stdc..string.strcpy(&(spec[0]), "HEAD");
106 		} else {
107 			libgit2.oid.git_oid_tostr(&(spec[0]), spec.length, &blameopts.newest_commit);
108 		}
109 
110 		core.stdc..string.strcat(&(spec[0]), ":");
111 		core.stdc..string.strcat(&(spec[0]), o.path);
112 
113 		libgit2.types.git_object* obj;
114 		libgit2.example.common.check_lg2(libgit2.revparse.git_revparse_single(&obj, repo, &(spec[0])), "Object lookup error", null);
115 		libgit2.types.git_blob* blob;
116 		libgit2.example.common.check_lg2(libgit2.blob.git_blob_lookup(&blob, repo, libgit2.object.git_object_id(obj)), "Blob lookup error", null);
117 		libgit2.object.git_object_free(obj);
118 
119 		const char* rawdata = cast(const char*)(libgit2.blob.git_blob_rawcontent(blob));
120 		libgit2.types.git_object_size_t rawsize = libgit2.blob.git_blob_rawsize(blob);
121 
122 		/** Produce the output. */
123 		int line = 1;
124 		libgit2.types.git_object_size_t i = 0;
125 		int break_on_null_hunk = 0;
126 
127 		while (i < rawsize) {
128 			const char* eol = cast(const char*)(core.stdc..string.memchr(rawdata + i, '\n', cast(size_t)(rawsize - i)));
129 			char[10] oid  = '\0';
130 			const (libgit2.blame.git_blame_hunk)* hunk = libgit2.blame.git_blame_get_hunk_byline(blame, line);
131 
132 			if ((break_on_null_hunk) && (!hunk)) {
133 				break;
134 			}
135 
136 			if (hunk != null) {
137 				char[128] sig  = '\0';
138 				break_on_null_hunk = 1;
139 
140 				libgit2.oid.git_oid_tostr(&(oid[0]), 10, &hunk.final_commit_id);
141 				libgit2.example.common.snprintf(&(sig[0]), 30, "%s <%s>", hunk.final_signature.name, hunk.final_signature.email);
142 
143 				core.stdc.stdio.printf("%s ( %-30s %3d) %.*s\n", &(oid[0]), &(sig[0]), line, cast(int)(eol - rawdata - i), rawdata + i);
144 			}
145 
146 			i = cast(int)(eol - rawdata + 1);
147 			line++;
148 		}
149 
150 		/** Cleanup. */
151 		libgit2.blob.git_blob_free(blob);
152 		libgit2.blame.git_blame_free(blame);
153 
154 		return 0;
155 	}
156 
157 /**
158  * Tell the user how to make this thing work.
159  */
160 nothrow @nogc
161 private void usage(const (char)* msg, const (char)* arg)
162 
163 	in
164 	{
165 	}
166 
167 	do
168 	{
169 		if ((msg != null) && (arg != null)) {
170 			core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s: %s\n", msg, arg);
171 		} else if (msg != null) {
172 			core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s\n", msg);
173 		}
174 
175 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "usage: blame [options] [<commit range>] <path>\n");
176 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "\n");
177 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "   <commit range>      example: `HEAD~10..HEAD`, or `1234abcd`\n");
178 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "   -L <n,m>            process only line range n-m, counting from 1\n");
179 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "   -M                  find line moves within and across files\n");
180 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "   -C                  find line copies within and across files\n");
181 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "   -F                  follow only the first parent commits\n");
182 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "\n");
183 		core.stdc.stdlib.exit(1);
184 	}
185 
186 pragma(inline, true)
187 pure nothrow @trusted @nogc
188 private bool is_option(const char* input, char c1, char c2)
189 
190 	in
191 	{
192 		assert(input != null);
193 		assert(c1 != c2);
194 		assert(std.ascii.isUpper(c1));
195 		assert(std.ascii.isLower(c2));
196 		assert(c1 == std.ascii.toUpper(c2));
197 	}
198 
199 	do
200 	{
201 		return (*input == '-') && ((*(input + 1) == c1) || (*(input + 1) == c2));
202 	}
203 
204 /**
205  * Parse the arguments.
206  */
207 nothrow @nogc
208 private void parse_opts(.blame_opts* o, int argc, char** argv)
209 
210 	in
211 	{
212 	}
213 
214 	do
215 	{
216 		char*[3] bare_args = null;
217 
218 		if (argc < 2) {
219 			.usage(null, null);
220 		}
221 
222 		for (int i = 1; i < argc; i++) {
223 			char* a = argv[i];
224 
225 			if (a[0] != '-') {
226 				i = 0;
227 
228 				while ((bare_args[i]) && (i < 3)) {
229 					++i;
230 				}
231 
232 				if (i >= 3) {
233 					.usage("Invalid argument set", null);
234 				}
235 
236 				bare_args[i] = a;
237 			} else if (!core.stdc..string.strcmp(a, "--")) {
238 				continue;
239 			} else if (.is_option(a, 'M', 'm')) {
240 				o.M = 1;
241 			} else if (.is_option(a, 'C', 'c')) {
242 				o.C = 1;
243 			} else if (.is_option(a, 'F', 'f')) {
244 				o.F = 1;
245 			} else if (.is_option(a, 'L', 'l')) {
246 				i++;
247 				a = argv[i];
248 
249 				if (i >= argc) {
250 					libgit2.example.common.fatal("Not enough arguments to -L", null);
251 				}
252 
253 				libgit2.example.common.check_lg2(core.stdc.stdio.sscanf(a, "%d,%d", &o.start_line, &o.end_line) - 2, "-L format error", null);
254 			} else {
255 				/* commit range */
256 				if (o.commitspec) {
257 					libgit2.example.common.fatal("Only one commit spec allowed", null);
258 				}
259 
260 				o.commitspec = a;
261 			}
262 		}
263 
264 		/* Handle the bare arguments */
265 		if (!bare_args[0]) {
266 			.usage("Please specify a path", null);
267 		}
268 
269 		o.path = bare_args[0];
270 
271 		if (bare_args[1]) {
272 			/* <commitspec> <path> */
273 			o.path = bare_args[1];
274 			o.commitspec = bare_args[0];
275 		}
276 
277 		if (bare_args[2]) {
278 			/* <oldcommit> <newcommit> <path> */
279 			char[128] spec  = '\0';
280 			o.path = bare_args[2];
281 			core.stdc.stdio.sprintf(&(spec[0]), "%s..%s", bare_args[0], bare_args[1]);
282 			o.commitspec = &(spec[0]);
283 		}
284 	}