Skip to content

fix(git): only announce the log cap on a walk git ran, and count it right - #4148

Open
KuSh wants to merge 1 commit into
rtk-ai:developfrom
KuSh:fix/4117-followup-log-probe
Open

KuSh wants to merge 1 commit into
rtk-ai:developfrom
KuSh:fix/4117-followup-log-probe

Conversation

@KuSh

@KuSh KuSh commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

walk_exceeds_limit asks git whether rtk git log's ten-commit cap actually cut anything, so the notice can be printed only when it did. It was wrong in both directions: silent on walks the cap had gutted, and loud on walks it never touched.

The first two items below are the reviewer's follow-ups on #4117. The rest are the same defect found elsewhere in the same probe while checking them, and all reproduce on develop today.

Two kinds of walk, two questions

Without a diff-based filter (-S, -G, --diff-filter, --find-object) the probe skips past the limit and asks whether anything is left. With one it has to count instead, because git applies --skip before such a filter and --max-count after it. Most of what follows is the counting branch getting an honest count.

What the probe forwards

It may drop what only decides how git prints, and must keep what decides which commits come back — a distinction the flags themselves do not respect.

Dropped. --line-prefix and --graph, because both put something in front of every record; --graph's rail is drawn with the characters a patch begins its lines with (| before a diff body, - before a removed line), so no trimming rule serves both. --reverse, because it moves --max-count: git drains the whole walk before showing anything, so the count bounds the walk rather than what came through the filter — eleven records against a selection of 836. --check, joining --exit-code: its non-zero exit reports on the diff, not on the command, and the probe read it as git refusing.

Kept. --graph also turns on parent rewriting, which decides which commits come back and not merely their order, so the probe asks for --parents in its place — same rewriting, and it prints nothing beside a %H record, not even on a merge. And -c/--cc/--remerge-diff and every --diff-merges format but off are kept whenever the walk selects by diff: they give merge commits a diff, and a diff-based filter selects on the diffs the walk produces. Off such a walk they are dropped as before, which keeps the saving where it is safe.

-c is the short spelling of --cc, and a short flag cannot be dropped by argument index the way the long ones are, because it shares its argument with whatever was clustered onto it. It goes through the cluster-aware strip the diff/show header already used, lifted behind a predicate here so the probe can widen what it drops without widening that header's own idea of patch shape.

The user's own --skip

RTK's --skip has to win, so the user's is stripped and re-applied — but only the non-counting branch re-applied it, and the count covered commits they had already skipped past.

What counts as a commit in the answer

Records are split on NUL as well as newline, because -z terminates them that way and the whole walk had been read as one line matching nothing.

A record is a commit only at an object name's exact length — %H does not abbreviate, for neither --abbrev-commit nor --abbrev=<n> nor core.abbrev, unlike %h — which keeps out the paths --name-only -z puts in a record of their own at column 0, where a file called cafebabe01 had counted as a commit. And only at column 0, which keeps out the diff context of a probe that kept a merge-diff flag, in the one case where that context is itself full-length hex: a file of checksums.

The notice is owed only when git ran the walk

The probe prunes --pretty/--format/--output, so it can succeed on arguments git rejected outright, and the cap notice landed under git's own fatal: line. Failure is not that test: git refuses a log with 128 (fatal), 129 (usage) or 128 + n (a signal), and keeps the small codes for what the diff machinery found in a walk it ran and printed in full — 1 for --exit-code, 2 for --check.

Measured

develop at 727ee6e against this branch, same repo and same build flow per row. sel is what git itself selects, counted with --no-patch --pretty=format:%H (plain --oneline does not suppress a combined patch, and counting its lines reads 14 selected commits as 149). The cap is 10, so the notice is owed exactly when sel > 10.

command sel develop this branch owed
-p -S LazyLock -z 17 silent notice notice
-p --pretty=bogus — (refused) fatal: + notice fatal: only silent
-p --check 20 silent notice notice
-p --line-prefix=zz -S LazyLock 17 silent notice notice
-p --reverse -S LazyLock 17 silent notice notice
-p --skip=20 -S LazyLock 5 notice silent silent
--binary -S NEEDLE 3 notice silent silent
--graph --cc -S NEEDLE 3 notice silent silent
-p -c -S evil 14 notice notice notice
-p --diff-merges=c -S evil 14 notice notice notice
--graph --full-history --stat -- p.txt 15 notice notice notice
--name-only -z -S NEEDLE --pickaxe-all 3 silent silent silent
-p --exit-code 25 notice notice notice

The last five are the guards: --exit-code makes a successful walk exit 1, and the -c/--diff-merges/--graph rows are the walks whose selection depends on flags the probe now keeps.

External diff program invocations for a ten-commit window, GIT_EXTERNAL_DIFF set:

command develop this branch
git log -p --ext-diff 10 10
git log --binary --ext-diff 11 10
git log --cc --ext-diff 11 10
git log -c --ext-diff 11 10
git log --remerge-diff --ext-diff 11 10
git log -c -S LazyLock --ext-diff 21 21

The last row is the deliberate limit of that saving: once a diff-based filter is present the probe has to run the walk the user ran, merge diffs and all, so there is nothing to save.

Tests

