Skip to content

Commit 5bef9f3

Browse files
committed
blame: consult diff process for no-hunk detection
When a diff process is configured via diff.<driver>.process, consult it during blame's per-commit diffing. If the process returns no hunks for a commit's changes to a file, treat the commit as having no changes, causing blame to attribute lines to earlier commits. The consultation happens at the pass_blame_to_parent() callsite using diff_process_fill_hunks(), matching how builtin_diff() in diff.c uses the same function. The existing diff_hunks() helper is unchanged; the callsite sets up xdi_diff() directly when external hunks are involved. The copy-detection callsite is unaffected since it does not use the diff process. The subprocess is long-running (one startup cost amortized across the blame traversal), but each commit in the file's history incurs a round-trip to the tool. Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
1 parent b63b200 commit 5bef9f3

2 files changed

Lines changed: 137 additions & 9 deletions

File tree

blame.c

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#include "tag.h"
2020
#include "trace2.h"
2121
#include "blame.h"
22+
#include "diff-process.h"
23+
#include "xdiff-interface.h"
2224
#include "alloc.h"
2325
#include "commit-slab.h"
2426
#include "bloom.h"
@@ -314,17 +316,25 @@ static struct commit *fake_working_tree_commit(struct repository *r,
314316

315317

316318

317-
static int diff_hunks(mmfile_t *file_a, mmfile_t *file_b,
318-
xdl_emit_hunk_consume_func_t hunk_func, void *cb_data, int xdl_opts)
319+
static int diff_hunks_xpp(mmfile_t *file_a, mmfile_t *file_b,
320+
xdl_emit_hunk_consume_func_t hunk_func,
321+
void *cb_data, xpparam_t *xpp)
319322
{
320-
xpparam_t xpp = {0};
321323
xdemitconf_t xecfg = {0};
322324
xdemitcb_t ecb = {NULL};
323325

324-
xpp.flags = xdl_opts;
325326
xecfg.hunk_func = hunk_func;
326327
ecb.priv = cb_data;
327-
return xdi_diff(file_a, file_b, &xpp, &xecfg, &ecb);
328+
return xdi_diff(file_a, file_b, xpp, &xecfg, &ecb);
329+
}
330+
331+
static int diff_hunks(mmfile_t *file_a, mmfile_t *file_b,
332+
xdl_emit_hunk_consume_func_t hunk_func, void *cb_data, int xdl_opts)
333+
{
334+
xpparam_t xpp = {0};
335+
336+
xpp.flags = xdl_opts;
337+
return diff_hunks_xpp(file_a, file_b, hunk_func, cb_data, &xpp);
328338
}
329339

330340
static const char *get_next_line(const char *start, const char *end)
@@ -1943,6 +1953,7 @@ static void pass_blame_to_parent(struct blame_scoreboard *sb,
19431953
struct blame_origin *parent, int ignore_diffs)
19441954
{
19451955
mmfile_t file_p, file_o;
1956+
xpparam_t xpp = {0};
19461957
struct blame_chunk_cb_data d;
19471958
struct blame_entry *newdest = NULL;
19481959

@@ -1961,10 +1972,21 @@ static void pass_blame_to_parent(struct blame_scoreboard *sb,
19611972
&sb->num_read_blob, ignore_diffs);
19621973
sb->num_get_patch++;
19631974

1964-
if (diff_hunks(&file_p, &file_o, blame_chunk_cb, &d, sb->xdl_opts))
1965-
die("unable to generate diff (%s -> %s)",
1966-
oid_to_hex(&parent->commit->object.oid),
1967-
oid_to_hex(&target->commit->object.oid));
1975+
xpp.flags = sb->xdl_opts;
1976+
/*
1977+
* If the diff process considers the files equivalent,
1978+
* skip the diff so blame looks past this commit.
1979+
*/
1980+
if (diff_process_fill_hunks(&sb->revs->diffopt, target->path,
1981+
&file_p, &file_o, &xpp)
1982+
!= DIFF_PROCESS_EQUIVALENT) {
1983+
if (diff_hunks_xpp(&file_p, &file_o, blame_chunk_cb,
1984+
&d, &xpp))
1985+
die("unable to generate diff (%s -> %s)",
1986+
oid_to_hex(&parent->commit->object.oid),
1987+
oid_to_hex(&target->commit->object.oid));
1988+
}
1989+
free(xpp.external_hunks);
19681990
/* The rest are the same as the parent */
19691991
blame_chunk(&d.dstq, &d.srcq, INT_MAX, d.offset, INT_MAX, 0,
19701992
parent, target, 0);

t/t4080-diff-process.sh

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,4 +551,110 @@ test_expect_success PYTHON 'diff process fallback on overlapping hunks' '
551551
test_grep "NEW5" actual
552552
'
553553

554+
#
555+
# Blame integration.
556+
#
557+
558+
test_expect_success PYTHON 'blame uses tool-provided hunks' '
559+
cat >blame-hunk.c <<-\EOF &&
560+
line1
561+
line2
562+
line3
563+
line4
564+
original5
565+
original6
566+
line7
567+
line8
568+
line9
569+
line10
570+
EOF
571+
git add blame-hunk.c &&
572+
git commit -m "add blame-hunk.c" &&
573+
ORIG=$(git rev-parse --short HEAD) &&
574+
575+
cat >blame-hunk.c <<-\EOF &&
576+
line1
577+
line2
578+
line3
579+
line4
580+
changed5
581+
changed6
582+
line7
583+
line8
584+
changed9
585+
changed10
586+
EOF
587+
git add blame-hunk.c &&
588+
git commit -m "change blame-hunk.c" &&
589+
CHANGE=$(git rev-parse --short HEAD) &&
590+
591+
# With fixed-hunk mode the tool reports only lines 5-6 as changed,
592+
# so blame should attribute lines 9-10 to the original commit
593+
# even though the builtin diff would show them as changed.
594+
git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk" \
595+
blame blame-hunk.c >actual &&
596+
sed -n "9p" actual >line9 &&
597+
sed -n "10p" actual >line10 &&
598+
test_grep "$ORIG" line9 &&
599+
test_grep "$ORIG" line10 &&
600+
sed -n "5p" actual >line5 &&
601+
sed -n "6p" actual >line6 &&
602+
test_grep "$CHANGE" line5 &&
603+
test_grep "$CHANGE" line6
604+
'
605+
606+
test_expect_success PYTHON 'blame skips commits with no hunks from diff process' '
607+
cat >blame.c <<-\EOF &&
608+
int main(void)
609+
{
610+
return 0;
611+
}
612+
EOF
613+
git add blame.c &&
614+
git commit -m "add blame.c" &&
615+
ORIG_COMMIT=$(git rev-parse --short HEAD) &&
616+
617+
cat >blame.c <<-\EOF &&
618+
int main(void)
619+
{
620+
return 0;
621+
}
622+
EOF
623+
git add blame.c &&
624+
git commit -m "reformat blame.c" &&
625+
BLAME_COMMIT=$(git rev-parse --short HEAD) &&
626+
627+
# Without no-hunks mode, blame attributes the change.
628+
git blame blame.c >without &&
629+
test_grep "$BLAME_COMMIT" without &&
630+
631+
# With no-hunks mode, the process considers the files equivalent
632+
# and blame skips the reformat commit, attributing to the original.
633+
git -c diff.cdiff.process="$BACKEND --mode=no-hunks" \
634+
blame blame.c >with &&
635+
test_grep ! "$BLAME_COMMIT" with &&
636+
test_grep "$ORIG_COMMIT" with
637+
'
638+
639+
test_expect_success PYTHON 'blame --no-ext-diff bypasses diff process' '
640+
rm -f backend.log &&
641+
git -c diff.cdiff.process="$BACKEND --mode=no-hunks --log=backend.log" \
642+
blame --no-ext-diff blame.c >actual &&
643+
# Without the process, blame attributes the reformat commit normally.
644+
test_grep "$BLAME_COMMIT" actual &&
645+
test_path_is_missing backend.log
646+
'
647+
648+
test_expect_success PYTHON 'blame --no-ext-diff uses builtin hunks' '
649+
# fixed-hunk mode would narrow blame to lines 5-6, but
650+
# --no-ext-diff should bypass it and use the builtin diff.
651+
rm -f backend.log &&
652+
git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk --log=backend.log" \
653+
blame --no-ext-diff blame-hunk.c >actual &&
654+
# Builtin diff attributes lines 9-10 to the change commit.
655+
sed -n "9p" actual >line9 &&
656+
test_grep "$CHANGE" line9 &&
657+
test_path_is_missing backend.log
658+
'
659+
554660
test_done

0 commit comments

Comments
 (0)