data, each terminated with a flush packet. The pathname is relative
to the repository root. When `diff.<name>.textconv` is also set,
the tool receives the textconv-transformed content rather than the
-raw blob. Git does not send binary files to the diff process.
+raw blob, matching what the consuming feature itself diffs: patch
+output is textconv'd, the summary formats (noted below) are not, and
+`git blame` applies textconv only under `--textconv`. Git does not
+send binary files to the diff process.
-----------------------
packet: git> command=hunks
display, exactly as it compacts its own diffs, so the tool controls
which lines are reported as changed, not the precise hunk boundaries.
Patch output features (word diff, function context, color) work
-normally. Summary formats such as `--stat` still compute their counts
-with the builtin diff for now; see "Which features consult the diff
+normally, as do summary formats like `--stat`. Not every feature
+consults the process, though; see "Which features consult the diff
process" below for the full picture and the reasoning behind it.
If no hunk lines precede the flush, followed by "success", Git
hunks without any further negotiation.
- `git blame`: a commit whose change the tool reports as equivalent is
skipped, and its lines are attributed to an earlier commit.
+- `--stat`, `--numstat`, and `--shortstat`: the inserted and deleted
+ counts come from the tool's hunks, so a file the tool calls
+ equivalent contributes no stat line, matching the empty patch that
+ `git diff` produces for it. These summary formats do not apply
+ textconv (just as the builtin summary path does not), so the tool
+ is consulted on the raw blob content even when a `textconv` is also
+ configured for patch output; this mirrors how builtin `--stat`
+ already counts raw lines rather than the textconv'd view. The
+ line-counting `--dirstat=lines` uses these same counts; the default
+ `--dirstat`, which weighs byte changes, is computed on its own and
+ does not consult the tool.
Features that ask a different question do not consult the process, by
design:
- `--raw`, `--name-only`, and `--name-status` compare object ids at
the tree level and never run a line-level diff at all.
-Some features ask "which lines changed" but still use the builtin
-algorithm for now, and may consult the process in a later change: the
-summary formats (`--stat`, `--numstat`, `--shortstat`); `git log -L`'s
-commit selection and parent range propagation (as distinct from its
-display, which is covered above); and combined diffs (`--cc` and merge
-diffs), whose protocol would have to be extended from a single old/new
-pair to one comparison per merge parent.
+Two cases ask "which lines changed" but still use the builtin
+algorithm, and may consult the process in a later change: `git log
+-L`'s commit selection and parent range propagation (as distinct from
+its display, which is covered above), and combined diffs (`--cc` and
+merge diffs), whose protocol would have to be extended from a single
+old/new pair to one comparison per merge parent.
`--no-ext-diff` and `--diff-algorithm` bypass the process entirely,
for every feature listed above. The whitespace-ignoring options
xecfg.ctxlen = o->context;
xecfg.interhunkctxlen = o->interhunkcontext;
xecfg.flags = XDL_EMIT_NO_HUNK_HDR;
-
- if (p->line_ranges) {
- struct line_range_filter lr_filter;
-
- line_range_filter_init(&lr_filter, p->line_ranges,
- diffstat_consume, diffstat);
-
- if (line_range_filter_diff(&lr_filter, &mf1, &mf2,
- &xpp, &xecfg))
+ /*
+ * Consult the diff process so --stat reflects the
+ * tool's view of which lines changed rather than the
+ * builtin line diff. --stat never applies textconv, so
+ * the tool is fed the same raw mmfiles the stat itself
+ * diffs (unlike builtin_diff, which consults the process
+ * on textconv'd content).
+ * When the tool reports the files as equivalent we skip
+ * xdiff entirely, leaving added and deleted at zero so
+ * the file is pruned below, just as builtin_diff() emits
+ * no patch for an equivalent file.
+ *
+ * Under -L, feed the tool's hunks through the same
+ * line-range filter the builtin stat uses, so a
+ * process-provided diff is scoped to the tracked range.
+ */
+ if (diff_process_fill_hunks(o, name_a, &mf1, &mf2,
+ one->oid_valid ? &one->oid : NULL,
+ two->oid_valid ? &two->oid : NULL,
+ &xpp)
+ != DIFF_PROCESS_EQUIVALENT) {
+ if (p->line_ranges) {
+ struct line_range_filter lr_filter;
+
+ line_range_filter_init(&lr_filter, p->line_ranges,
+ diffstat_consume, diffstat);
+
+ if (line_range_filter_diff(&lr_filter, &mf1, &mf2,
+ &xpp, &xecfg))
+ die("unable to generate diffstat for %s",
+ one->path);
+ } else if (xdi_diff_outf(&mf1, &mf2, NULL, diffstat_consume,
+ diffstat, &xpp, &xecfg))
die("unable to generate diffstat for %s",
one->path);
- } else if (xdi_diff_outf(&mf1, &mf2, NULL,
- diffstat_consume, diffstat, &xpp, &xecfg))
- die("unable to generate diffstat for %s", one->path);
+ }
+ free(xpp.external_hunks);
if (DIFF_FILE_VALID(one) && DIFF_FILE_VALID(two)) {
struct diffstat_file *file =
test_must_be_empty stderr
'
+test_expect_success 'diff process --stat is fed raw, not textconv, content' '
+ # Reuses textconv.c from the previous test (committed "hello
+ # world", modified to "goodbye world"). Unlike patch output,
+ # --stat does not apply textconv, so the tool sees raw lowercase
+ # content here even with a textconv configured.
+ test_when_finished "rm -f backend.log" &&
+ git -c diff.cdiff.textconv="./uppercase-filter" \
+ -c diff.cdiff.process="$BACKEND --log=backend.log" \
+ diff --stat -- textconv.c >actual 2>stderr &&
+ test_grep "pathname=textconv.c" backend.log &&
+ test_grep "old=hello world" backend.log &&
+ test_grep "new=goodbye world" backend.log &&
+ test_must_be_empty stderr
+'
+
#
# Downstream features: word diff, log, equivalent files, exit code.
#
diff --exit-code newfile.c
'
+test_expect_success 'diff process feeds --numstat counts' '
+ # fixed-hunk reports only lines 5-6 as changed, so the stat
+ # counts come from the tool (2/2), not the builtin diff (4/4).
+ test_when_finished "rm -f backend.log" &&
+ git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk --log=backend.log" \
+ diff --numstat boundary.c >actual 2>stderr &&
+ printf "2\t2\tboundary.c\n" >expect &&
+ test_cmp expect actual &&
+ test_grep "command=hunks pathname=boundary.c" backend.log &&
+ test_must_be_empty stderr
+'
+
+test_expect_success 'diff process --numstat sums multi-hunk counts' '
+ # multi-hunk reports both 2-line regions (5-6 and 9-10), so the
+ # counts add up across both hunks: 4 inserted, 4 deleted. This
+ # exercises the two-region hunk path through builtin_diffstat.
+ git -c diff.cdiff.process="$BACKEND --mode=multi-hunk" \
+ diff --numstat boundary.c >actual &&
+ printf "4\t4\tboundary.c\n" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'diff process equivalent files produce no --stat line' '
+ # A file the tool calls equivalent contributes no stat line,
+ # matching the empty patch that git diff produces for it.
+ test_when_finished "rm -f backend.log" &&
+ git -c diff.cdiff.process="$BACKEND --mode=no-hunks --log=backend.log" \
+ diff --stat worddiff.c >actual 2>stderr &&
+ test_must_be_empty actual &&
+ test_grep "command=hunks pathname=worddiff.c" backend.log &&
+ test_must_be_empty stderr
+'
+
+test_expect_success 'diff process feeds --shortstat counts' '
+ # fixed-hunk reports lines 5-6 only, so the summary counts come
+ # from the tool (2 insertions, 2 deletions), not builtin (4/4).
+ git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk" \
+ diff --shortstat boundary.c >actual &&
+ test_grep "2 insertions" actual &&
+ test_grep "2 deletions" actual
+'
+
+test_expect_success 'diff process scopes --stat to the tracked range under log -L' '
+ test_when_finished "rm -f backend.log" &&
+ cat >rangestat.c <<-\EOF &&
+ line1
+ line2
+ line3
+ line4
+ OLD5
+ OLD6
+ line7
+ line8
+ OLD9
+ OLD10
+ EOF
+ git add rangestat.c &&
+ git commit -m "add rangestat.c" &&
+
+ cat >rangestat.c <<-\EOF &&
+ line1
+ line2
+ line3
+ line4
+ NEW5
+ NEW6
+ line7
+ line8
+ NEW9
+ NEW10
+ EOF
+ git add rangestat.c &&
+ git commit -m "change rangestat.c" &&
+
+ # The file changes at lines 5-6 and 9-10, but fixed-hunk reports
+ # only 5-6. The builtin line diff counts both regions (4/4); the
+ # tool hunks flow through the same line-range filter the stat uses,
+ # so the range-scoped stat reflects the tool view instead (2/2).
+ git log --no-ext-diff -L1,10:rangestat.c --oneline --stat >builtin &&
+ test_grep "4 insertions(+), 4 deletions(-)" builtin &&
+
+ git -c diff.cdiff.process="$BACKEND --mode=fixed-hunk --log=backend.log" \
+ log -L1,10:rangestat.c --oneline --stat >actual &&
+ test_grep "2 insertions(+), 2 deletions(-)" actual &&
+ test_grep ! "4 insertions" actual &&
+ test_grep "command=hunks pathname=rangestat.c" backend.log
+'
+
+test_expect_success 'diff process equivalent file makes --stat --exit-code succeed' '
+ # The tool reports worddiff.c equivalent, so --exit-code reports
+ # no change (0); the builtin diff would report a change (1).
+ git -c diff.cdiff.process="$BACKEND --mode=no-hunks" \
+ diff --stat --exit-code worddiff.c &&
+ test_expect_code 1 git diff --no-ext-diff --stat --exit-code worddiff.c
+'
+
+test_expect_success 'diff process --numstat with mixed equivalent and changed files' '
+ test_when_finished "rm -f c.log h.log" &&
+ # Self-contained fixtures: *.c uses whole-file (changed); *.mh
+ # uses no-hunks (equivalent).
+ echo "*.mh diff=hdiff" >>.gitattributes &&
+ git add .gitattributes &&
+ printf "int a(void) { return 1; }\n" >mixed.c &&
+ printf "int b(void) { return 1; }\n" >mixed.mh &&
+ git add mixed.c mixed.mh &&
+ git commit -m "add mixed fixtures" &&
+ printf "int a(void) { return 2; }\n" >mixed.c &&
+ printf "int b(void) { return 2; }\n" >mixed.mh &&
+ git -c diff.cdiff.process="$BACKEND --mode=whole-file --log=c.log" \
+ -c diff.hdiff.process="$BACKEND --mode=no-hunks --log=h.log" \
+ diff --numstat mixed.c mixed.mh >actual 2>stderr &&
+ test_grep "mixed.c" actual &&
+ test_grep ! "mixed.mh" actual &&
+ test_grep "pathname=mixed.c" c.log &&
+ test_grep "pathname=mixed.mh" h.log &&
+ test_must_be_empty stderr
+'
+
+test_expect_success POSIXPERM 'diff process keeps mode-only change in --stat' '
+ test_when_finished "rm -f backend.log" &&
+ cat >modeonly.c <<-\EOF &&
+ int m(void) { return 1; }
+ EOF
+ git add modeonly.c &&
+ git commit -m "add modeonly.c" &&
+ cat >modeonly.c <<-\EOF &&
+ int m(void) { return 2; }
+ EOF
+ git add modeonly.c &&
+ test_chmod +x modeonly.c &&
+ git commit -m "edit and chmod modeonly.c" &&
+ # Content and mode both changed, but no-hunks reports the content
+ # equivalent. The tool is consulted (counts are zero, not the
+ # builtin 1/1), yet the mode change keeps the file from being
+ # pruned.
+ git -c diff.cdiff.process="$BACKEND --mode=no-hunks --log=backend.log" \
+ diff --stat HEAD^ HEAD >actual 2>stderr &&
+ test_grep "modeonly.c" actual &&
+ test_grep "command=hunks pathname=modeonly.c" backend.log &&
+ test_grep ! "1 insertion" actual &&
+ test_must_be_empty stderr
+'
+
+test_expect_success 'diff process not consulted for default --dirstat' '
+ # The default (change-based) --dirstat algorithm counts via its
+ # own path and never contacts the tool (here --dirstat=0 just
+ # sets a 0% threshold), so the change is still reported even
+ # though no-hunks would call it equivalent. --dirstat=lines
+ # instead uses the process-aware stat path.
+ test_when_finished "rm -f backend.log" &&
+ mkdir -p dsub &&
+ printf "a\nb\nc\n" >dsub/d.c &&
+ git add dsub/d.c &&
+ git commit -m "add dsub/d.c" &&
+ printf "a\nB\nc\n" >dsub/d.c &&
+ git -c diff.cdiff.process="$BACKEND --mode=no-hunks --log=backend.log" \
+ diff --dirstat=0 dsub/d.c >actual &&
+ test_grep "dsub" actual &&
+ test_path_is_missing backend.log
+'
+
#
# Bypass mechanisms: flags and commands that skip the diff process.
#