Skip to content

Commit 48edf3a

Browse files
peffgitster
authored andcommitted
diff: clear emitted_symbols flag after use
There's an odd bug when "log --color-moved" is used with the combination of "--cc --stat -p": the stat for merge commits is erroneously shown with the diff of the _next_ commit. The included test demonstrates the issue. Our history looks something like this: A-B-M--D \ / C When we run "git log --cc --stat -p --color-moved" starting at D, we get this sequence of events: 1. The diff for D is using -p, so diff_flush() calls into diff_flush_patch_all_file_pairs(). There we see that o->color_moved is in effect, so we point o->emitted_symbols to a static local struct, causing diff_flush_patch() to queue the symbols instead of actually writing them out. We then do our move detection, emit the symbols, and clear the struct. But we leave o->emitted_symbols pointing to our struct. 2. Next we compute the diff for M. This is a merge, so we use the combined diff code. In find_paths_generic(), we compute the pairwise diff between each commit and its parent. Normally this is done with DIFF_FORMAT_NO_OUTPUT, since we're just looking for intersecting paths. But since "--stat --cc" shows the first-parent stat, and since we're computing that diff anyway, we enable DIFF_FORMAT_DIFFSTAT for the first parent. This outputs the stat information immediately, saving us from running a separate first-parent diff later. But where does that output go? Normally it goes directly to stdout, but because o->emitted_symbols is set, we queue it. As a result, we don't actually print the diffstat for the merge commit (yet), which is wrong. 3. Next we compute the diff for C. We're actually showing a patch again, so we end up in diff_flush_patch_all_file_pairs(), but this time we have the queued stat from step 2 waiting in our struct. We add new elements to it for C's diff, and then flush the whole thing. And we see the diffstat from M as part of C's diff, which is wrong. So triggering the bug really does require the combination of all of those options. To fix it, we can simply restore o->emitted_symbols to NULL after flushing it, so that it does not affect anything outside of diff_flush_patch_all_file_pairs(). This intuitively makes sense, since nobody outside of that function is going to bother flushing it, so we would not want them to write to it either. In fact, we could take this a step further and turn the local "esm" struct into a non-static variable that goes away after the function ends. However, since it contains a dynamically sized array, we benefit from amortizing the cost of allocations over many calls. So we'll leave it as static to retain that benefit. But let's push the zero-ing of esm.nr into the conditional for "if (o->emitted_symbols)" to make it clear that we do not expect esm to hold any values if we did not just try to use it. With the code as it is written now, if we did encounter such a case (which I think would be a bug), we'd silently leak those values without even bothering to display them. With this change, we'd at least eventually show them, and somebody would notice. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 426fd36 commit 48edf3a

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

diff.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5885,8 +5885,10 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)
58855885

58865886
for (i = 0; i < esm.nr; i++)
58875887
free((void *)esm.buf[i].line);
5888+
esm.nr = 0;
5889+
5890+
o->emitted_symbols = NULL;
58885891
}
5889-
esm.nr = 0;
58905892
}
58915893

58925894
void diff_flush(struct diff_options *options)

t/t4066-diff-emit-delay.sh

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/bin/sh
2+
3+
test_description='test combined/stat/moved interaction'
4+
. ./test-lib.sh
5+
6+
# This test covers a weird 3-way interaction between "--cc -p", which will run
7+
# the combined diff code, along with "--stat", which will be computed as a
8+
# first-parent stat during the combined diff, and "--color-moved", which
9+
# enables the emitted_symbols list to store the diff in memory.
10+
11+
test_expect_success 'set up history with a merge' '
12+
test_commit A &&
13+
test_commit B &&
14+
git checkout -b side HEAD^ &&
15+
test_commit C &&
16+
git merge -m M master &&
17+
test_commit D
18+
'
19+
20+
test_expect_success 'log --cc -p --stat --color-moved' '
21+
cat >expect <<-\EOF &&
22+
commit D
23+
---
24+
D.t | 1 +
25+
1 file changed, 1 insertion(+)
26+
27+
diff --git a/D.t b/D.t
28+
new file mode 100644
29+
index 0000000..1784810
30+
--- /dev/null
31+
+++ b/D.t
32+
@@ -0,0 +1 @@
33+
+D
34+
commit M
35+
36+
B.t | 1 +
37+
1 file changed, 1 insertion(+)
38+
commit C
39+
---
40+
C.t | 1 +
41+
1 file changed, 1 insertion(+)
42+
43+
diff --git a/C.t b/C.t
44+
new file mode 100644
45+
index 0000000..3cc58df
46+
--- /dev/null
47+
+++ b/C.t
48+
@@ -0,0 +1 @@
49+
+C
50+
commit B
51+
---
52+
B.t | 1 +
53+
1 file changed, 1 insertion(+)
54+
55+
diff --git a/B.t b/B.t
56+
new file mode 100644
57+
index 0000000..223b783
58+
--- /dev/null
59+
+++ b/B.t
60+
@@ -0,0 +1 @@
61+
+B
62+
commit A
63+
---
64+
A.t | 1 +
65+
1 file changed, 1 insertion(+)
66+
67+
diff --git a/A.t b/A.t
68+
new file mode 100644
69+
index 0000000..f70f10e
70+
--- /dev/null
71+
+++ b/A.t
72+
@@ -0,0 +1 @@
73+
+A
74+
EOF
75+
git log --format="commit %s" --cc -p --stat --color-moved >actual &&
76+
test_cmp expect actual
77+
'
78+
79+
test_done

0 commit comments

Comments
 (0)