builtin: patch-id: add --verbatim as a command mode

There are situations where the user might not want the default setting where patch-id strips all whitespace. They might be working in a language where white space is syntactically important, or they might have CI testing that enforces strict whitespace linting. In these cases, a whitespace change would result in the patch fundamentally changing, and thus deserving of a different id. Add a new mode that is exclusive of --stable and --unstable called --verbatim. It also corresponds to the config patchid.verbatim = true. In this mode, the stable algorithm is used and whitespace is not stripped from the patch text. Users of --unstable mainly care about compatibility with old git versions, which unstripping the whitespace would break. Thus there isn't a usecase for the combination of --verbatim and --unstable, and we don't expose this so as to not add maintainence burden. Signed-off-by: Jerry Zhang <jerry@skydio.com> fixes https://github.com/Skydio/revup/issues/2 Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jerry Zhang committed Oct 24, 2022 at 20:07 UTC 2871f4d447214874e13cf764ab3a170c9d844ca2
3 files changed +124 -39
Documentation/git-patch-id.txt
+16 -8
@@ -8,18 +8,18 @@ git-patch-id - Compute unique ID for a patch
8 SYNOPSIS
9 --------
10 [verse]
11 -'git patch-id' [--stable | --unstable]
11 +'git patch-id' [--stable | --unstable | --verbatim]
12
13 DESCRIPTION
14 -----------
15 Read a patch from the standard input and compute the patch ID for it.
16
17 A "patch ID" is nothing but a sum of SHA-1 of the file diffs associated with a
18 -patch, with whitespace and line numbers ignored. As such, it's "reasonably
19 -stable", but at the same time also reasonably unique, i.e., two patches that
20 -have the same "patch ID" are almost guaranteed to be the same thing.
18 +patch, with line numbers ignored. As such, it's "reasonably stable", but at
19 +the same time also reasonably unique, i.e., two patches that have the same
20 +"patch ID" are almost guaranteed to be the same thing.
21
22 -IOW, you can use this thing to look for likely duplicate commits.
22 +The main usecase for this command is to look for likely duplicate commits.
23
24 When dealing with 'git diff-tree' output, it takes advantage of
25 the fact that the patch is prefixed with the object name of the
@@ -30,6 +30,12 @@ This can be used to make a mapping from patch ID to commit ID.
30 OPTIONS
31 -------
32
33 +--verbatim::
34 + Calculate the patch-id of the input as it is given, do not strip
35 + any whitespace.
36 +
37 + This is the default if patchid.verbatim is true.
38 +
39 --stable::
40 Use a "stable" sum of hashes as the patch ID. With this option:
41 - Reordering file diffs that make up a patch does not affect the ID.
@@ -45,14 +51,16 @@ OPTIONS
51 of "-O<orderfile>", thereby making existing databases storing such
52 "unstable" or historical patch-ids unusable.
53
54 + - All whitespace within the patch is ignored and does not affect the id.
55 +
56 This is the default if patchid.stable is set to true.
57
58 --unstable::
59 Use an "unstable" hash as the patch ID. With this option,
60 the result produced is compatible with the patch-id value produced
53 - by git 1.9 and older. Users with pre-existing databases storing
54 - patch-ids produced by git 1.9 and older (who do not deal with reordered
55 - patches) may want to use this option.
61 + by git 1.9 and older and whitespace is ignored. Users with pre-existing
62 + databases storing patch-ids produced by git 1.9 and older (who do not deal
63 + with reordered patches) may want to use this option.
64
65 This is the default.
66
builtin/patch-id.c
+48 -25
@@ -2,6 +2,7 @@
2 #include "builtin.h"
3 #include "config.h"
4 #include "diff.h"
5 +#include "parse-options.h"
6
7 static void flush_current_id(int patchlen, struct object_id *id, struct object_id *result)
8 {
@@ -57,7 +58,7 @@ static int scan_hunk_header(const char *p, int *p_before, int *p_after)
58 }
59
60 static int get_one_patchid(struct object_id *next_oid, struct object_id *result,
60 - struct strbuf *line_buf, int stable)
61 + struct strbuf *line_buf, int stable, int verbatim)
62 {
63 int patchlen = 0, found_next = 0;
64 int before = -1, after = -1;
@@ -76,8 +77,11 @@ static int get_one_patchid(struct object_id *next_oid, struct object_id *result,
77 if (!skip_prefix(line, "diff-tree ", &p) &&
78 !skip_prefix(line, "commit ", &p) &&
79 !skip_prefix(line, "From ", &p) &&
79 - starts_with(line, "\\ ") && 12 < strlen(line))
80 + starts_with(line, "\\ ") && 12 < strlen(line)) {
81 + if (verbatim)
82 + the_hash_algo->update_fn(&ctx, line, strlen(line));
83 continue;
84 + }
85
86 if (!get_oid_hex(p, next_oid)) {
87 found_next = 1;
@@ -152,8 +156,8 @@ static int get_one_patchid(struct object_id *next_oid, struct object_id *result,
156 if (line[0] == '+' || line[0] == ' ')
157 after--;
158
155 - /* Compute the sha without whitespace */
156 - len = remove_space(line);
159 + /* Add line to hash algo (possibly removing whitespace) */
160 + len = verbatim ? strlen(line) : remove_space(line);
161 patchlen += len;
162 the_hash_algo->update_fn(&ctx, line, len);
163 }
@@ -166,7 +170,7 @@ static int get_one_patchid(struct object_id *next_oid, struct object_id *result,
170 return patchlen;
171 }
172
169 -static void generate_id_list(int stable)
173 +static void generate_id_list(int stable, int verbatim)
174 {
175 struct object_id oid, n, result;
176 int patchlen;
@@ -174,21 +178,32 @@ static void generate_id_list(int stable)
178
179 oidclr(&oid);
180 while (!feof(stdin)) {
177 - patchlen = get_one_patchid(&n, &result, &line_buf, stable);
181 + patchlen = get_one_patchid(&n, &result, &line_buf, stable, verbatim);
182 flush_current_id(patchlen, &oid, &result);
183 oidcpy(&oid, &n);
184 }
185 strbuf_release(&line_buf);
186 }
187
184 -static const char patch_id_usage[] = "git patch-id [--stable | --unstable]";
188 +static const char *const patch_id_usage[] = {
189 + N_("git patch-id [--stable | --unstable | --verbatim]"), NULL
190 +};
191 +
192 +struct patch_id_opts {
193 + int stable;
194 + int verbatim;
195 +};
196
197 static int git_patch_id_config(const char *var, const char *value, void *cb)
198 {
188 - int *stable = cb;
199 + struct patch_id_opts *opts = cb;
200
201 if (!strcmp(var, "patchid.stable")) {
191 - *stable = git_config_bool(var, value);
202 + opts->stable = git_config_bool(var, value);
203 + return 0;
204 + }
205 + if (!strcmp(var, "patchid.verbatim")) {
206 + opts->verbatim = git_config_bool(var, value);
207 return 0;
208 }
209
@@ -197,21 +212,29 @@ static int git_patch_id_config(const char *var, const char *value, void *cb)
212
213 int cmd_patch_id(int argc, const char **argv, const char *prefix)
214 {
200 - int stable = -1;
201 -
202 - git_config(git_patch_id_config, &stable);
203 -
204 - /* If nothing is set, default to unstable. */
205 - if (stable < 0)
206 - stable = 0;
207 -
208 - if (argc == 2 && !strcmp(argv[1], "--stable"))
209 - stable = 1;
210 - else if (argc == 2 && !strcmp(argv[1], "--unstable"))
211 - stable = 0;
212 - else if (argc != 1)
213 - usage(patch_id_usage);
214 -
215 - generate_id_list(stable);
215 + /* if nothing is set, default to unstable */
216 + struct patch_id_opts config = {0, 0};
217 + int opts = 0;
218 + struct option builtin_patch_id_options[] = {
219 + OPT_CMDMODE(0, "unstable", &opts,
220 + N_("use the unstable patch-id algorithm"), 1),
221 + OPT_CMDMODE(0, "stable", &opts,
222 + N_("use the stable patch-id algorithm"), 2),
223 + OPT_CMDMODE(0, "verbatim", &opts,
224 + N_("don't strip whitespace from the patch"), 3),
225 + OPT_END()
226 + };
227 +
228 + git_config(git_patch_id_config, &config);
229 +
230 + /* verbatim implies stable */
231 + if (config.verbatim)
232 + config.stable = 1;
233 +
234 + argc = parse_options(argc, argv, prefix, builtin_patch_id_options,
235 + patch_id_usage, 0);
236 +
237 + generate_id_list(opts ? opts > 1 : config.stable,
238 + opts ? opts == 3 : config.verbatim);
239 return 0;
240 }
t/t4204-patch-id.sh
+60 -6
@@ -8,13 +8,13 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
8 . ./test-lib.sh
9
10 test_expect_success 'setup' '
11 - as="a a a a a a a a" && # eight a
12 - test_write_lines $as >foo &&
13 - test_write_lines $as >bar &&
11 + str="ab cd ef gh ij kl mn op" &&
12 + test_write_lines $str >foo &&
13 + test_write_lines $str >bar &&
14 git add foo bar &&
15 git commit -a -m initial &&
16 - test_write_lines $as b >foo &&
17 - test_write_lines $as b >bar &&
16 + test_write_lines $str b >foo &&
17 + test_write_lines $str b >bar &&
18 git commit -a -m first &&
19 git checkout -b same main &&
20 git commit --amend -m same-msg &&
@@ -22,8 +22,23 @@ test_expect_success 'setup' '
22 echo c >foo &&
23 echo c >bar &&
24 git commit --amend -a -m notsame-msg &&
25 + git checkout -b with_space main~ &&
26 + cat >foo <<-\EOF &&
27 + a b
28 + c d
29 + e f
30 + g h
31 + i j
32 + k l
33 + m n
34 + op
35 + EOF
36 + cp foo bar &&
37 + git add foo bar &&
38 + git commit --amend -m "with spaces" &&
39 test_write_lines bar foo >bar-then-foo &&
40 test_write_lines foo bar >foo-then-bar
41 +
42 '
43
44 test_expect_success 'patch-id output is well-formed' '
@@ -128,9 +143,21 @@ test_patch_id_file_order () {
143 git format-patch -1 --stdout -O foo-then-bar >format-patch.output &&
144 calc_patch_id <format-patch.output "ordered-$name" "$@" &&
145 cmp_patch_id $relevant "$name" "ordered-$name"
146 +}
147
148 +test_patch_id_whitespace () {
149 + relevant="$1"
150 + shift
151 + name="ws-${1}-$relevant"
152 + shift
153 + get_top_diff "main~" >top-diff.output &&
154 + calc_patch_id <top-diff.output "$name" "$@" &&
155 + get_top_diff "with_space" >top-diff.output &&
156 + calc_patch_id <top-diff.output "ws-$name" "$@" &&
157 + cmp_patch_id $relevant "$name" "ws-$name"
158 }
159
160 +
161 # combined test for options: add more tests here to make them
162 # run with all options
163 test_patch_id () {
@@ -146,6 +173,14 @@ test_expect_success 'file order is relevant with --unstable' '
173 test_patch_id_file_order relevant --unstable --unstable
174 '
175
176 +test_expect_success 'whitespace is relevant with --verbatim' '
177 + test_patch_id_whitespace relevant --verbatim --verbatim
178 +'
179 +
180 +test_expect_success 'whitespace is irrelevant without --verbatim' '
181 + test_patch_id_whitespace irrelevant --stable --stable
182 +'
183 +
184 #Now test various option combinations.
185 test_expect_success 'default is unstable' '
186 test_patch_id relevant default
@@ -161,6 +196,17 @@ test_expect_success 'patchid.stable = false is unstable' '
196 test_patch_id relevant patchid.stable=false
197 '
198
199 +test_expect_success 'patchid.verbatim = true is correct and stable' '
200 + test_config patchid.verbatim true &&
201 + test_patch_id_whitespace relevant patchid.verbatim=true &&
202 + test_patch_id irrelevant patchid.verbatim=true
203 +'
204 +
205 +test_expect_success 'patchid.verbatim = false is unstable' '
206 + test_config patchid.verbatim false &&
207 + test_patch_id relevant patchid.verbatim=false
208 +'
209 +
210 test_expect_success '--unstable overrides patchid.stable = true' '
211 test_config patchid.stable true &&
212 test_patch_id relevant patchid.stable=true--unstable --unstable
@@ -171,6 +217,11 @@ test_expect_success '--stable overrides patchid.stable = false' '
217 test_patch_id irrelevant patchid.stable=false--stable --stable
218 '
219
220 +test_expect_success '--verbatim overrides patchid.stable = false' '
221 + test_config patchid.stable false &&
222 + test_patch_id_whitespace relevant stable=false--verbatim --verbatim
223 +'
224 +
225 test_expect_success 'patch-id supports git-format-patch MIME output' '
226 get_patch_id main &&
227 git checkout same &&
@@ -225,7 +276,10 @@ test_expect_success 'patch-id handles no-nl-at-eof markers' '
276 EOF
277 calc_patch_id nonl <nonl &&
278 calc_patch_id withnl <withnl &&
228 - test_cmp patch-id_nonl patch-id_withnl
279 + test_cmp patch-id_nonl patch-id_withnl &&
280 + calc_patch_id nonl-inc-ws --verbatim <nonl &&
281 + calc_patch_id withnl-inc-ws --verbatim <withnl &&
282 + ! test_cmp patch-id_nonl-inc-ws patch-id_withnl-inc-ws
283 '
284
285 test_expect_success 'patch-id handles diffs with one line of before/after' '