1 /*
2  * libgit2 "diff" example - shows how to use the diff 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 the use of the libgit2 diff APIs to
16  * create `libgit2.diff.git_diff` objects and display them, emulating a number of
17  * core Git `diff` command line options.
18  *
19  * This covers on a portion of the core Git diff options and doesn't
20  * have particularly good error handling, but it should show most of
21  * the core libgit2 diff APIs, including various types of diffs and
22  * how to do renaming detection and patch formatting.
23  *
24  * License: $(LINK2 https://creativecommons.org/publicdomain/zero/1.0/, CC0 1.0 Universal)
25  */
26 module libgit2.example.diff;
27 
28 
29 private static import core.stdc.stdio;
30 private static import core.stdc.stdlib;
31 private static import core.stdc.string;
32 private static import libgit2.buffer;
33 private static import libgit2.diff;
34 private static import libgit2.example.args;
35 private static import libgit2.example.common;
36 private static import libgit2.patch;
37 private static import libgit2.tree;
38 private static import libgit2.types;
39 
40 private const (char)*[] colors =
41 [
42 	"\033[m", /* reset */
43 	"\033[1m", /* bold */
44 	"\033[31m", /* red */
45 	"\033[32m", /* green */
46 	"\033[36m", /* cyan */
47 ];
48 
49 public enum
50 {
51 	OUTPUT_DIFF = 1 << 0,
52 	OUTPUT_STAT = 1 << 1,
53 	OUTPUT_SHORTSTAT = 1 << 2,
54 	OUTPUT_NUMSTAT = 1 << 3,
55 	OUTPUT_SUMMARY = 1 << 4,
56 }
57 
58 public enum
59 {
60 	CACHE_NORMAL = 0,
61 	CACHE_ONLY = 1,
62 	CACHE_NONE = 2,
63 }
64 
65 /**
66  * The 'diff_options' struct captures all the various parsed command line options.
67  */
68 extern (C)
69 public struct diff_options
70 {
71 	libgit2.diff.git_diff_options diffopts;
72 	libgit2.diff.git_diff_find_options findopts;
73 	int color;
74 	int no_index;
75 	int cache;
76 	int output;
77 	libgit2.diff.git_diff_format_t format = cast(libgit2.diff.git_diff_format_t)(0);
78 	const (char)* treeish1;
79 	const (char)* treeish2;
80 	const (char)* dir;
81 }
82 
83 /*
84  * These functions are implemented at the end
85  *
86  * - usage()
87  * - parse_opts()
88  * - color_printer()
89  * - diff_print_stats()
90  * - compute_diff_no_index()
91  */
92 
93 extern (C)
94 nothrow @nogc
95 public int lg2_diff(libgit2.types.git_repository* repo, int argc, char** argv)
96 
97 	in
98 	{
99 	}
100 
101 	do
102 	{
103 		libgit2.types.git_tree* t1 = null;
104 		libgit2.types.git_tree* t2 = null;
105 		libgit2.diff.git_diff* diff;
106 
107 		.diff_options o = {libgit2.diff.GIT_DIFF_OPTIONS_INIT(), libgit2.diff.GIT_DIFF_FIND_OPTIONS_INIT(), -1, -1, 0, 0, libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_PATCH, null, null, "."};
108 
109 		.parse_opts(&o, argc, argv);
110 
111 		/**
112 		 * Possible argument patterns:
113 		 *
114 		 *  * &lt;sha1&gt; &lt;sha2&gt;
115 		 *  * &lt;sha1&gt; --cached
116 		 *  * &lt;sha1&gt;
117 		 *  * --cached
118 		 *  * --nocache (don't use index data in diff at all)
119 		 *  * --no-index &lt;file1&gt; &lt;file2&gt;
120 		 *  * nothing
121 		 *
122 		 * Currently ranged arguments like &lt;sha1&gt;..&lt;sha2&gt; and &lt;sha1&gt;...&lt;sha2&gt;
123 		 * are not supported in this example
124 		 */
125 
126 		if (o.no_index >= 0) {
127 			.compute_diff_no_index(&diff, &o);
128 		} else {
129 			if (o.treeish1 != null) {
130 				libgit2.example.common.treeish_to_tree(&t1, repo, o.treeish1);
131 			}
132 
133 			if (o.treeish2 != null) {
134 				libgit2.example.common.treeish_to_tree(&t2, repo, o.treeish2);
135 			}
136 
137 			if ((t1 != null) && (t2 != null)) {
138 				libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_tree(&diff, repo, t1, t2, &o.diffopts), "diff trees", null);
139 			} else if (o.cache != .CACHE_NORMAL) {
140 				if (t1 == null) {
141 					libgit2.example.common.treeish_to_tree(&t1, repo, "HEAD");
142 				}
143 
144 				if (o.cache == .CACHE_NONE) {
145 					libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_workdir(&diff, repo, t1, &o.diffopts), "diff tree to working directory", null);
146 				} else {
147 					libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_index(&diff, repo, t1, null, &o.diffopts), "diff tree to index", null);
148 				}
149 			} else if (t1 != null) {
150 				libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_workdir_with_index(&diff, repo, t1, &o.diffopts), "diff tree to working directory", null);
151 			} else {
152 				libgit2.example.common.check_lg2(libgit2.diff.git_diff_index_to_workdir(&diff, repo, null, &o.diffopts), "diff index to working directory", null);
153 			}
154 
155 			/** Apply rename and copy detection if requested. */
156 
157 			if ((o.findopts.flags & libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_ALL) != 0) {
158 				libgit2.example.common.check_lg2(libgit2.diff.git_diff_find_similar(diff, &o.findopts), "finding renames and copies", null);
159 			}
160 		}
161 
162 		/** Generate simple output using libgit2 display helper. */
163 
164 		if (!o.output) {
165 			o.output = .OUTPUT_DIFF;
166 		}
167 
168 		if (o.output != .OUTPUT_DIFF) {
169 			.diff_print_stats(diff, &o);
170 		}
171 
172 		if ((o.output & .OUTPUT_DIFF) != 0) {
173 			if (o.color >= 0) {
174 				core.stdc.stdio.fputs(.colors[0], core.stdc.stdio.stdout);
175 			}
176 
177 			libgit2.example.common.check_lg2(libgit2.diff.git_diff_print(diff, o.format, &.color_printer, &o.color), "displaying diff", null);
178 
179 			if (o.color >= 0) {
180 				core.stdc.stdio.fputs(.colors[0], core.stdc.stdio.stdout);
181 			}
182 		}
183 
184 		/** Cleanup before exiting. */
185 		libgit2.diff.git_diff_free(diff);
186 		libgit2.tree.git_tree_free(t1);
187 		libgit2.tree.git_tree_free(t2);
188 
189 		return 0;
190 	}
191 
192 nothrow @nogc
193 private void compute_diff_no_index(libgit2.diff.git_diff** diff, .diff_options* o)
194 
195 	in
196 	{
197 	}
198 
199 	do
200 	{
201 		if ((!o.treeish1) || (!o.treeish2)) {
202 			.usage("two files should be provided as arguments", null);
203 		}
204 
205 		char* file1_str = libgit2.example.common.read_file(o.treeish1);
206 
207 		if (file1_str == null) {
208 			.usage("file cannot be read", o.treeish1);
209 		}
210 
211 		char* file2_str = libgit2.example.common.read_file(o.treeish2);
212 
213 		if (file2_str == null) {
214 			.usage("file cannot be read", o.treeish2);
215 		}
216 
217 		libgit2.patch.git_patch* patch = null;
218 		libgit2.example.common.check_lg2(libgit2.patch.git_patch_from_buffers(&patch, file1_str, core.stdc..string.strlen(file1_str), o.treeish1, file2_str, core.stdc..string.strlen(file2_str), o.treeish2, &o.diffopts), "patch buffers", null);
219 		libgit2.buffer.git_buf buf = libgit2.buffer.git_buf.init;
220 		libgit2.example.common.check_lg2(libgit2.patch.git_patch_to_buf(&buf, patch), "patch to buf", null);
221 		libgit2.example.common.check_lg2(libgit2.diff.git_diff_from_buffer(diff, buf.ptr_, buf.size), "diff from patch", null);
222 		libgit2.patch.git_patch_free(patch);
223 		libgit2.buffer.git_buf_dispose(&buf);
224 		core.stdc.stdlib.free(file1_str);
225 		core.stdc.stdlib.free(file2_str);
226 	}
227 
228 nothrow @nogc
229 private void usage(const (char)* message, const (char)* arg)
230 
231 	in
232 	{
233 	}
234 
235 	do
236 	{
237 		if ((message != null) && (arg != null)) {
238 			core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s: %s\n", message, arg);
239 		} else if (message != null) {
240 			core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s\n", message);
241 		}
242 
243 		core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "usage: diff [<tree-oid> [<tree-oid>]]\n");
244 		core.stdc.stdlib.exit(1);
245 	}
246 
247 /**
248  * This implements very rudimentary colorized output.
249  */
250 extern (C)
251 nothrow @nogc
252 private int color_printer(const (libgit2.diff.git_diff_delta)* delta, const (libgit2.diff.git_diff_hunk)* hunk, const (libgit2.diff.git_diff_line)* line, void* data)
253 
254 	in
255 	{
256 	}
257 
258 	do
259 	{
260 		int* last_color = cast(int*)(data);
261 		int color = 0;
262 
263 		//cast(void)(delta);
264 		//cast(void)(hunk);
265 
266 		if (*last_color >= 0) {
267 			switch (line.origin) {
268 				case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_ADDITION:
269 					color = 3;
270 
271 					break;
272 
273 				case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_DELETION:
274 					color = 2;
275 
276 					break;
277 
278 				case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_ADD_EOFNL:
279 					color = 3;
280 
281 					break;
282 
283 				case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_DEL_EOFNL:
284 					color = 2;
285 
286 					break;
287 
288 				case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_FILE_HDR:
289 					color = 1;
290 
291 					break;
292 
293 				case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_HUNK_HDR:
294 					color = 4;
295 
296 					break;
297 
298 				default:
299 					break;
300 			}
301 
302 			if (color != *last_color) {
303 				if ((*last_color == 1) || (color == 1)) {
304 					core.stdc.stdio.fputs(.colors[0], core.stdc.stdio.stdout);
305 				}
306 
307 				core.stdc.stdio.fputs(.colors[color], core.stdc.stdio.stdout);
308 				*last_color = color;
309 			}
310 		}
311 
312 		return libgit2.example.common.diff_output(delta, hunk, line, cast(void*)(core.stdc.stdio.stdout));
313 	}
314 
315 /**
316  * Parse arguments as copied from git-diff.
317  */
318 nothrow @nogc
319 private void parse_opts(.diff_options* o, int argc, char** argv)
320 
321 	in
322 	{
323 	}
324 
325 	do
326 	{
327 		libgit2.example.args.args_info args = libgit2.example.args.ARGS_INFO_INIT(argc, argv);
328 
329 		for (args.pos = 1; args.pos < argc; ++args.pos) {
330 			const (char)* a = argv[args.pos];
331 
332 			if (a[0] != '-') {
333 				if (o.treeish1 == null) {
334 					o.treeish1 = a;
335 				} else if (o.treeish2 == null) {
336 					o.treeish2 = a;
337 				} else {
338 					.usage("Only one or two tree identifiers can be provided", null);
339 				}
340 			} else if ((!core.stdc..string.strcmp(a, "-p")) || (!core.stdc..string.strcmp(a, "-u")) || (!core.stdc..string.strcmp(a, "--patch"))) {
341 				o.output |= .OUTPUT_DIFF;
342 				o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_PATCH;
343 			} else if (!core.stdc..string.strcmp(a, "--cached")) {
344 				o.cache = .CACHE_ONLY;
345 
346 				if (o.no_index >= 0) {
347 					.usage("--cached and --no-index are incompatible", null);
348 				}
349 			} else if (!core.stdc..string.strcmp(a, "--nocache")) {
350 				o.cache = .CACHE_NONE;
351 			} else if ((!core.stdc..string.strcmp(a, "--name-only")) || (!core.stdc..string.strcmp(a, "--format=name"))) {
352 				o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_NAME_ONLY;
353 			} else if ((!core.stdc..string.strcmp(a, "--name-status")) || (!core.stdc..string.strcmp(a, "--format=name-status"))) {
354 				o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_NAME_STATUS;
355 			} else if ((!core.stdc..string.strcmp(a, "--raw")) || (!core.stdc..string.strcmp(a, "--format=raw"))) {
356 				o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_RAW;
357 			} else if (!core.stdc..string.strcmp(a, "--format=diff-index")) {
358 				o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_RAW;
359 				o.diffopts.id_abbrev = 40;
360 			} else if (!core.stdc..string.strcmp(a, "--no-index")) {
361 				o.no_index = 0;
362 
363 				if (o.cache == .CACHE_ONLY) {
364 					.usage("--cached and --no-index are incompatible", null);
365 				}
366 			} else if (!core.stdc..string.strcmp(a, "--color")) {
367 				o.color = 0;
368 			} else if (!core.stdc..string.strcmp(a, "--no-color")) {
369 				o.color = -1;
370 			} else if (!core.stdc..string.strcmp(a, "-R")) {
371 				o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_REVERSE;
372 			} else if ((!core.stdc..string.strcmp(a, "-a")) || (!core.stdc..string.strcmp(a, "--text"))) {
373 				o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_FORCE_TEXT;
374 			} else if (!core.stdc..string.strcmp(a, "--ignore-space-at-eol")) {
375 				o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_IGNORE_WHITESPACE_EOL;
376 			} else if ((!core.stdc..string.strcmp(a, "-b")) || (!core.stdc..string.strcmp(a, "--ignore-space-change"))) {
377 				o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_IGNORE_WHITESPACE_CHANGE;
378 			} else if ((!core.stdc..string.strcmp(a, "-w")) || (!core.stdc..string.strcmp(a, "--ignore-all-space"))) {
379 				o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_IGNORE_WHITESPACE;
380 			} else if (!core.stdc..string.strcmp(a, "--ignored")) {
381 				o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_INCLUDE_IGNORED;
382 			} else if (!core.stdc..string.strcmp(a, "--untracked")) {
383 				o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_INCLUDE_UNTRACKED;
384 			} else if (!core.stdc..string.strcmp(a, "--patience")) {
385 				o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_PATIENCE;
386 			} else if (!core.stdc..string.strcmp(a, "--minimal")) {
387 				o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_MINIMAL;
388 			} else if (!core.stdc..string.strcmp(a, "--stat")) {
389 				o.output |= .OUTPUT_STAT;
390 			} else if (!core.stdc..string.strcmp(a, "--numstat")) {
391 				o.output |= .OUTPUT_NUMSTAT;
392 			} else if (!core.stdc..string.strcmp(a, "--shortstat")) {
393 				o.output |= .OUTPUT_SHORTSTAT;
394 			} else if (!core.stdc..string.strcmp(a, "--summary")) {
395 				o.output |= .OUTPUT_SUMMARY;
396 			} else if (libgit2.example.args.match_uint16_arg(&o.findopts.rename_threshold, &args, "-M") || libgit2.example.args.match_uint16_arg(&o.findopts.rename_threshold, &args, "--find-renames")) {
397 				o.findopts.flags |= libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_RENAMES;
398 			} else if (libgit2.example.args.match_uint16_arg(&o.findopts.copy_threshold, &args, "-C") || libgit2.example.args.match_uint16_arg(&o.findopts.copy_threshold, &args, "--find-copies")) {
399 				o.findopts.flags |= libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_COPIES;
400 			} else if (!core.stdc..string.strcmp(a, "--find-copies-harder")) {
401 				o.findopts.flags |= libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED;
402 			} else if (libgit2.example.args.is_prefixed(a, "-B") || libgit2.example.args.is_prefixed(a, "--break-rewrites")) {
403 				/* TODO: parse thresholds */
404 				o.findopts.flags |= libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_REWRITES;
405 			} else if ((!libgit2.example.args.match_uint32_arg(&o.diffopts.context_lines, &args, "-U")) && (!libgit2.example.args.match_uint32_arg(&o.diffopts.context_lines, &args, "--unified")) && (!libgit2.example.args.match_uint32_arg(&o.diffopts.interhunk_lines, &args, "--inter-hunk-context")) && !libgit2.example.args.match_uint16_arg(&o.diffopts.id_abbrev, &args, "--abbrev") && !libgit2.example.args.match_str_arg(&o.diffopts.old_prefix, &args, "--src-prefix") && (!libgit2.example.args.match_str_arg(&o.diffopts.new_prefix, &args, "--dst-prefix")) && (!libgit2.example.args.match_str_arg(&o.dir, &args, "--git-dir"))) {
406 				.usage("Unknown command line argument", a);
407 			}
408 		}
409 	}
410 
411 /**
412  * Display diff output with "--stat", "--numstat", or "--shortstat"
413  */
414 nothrow @nogc
415 private void diff_print_stats(libgit2.diff.git_diff* diff, .diff_options* o)
416 
417 	in
418 	{
419 	}
420 
421 	do
422 	{
423 		libgit2.diff.git_diff_stats* stats;
424 		libgit2.example.common.check_lg2(libgit2.diff.git_diff_get_stats(&stats, diff), "generating stats for diff", null);
425 
426 		libgit2.diff.git_diff_stats_format_t format = libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_NONE;
427 
428 		if (o.output & .OUTPUT_STAT) {
429 			format |= libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_FULL;
430 		}
431 
432 		if (o.output & .OUTPUT_SHORTSTAT) {
433 			format |= libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_SHORT;
434 		}
435 
436 		if (o.output & .OUTPUT_NUMSTAT) {
437 			format |= libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_NUMBER;
438 		}
439 
440 		if (o.output & .OUTPUT_SUMMARY) {
441 			format |= libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_INCLUDE_SUMMARY;
442 		}
443 
444 		libgit2.buffer.git_buf b;
445 		libgit2.example.common.check_lg2(libgit2.diff.git_diff_stats_to_buf(&b, stats, format, 80), "formatting stats", null);
446 
447 		b = libgit2.buffer.GIT_BUF_INIT();
448 		core.stdc.stdio.fputs(b.ptr_, core.stdc.stdio.stdout);
449 
450 		libgit2.buffer.git_buf_dispose(&b);
451 		libgit2.diff.git_diff_stats_free(stats);
452 	}