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