Skip to content

Commit 39ff53a

Browse files
committed
blame: consult diff process for zero-hunk detection
When a diff process is configured via diff.<driver>.process, consult it during blame's per-commit diffing. If the process returns zero 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 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 c25647c commit 39ff53a

3 files changed

Lines changed: 74 additions & 4 deletions

File tree

Documentation/gitattributes.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,9 @@ The tool responds with lines of the form
857857

858858
If the tool returns zero hunks with `status=success`, Git treats
859859
the file as having no changes and produces no diff output.
860+
`git blame` also consults the diff process and skips commits
861+
where it reports zero hunks, attributing lines to earlier commits
862+
instead.
860863

861864
Tools should ignore unknown keys in the per-file request to
862865
remain forward-compatible.

blame.c

Lines changed: 39 additions & 4 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 "userdiff.h"
2224
#include "alloc.h"
2325
#include "commit-slab.h"
2426
#include "bloom.h"
@@ -315,16 +317,47 @@ static struct commit *fake_working_tree_commit(struct repository *r,
315317

316318

317319
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)
320+
xdl_emit_hunk_consume_func_t hunk_func, void *cb_data,
321+
int xdl_opts, struct index_state *istate,
322+
const char *path)
319323
{
320324
xpparam_t xpp = {0};
321325
xdemitconf_t xecfg = {0};
322326
xdemitcb_t ecb = {NULL};
327+
struct xdl_hunk *ext_hunks = NULL;
328+
int ret;
323329

324330
xpp.flags = xdl_opts;
325331
xecfg.hunk_func = hunk_func;
326332
ecb.priv = cb_data;
327-
return xdi_diff(file_a, file_b, &xpp, &xecfg, &ecb);
333+
334+
if (path && istate) {
335+
struct userdiff_driver *drv;
336+
drv = userdiff_find_by_path(istate, path);
337+
if (drv && drv->process) {
338+
size_t nr = 0;
339+
if (!diff_process_get_hunks(drv, path,
340+
file_a->ptr, file_a->size,
341+
file_b->ptr, file_b->size,
342+
&ext_hunks, &nr)) {
343+
if (!nr) {
344+
/*
345+
* Zero hunks: the diff process
346+
* considers these files equivalent.
347+
* Skip so blame looks past this
348+
* commit.
349+
*/
350+
return 0;
351+
}
352+
xpp.external_hunks = ext_hunks;
353+
xpp.external_hunks_nr = nr;
354+
}
355+
}
356+
}
357+
358+
ret = xdi_diff(file_a, file_b, &xpp, &xecfg, &ecb);
359+
free(ext_hunks);
360+
return ret;
328361
}
329362

330363
static const char *get_next_line(const char *start, const char *end)
@@ -1961,7 +1994,8 @@ static void pass_blame_to_parent(struct blame_scoreboard *sb,
19611994
&sb->num_read_blob, ignore_diffs);
19621995
sb->num_get_patch++;
19631996

1964-
if (diff_hunks(&file_p, &file_o, blame_chunk_cb, &d, sb->xdl_opts))
1997+
if (diff_hunks(&file_p, &file_o, blame_chunk_cb, &d, sb->xdl_opts,
1998+
sb->revs->diffopt.repo->index, target->path))
19651999
die("unable to generate diff (%s -> %s)",
19662000
oid_to_hex(&parent->commit->object.oid),
19672001
oid_to_hex(&target->commit->object.oid));
@@ -2114,7 +2148,8 @@ static void find_copy_in_blob(struct blame_scoreboard *sb,
21142148
* file_p partially may match that image.
21152149
*/
21162150
memset(split, 0, sizeof(struct blame_entry [3]));
2117-
if (diff_hunks(file_p, &file_o, handle_split_cb, &d, sb->xdl_opts))
2151+
if (diff_hunks(file_p, &file_o, handle_split_cb, &d, sb->xdl_opts,
2152+
NULL, NULL))
21182153
die("unable to generate diff (%s)",
21192154
oid_to_hex(&parent->commit->object.oid));
21202155
/* remainder, if any, all match the preimage */

t/t4080-diff-process.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,4 +335,36 @@ test_expect_success PYTHON 'diff process zero hunks suppresses diff output' '
335335
test_must_be_empty actual
336336
'
337337

338+
test_expect_success PYTHON 'blame skips commits with zero hunks from diff process' '
339+
cat >blame.c <<-\EOF &&
340+
int main(void)
341+
{
342+
return 0;
343+
}
344+
EOF
345+
git add blame.c &&
346+
git commit -m "add blame.c" &&
347+
348+
cat >blame.c <<-\EOF &&
349+
int main(void)
350+
{
351+
return 0;
352+
}
353+
EOF
354+
git add blame.c &&
355+
git commit -m "reformat blame.c" &&
356+
BLAME_COMMIT=$(git rev-parse --short HEAD) &&
357+
358+
# Without zero-hunk mode, blame attributes the change.
359+
git blame blame.c >without &&
360+
test_grep "$BLAME_COMMIT" without &&
361+
362+
# With zero-hunk mode, the process considers the files equivalent
363+
# and blame skips the reformat commit.
364+
git -c diff.cdiff.process="$BACKEND --mode=zero-hunk" \
365+
blame blame.c >with &&
366+
! test_grep "$BLAME_COMMIT" with
367+
'
368+
369+
338370
test_done

0 commit comments

Comments
 (0)