1 /*
2  * Copyright (C) the libgit2 contributors. All rights reserved.
3  *
4  * This file is part of libgit2, distributed under the GNU GPL v2 with
5  * a Linking Exception. For full terms see the included COPYING file.
6  */
7 
8 #include "diff.h"
9 
10 #include "git2/version.h"
11 #include "diff_generate.h"
12 #include "patch.h"
13 #include "commit.h"
14 #include "index.h"
15 
16 struct patch_id_args {
17 	git_hash_ctx ctx;
18 	git_oid result;
19 	int first_file;
20 };
21 
diff_delta__path(const git_diff_delta * delta)22 GIT_INLINE(const char *) diff_delta__path(const git_diff_delta *delta)
23 {
24 	const char *str = delta->old_file.path;
25 
26 	if (!str ||
27 		delta->status == GIT_DELTA_ADDED ||
28 		delta->status == GIT_DELTA_RENAMED ||
29 		delta->status == GIT_DELTA_COPIED)
30 		str = delta->new_file.path;
31 
32 	return str;
33 }
34 
git_diff_delta__cmp(const void * a,const void * b)35 int git_diff_delta__cmp(const void *a, const void *b)
36 {
37 	const git_diff_delta *da = a, *db = b;
38 	int val = strcmp(diff_delta__path(da), diff_delta__path(db));
39 	return val ? val : ((int)da->status - (int)db->status);
40 }
41 
git_diff_delta__casecmp(const void * a,const void * b)42 int git_diff_delta__casecmp(const void *a, const void *b)
43 {
44 	const git_diff_delta *da = a, *db = b;
45 	int val = strcasecmp(diff_delta__path(da), diff_delta__path(db));
46 	return val ? val : ((int)da->status - (int)db->status);
47 }
48 
git_diff__entry_cmp(const void * a,const void * b)49 int git_diff__entry_cmp(const void *a, const void *b)
50 {
51 	const git_index_entry *entry_a = a;
52 	const git_index_entry *entry_b = b;
53 
54 	return strcmp(entry_a->path, entry_b->path);
55 }
56 
git_diff__entry_icmp(const void * a,const void * b)57 int git_diff__entry_icmp(const void *a, const void *b)
58 {
59 	const git_index_entry *entry_a = a;
60 	const git_index_entry *entry_b = b;
61 
62 	return strcasecmp(entry_a->path, entry_b->path);
63 }
64 
git_diff_free(git_diff * diff)65 void git_diff_free(git_diff *diff)
66 {
67 	if (!diff)
68 		return;
69 
70 	GIT_REFCOUNT_DEC(diff, diff->free_fn);
71 }
72 
git_diff_addref(git_diff * diff)73 void git_diff_addref(git_diff *diff)
74 {
75 	GIT_REFCOUNT_INC(diff);
76 }
77 
git_diff_num_deltas(const git_diff * diff)78 size_t git_diff_num_deltas(const git_diff *diff)
79 {
80 	GIT_ASSERT_ARG(diff);
81 	return diff->deltas.length;
82 }
83 
git_diff_num_deltas_of_type(const git_diff * diff,git_delta_t type)84 size_t git_diff_num_deltas_of_type(const git_diff *diff, git_delta_t type)
85 {
86 	size_t i, count = 0;
87 	const git_diff_delta *delta;
88 
89 	GIT_ASSERT_ARG(diff);
90 
91 	git_vector_foreach(&diff->deltas, i, delta) {
92 		count += (delta->status == type);
93 	}
94 
95 	return count;
96 }
97 
git_diff_get_delta(const git_diff * diff,size_t idx)98 const git_diff_delta *git_diff_get_delta(const git_diff *diff, size_t idx)
99 {
100 	GIT_ASSERT_ARG_WITH_RETVAL(diff, NULL);
101 	return git_vector_get(&diff->deltas, idx);
102 }
103 
git_diff_is_sorted_icase(const git_diff * diff)104 int git_diff_is_sorted_icase(const git_diff *diff)
105 {
106 	return (diff->opts.flags & GIT_DIFF_IGNORE_CASE) != 0;
107 }
108 
git_diff_get_perfdata(git_diff_perfdata * out,const git_diff * diff)109 int git_diff_get_perfdata(git_diff_perfdata *out, const git_diff *diff)
110 {
111 	GIT_ASSERT_ARG(out);
112 	GIT_ERROR_CHECK_VERSION(out, GIT_DIFF_PERFDATA_VERSION, "git_diff_perfdata");
113 	out->stat_calls = diff->perf.stat_calls;
114 	out->oid_calculations = diff->perf.oid_calculations;
115 	return 0;
116 }
117 
git_diff_foreach(git_diff * diff,git_diff_file_cb file_cb,git_diff_binary_cb binary_cb,git_diff_hunk_cb hunk_cb,git_diff_line_cb data_cb,void * payload)118 int git_diff_foreach(
119 	git_diff *diff,
120 	git_diff_file_cb file_cb,
121 	git_diff_binary_cb binary_cb,
122 	git_diff_hunk_cb hunk_cb,
123 	git_diff_line_cb data_cb,
124 	void *payload)
125 {
126 	int error = 0;
127 	git_diff_delta *delta;
128 	size_t idx;
129 
130 	GIT_ASSERT_ARG(diff);
131 
132 	git_vector_foreach(&diff->deltas, idx, delta) {
133 		git_patch *patch;
134 
135 		/* check flags against patch status */
136 		if (git_diff_delta__should_skip(&diff->opts, delta))
137 			continue;
138 
139 		if ((error = git_patch_from_diff(&patch, diff, idx)) != 0)
140 			break;
141 
142 		error = git_patch__invoke_callbacks(patch, file_cb, binary_cb,
143 						    hunk_cb, data_cb, payload);
144 		git_patch_free(patch);
145 
146 		if (error)
147 			break;
148 	}
149 
150 	return error;
151 }
152 
diff_format_email_append_header_tobuf(git_buf * out,const git_oid * id,const git_signature * author,const char * summary,const char * body,size_t patch_no,size_t total_patches,bool exclude_patchno_marker)153 static int diff_format_email_append_header_tobuf(
154 	git_buf *out,
155 	const git_oid *id,
156 	const git_signature *author,
157 	const char *summary,
158 	const char *body,
159 	size_t patch_no,
160 	size_t total_patches,
161 	bool exclude_patchno_marker)
162 {
163 	char idstr[GIT_OID_HEXSZ + 1];
164 	char date_str[GIT_DATE_RFC2822_SZ];
165 	int error = 0;
166 
167 	git_oid_fmt(idstr, id);
168 	idstr[GIT_OID_HEXSZ] = '\0';
169 
170 	if ((error = git__date_rfc2822_fmt(date_str, sizeof(date_str),
171 		&author->when)) < 0)
172 		return error;
173 
174 	error = git_buf_printf(out,
175 				"From %s Mon Sep 17 00:00:00 2001\n" \
176 				"From: %s <%s>\n" \
177 				"Date: %s\n" \
178 				"Subject: ",
179 				idstr,
180 				author->name, author->email,
181 				date_str);
182 
183 	if (error < 0)
184 		return error;
185 
186 	if (!exclude_patchno_marker) {
187 		if (total_patches == 1) {
188 			error = git_buf_puts(out, "[PATCH] ");
189 		} else {
190 			error = git_buf_printf(out, "[PATCH %"PRIuZ"/%"PRIuZ"] ",
191 				patch_no, total_patches);
192 		}
193 
194 		if (error < 0)
195 			return error;
196 	}
197 
198 	error = git_buf_printf(out, "%s\n\n", summary);
199 
200 	if (body) {
201 		git_buf_puts(out, body);
202 
203 		if (out->ptr[out->size - 1] != '\n')
204 			git_buf_putc(out, '\n');
205 	}
206 
207 	return error;
208 }
209 
diff_format_email_append_patches_tobuf(git_buf * out,git_diff * diff)210 static int diff_format_email_append_patches_tobuf(
211 	git_buf *out,
212 	git_diff *diff)
213 {
214 	size_t i, deltas;
215 	int error = 0;
216 
217 	deltas = git_diff_num_deltas(diff);
218 
219 	for (i = 0; i < deltas; ++i) {
220 		git_patch *patch = NULL;
221 
222 		if ((error = git_patch_from_diff(&patch, diff, i)) >= 0)
223 			error = git_patch_to_buf(out, patch);
224 
225 		git_patch_free(patch);
226 
227 		if (error < 0)
228 			break;
229 	}
230 
231 	return error;
232 }
233 
git_diff_format_email(git_buf * out,git_diff * diff,const git_diff_format_email_options * opts)234 int git_diff_format_email(
235 	git_buf *out,
236 	git_diff *diff,
237 	const git_diff_format_email_options *opts)
238 {
239 	git_diff_stats *stats = NULL;
240 	char *summary = NULL, *loc = NULL;
241 	bool ignore_marker;
242 	unsigned int format_flags = 0;
243 	size_t allocsize;
244 	int error;
245 
246 	GIT_ASSERT_ARG(out);
247 	GIT_ASSERT_ARG(diff);
248 	GIT_ASSERT_ARG(opts && opts->summary && opts->id && opts->author);
249 
250 	GIT_ERROR_CHECK_VERSION(opts,
251 		GIT_DIFF_FORMAT_EMAIL_OPTIONS_VERSION,
252 		"git_format_email_options");
253 
254 	ignore_marker = (opts->flags &
255 		GIT_DIFF_FORMAT_EMAIL_EXCLUDE_SUBJECT_PATCH_MARKER) != 0;
256 
257 	if (!ignore_marker) {
258 		if (opts->patch_no > opts->total_patches) {
259 			git_error_set(GIT_ERROR_INVALID,
260 				"patch %"PRIuZ" out of range. max %"PRIuZ,
261 				opts->patch_no, opts->total_patches);
262 			return -1;
263 		}
264 
265 		if (opts->patch_no == 0) {
266 			git_error_set(GIT_ERROR_INVALID,
267 				"invalid patch no %"PRIuZ". should be >0", opts->patch_no);
268 			return -1;
269 		}
270 	}
271 
272 	/* the summary we receive may not be clean.
273 	 * it could potentially contain new line characters
274 	 * or not be set, sanitize, */
275 	if ((loc = strpbrk(opts->summary, "\r\n")) != NULL) {
276 		size_t offset = 0;
277 
278 		if ((offset = (loc - opts->summary)) == 0) {
279 			git_error_set(GIT_ERROR_INVALID, "summary is empty");
280 			error = -1;
281 			goto on_error;
282 		}
283 
284 		GIT_ERROR_CHECK_ALLOC_ADD(&allocsize, offset, 1);
285 		summary = git__calloc(allocsize, sizeof(char));
286 		GIT_ERROR_CHECK_ALLOC(summary);
287 
288 		strncpy(summary, opts->summary, offset);
289 	}
290 
291 	error = diff_format_email_append_header_tobuf(out,
292 		opts->id, opts->author, summary == NULL ? opts->summary : summary,
293 		opts->body, opts->patch_no, opts->total_patches, ignore_marker);
294 
295 	if (error < 0)
296 		goto on_error;
297 
298 	format_flags = GIT_DIFF_STATS_FULL | GIT_DIFF_STATS_INCLUDE_SUMMARY;
299 
300 	if ((error = git_buf_puts(out, "---\n")) < 0 ||
301 		(error = git_diff_get_stats(&stats, diff)) < 0 ||
302 		(error = git_diff_stats_to_buf(out, stats, format_flags, 0)) < 0 ||
303 		(error = git_buf_putc(out, '\n')) < 0 ||
304 		(error = diff_format_email_append_patches_tobuf(out, diff)) < 0)
305 			goto on_error;
306 
307 	error = git_buf_puts(out, "--\nlibgit2 " LIBGIT2_VERSION "\n\n");
308 
309 on_error:
310 	git__free(summary);
311 	git_diff_stats_free(stats);
312 
313 	return error;
314 }
315 
git_diff_commit_as_email(git_buf * out,git_repository * repo,git_commit * commit,size_t patch_no,size_t total_patches,uint32_t flags,const git_diff_options * diff_opts)316 int git_diff_commit_as_email(
317 	git_buf *out,
318 	git_repository *repo,
319 	git_commit *commit,
320 	size_t patch_no,
321 	size_t total_patches,
322 	uint32_t flags,
323 	const git_diff_options *diff_opts)
324 {
325 	git_diff *diff = NULL;
326 	git_diff_format_email_options opts =
327 		GIT_DIFF_FORMAT_EMAIL_OPTIONS_INIT;
328 	int error;
329 
330 	GIT_ASSERT_ARG(out);
331 	GIT_ASSERT_ARG(repo);
332 	GIT_ASSERT_ARG(commit);
333 
334 	opts.flags = flags;
335 	opts.patch_no = patch_no;
336 	opts.total_patches = total_patches;
337 	opts.id = git_commit_id(commit);
338 	opts.summary = git_commit_summary(commit);
339 	opts.body = git_commit_body(commit);
340 	opts.author = git_commit_author(commit);
341 
342 	if ((error = git_diff__commit(&diff, repo, commit, diff_opts)) < 0)
343 		return error;
344 
345 	error = git_diff_format_email(out, diff, &opts);
346 
347 	git_diff_free(diff);
348 	return error;
349 }
350 
git_diff_options_init(git_diff_options * opts,unsigned int version)351 int git_diff_options_init(git_diff_options *opts, unsigned int version)
352 {
353 	GIT_INIT_STRUCTURE_FROM_TEMPLATE(
354 		opts, version, git_diff_options, GIT_DIFF_OPTIONS_INIT);
355 	return 0;
356 }
357 
358 #ifndef GIT_DEPRECATE_HARD
git_diff_init_options(git_diff_options * opts,unsigned int version)359 int git_diff_init_options(git_diff_options *opts, unsigned int version)
360 {
361 	return git_diff_options_init(opts, version);
362 }
363 #endif
364 
git_diff_find_options_init(git_diff_find_options * opts,unsigned int version)365 int git_diff_find_options_init(
366 	git_diff_find_options *opts, unsigned int version)
367 {
368 	GIT_INIT_STRUCTURE_FROM_TEMPLATE(
369 		opts, version, git_diff_find_options, GIT_DIFF_FIND_OPTIONS_INIT);
370 	return 0;
371 }
372 
373 #ifndef GIT_DEPRECATE_HARD
git_diff_find_init_options(git_diff_find_options * opts,unsigned int version)374 int git_diff_find_init_options(
375 	git_diff_find_options *opts, unsigned int version)
376 {
377 	return git_diff_find_options_init(opts, version);
378 }
379 #endif
380 
git_diff_format_email_options_init(git_diff_format_email_options * opts,unsigned int version)381 int git_diff_format_email_options_init(
382 	git_diff_format_email_options *opts, unsigned int version)
383 {
384 	GIT_INIT_STRUCTURE_FROM_TEMPLATE(
385 		opts, version, git_diff_format_email_options,
386 		GIT_DIFF_FORMAT_EMAIL_OPTIONS_INIT);
387 	return 0;
388 }
389 
390 #ifndef GIT_DEPRECATE_HARD
git_diff_format_email_init_options(git_diff_format_email_options * opts,unsigned int version)391 int git_diff_format_email_init_options(
392 	git_diff_format_email_options *opts, unsigned int version)
393 {
394 	return git_diff_format_email_options_init(opts, version);
395 }
396 #endif
397 
flush_hunk(git_oid * result,git_hash_ctx * ctx)398 static int flush_hunk(git_oid *result, git_hash_ctx *ctx)
399 {
400 	git_oid hash;
401 	unsigned short carry = 0;
402 	int error, i;
403 
404 	if ((error = git_hash_final(&hash, ctx)) < 0 ||
405 	    (error = git_hash_init(ctx)) < 0)
406 		return error;
407 
408 	for (i = 0; i < GIT_OID_RAWSZ; i++) {
409 		carry += result->id[i] + hash.id[i];
410 		result->id[i] = (unsigned char)carry;
411 		carry >>= 8;
412 	}
413 
414 	return 0;
415 }
416 
strip_spaces(git_buf * buf)417 static void strip_spaces(git_buf *buf)
418 {
419 	char *src = buf->ptr, *dst = buf->ptr;
420 	char c;
421 	size_t len = 0;
422 
423 	while ((c = *src++) != '\0') {
424 		if (!git__isspace(c)) {
425 			*dst++ = c;
426 			len++;
427 		}
428 	}
429 
430 	git_buf_truncate(buf, len);
431 }
432 
diff_patchid_print_callback_to_buf(const git_diff_delta * delta,const git_diff_hunk * hunk,const git_diff_line * line,void * payload)433 static int diff_patchid_print_callback_to_buf(
434 	const git_diff_delta *delta,
435 	const git_diff_hunk *hunk,
436 	const git_diff_line *line,
437 	void *payload)
438 {
439 	struct patch_id_args *args = (struct patch_id_args *) payload;
440 	git_buf buf = GIT_BUF_INIT;
441 	int error = 0;
442 
443 	if (line->origin == GIT_DIFF_LINE_CONTEXT_EOFNL ||
444 	    line->origin == GIT_DIFF_LINE_ADD_EOFNL ||
445 	    line->origin == GIT_DIFF_LINE_DEL_EOFNL)
446 		goto out;
447 
448 	if ((error = git_diff_print_callback__to_buf(delta, hunk,
449 						     line, &buf)) < 0)
450 		goto out;
451 
452 	strip_spaces(&buf);
453 
454 	if (line->origin == GIT_DIFF_LINE_FILE_HDR &&
455 	    !args->first_file &&
456 	    (error = flush_hunk(&args->result, &args->ctx) < 0))
457 			goto out;
458 
459 	if ((error = git_hash_update(&args->ctx, buf.ptr, buf.size)) < 0)
460 		goto out;
461 
462 	if (line->origin == GIT_DIFF_LINE_FILE_HDR && args->first_file)
463 		args->first_file = 0;
464 
465 out:
466 	git_buf_dispose(&buf);
467 	return error;
468 }
469 
git_diff_patchid_options_init(git_diff_patchid_options * opts,unsigned int version)470 int git_diff_patchid_options_init(git_diff_patchid_options *opts, unsigned int version)
471 {
472 	GIT_INIT_STRUCTURE_FROM_TEMPLATE(
473 		opts, version, git_diff_patchid_options, GIT_DIFF_PATCHID_OPTIONS_INIT);
474 	return 0;
475 }
476 
git_diff_patchid(git_oid * out,git_diff * diff,git_diff_patchid_options * opts)477 int git_diff_patchid(git_oid *out, git_diff *diff, git_diff_patchid_options *opts)
478 {
479 	struct patch_id_args args;
480 	int error;
481 
482 	GIT_ERROR_CHECK_VERSION(
483 		opts, GIT_DIFF_PATCHID_OPTIONS_VERSION, "git_diff_patchid_options");
484 
485 	memset(&args, 0, sizeof(args));
486 	args.first_file = 1;
487 	if ((error = git_hash_ctx_init(&args.ctx)) < 0)
488 		goto out;
489 
490 	if ((error = git_diff_print(diff,
491 				    GIT_DIFF_FORMAT_PATCH_ID,
492 				    diff_patchid_print_callback_to_buf,
493 				    &args)) < 0)
494 		goto out;
495 
496 	if ((error = (flush_hunk(&args.result, &args.ctx))) < 0)
497 		goto out;
498 
499 	git_oid_cpy(out, &args.result);
500 
501 out:
502 	git_hash_ctx_cleanup(&args.ctx);
503 	return error;
504 }
505