]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Merge branch 'mm/line-log-limited-ops' into seen
authorJunio C Hamano <gitster@pobox.com>
Thu, 30 Jul 2026 17:44:34 +0000 (10:44 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 30 Jul 2026 17:44:34 +0000 (10:44 -0700)
The 'git log -L<range>:<path>' command has been taught to limit
various 'diff' operations, such as '--stat', '--check', and '-G', to
the specified range and path.

* mm/line-log-limited-ops:
  diffcore-pickaxe: scope -G to the -L tracked range
  diff: support --check with -L line ranges
  line-log: support diff stat formats with -L
  diff: extract a line-range diff helper for reuse
  diff: emit -L hunk headers via xdiff's formatter
  diff: simplify the line-range filter by classifying removals immediately
  diff: rename and group the line-range filter for clarity

1  2 
diff.c
diffcore-pickaxe.c
revision.c
t/t4211-line-log.sh
xdiff-interface.c

diff --cc diff.c
Simple merge
Simple merge
diff --cc revision.c
Simple merge
index d0a834ed8f5bbac60ce4b245be215dadf10bb321,bac74d44794778e1d31fb904f8027ae7fe722921..429ecd3a51c19b8efd22d53896858e0cc24ab986
@@@ -731,13 -722,138 +722,138 @@@ test_expect_success '-L with -S filter
  test_expect_success '-L with -G filters to diff-text matches' '
        git checkout parent-oids &&
        git log -L:func2:file.c -G "F2 [+] 2" --format= >actual &&
-       # -G greps the whole-file diff text, not just the tracked range;
-       # combined with -L, this selects commits that both touch func2
-       # and have "F2 + 2" in their diff.
+       # -G greps the diff text, and under -L only the lines in the
+       # tracked range (unlike -S above, which searches the whole file);
+       # this selects commits whose change to func2 contains "F2 + 2".
        test $(grep -c "^diff --git" actual) = 1 &&
 -      grep "F2 + 2" actual
 +      test_grep "F2 + 2" actual
  '
  
+ test_expect_success 'setup for trailing deletion test' '
+       git checkout --orphan trailing-del &&
+       git reset --hard &&
+       cat >file.c <<-\EOF &&
+       void tracked()
+       {
+           return 1;
+       }
+       // trailing comment
+       EOF
+       git add file.c &&
+       test_tick &&
+       git commit -m "add file with trailing comment" &&
+       # Modify tracked() AND delete the trailing comment in
+       # one commit, so the commit touches the tracked range
+       # and is not filtered out by the revision walker.
+       cat >file.c <<-\EOF &&
+       void tracked()
+       {
+           return 2;
+       }
+       EOF
+       git commit -a -m "modify tracked and delete trailing comment"
+ '
+ test_expect_success '-L does not include deletions past end of tracked range' '
+       git log -L:tracked:file.c --format= -1 -p >actual &&
+       # The trailing comment deletion is outside the tracked
+       # range and should not appear in the patch output.
+       test_grep "return 2" actual &&
+       test_grep ! "trailing comment" actual
+ '
+ test_expect_success '-L includes leading deletions resolved by in-range line' '
+       git checkout --orphan leading-del &&
+       git reset --hard &&
+       cat >file.c <<-\EOF &&
+       // leading comment
+       void tracked()
+       {
+           return 1;
+       }
+       EOF
+       git add file.c &&
+       test_tick &&
+       git commit -m "add file with leading comment" &&
+       cat >file.c <<-\EOF &&
+       void tracked()
+       {
+           return 2;
+       }
+       EOF
+       git commit -a -m "modify tracked and delete leading comment" &&
+       git log -L:tracked:file.c --format= -1 -p >actual &&
+       # The leading comment deletion is resolved by the next
+       # non-removal line (void tracked), which is in range: a
+       # removal is classified by the position of the following
+       # line, so it joins the range that line falls in.
+       test_grep "return 2" actual &&
+       test_grep "leading comment" actual
+ '
+ test_expect_success 'setup for line-range filter edge cases' '
+       git checkout --orphan filter-edge &&
+       git reset --hard &&
+       cat >file.c <<-\EOF &&
+       void before()
+       {
+           return 0;
+       }
+       void tracked()
+       {
+           int a = 1;
+           int b = 2;
+           int c = 3;
+           return a + b + c;
+       }
+       void after()
+       {
+           return 9;
+       }
+       EOF
+       git add file.c &&
+       test_tick &&
+       git commit -m "initial"
+ '
+ test_expect_success '-L change at exact first line of range' '
+       git checkout filter-edge &&
+       # Change the function signature (first line of range)
+       sed "s/void tracked/int tracked/" file.c >tmp &&
+       mv tmp file.c &&
+       git commit -a -m "change first line" &&
+       git log -L:tracked:file.c -p --format=%s -1 >actual &&
+       test_grep "change first line" actual &&
+       test_grep "+int tracked" actual &&
+       test_grep "\\-void tracked" actual
+ '
+ test_expect_success '-L change at exact last line of range' '
+       git checkout filter-edge &&
+       git reset --hard HEAD~1 &&
+       # Change the closing brace line (last line of range)
+       sed "s/^}$/} \/\/ end tracked/" file.c >tmp &&
+       mv tmp file.c &&
+       git commit -a -m "change last line" &&
+       git log -L:tracked:file.c -p --format=%s -1 >actual &&
+       test_grep "change last line" actual &&
+       test_grep "end tracked" actual
+ '
+ test_expect_success '-L pure deletion in range (no additions)' '
+       git checkout filter-edge &&
+       git reset --hard HEAD~1 &&
+       # Delete a line inside tracked() without adding anything
+       sed "/int c/d" file.c >tmp &&
+       mv tmp file.c &&
+       git commit -a -m "pure deletion" &&
+       git log -L:tracked:file.c -p --format=%s -1 >actual &&
+       test_grep "pure deletion" actual &&
+       test_grep "\\-.*int c" actual
+ '
  test_expect_success '-L with --diff-filter=M excludes root commit' '
        git checkout parent-oids &&
        git log -L:func2:file.c --diff-filter=M --format=%s --no-patch >actual &&
@@@ -781,24 -1018,162 +1018,182 @@@ test_expect_success '--summary shows ne
        test_grep "create mode 100644 file.c" actual
  '
  
+ test_expect_success 'setup for --check test' '
+       git checkout --orphan check-test &&
+       git reset --hard &&
+       cat >check.c <<-\EOF &&
+       void tracked()
+       {
+           return;
+       }
+       void other()
+       {
+           return;
+       }
+       EOF
+       git add check.c &&
+       test_tick &&
+       git commit -m "add check.c" &&
+       # Introduce trailing whitespace errors in both functions
+       sed "s/return;/return; /" check.c >check.c.tmp &&
+       mv check.c.tmp check.c &&
+       git commit -a -m "introduce trailing whitespace"
+ '
+ test_expect_success '--check scoped to tracked range with correct file line' '
+       # tracked() trailing whitespace is at check.c:3; report it with the
+       # real file line number, not a count from the start of the range
+       # hunk.  other() at check.c:8 is outside the range and is excluded.
+       test_must_fail git log -L:tracked:check.c --check --format= >actual &&
+       test_grep "check.c:3: trailing whitespace" actual &&
+       test_grep ! "check.c:8:" actual
+ '
+ test_expect_success '--check reports each of several tracked ranges' '
+       # Track both functions as separate ranges.  Each range is flushed
+       # as its own hunk, so the second error must report its real file
+       # line (check.c:8), not continue the numbering from the first
+       # range (check.c:3).
+       test_must_fail git log -L:tracked:check.c -L:other:check.c \
+               --check --format= >actual &&
+       test_grep "check.c:3: trailing whitespace" actual &&
+       test_grep "check.c:8: trailing whitespace" actual
+ '
+ test_expect_success '--check line numbers stay correct across a gap in one range' '
+       git checkout --orphan check-gap &&
+       git reset --hard &&
+       cat >gap.c <<-\EOF &&
+       void tracked()
+       {
+           int a = 1;
+           int b = 2;
+           int c = 3;
+           int d = 4;
+           int e = 5;
+           int g = 7;
+           return;
+       }
+       EOF
+       git add gap.c &&
+       test_tick &&
+       git commit -m "add gap.c" &&
+       # Two trailing-whitespace errors within one tracked range,
+       # separated by clean lines.  ctxlen is inflated to the range span,
+       # so they land in a single xdiff hunk with the gap as context;
+       # both must report their real file line number, with the context
+       # lines between them counted.
+       sed -e "s/int a = 1;/int a = 1; /" -e "s/int g = 7;/int g = 7; /" gap.c >tmp &&
+       mv tmp gap.c &&
+       git commit -a -m "ws errors with a gap" &&
+       test_must_fail git log -L:tracked:gap.c --check --format= >actual &&
+       test_grep "gap.c:3: trailing whitespace" actual &&
+       test_grep "gap.c:8: trailing whitespace" actual
+ '
+ test_expect_success '--check does not report blank-at-eof outside the range' '
+       git checkout --orphan check-eof &&
+       git reset --hard &&
+       printf "void tracked()\n{\n    return;\n}\n\nint tail = 1;\n" >eof.c &&
+       git add eof.c &&
+       test_tick &&
+       git commit -m "add eof.c" &&
+       # One commit introduces a trailing-whitespace error inside tracked()
+       # (line 3) and a blank line at end of file (line 7, outside the
+       # range).  The blank-at-eof check scans the whole file, so it must be
+       # scoped: report the in-range error, not the out-of-range EOF blank.
+       printf "void tracked()\n{\n    return; \n}\n\nint tail = 1;\n\n" >eof.c &&
+       git commit -a -m "ws in range, blank at eof out of range" &&
+       test_must_fail git log -L:tracked:eof.c --check --format= >actual &&
+       test_grep "eof.c:3: trailing whitespace" actual &&
+       test_grep ! "blank line at EOF" actual
+ '
+ test_expect_success '-L -G is scoped to the tracked range' '
+       git checkout --orphan grep-scope &&
+       git reset --hard &&
+       cat >gp.c <<-\EOF &&
+       int func1()
+       {
+           return ALPHA;
+       }
+       int func2()
+       {
+           return BETA;
+       }
+       EOF
+       git add gp.c &&
+       test_tick &&
+       git commit -m "add gp.c" &&
+       sed -e "s/ALPHA/ALPHA2/" -e "s/BETA/BETA2/" gp.c >tmp &&
+       mv tmp gp.c &&
+       git commit -a -m "touch both functions" &&
+       # The commit changes ALPHA (func1) and BETA (func2).  Tracking func2,
+       # -G BETA matches its in-range change; -G ALPHA must not, since ALPHA
+       # changes only outside the tracked range.
+       git log -L:func2:gp.c -G BETA --format=%s >actual &&
+       test_grep "touch both functions" actual &&
+       git log -L:func2:gp.c -G ALPHA --format=%s >actual &&
+       test_grep ! "touch both functions" actual
+ '
+ test_expect_success '-L -G searches the whole file under textconv' '
+       git checkout --orphan grep-textconv &&
+       git reset --hard &&
+       cat >tc.c <<-\EOF &&
+       int func1()
+       {
+           return F1;
+       }
+       int func2()
+       {
+           return F2;
+       }
+       EOF
+       git add tc.c &&
+       test_tick &&
+       git commit -m "add tc.c" &&
+       # One commit changes func1 and func2; MAGIC lands only in the
+       # func2 change, outside func1.
+       sed -e "s/F1/F1 + 1/" -e "s/return F2/return MAGIC/" tc.c >tmp &&
+       mv tmp tc.c &&
+       git commit -a -m "change both funcs" &&
+       echo "tc.c diff=tc" >.gitattributes &&
+       # Without a textconv driver, -G is scoped to func1, so MAGIC (only
+       # in the func2 change) does not select the commit.
+       git log -L:func1:tc.c -G MAGIC --format=%s --no-patch >actual &&
+       test_must_be_empty actual &&
+       # A textconv driver makes the range (original-file line numbers)
+       # meaningless against the driver output, so -G falls back to the
+       # whole file and MAGIC now selects the commit.
+       git config diff.tc.textconv cat &&
+       git log -L:func1:tc.c -G MAGIC --format=%s --no-patch >actual &&
+       test_grep "change both funcs" actual
+ '
 +test_expect_success 'get_commit_action() does not mutate a not-yet-walked commit' '
 +      git init peek &&
 +      (
 +              cd peek &&
 +              test_write_lines 1 2 3 4 5 >f.c &&
 +              git add f.c && test_tick && git commit -m base &&
 +              test_write_lines 1 two 3 4 5 >f.c &&
 +              test_tick && git commit -am change &&
 +
 +              # Peek HEAD^, which the walk has not reached (the out-of-order
 +              # call a lookahead makes), and confirm get_commit_action() leaves
 +              # it untouched.  A side effect is invisible in the commit list
 +              # (range merges are idempotent), so the helper reports whether the
 +              # call mutated the peeked commit at all.
 +              echo "mutated 0" >expect &&
 +              test-tool revision-walking line-log-peek HEAD^ 1,3:f.c HEAD >actual &&
 +              test_cmp expect actual
 +      )
 +'
 +
  test_done
Simple merge