patch-id: make it stable against hunk reordering

Patch id changes if users reorder file diffs that make up a patch. As the result is functionally equivalent, a different patch id is surprising to many users. In particular, reordering files using diff -O is helpful to make patches more readable (e.g. API header diff before implementation diff). Add an option to change patch-id behaviour making it stable against these kinds of patch change: calculate SHA1 hash for each hunk separately and sum all hashes (using a symmetrical sum) to get patch id We use a 20byte sum and not xor - since xor would give 0 output for patches that have two identical diffs, which isn't all that unlikely (e.g. append the same line in two places). The new behaviour is enabled - when patchid.stable is true - when --stable flag is present Using a new flag --unstable or setting patchid.stable to false force the historical behaviour. In the documentation, clarify that patch ID can now be a sum of hashes, not a hash. Document how command line and config options affect the behaviour. Signed-off-by: Michael S. Tsirkin <mst@redhat.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michael S. Tsirkin committed Apr 27, 2014 at 21:15 UTC 30e12b924b57b15e707f1749f2e5af15f1c7fe09
2 files changed +91 -20
Documentation/git-patch-id.txt
+32 -5
@@ -8,14 +8,14 @@ git-patch-id - Compute unique ID for a patch
8 SYNOPSIS
9 --------
10 [verse]
11 -'git patch-id' < <patch>
11 +'git patch-id' [--stable | --unstable] < <patch>
12
13 DESCRIPTION
14 -----------
15 -A "patch ID" is nothing but a SHA-1 of the diff associated with a patch, with
16 -whitespace and line numbers ignored. As such, it's "reasonably stable", but at
17 -the same time also reasonably unique, i.e., two patches that have the same "patch
18 -ID" are almost guaranteed to be the same thing.
15 +A "patch ID" is nothing but a sum of SHA-1 of the file diffs associated with a
16 +patch, with whitespace and line numbers ignored. As such, it's "reasonably
17 +stable", but at the same time also reasonably unique, i.e., two patches that
18 +have the same "patch ID" are almost guaranteed to be the same thing.
19
20 IOW, you can use this thing to look for likely duplicate commits.
21
@@ -27,6 +27,33 @@ This can be used to make a mapping from patch ID to commit ID.
27
28 OPTIONS
29 -------
30 +
31 +--stable::
32 + Use a "stable" sum of hashes as the patch ID. With this option:
33 + - Reordering file diffs that make up a patch does not affect the ID.
34 + In particular, two patches produced by comparing the same two trees
35 + with two different settings for "-O<orderfile>" result in the same
36 + patch ID signature, thereby allowing the computed result to be used
37 + as a key to index some meta-information about the change between
38 + the two trees;
39 +
40 + - Result is different from the value produced by git 1.9 and older
41 + or produced when an "unstable" hash (see --unstable below) is
42 + configured - even when used on a diff output taken without any use
43 + of "-O<orderfile>", thereby making existing databases storing such
44 + "unstable" or historical patch-ids unusable.
45 +
46 + This is the default if patchid.stable is set to true.
47 +
48 +--unstable::
49 + Use an "unstable" hash as the patch ID. With this option,
50 + the result produced is compatible with the patch-id value produced
51 + by git 1.9 and older. Users with pre-existing databases storing
52 + patch-ids produced by git 1.9 and older (who do not deal with reordered
53 + patches) may want to use this option.
54 +
55 + This is the default.
56 +
57 <patch>::
58 The diff to create the ID of.
59
builtin/patch-id.c
+59 -15
@@ -1,17 +1,14 @@
1 #include "builtin.h"
2
3 -static void flush_current_id(int patchlen, unsigned char *id, git_SHA_CTX *c)
3 +static void flush_current_id(int patchlen, unsigned char *id, unsigned char *result)
4 {
5 - unsigned char result[20];
5 char name[50];
6
7 if (!patchlen)
8 return;
9
11 - git_SHA1_Final(result, c);
10 memcpy(name, sha1_to_hex(id), 41);
11 printf("%s %s\n", sha1_to_hex(result), name);
14 - git_SHA1_Init(c);
12 }
13
14 static int remove_space(char *line)
@@ -56,10 +53,31 @@ static int scan_hunk_header(const char *p, int *p_before, int *p_after)
53 return 1;
54 }
55
59 -static int get_one_patchid(unsigned char *next_sha1, git_SHA_CTX *ctx, struct strbuf *line_buf)
56 +static void flush_one_hunk(unsigned char *result, git_SHA_CTX *ctx)
57 +{
58 + unsigned char hash[20];
59 + unsigned short carry = 0;
60 + int i;
61 +
62 + git_SHA1_Final(hash, ctx);
63 + git_SHA1_Init(ctx);
64 + /* 20-byte sum, with carry */
65 + for (i = 0; i < 20; ++i) {
66 + carry += result[i] + hash[i];
67 + result[i] = carry;
68 + carry >>= 8;
69 + }
70 +}
71 +
72 +static int get_one_patchid(unsigned char *next_sha1, unsigned char *result,
73 + struct strbuf *line_buf, int stable)
74 {
75 int patchlen = 0, found_next = 0;
76 int before = -1, after = -1;
77 + git_SHA_CTX ctx;
78 +
79 + git_SHA1_Init(&ctx);
80 + hashclr(result);
81
82 while (strbuf_getwholeline(line_buf, stdin, '\n') != EOF) {
83 char *line = line_buf->buf;
@@ -107,6 +125,8 @@ static int get_one_patchid(unsigned char *next_sha1, git_SHA_CTX *ctx, struct st
125 break;
126
127 /* Else we're parsing another header. */
128 + if (stable)
129 + flush_one_hunk(result, &ctx);
130 before = after = -1;
131 }
132
@@ -119,39 +139,63 @@ static int get_one_patchid(unsigned char *next_sha1, git_SHA_CTX *ctx, struct st
139 /* Compute the sha without whitespace */
140 len = remove_space(line);
141 patchlen += len;
122 - git_SHA1_Update(ctx, line, len);
142 + git_SHA1_Update(&ctx, line, len);
143 }
144
145 if (!found_next)
146 hashclr(next_sha1);
147
148 + flush_one_hunk(result, &ctx);
149 +
150 return patchlen;
151 }
152
131 -static void generate_id_list(void)
153 +static void generate_id_list(int stable)
154 {
133 - unsigned char sha1[20], n[20];
134 - git_SHA_CTX ctx;
155 + unsigned char sha1[20], n[20], result[20];
156 int patchlen;
157 struct strbuf line_buf = STRBUF_INIT;
158
138 - git_SHA1_Init(&ctx);
159 hashclr(sha1);
160 while (!feof(stdin)) {
141 - patchlen = get_one_patchid(n, &ctx, &line_buf);
142 - flush_current_id(patchlen, sha1, &ctx);
161 + patchlen = get_one_patchid(n, result, &line_buf, stable);
162 + flush_current_id(patchlen, sha1, result);
163 hashcpy(sha1, n);
164 }
165 strbuf_release(&line_buf);
166 }
167
148 -static const char patch_id_usage[] = "git patch-id < patch";
168 +static const char patch_id_usage[] = "git patch-id [--stable | --unstable] < patch";
169 +
170 +static int git_patch_id_config(const char *var, const char *value, void *cb)
171 +{
172 + int *stable = cb;
173 +
174 + if (!strcmp(var, "patchid.stable")) {
175 + *stable = git_config_bool(var, value);
176 + return 0;
177 + }
178 +
179 + return git_default_config(var, value, cb);
180 +}
181
182 int cmd_patch_id(int argc, const char **argv, const char *prefix)
183 {
152 - if (argc != 1)
184 + int stable = -1;
185 +
186 + git_config(git_patch_id_config, &stable);
187 +
188 + /* If nothing is set, default to unstable. */
189 + if (stable < 0)
190 + stable = 0;
191 +
192 + if (argc == 2 && !strcmp(argv[1], "--stable"))
193 + stable = 1;
194 + else if (argc == 2 && !strcmp(argv[1], "--unstable"))
195 + stable = 0;
196 + else if (argc != 1)
197 usage(patch_id_usage);
198
155 - generate_id_list();
199 + generate_id_list(stable);
200 return 0;
201 }