diff: support anchoring line(s)
Teach diff a new algorithm, one that attempts to prevent user-specified lines from appearing as a deletion or addition in the end result. The end user can use this by specifying "--anchored=<text>" one or more times when using Git commands like "diff" and "show". Signed-off-by: Jonathan Tan <jonathantanmy@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Jonathan Tan committed
Nov 27, 2017 at 11:47 UTC
2477ab2ea8651920a9909f6d05b15ad9004a6c64
6 files changed
+169
-7
Documentation/diff-options.txt
+10
@@ -80,6 +80,16 @@ endif::git-format-patch[]
80
--histogram::
81
Generate a diff using the "histogram diff" algorithm.
82
83
+--anchored=<text>::
84
+ Generate a diff using the "anchored diff" algorithm.
85
++
86
+This option may be specified more than once.
87
++
88
+If a line exists in both the source and destination, exists only once,
89
+and starts with this text, this algorithm attempts to prevent it from
90
+appearing as a deletion or addition in the output. It uses the "patience
91
+diff" algorithm internally.
92
+
93
--diff-algorithm={patience|minimal|histogram|myers}::
94
Choose a diff algorithm. The variants are as follows:
95
+
diff.c
+20
-2
@@ -3210,6 +3210,8 @@ static void builtin_diff(const char *name_a,
3210
ecbdata.opt = o;
3211
ecbdata.header = header.len ? &header : NULL;
3212
xpp.flags = o->xdl_opts;
3213
+ xpp.anchors = o->anchors;
3214
+ xpp.anchors_nr = o->anchors_nr;
3215
xecfg.ctxlen = o->context;
3216
xecfg.interhunkctxlen = o->interhunkcontext;
3217
xecfg.flags = XDL_EMIT_FUNCNAMES;
@@ -3302,6 +3304,8 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
3304
memset(&xpp, 0, sizeof(xpp));
3305
memset(&xecfg, 0, sizeof(xecfg));
3306
xpp.flags = o->xdl_opts;
3307
+ xpp.anchors = o->anchors;
3308
+ xpp.anchors_nr = o->anchors_nr;
3309
xecfg.ctxlen = o->context;
3310
xecfg.interhunkctxlen = o->interhunkcontext;
3311
if (xdi_diff_outf(&mf1, &mf2, diffstat_consume, diffstat,
@@ -4594,9 +4598,18 @@ int diff_opt_parse(struct diff_options *options,
4598
DIFF_XDL_SET(options, INDENT_HEURISTIC);
4599
else if (!strcmp(arg, "--no-indent-heuristic"))
4600
DIFF_XDL_CLR(options, INDENT_HEURISTIC);
4597
- else if (!strcmp(arg, "--patience"))
4601
+ else if (!strcmp(arg, "--patience")) {
4602
+ int i;
4603
options->xdl_opts = DIFF_WITH_ALG(options, PATIENCE_DIFF);
4599
- else if (!strcmp(arg, "--histogram"))
4604
+ /*
4605
+ * Both --patience and --anchored use PATIENCE_DIFF
4606
+ * internally, so remove any anchors previously
4607
+ * specified.
4608
+ */
4609
+ for (i = 0; i < options->anchors_nr; i++)
4610
+ free(options->anchors[i]);
4611
+ options->anchors_nr = 0;
4612
+ } else if (!strcmp(arg, "--histogram"))
4613
options->xdl_opts = DIFF_WITH_ALG(options, HISTOGRAM_DIFF);
4614
else if ((argcount = parse_long_opt("diff-algorithm", av, &optarg))) {
4615
long value = parse_algorithm_value(optarg);
@@ -4608,6 +4621,11 @@ int diff_opt_parse(struct diff_options *options,
4621
options->xdl_opts &= ~XDF_DIFF_ALGORITHM_MASK;
4622
options->xdl_opts |= value;
4623
return argcount;
4624
+ } else if (skip_prefix(arg, "--anchored=", &arg)) {
4625
+ options->xdl_opts = DIFF_WITH_ALG(options, PATIENCE_DIFF);
4626
+ ALLOC_GROW(options->anchors, options->anchors_nr + 1,
4627
+ options->anchors_alloc);
4628
+ options->anchors[options->anchors_nr++] = xstrdup(arg);
4629
}
4630
4631
/* flags options */
diff.h
+4
@@ -166,6 +166,10 @@ struct diff_options {
166
const char *stat_sep;
167
long xdl_opts;
168
169
+ /* see Documentation/diff-options.txt */
170
+ char **anchors;
171
+ size_t anchors_nr, anchors_alloc;
172
+
173
int stat_width;
174
int stat_name_width;
175
int stat_graph_width;
t/t4065-diff-anchored.sh
new
+94
@@ -0,0 +1,94 @@
1
+#!/bin/sh
2
+
3
+test_description='anchored diff algorithm'
4
+
5
+. ./test-lib.sh
6
+
7
+test_expect_success '--anchored' '
8
+ printf "a\nb\nc\n" >pre &&
9
+ printf "c\na\nb\n" >post &&
10
+
11
+ # normally, c is moved to produce the smallest diff
12
+ test_expect_code 1 git diff --no-index pre post >diff &&
13
+ grep "^+c" diff &&
14
+
15
+ # with anchor, a is moved
16
+ test_expect_code 1 git diff --no-index --anchored=c pre post >diff &&
17
+ grep "^+a" diff
18
+'
19
+
20
+test_expect_success '--anchored multiple' '
21
+ printf "a\nb\nc\nd\ne\nf\n" >pre &&
22
+ printf "c\na\nb\nf\nd\ne\n" >post &&
23
+
24
+ # with 1 anchor, c is not moved, but f is moved
25
+ test_expect_code 1 git diff --no-index --anchored=c pre post >diff &&
26
+ grep "^+a" diff && # a is moved instead of c
27
+ grep "^+f" diff &&
28
+
29
+ # with 2 anchors, c and f are not moved
30
+ test_expect_code 1 git diff --no-index --anchored=c --anchored=f pre post >diff &&
31
+ grep "^+a" diff &&
32
+ grep "^+d" diff # d is moved instead of f
33
+'
34
+
35
+test_expect_success '--anchored with nonexistent line has no effect' '
36
+ printf "a\nb\nc\n" >pre &&
37
+ printf "c\na\nb\n" >post &&
38
+
39
+ test_expect_code 1 git diff --no-index --anchored=x pre post >diff &&
40
+ grep "^+c" diff
41
+'
42
+
43
+test_expect_success '--anchored with non-unique line has no effect' '
44
+ printf "a\nb\nc\nd\ne\nc\n" >pre &&
45
+ printf "c\na\nb\nc\nd\ne\n" >post &&
46
+
47
+ test_expect_code 1 git diff --no-index --anchored=c pre post >diff &&
48
+ grep "^+c" diff
49
+'
50
+
51
+test_expect_success 'diff still produced with impossible multiple --anchored' '
52
+ printf "a\nb\nc\n" >pre &&
53
+ printf "c\na\nb\n" >post &&
54
+
55
+ test_expect_code 1 git diff --no-index --anchored=a --anchored=c pre post >diff &&
56
+ mv post expected_post &&
57
+
58
+ # Ensure that the diff is correct by applying it and then
59
+ # comparing the result with the original
60
+ git apply diff &&
61
+ diff expected_post post
62
+'
63
+
64
+test_expect_success 'later algorithm arguments override earlier ones' '
65
+ printf "a\nb\nc\n" >pre &&
66
+ printf "c\na\nb\n" >post &&
67
+
68
+ test_expect_code 1 git diff --no-index --patience --anchored=c pre post >diff &&
69
+ grep "^+a" diff &&
70
+
71
+ test_expect_code 1 git diff --no-index --anchored=c --patience pre post >diff &&
72
+ grep "^+c" diff &&
73
+
74
+ test_expect_code 1 git diff --no-index --histogram --anchored=c pre post >diff &&
75
+ grep "^+a" diff &&
76
+
77
+ test_expect_code 1 git diff --no-index --anchored=c --histogram pre post >diff &&
78
+ grep "^+c" diff
79
+'
80
+
81
+test_expect_success '--anchored works with other commands like "git show"' '
82
+ printf "a\nb\nc\n" >file &&
83
+ git add file &&
84
+ git commit -m foo &&
85
+ printf "c\na\nb\n" >file &&
86
+ git add file &&
87
+ git commit -m foo &&
88
+
89
+ # with anchor, a is moved
90
+ git show --patience --anchored=c >diff &&
91
+ grep "^+a" diff
92
+'
93
+
94
+test_done
xdiff/xdiff.h
+4
@@ -86,6 +86,10 @@ typedef struct s_mmbuffer {
86
87
typedef struct s_xpparam {
88
unsigned long flags;
89
+
90
+ /* See Documentation/diff-options.txt. */
91
+ char **anchors;
92
+ size_t anchors_nr;
93
} xpparam_t;
94
95
typedef struct s_xdemitcb {
xdiff/xpatience.c
+37
-5
@@ -62,6 +62,12 @@ struct hashmap {
62
* initially, "next" reflects only the order in file1.
63
*/
64
struct entry *next, *previous;
65
+
66
+ /*
67
+ * If 1, this entry can serve as an anchor. See
68
+ * Documentation/diff-options.txt for more information.
69
+ */
70
+ unsigned anchor : 1;
71
} *entries, *first, *last;
72
/* were common records found? */
73
unsigned long has_matches;
@@ -70,8 +76,19 @@ struct hashmap {
76
xpparam_t const *xpp;
77
};
78
79
+static int is_anchor(xpparam_t const *xpp, const char *line)
80
+{
81
+ int i;
82
+ for (i = 0; i < xpp->anchors_nr; i++) {
83
+ if (!strncmp(line, xpp->anchors[i], strlen(xpp->anchors[i])))
84
+ return 1;
85
+ }
86
+ return 0;
87
+}
88
+
89
/* The argument "pass" is 1 for the first file, 2 for the second. */
74
-static void insert_record(int line, struct hashmap *map, int pass)
90
+static void insert_record(xpparam_t const *xpp, int line, struct hashmap *map,
91
+ int pass)
92
{
93
xrecord_t **records = pass == 1 ?
94
map->env->xdf1.recs : map->env->xdf2.recs;
@@ -110,6 +127,7 @@ static void insert_record(int line, struct hashmap *map, int pass)
127
return;
128
map->entries[index].line1 = line;
129
map->entries[index].hash = record->ha;
130
+ map->entries[index].anchor = is_anchor(xpp, map->env->xdf1.recs[line - 1]->ptr);
131
if (!map->first)
132
map->first = map->entries + index;
133
if (map->last) {
@@ -147,11 +165,11 @@ static int fill_hashmap(mmfile_t *file1, mmfile_t *file2,
165
166
/* First, fill with entries from the first file */
167
while (count1--)
150
- insert_record(line1++, result, 1);
168
+ insert_record(xpp, line1++, result, 1);
169
170
/* Then search for matches in the second file */
171
while (count2--)
154
- insert_record(line2++, result, 2);
172
+ insert_record(xpp, line2++, result, 2);
173
174
return 0;
175
}
@@ -192,14 +210,28 @@ static struct entry *find_longest_common_sequence(struct hashmap *map)
210
int longest = 0, i;
211
struct entry *entry;
212
213
+ /*
214
+ * If not -1, this entry in sequence must never be overridden.
215
+ * Therefore, overriding entries before this has no effect, so
216
+ * do not do that either.
217
+ */
218
+ int anchor_i = -1;
219
+
220
for (entry = map->first; entry; entry = entry->next) {
221
if (!entry->line2 || entry->line2 == NON_UNIQUE)
222
continue;
223
i = binary_search(sequence, longest, entry);
224
entry->previous = i < 0 ? NULL : sequence[i];
200
- sequence[++i] = entry;
201
- if (i == longest)
225
+ ++i;
226
+ if (i <= anchor_i)
227
+ continue;
228
+ sequence[i] = entry;
229
+ if (entry->anchor) {
230
+ anchor_i = i;
231
+ longest = anchor_i + 1;
232
+ } else if (i == longest) {
233
longest++;
234
+ }
235
}
236
237
/* No common unique lines were found */