Commit fb439c975a2de33f5b0c317f3fdea49dc94b27dc

Patrick Steinhardt 2019-11-28T14:41:58

Merge pull request #5306 from herrerog/patchid diff: complete support for git patchid

diff --git a/include/git2/diff.h b/include/git2/diff.h
index 490b406..3976ab1 100644
--- a/include/git2/diff.h
+++ b/include/git2/diff.h
@@ -1093,6 +1093,7 @@ typedef enum {
 	GIT_DIFF_FORMAT_RAW          = 3u, /**< like git diff --raw */
 	GIT_DIFF_FORMAT_NAME_ONLY    = 4u, /**< like git diff --name-only */
 	GIT_DIFF_FORMAT_NAME_STATUS  = 5u, /**< like git diff --name-status */
+	GIT_DIFF_FORMAT_PATCH_ID     = 6u, /**< git diff as used by git patch-id */
 } git_diff_format_t;
 
 /**
diff --git a/src/diff.c b/src/diff.c
index 15a32ed..47f49d9 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -426,81 +426,38 @@ static void strip_spaces(git_buf *buf)
 	git_buf_truncate(buf, len);
 }
 
-static int file_cb(
+int git_diff_patchid_print_callback__to_buf(
 	const git_diff_delta *delta,
-	float progress,
+	const git_diff_hunk *hunk,
+	const git_diff_line *line,
 	void *payload)
 {
 	struct patch_id_args *args = (struct patch_id_args *) payload;
 	git_buf buf = GIT_BUF_INIT;
-	int error;
-
-	GIT_UNUSED(progress);
+	int error = 0;
 
-	if (!args->first_file &&
-	    (error = flush_hunk(&args->result, &args->ctx)) < 0)
-		goto out;
-	args->first_file = 0;
-
-	if ((error = git_buf_printf(&buf,
-				    "diff--gita/%sb/%s---a/%s+++b/%s",
-				    delta->old_file.path,
-				    delta->new_file.path,
-				    delta->old_file.path,
-				    delta->new_file.path)) < 0)
+	if (line->origin == GIT_DIFF_LINE_CONTEXT_EOFNL ||
+	    line->origin == GIT_DIFF_LINE_ADD_EOFNL ||
+	    line->origin == GIT_DIFF_LINE_DEL_EOFNL)
 		goto out;
 
-	strip_spaces(&buf);
-
-	if ((error = git_hash_update(&args->ctx, buf.ptr, buf.size)) < 0)
+	if ((error = git_diff_print_callback__to_buf(delta, hunk,
+						     line, &buf)) < 0)
 		goto out;
 
-out:
-	git_buf_dispose(&buf);
-	return error;
-}
-
-static int patchid_line_cb(
-	const git_diff_delta *delta,
-	const git_diff_hunk *hunk,
-	const git_diff_line *line,
-	void *payload)
-{
-	struct patch_id_args *args = (struct patch_id_args *) payload;
-	git_buf buf = GIT_BUF_INIT;
-	int error;
-
-	GIT_UNUSED(delta);
-	GIT_UNUSED(hunk);
-
-	switch (line->origin) {
-	    case GIT_DIFF_LINE_ADDITION:
-		git_buf_putc(&buf, '+');
-		break;
-	    case GIT_DIFF_LINE_DELETION:
-		git_buf_putc(&buf, '-');
-		break;
-	    case GIT_DIFF_LINE_CONTEXT:
-		break;
-	    case GIT_DIFF_LINE_CONTEXT_EOFNL:
-	    case GIT_DIFF_LINE_ADD_EOFNL:
-	    case GIT_DIFF_LINE_DEL_EOFNL:
-		/*
-		 * Ignore EOF without newlines for patch IDs as whitespace is
-		 * not supposed to be significant.
-		 */
-		return 0;
-	    default:
-		git_error_set(GIT_ERROR_PATCH, "invalid line origin for patch");
-		return -1;
-	}
-
-	git_buf_put(&buf, line->content, line->content_len);
 	strip_spaces(&buf);
 
+	if (line->origin == GIT_DIFF_LINE_FILE_HDR &&
+	    !args->first_file &&
+	    (error = flush_hunk(&args->result, &args->ctx) < 0))
+			goto out;
+
 	if ((error = git_hash_update(&args->ctx, buf.ptr, buf.size)) < 0)
 		goto out;
 
+	if (line->origin == GIT_DIFF_LINE_FILE_HDR && args->first_file)
+		args->first_file = 0;
+
 out:
 	git_buf_dispose(&buf);
 	return error;