tests/git_log_cap_notice_test.rs grows one test per defect, plus fixtures for an evil-merge repo, a TREESAME-merge repo, a checksum file and a tree of hex-named paths:

  • a_nul_separated_diff_selected_walk_announces_the_cap
  • a_walk_git_refused_carries_no_cap_notice
  • a_check_run_still_announces_the_cap
  • a_merge_diff_walk_is_measured_with_its_merges_still_in_it
  • a_graphed_diff_selected_walk_is_measured_like_any_other
  • a_graphed_walk_keeps_the_commits_its_parent_rewriting_adds
  • a_reversed_diff_selected_walk_is_still_measured_by_what_it_selects
  • a_diff_selected_walk_under_a_user_skip_is_measured_from_where_they_started
  • a_line_prefix_does_not_hide_a_diff_selected_cap
  • a_path_that_looks_like_an_object_name_is_not_counted_as_a_commit
  • a_file_of_bare_hex_words_is_not_counted_as_a_walk
  • the_probe_does_not_rerun_the_users_diff_program, extended with --binary, --cc and -c

Beside the functions in git_cmd.rs: a_commit_name_is_a_whole_object_name_at_column_zero, and four over log_probe_args covering the merge-diff family on filtered and unfiltered walks, both --diff-merges spellings, cluster rebuilding, and a pathspec past --.

Each fix was verified to fail exactly one of these tests when reverted alone, and CAPPED_SHAPES lost three duplicate entries it had accumulated. cargo fmt --all --check, cargo clippy --all-targets and cargo test --all are clean.

🤖 Generated with Claude Code

@KuSh
KuSh force-pushed the fix/4117-followup-log-probe branch from c2ad379 to 7d14fcb Compare September 20, 2026 18:50
@rtk-wshm-sync-bot rtk-wshm-sync-bot Bot added bug Something isn't working git labels Sep 20, 2026
@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

🐛 Type bug-fix
🟢 Risk low

Summary

Fixes four bugs in rtk git log's cap-notice probe (walk_exceeds_limit): -z NUL-separated output silenced the count, the notice could print alongside git's own fatal error for a refused command, --check was misread as a refusal, and --binary/--cc/-c/--remerge-diff left patch output that got miscounted as commits due to indentation. Also refactors args_without_patch_shape into a generic args_without_flags helper and adds extensive new tests.

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

Linked issues: #4117


Analyzed automatically by wshm · This is an automated analysis, not a human review.

…ight

`walk_exceeds_limit` asks git whether `rtk git log`'s ten-commit cap
actually cut anything. It was wrong in both directions: silent on walks
the cap had gutted, and loud on walks it never touched.

Two kinds of walk, two questions. Without a diff-based filter (`-S`,
`-G`, `--diff-filter`, `--find-object`) the probe skips past the limit
and asks whether anything is left. With one it has to count instead,
because git applies `--skip` before such a filter and `--max-count`
after it. Most of what follows is the counting branch getting an honest
count, and the notice being owed at all.

What the probe forwards. It may drop what only decides how git prints,
and must keep what decides which commits come back -- a distinction the
flags do not respect. `--line-prefix` and `--graph` are dropped because
both put something in front of every record, and `--graph` draws its
rail with the characters a patch begins its lines with (`|` before a
diff body, `-` before a removed line), so no trimming rule serves both.
But `--graph` also turns on parent rewriting, which decides which
commits come back: one path here holds a single commit without it and
fifteen with it, so the probe asks for `--parents` instead, which turns
on the same rewriting and prints nothing beside a `%H` record.
`--reverse` is dropped because it moves `--max-count`: git drains the
whole walk before showing anything, so the count bounds the walk rather
than what came through the filter, and eleven records became six
against a selection of 836. `--check` joins `--exit-code`, whose
non-zero exit reports on the diff rather than on the command. And
`-c`/`--cc`/`--remerge-diff` and every `--diff-merges` format but `off`
are kept whenever the walk selects by diff: they give merge commits a
diff, and a diff-based filter selects on the diffs the walk produces.
On a repository whose merges carry content from neither parent, `git
log -S evil` selects nothing where `git log -c -S evil` selects 14. Off
such a walk they are still dropped, which spares a configured
`diff.external` the eleventh run of a ten-commit window.

The user's own `--skip` is now reproduced in the counting branch too.
RTK's has to win, so the user's is stripped and re-applied -- but only
the other branch re-applied it, and the count covered commits they had
already skipped past: `--skip=20 -S NEEDLE` leaves five commits and
announced a cap of ten.

What counts as a commit in the answer. Records are split on NUL as well
as newline, because `-z` terminates them that way and the whole walk
had been read as one line matching nothing. A record is a commit only
at an object name's exact length -- `%H` does not abbreviate, for
neither `--abbrev-commit` nor `core.abbrev` -- which keeps out the
paths `--name-only -z` puts in a record of their own, where a file
called `cafebabe01` had counted as a commit. And only at column 0,
which keeps out the diff context of a probe that kept a merge-diff
flag, in the case where that context is itself full-length hex: a file
of checksums.

Last, the notice is gated on git not having refused. The probe prunes
`--pretty`/`--format`/`--output`, so it can succeed on arguments git
rejected outright, and the notice landed under git's own `fatal:` line.
Failure is not that test: git refuses with 128, 129 or 128 + n and
keeps the small codes for what the diff machinery found in a walk it
ran and printed in full -- 1 for `--exit-code`, 2 for `--check`.

The strip these go through is the one the `diff`/`show` header already
used, lifted behind a predicate so the probe can widen it without
widening that header's own idea of patch shape. Going through it is
what lets `-c` be dropped at all: a short flag shares its argument with
whatever was clustered onto it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@KuSh
KuSh force-pushed the fix/4117-followup-log-probe branch from 4f70536 to 362c6f7 Compare October 7, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working git

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant