Skip to content

Commit 4e76832

Browse files
peffgitster
authored andcommitted
blame: output porcelain "previous" header for each file
It's possible for content currently found in one file to have originated in two separate files, each of which may have been modified in some single older commit. The --porcelain output generates an incorrect "previous" header in this case, whereas --line-porcelain gets it right. The problem is that the porcelain output tries to omit repeated details of commits, and treats "previous" as a property of the commit, when it is really a property of the blamed block of lines. Let's look at an example. In a case like this, you might see this output from --line-porcelain: SOME_SHA1 1 1 1 author ... committer ... previous SOME_SHA1^ file_one filename file_one ...some line content... SOME_SHA1 2 1 1 author ... committer ... previous SOME_SHA1^ file_two filename file_two ...some different content.... The "filename" fields tell us that the two lines are from two different files. But notice that the filename also appears in the "previous" field, which tells us where to start a re-blame. The second content line never appeared in file_one at all, so we would obviously need to re-blame from file_two (or possibly even some other file, if had just been renamed to file_two in SOME_SHA1). So far so good. Now here's what --porcelain looks like: SOME_SHA1 1 1 1 author ... committer ... previous SOME_SHA1^ file_one filename file_one ...some line content... SOME_SHA1 2 1 1 filename file_two ...some different content.... We've dropped the author and committer fields from the second line, as they would just be repeats. But we can't omit "filename", because it depends on the actual block of blamed lines, not just the commit. This is handled by emit_porcelain_details(), which will show the filename either if it is the first mention of the commit _or_ if the commit has multiple paths in it. But we don't give "previous" the same handling. It's written inside emit_one_suspect_detail(), which bails early if we've already seen that commit. And so the output above is wrong; a reader would assume that the correct place to re-blame line two is from file_one, but that's obviously nonsense. Let's treat "previous" the same as "filename", and show it fresh whenever we know we are in a confusing case like this. Signed-off-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent ed58d80 commit 4e76832

File tree

2 files changed

+131
-9
lines changed

2 files changed

+131
-9
lines changed

builtin/blame.c

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,13 +1700,23 @@ static void get_commit_info(struct commit *commit,
17001700
}
17011701

17021702
/*
1703+
* Write out any suspect information which depends on the path. This must be
1704+
* handled separately from emit_one_suspect_detail(), because a given commit
1705+
* may have changes in multiple paths. So this needs to appear each time
1706+
* we mention a new group.
1707+
*
17031708
* To allow LF and other nonportable characters in pathnames,
17041709
* they are c-style quoted as needed.
17051710
*/
1706-
static void write_filename_info(const char *path)
1711+
static void write_filename_info(struct origin *suspect)
17071712
{
1713+
if (suspect->previous) {
1714+
struct origin *prev = suspect->previous;
1715+
printf("previous %s ", oid_to_hex(&prev->commit->object.oid));
1716+
write_name_quoted(prev->path, stdout, '\n');
1717+
}
17081718
printf("filename ");
1709-
write_name_quoted(path, stdout, '\n');
1719+
write_name_quoted(suspect->path, stdout, '\n');
17101720
}
17111721

17121722
/*
@@ -1735,11 +1745,6 @@ static int emit_one_suspect_detail(struct origin *suspect, int repeat)
17351745
printf("summary %s\n", ci.summary.buf);
17361746
if (suspect->commit->object.flags & UNINTERESTING)
17371747
printf("boundary\n");
1738-
if (suspect->previous) {
1739-
struct origin *prev = suspect->previous;
1740-
printf("previous %s ", oid_to_hex(&prev->commit->object.oid));
1741-
write_name_quoted(prev->path, stdout, '\n');
1742-
}
17431748

17441749
commit_info_destroy(&ci);
17451750

@@ -1760,7 +1765,7 @@ static void found_guilty_entry(struct blame_entry *ent,
17601765
oid_to_hex(&suspect->commit->object.oid),
17611766
ent->s_lno + 1, ent->lno + 1, ent->num_lines);
17621767
emit_one_suspect_detail(suspect, 0);
1763-
write_filename_info(suspect->path);
1768+
write_filename_info(suspect);
17641769
maybe_flush_or_die(stdout, "stdout");
17651770
}
17661771
pi->blamed_lines += ent->num_lines;
@@ -1884,7 +1889,7 @@ static void emit_porcelain_details(struct origin *suspect, int repeat)
18841889
{
18851890
if (emit_one_suspect_detail(suspect, repeat) ||
18861891
(suspect->commit->object.flags & MORE_THAN_ONE_PATH))
1887-
write_filename_info(suspect->path);
1892+
write_filename_info(suspect);
18881893
}
18891894

18901895
static void emit_porcelain(struct scoreboard *sb, struct blame_entry *ent,

t/t8011-blame-split-file.sh

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/bin/sh
2+
3+
test_description='
4+
The general idea is that we have a single file whose lines come from
5+
multiple other files, and those individual files were modified in the same
6+
commits. That means that we will see the same commit in multiple contexts,
7+
and each one should be attributed to the correct file.
8+
9+
Note that we need to use "blame -C" to find the commit for all lines. We will
10+
not bother testing that the non-C case fails to find it. That is how blame
11+
behaves now, but it is not a property we want to make sure is retained.
12+
'
13+
. ./test-lib.sh
14+
15+
# help avoid typing and reading long strings of similar lines
16+
# in the tests below
17+
generate_expect () {
18+
while read nr data
19+
do
20+
i=0
21+
while test $i -lt $nr
22+
do
23+
echo $data
24+
i=$((i + 1))
25+
done
26+
done
27+
}
28+
29+
test_expect_success 'setup split file case' '
30+
# use lines long enough to trigger content detection
31+
test_seq 1000 1010 >one &&
32+
test_seq 2000 2010 >two &&
33+
git add one two &&
34+
test_commit base &&
35+
36+
sed "6s/^/modified /" <one >one.tmp &&
37+
mv one.tmp one &&
38+
sed "6s/^/modified /" <two >two.tmp &&
39+
mv two.tmp two &&
40+
git add -u &&
41+
test_commit modified &&
42+
43+
cat one two >combined &&
44+
git add combined &&
45+
git rm one two &&
46+
test_commit combined
47+
'
48+
49+
test_expect_success 'setup simulated porcelain' '
50+
# This just reads porcelain-ish output and tries
51+
# to output the value of a given field for each line (either by
52+
# reading the field that accompanies this line, or referencing
53+
# the information found last time the commit was mentioned).
54+
cat >read-porcelain.pl <<-\EOF
55+
my $field = shift;
56+
while (<>) {
57+
if (/^[0-9a-f]{40} /) {
58+
flush();
59+
$hash = $&;
60+
} elsif (/^$field (.*)/) {
61+
$cache{$hash} = $1;
62+
}
63+
}
64+
flush();
65+
66+
sub flush {
67+
return unless defined $hash;
68+
if (defined $cache{$hash}) {
69+
print "$cache{$hash}\n";
70+
} else {
71+
print "NONE\n";
72+
}
73+
}
74+
EOF
75+
'
76+
77+
for output in porcelain line-porcelain
78+
do
79+
test_expect_success "generate --$output output" '
80+
git blame --root -C --$output combined >output
81+
'
82+
83+
test_expect_success "$output output finds correct commits" '
84+
generate_expect >expect <<-\EOF &&
85+
5 base
86+
1 modified
87+
10 base
88+
1 modified
89+
5 base
90+
EOF
91+
perl read-porcelain.pl summary <output >actual &&
92+
test_cmp expect actual
93+
'
94+
95+
test_expect_success "$output output shows correct filenames" '
96+
generate_expect >expect <<-\EOF &&
97+
11 one
98+
11 two
99+
EOF
100+
perl read-porcelain.pl filename <output >actual &&
101+
test_cmp expect actual
102+
'
103+
104+
test_expect_success "$output output shows correct previous pointer" '
105+
generate_expect >expect <<-EOF &&
106+
5 NONE
107+
1 $(git rev-parse modified^) one
108+
10 NONE
109+
1 $(git rev-parse modified^) two
110+
5 NONE
111+
EOF
112+
perl read-porcelain.pl previous <output >actual &&
113+
test_cmp expect actual
114+
'
115+
done
116+
117+
test_done

0 commit comments

Comments
 (0)