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