Skip to content

Commit 3dadafa

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. A new diff_hunks_xpp() variant accepts a pre-populated xpparam_t so callers can pass external hunks, while the existing diff_hunks() retains its original signature and behavior. 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 6ec6716 commit 3dadafa

2 files changed

Lines changed: 136 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: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,4 +445,109 @@ test_expect_success 'diff process skipped when tool omits capability' '
445445
test_must_be_empty stderr
446446
'
447447

448+
#
449+
# Blame integration.
450+
#
451+
452+
test_expect_success 'blame uses tool-provided hunks' '
453+
cat >blame-hunk.c <<-\EOF &&
454+
line1
455+
line2
456+
line3
457+
line4
458+
original5
459+
original6
460+
line7
461+
line8
462+
line9
463+
line10
464+
EOF
465+
git add blame-hunk.c &&
466+
git commit -m "add blame-hunk.c" &&
467+
ORIG=$(git rev-parse --short HEAD) &&
468+
469+
cat >blame-hunk.c <<-\EOF &&
470+
line1
471+
line2
472+
line3
473+
line4
474+
changed5
475+
changed6
476+
line7
477+
line8
478+
changed9
479+
changed10
480+
EOF
481+
git add blame-hunk.c &&
482+
git commit -m "change blame-hunk.c" &&
483+
CHANGE=$(git rev-parse --short HEAD) &&
484+
485+
# With fixed-hunk mode the tool reports only lines 5-6 as changed,
486+
# so blame should attribute lines 9-10 to the original commit
487+
# even though the builtin diff would show them as changed.
488+
git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk" \
489+
blame blame-hunk.c >actual &&
490+
sed -n "9p" actual >line9 &&
491+
sed -n "10p" actual >line10 &&
492+
test_grep "$ORIG" line9 &&
493+
test_grep "$ORIG" line10 &&
494+
sed -n "5p" actual >line5 &&
495+
sed -n "6p" actual >line6 &&
496+
test_grep "$CHANGE" line5 &&
497+
test_grep "$CHANGE" line6
498+
'
499+
500+
test_expect_success 'blame skips commits with no hunks from diff process' '
501+
cat >blame.c <<-\EOF &&
502+
int main(void) {
503+
return 0;
504+
}
505+
EOF
506+
git add blame.c &&
507+
git commit -m "add blame.c" &&
508+
ORIG_COMMIT=$(git rev-parse --short HEAD) &&
509+
510+
cat >blame.c <<-\EOF &&
511+
int main(void)
512+
{
513+
return 0;
514+
}
515+
EOF
516+
git add blame.c &&
517+
git commit -m "reformat blame.c" &&
518+
BLAME_COMMIT=$(git rev-parse --short HEAD) &&
519+
520+
# Without no-hunks mode, blame attributes the change.
521+
git blame blame.c >without &&
522+
test_grep "$BLAME_COMMIT" without &&
523+
524+
# With no-hunks mode, the process considers the files equivalent
525+
# and blame skips the reformat commit, attributing to the original.
526+
git -c diff.cdiff.process="$BACKEND --mode=no-hunks" \
527+
blame blame.c >with &&
528+
test_grep ! "$BLAME_COMMIT" with &&
529+
test_grep "$ORIG_COMMIT" with
530+
'
531+
532+
test_expect_success 'blame --no-ext-diff bypasses diff process' '
533+
test_when_finished "rm -f backend.log" &&
534+
git -c diff.cdiff.process="$BACKEND --mode=no-hunks --log=backend.log" \
535+
blame --no-ext-diff blame.c >actual &&
536+
# Without the process, blame attributes the reformat commit normally.
537+
test_grep "$BLAME_COMMIT" actual &&
538+
test_path_is_missing backend.log
539+
'
540+
541+
test_expect_success 'blame --no-ext-diff uses builtin hunks' '
542+
# fixed-hunk mode would narrow blame to lines 5-6, but
543+
# --no-ext-diff should bypass it and use the builtin diff.
544+
test_when_finished "rm -f backend.log" &&
545+
git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk --log=backend.log" \
546+
blame --no-ext-diff blame-hunk.c >actual &&
547+
# Builtin diff attributes lines 9-10 to the change commit.
548+
sed -n "9p" actual >line9 &&
549+
test_grep "$CHANGE" line9 &&
550+
test_path_is_missing backend.log
551+
'
552+
448553
test_done

0 commit comments

Comments
 (0)