From: "Michael Montalbo via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: "D. Ben Knoble" <ben.knoble@gmail.com>,
Michael Montalbo <mmontalbo@gmail.com>,
Michael Montalbo <mmontalbo@gmail.com>
Subject: [PATCH v2 5/7] line-log: support diff stat formats with -L
Date: Sat, 27 Jun 2026 17:28:59 +0000 [thread overview]
Message-ID: <3d0091b549114f6bf32c9fb7626da9fc2583e88b.1782581342.git.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2152.v2.git.1782581342.gitgitgadget@gmail.com>
From: Michael Montalbo <mmontalbo@gmail.com>
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>
---
Documentation/line-range-options.adoc | 12 ++-
diff.c | 13 ++-
revision.c | 6 +-
t/t4211-line-log.sh | 150 ++++++++++++++++++++++----
4 files changed, 155 insertions(+), 26 deletions(-)
diff --git a/Documentation/line-range-options.adoc b/Documentation/line-range-options.adoc
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 --git a/diff.c b/diff.c
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);
diff --git a/revision.c b/revision.c
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;
diff --git a/t/t4211-line-log.sh b/t/t4211-line-log.sh
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 &&
--
gitgitgadget
next prev parent reply other threads:[~2026-06-27 17:29 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-18 18:16 [PATCH 0/7] line-log: range-scope stat, check, and -G under -L Michael Montalbo via GitGitGadget
2026-06-18 18:16 ` [PATCH 1/7] diff: rename and group the line-range filter for clarity Michael Montalbo via GitGitGadget
2026-06-18 18:16 ` [PATCH 2/7] diff: simplify the line-range filter by classifying removals immediately Michael Montalbo via GitGitGadget
2026-06-18 18:16 ` [PATCH 3/7] diff: emit -L hunk headers via xdiff's formatter Michael Montalbo via GitGitGadget
2026-06-18 18:16 ` [PATCH 4/7] diff: extract a line-range diff helper for reuse Michael Montalbo via GitGitGadget
2026-06-18 18:16 ` [PATCH 5/7] line-log: support diff stat formats with -L Michael Montalbo via GitGitGadget
2026-06-18 22:00 ` Junio C Hamano
2026-06-23 2:25 ` Michael Montalbo
2026-06-18 18:16 ` [PATCH 6/7] diff: support --check with -L line ranges Michael Montalbo via GitGitGadget
2026-06-18 18:16 ` [PATCH 7/7] diffcore-pickaxe: scope -G to the -L tracked range Michael Montalbo via GitGitGadget
2026-06-27 17:28 ` [PATCH v2 0/7] line-log: scope stat, check, and -G to -L line ranges Michael Montalbo via GitGitGadget
2026-06-27 17:28 ` [PATCH v2 1/7] diff: rename and group the line-range filter for clarity Michael Montalbo via GitGitGadget
2026-06-27 17:28 ` [PATCH v2 2/7] diff: simplify the line-range filter by classifying removals immediately Michael Montalbo via GitGitGadget
2026-06-27 17:28 ` [PATCH v2 3/7] diff: emit -L hunk headers via xdiff's formatter Michael Montalbo via GitGitGadget
2026-06-27 17:28 ` [PATCH v2 4/7] diff: extract a line-range diff helper for reuse Michael Montalbo via GitGitGadget
2026-06-27 17:28 ` Michael Montalbo via GitGitGadget [this message]
2026-06-27 17:29 ` [PATCH v2 6/7] diff: support --check with -L line ranges Michael Montalbo via GitGitGadget
2026-06-27 17:29 ` [PATCH v2 7/7] diffcore-pickaxe: scope -G to the -L tracked range Michael Montalbo via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=3d0091b549114f6bf32c9fb7626da9fc2583e88b.1782581342.git.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=ben.knoble@gmail.com \
--cc=git@vger.kernel.org \
--cc=mmontalbo@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.