Reuse the line_range_filter in builtin_diffstat() so the stat formats
count only the lines within the tracked range. When a filepair carries
line_ranges, the filter wraps diffstat_consume() as its output callback,
forwarding only the lines inside the range for counting.
flush_range_hunk() replays buffered content through diffstat_consume(),
which ignores synthetic @@ headers since it only counts '+' and '-'
lines.
Expand the output format allowlist in setup_revisions() to accept
--stat, --numstat, and --shortstat with -L.
Leave --dirstat out of the allowlist so it is rejected like any other
unsupported format. Its default mode counts each file's whole-file
byte damage via diffcore_count_changes(), outside the line-based
pipeline that the -L filter scopes, so bare --dirstat cannot honor the
tracked range. The --dirstat=lines mode could: it aggregates the same
per-file line counts as --numstat, which -L already scopes. But
accepting only that sub-mode while bare --dirstat keeps erroring is a
confusing split, so the whole format is deferred to a follow-up;
--numstat already reports the exact per-file counts within the tracked
range.
Also drop "yet" from the generic -L rejection message ("does not
yet support the requested diff format"). Some rejected formats do
not fit a line range at all, so "yet" wrongly implied they are all
just awaiting support.
Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Michael Montalbo committedJun 27, 2026 at 17:28 UTC660aae7323bca80e4441479ccd3b83e60dc16477
4 files changed+155-26
Documentation/line-range-options.adoc
+8-4
index 72f639b5e7..a111a492b4 100644--- a/Documentation/line-range-options.adoc+++ b/Documentation/line-range-options.adoc@@ -9,10 +9,14 @@ _<start>_ and _<end>_ (or _<funcname>_) must exist in the starting revision. You can specify this option more than once. Implies `--patch`. Patch output can be suppressed using `--no-patch`.- Non-patch diff formats `--raw`, `--name-only`, `--name-status`,- and `--summary` are supported. Diff stat formats- (`--stat`, `--numstat`, `--shortstat`, `--dirstat`) are not- currently implemented.+ The following non-patch diff formats are supported: `--raw`,+ `--name-only`, `--name-status`, `--summary`,+ `--stat`, `--numstat`, and `--shortstat`.+ The stat formats count only lines within the tracked range.+ `--dirstat` is not supported+ with `-L`: it summarizes change as each directory's share of+ the total churn, not as counts for the tracked lines. Use+ `--numstat` for exact per-file counts within the range. + Patch formatting options such as `--word-diff`, `--color-moved`, `--no-prefix`, and whitespace options (`-w`, `-b`) are supported,
diff.c
+12-1
index 6233a96bf0..026fafeb90 100644--- a/diff.c+++ b/diff.c@@ -4289,7 +4289,18 @@ static void builtin_diffstat(const char *name_a, const char *name_b, xecfg.ctxlen = o->context; xecfg.interhunkctxlen = o->interhunkcontext; xecfg.flags = XDL_EMIT_NO_HUNK_HDR;- if (xdi_diff_outf(&mf1, &mf2, NULL,++ 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);
revision.c
+4-2
index 6a8101e8b7..2c76e15778 100644--- a/revision.c+++ b/revision.c@@ -3193,8 +3193,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s (revs->diffopt.output_format & ~(DIFF_FORMAT_PATCH | DIFF_FORMAT_NO_OUTPUT | DIFF_FORMAT_RAW | DIFF_FORMAT_NAME |- DIFF_FORMAT_NAME_STATUS | DIFF_FORMAT_SUMMARY))))- die(_("-L does not yet support the requested diff format"));+ DIFF_FORMAT_NAME_STATUS | DIFF_FORMAT_SUMMARY |+ DIFF_FORMAT_NUMSTAT | DIFF_FORMAT_DIFFSTAT |+ DIFF_FORMAT_SHORTSTAT))))+ die(_("-L does not support the requested diff format")); if (revs->expand_tabs_in_log < 0) revs->expand_tabs_in_log = revs->expand_tabs_in_log_default;
t/t4211-line-log.sh
+131-19
index e9691066de..b9ca336dbc 100755--- a/t/t4211-line-log.sh+++ b/t/t4211-line-log.sh@@ -176,24 +176,15 @@ test_expect_success '--name-status shows status and path' ' test_grep ! "^@@" actual '-test_expect_success '--stat is not yet supported with -L' '- test_must_fail git log -L1,24:b.c --stat 2>err &&- test_grep "does not yet support" err-'--test_expect_success '--numstat is not yet supported with -L' '- test_must_fail git log -L1,24:b.c --numstat 2>err &&- test_grep "does not yet support" err-'--test_expect_success '--shortstat is not yet supported with -L' '- test_must_fail git log -L1,24:b.c --shortstat 2>err &&- test_grep "does not yet support" err-'--test_expect_success '--dirstat is not yet supported with -L' '+test_expect_success '--dirstat is not supported with -L' '+ # --dirstat is not supported with -L: its default mode measures+ # whole-file change, not the tracked lines, and the+ # --dirstat=lines variant is deferred too, so both forms are+ # rejected like any other unsupported format. test_must_fail git log -L1,24:b.c --dirstat 2>err &&- test_grep "does not yet support" err+ test_grep "does not support" err &&+ test_must_fail git log -L1,24:b.c --dirstat=lines 2>err &&+ test_grep "does not support" err ' test_expect_success 'setup for checking fancy rename following' '@@ -887,9 +878,9 @@ test_expect_success '-L with -S suppresses non-matching commits' ' test_cmp expect actual '-test_expect_success '--full-diff is not yet supported with -L' '+test_expect_success '--full-diff is not supported with -L' ' test_must_fail git log -L1,24:b.c --full-diff 2>err &&- test_grep "does not yet support" err+ test_grep "does not support" err ' test_expect_success '-L --oneline has no extra blank line before diff' '@@ -900,6 +891,127 @@ test_expect_success '-L --oneline has no extra blank line before diff' ' test_grep "^diff --git" line2 '+test_expect_success 'setup for stat range-scoping tests' '+ git checkout --orphan stat-scoping &&+ git reset --hard &&+ cat >file.c <<-\EOF &&+ int func1()+ {+ return F1;+ }++ int func2()+ {+ return F2;+ }+ EOF+ git add file.c &&+ test_tick &&+ git commit -m "Add func1() and func2()" &&++ # Modify both functions in a single commit so that+ # whole-file stats differ from the counts for the tracked range.+ sed -e "s/F1/F1 + 1/" -e "s/F2/F2 + 2/" file.c >tmp &&+ mv tmp file.c &&+ git commit -a -m "Modify both functions"+'++test_expect_success '--numstat counts only lines in tracked range' '+ # "Modify both functions" changes one line in func1 and one in+ # func2. Whole-file numstat would show 2 added, 2 deleted.+ # numstat for func2 within the tracked range should show only 1 and 1.+ git log -L:func2:file.c --numstat --format=%s -1 >actual &&+ test_grep "Modify both functions" actual &&+ test_grep "^1 1 file.c$" actual &&+ test_grep ! "^diff --git" actual+'++test_expect_success '--numstat counts only additions for root commit' '+ # Root commit creates both func1 (4 lines) and func2 (4 lines).+ # Whole-file numstat would show 9 lines added. numstat for func2+ # within the tracked range should show only 4.+ git log -L:func2:file.c --numstat --format=%s >actual &&+ test_grep "Add func1() and func2()" actual &&+ test_grep "^4 0 file.c$" actual &&+ test_grep ! "^diff --git" actual+'++test_expect_success '--stat counts only lines in tracked range' '+ git log -L:func2:file.c --stat --format=%s -1 >actual &&+ test_grep "Modify both functions" actual &&+ test_grep "file.c |" actual &&+ test_grep "1 insertion" actual &&+ test_grep "1 deletion" actual &&+ test_grep ! "^diff --git" actual+'++test_expect_success '--shortstat counts only lines in tracked range' '+ # --shortstat prints only the summary line: no per-file "file.c |"+ # line. Counts cover only the tracked range, as for --numstat above.+ git log -L:func2:file.c --shortstat --format=%s -1 >actual &&+ test_grep "Modify both functions" actual &&+ test_grep "1 insertion" actual &&+ test_grep "1 deletion" actual &&+ test_grep ! "file.c |" actual &&+ test_grep ! "^diff --git" actual+'++test_expect_success '--numstat across renames and multiple commits' '+ # parallel-change carries the tracked function f across an a.c -> b.c+ # rename and a merge of two parallel histories. With -M, --numstat+ # follows the rename and reports added/removed counts for f within+ # the tracked range (not whole-file) per commit; the file column flips from+ # b.c to a.c at the rename as the walk goes back in time. Commits+ # that do not change the range of f emit no row (the merge and the+ # pure file-move produce nothing), so there are fewer rows than+ # commits.+ git checkout parallel-change &&+ git log -M -L ":f:b.c" --format= --numstat >actual &&+ cat >expect <<-\EOF &&+ 1 1 b.c+ 1 1 a.c+ 1 1 a.c+ 1 1 a.c+ 1 0 a.c+ 13 0 a.c+ EOF+ test_cmp expect actual+'++test_expect_success '-L multiple ranges with --numstat excludes untracked change' '+ git checkout --orphan multi-range &&+ git reset --hard &&+ cat >m.c <<-\EOF &&+ int func1()+ {+ return F1;+ }++ int func2()+ {+ return F2;+ }++ int func3()+ {+ return F3;+ }+ EOF+ git add m.c &&+ test_tick &&+ git commit -m "add m.c" &&+ # Change all three functions but track only func1 and func2.+ # Whole-file numstat would be 3 3; a 2 2 result proves the+ # untracked func3 change is excluded and the two ranges just sum.+ sed -e "s/F1/F1 + 1/" -e "s/F2/F2 + 2/" -e "s/F3/F3 + 3/" m.c >tmp &&+ mv tmp m.c &&+ git commit -a -m "Modify all three functions" &&+ git log -L:func1:m.c -L:func2:m.c --numstat --format=%s -1 >actual &&+ test_grep "Modify all three functions" actual &&+ test_grep "^2 2 m.c$" actual &&+ test_grep ! "^3 3 m.c$" actual+'+ test_expect_success '--summary shows new file on root commit' ' git checkout parent-oids && git log -L:func2:file.c --summary --format= >actual &&