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