@@ -526,7 +483,10 @@ int git_diff_patchid(git_oid *out, git_diff *diff, git_diff_patchid_options *opt
 	if ((error = git_hash_ctx_init(&args.ctx)) < 0)
 		goto out;
 
-	if ((error = git_diff_foreach(diff, file_cb, NULL, NULL, patchid_line_cb, &args)) < 0)
+	if ((error = git_diff_print(diff,
+				    GIT_DIFF_FORMAT_PATCH_ID,
+				    git_diff_patchid_print_callback__to_buf,
+				    &args)) < 0)
 		goto out;
 
 	if ((error = (flush_hunk(&args.result, &args.ctx))) < 0)
diff --git a/src/diff.h b/src/diff.h
index 93374b9..1a4ee47 100644
--- a/src/diff.h
+++ b/src/diff.h
@@ -57,7 +57,8 @@ extern int git_diff_delta__format_file_header(
 	const git_diff_delta *delta,
 	const char *oldpfx,
 	const char *newpfx,
-	int oid_strlen);
+	int oid_strlen,
+	bool print_index);
 
 extern int git_diff_delta__cmp(const void *a, const void *b);
 extern int git_diff_delta__casecmp(const void *a, const void *b);
diff --git a/src/diff_print.c b/src/diff_print.c
index f1aac5e..369e5c1 100644
--- a/src/diff_print.c
+++ b/src/diff_print.c
@@ -269,7 +269,8 @@ static int diff_print_modes(
 }
 
 static int diff_print_oid_range(
-	git_buf *out, const git_diff_delta *delta, int id_strlen)
+	git_buf *out, const git_diff_delta *delta, int id_strlen,
+	bool print_index)
 {
 	char start_oid[GIT_OID_HEXSZ+1], end_oid[GIT_OID_HEXSZ+1];
 
@@ -293,8 +294,9 @@ static int diff_print_oid_range(
 	git_oid_tostr(end_oid, id_strlen + 1, &delta->new_file.id);
 
 	if (delta->old_file.mode == delta->new_file.mode) {
-		git_buf_printf(out, "index %s..%s %o\n",
-			start_oid, end_oid, delta->old_file.mode);
+		if (print_index)
+			git_buf_printf(out, "index %s..%s %o\n",
+				start_oid, end_oid, delta->old_file.mode);
 	} else {
 		if (delta->old_file.mode == 0)
 			git_buf_printf(out, "new file mode %o\n", delta->new_file.mode);
@@ -303,7 +305,8 @@ static int diff_print_oid_range(
 		else
 			diff_print_modes(out, delta);
 
-		git_buf_printf(out, "index %s..%s\n", start_oid, end_oid);
+		if (print_index)
+			git_buf_printf(out, "index %s..%s\n", start_oid, end_oid);
 	}
 
 	return git_buf_oom(out) ? -1 : 0;
@@ -400,7 +403,8 @@ int git_diff_delta__format_file_header(
 	const git_diff_delta *delta,
 	const char *oldpfx,
 	const char *newpfx,
-	int id_strlen)
+	int id_strlen,
+	bool print_index)
 {
 	git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT;
 	bool unchanged = delta_is_unchanged(delta);
@@ -431,7 +435,8 @@ int git_diff_delta__format_file_header(
 	}
 
 	if (!unchanged) {
-		if ((error = diff_print_oid_range(out, delta, id_strlen)) < 0)
+		if ((error = diff_print_oid_range(out, delta,
+						  id_strlen, print_index)) < 0)
 			goto done;
 
 		if ((delta->flags & GIT_DIFF_FLAG_BINARY) == 0)
@@ -566,6 +571,7 @@ static int diff_print_patch_file(
 		(pi->flags & GIT_DIFF_FORCE_BINARY);
 	bool show_binary = !!(pi->flags & GIT_DIFF_SHOW_BINARY);
 	int id_strlen = pi->id_strlen;
+	bool print_index = (pi->format != GIT_DIFF_FORMAT_PATCH_ID);
 
 	if (binary && show_binary)
 		id_strlen = delta->old_file.id_abbrev ? delta->old_file.id_abbrev :
@@ -582,7 +588,8 @@ static int diff_print_patch_file(
 		return 0;
 
 	if ((error = git_diff_delta__format_file_header(
-			pi->buf, delta, oldpfx, newpfx, id_strlen)) < 0)
+			pi->buf, delta, oldpfx, newpfx,
+			id_strlen, print_index)) < 0)
 		return error;
 
 	pi->line.origin      = GIT_DIFF_LINE_FILE_HDR;
@@ -670,6 +677,11 @@ int git_diff_print(
 		print_hunk = diff_print_patch_hunk;
 		print_line = diff_print_patch_line;
 		break;
+	case GIT_DIFF_FORMAT_PATCH_ID:
+		print_file = diff_print_patch_file;
+		print_binary = diff_print_patch_binary;
+		print_line = diff_print_patch_line;
+		break;
 	case GIT_DIFF_FORMAT_PATCH_HEADER:
 		print_file = diff_print_patch_file;
 		break;
diff --git a/src/patch.c b/src/patch.c
index d19e683..82181bb 100644
--- a/src/patch.c
+++ b/src/patch.c
@@ -79,7 +79,7 @@ size_t git_patch_size(
 		git_buf file_header = GIT_BUF_INIT;
 
 		if (git_diff_delta__format_file_header(
-			&file_header, patch->delta, NULL, NULL, 0) < 0)
+			&file_header, patch->delta, NULL, NULL, 0, true) < 0)
 			git_error_clear();
 		else
 			out += git_buf_len(&file_header);
diff --git a/src/patch_parse.c b/src/patch_parse.c
index 35fd65a..dad1813 100644
--- a/src/patch_parse.c
+++ b/src/patch_parse.c
@@ -231,9 +231,9 @@ static int parse_header_git_deletedfilemode(
 	git_patch_parsed *patch,
 	git_patch_parse_ctx *ctx)
 {
-	git__free((char *)patch->base.delta->old_file.path);
+	git__free((char *)patch->base.delta->new_file.path);
 
-	patch->base.delta->old_file.path = NULL;
+	patch->base.delta->new_file.path = NULL;
 	patch->base.delta->status = GIT_DELTA_DELETED;
 	patch->base.delta->nfiles = 1;
 
@@ -244,9 +244,9 @@ static int parse_header_git_newfilemode(
 	git_patch_parsed *patch,
 	git_patch_parse_ctx *ctx)
 {
-	git__free((char *)patch->base.delta->new_file.path);
+	git__free((char *)patch->base.delta->old_file.path);
 
-	patch->base.delta->new_file.path = NULL;
+	patch->base.delta->old_file.path = NULL;
 	patch->base.delta->status = GIT_DELTA_ADDED;
 	patch->base.delta->nfiles = 1;
 
@@ -884,6 +884,11 @@ static int parse_patch_binary_nodata(
 	if (!old || !new)
 		return git_parse_err("corrupt binary data without paths at line %"PRIuZ, ctx->parse_ctx.line_num);
 
+	if (patch->base.delta->status == GIT_DELTA_ADDED)
+		old = "/dev/null";
+	else if (patch->base.delta->status == GIT_DELTA_DELETED)
+		new = "/dev/null";
+
 	if (git_parse_advance_expected_str(&ctx->parse_ctx, "Binary files ") < 0 ||
 	    git_parse_advance_expected_str(&ctx->parse_ctx, old) < 0 ||
 	    git_parse_advance_expected_str(&ctx->parse_ctx, " and ") < 0 ||
diff --git a/tests/diff/patchid.c b/tests/diff/patchid.c
index 75a2aa8..621a720 100644
--- a/tests/diff/patchid.c
+++ b/tests/diff/patchid.c
@@ -20,6 +20,39 @@ void test_diff_patchid__simple_commit(void)
 	verify_patch_id(PATCH_SIMPLE_COMMIT, "06094b1948b878b7d9ff7560b4eae672a014b0ec");
 }
 
+void test_diff_patchid__deleted_file(void)
+{
+	verify_patch_id(PATCH_DELETE_ORIGINAL, "d18507fe189f49c028b32c8c34e1ad98dd6a1aad");
+	verify_patch_id(PATCH_DELETED_FILE_2_HUNKS, "f31412498a17e6c3fbc635f2c5f9aa3ef4c1a9b7");
+}
+
+void test_diff_patchid__created_file(void)
+{
+	verify_patch_id(PATCH_ADD_ORIGINAL, "a7d39379308021465ae2ce65e338c048a3110db6");
+}
+
+void test_diff_patchid__binary_file(void)
+{
+	verify_patch_id(PATCH_ADD_BINARY_NOT_PRINTED, "2b31236b485faa30cf4dd33e4d6539829996739f");
+}
+
+void test_diff_patchid__renamed_file(void)
+{
+	verify_patch_id(PATCH_RENAME_EXACT, "4666d50cea4976f6f727448046d43461912058fd");
+	verify_patch_id(PATCH_RENAME_SIMILAR, "a795087575fcb940227be524488bedd6b3d3f438");
+}
+
+void test_diff_patchid__modechange(void)
+{
+	verify_patch_id(PATCH_MODECHANGE_UNCHANGED, "dbf3423ee98375ef1c72a79fbd29a049a2bae771");
+	verify_patch_id(PATCH_MODECHANGE_MODIFIED, "93aba696e1bbd2bbb73e3e3e62ed71f232137657");
+}
+
+void test_diff_patchid__shuffle_hunks(void)
+{
+	verify_patch_id(PATCH_DELETED_FILE_2_HUNKS_SHUFFLED, "f31412498a17e6c3fbc635f2c5f9aa3ef4c1a9b7");
+}
+
 void test_diff_patchid__filename_with_spaces(void)
 {
 	verify_patch_id(PATCH_APPEND_NO_NL, "f0ba05413beaef743b630e796153839462ee477a");
diff --git a/tests/patch/patch_common.h b/tests/patch/patch_common.h
index 92ab769..6601685 100644
--- a/tests/patch/patch_common.h
+++ b/tests/patch/patch_common.h
@@ -385,6 +385,38 @@
 	"@@ -9,0 +10 @@ below it!\n" \
 	"+insert at end\n"
 
+#define PATCH_DELETED_FILE_2_HUNKS \
+	"diff --git a/a b/a\n" \
+	"index 7f129fd..af431f2 100644\n" \
+	"--- a/a\n" \
+	"+++ b/a\n" \
+	"@@ -1 +1 @@\n" \
+	"-a contents 2\n" \
+	"+a contents\n" \
+	"diff --git a/c/d b/c/d\n" \
+	"deleted file mode 100644\n" \
+	"index 297efb8..0000000\n" \
+	"--- a/c/d\n" \
+	"+++ /dev/null\n" \
+	"@@ -1 +0,0 @@\n" \
+	"-c/d contents\n"
+
+#define PATCH_DELETED_FILE_2_HUNKS_SHUFFLED \
+	"diff --git a/c/d b/c/d\n" \
+	"deleted file mode 100644\n" \
+	"index 297efb8..0000000\n" \
+	"--- a/c/d\n" \
+	"+++ /dev/null\n" \
+	"@@ -1 +0,0 @@\n" \
+	"-c/d contents\n" \
+	"diff --git a/a b/a\n" \
+	"index 7f129fd..af431f2 100644\n" \
+	"--- a/a\n" \
+	"+++ b/a\n" \
+	"@@ -1 +1 @@\n" \
+	"-a contents 2\n" \
+	"+a contents\n"
+
 #define PATCH_SIMPLE_COMMIT \
 	"commit 15e119375018fba121cf58e02a9f17fe22df0df8\n" \
 	"Author: Edward Thomson <ethomson@edwardthomson.com>\n" \
@@ -878,6 +910,12 @@
 	"index 27184d9..7c94f9e 100644\n" \
 	"Binary files a/binary.bin and b/binary.bin differ\n"
 
+#define PATCH_ADD_BINARY_NOT_PRINTED \
+	"diff --git a/test.bin b/test.bin\n" \
+	"new file mode 100644\n" \
+	"index 0000000..9e0f96a\n" \
+	"Binary files /dev/null and b/test.bin differ\n"
+
 #define PATCH_ORIGINAL_NEW_FILE_WITH_SPACE \
 	"diff --git a/sp ace.txt b/sp ace.txt\n" \
 	"new file mode 100644\n" \
diff --git a/tests/patch/print.c b/tests/patch/print.c
index 4703c1d..c4ff479 100644
--- a/tests/patch/print.c
+++ b/tests/patch/print.c
@@ -172,3 +172,9 @@ void test_patch_print__binary_not_shown(void)
 	patch_print_from_patchfile(PATCH_BINARY_NOT_PRINTED,
 		strlen(PATCH_BINARY_NOT_PRINTED));
 }
+
+void test_patch_print__binary_add_not_shown(void)
+{
+	patch_print_from_patchfile(PATCH_ADD_BINARY_NOT_PRINTED,
+		strlen(PATCH_ADD_BINARY_NOT_PRINTED));
+}