Skip to content

refactor(git): tokenize git show's objects instead of walking clusters - #4001

Merged
KuSh merged 1 commit into
rtk-ai:developfrom
KuSh:fix/show-arg-tokenizer
Oct 5, 2026
Merged

KuSh merged 1 commit into
rtk-ai:developfrom
KuSh:fix/show-arg-tokenizer

Conversation

@KuSh

@KuSh KuSh commented Sep 12, 2026 •

Copy link
Copy Markdown
Collaborator

What the TODOs were

src/cmds/git/git.rs carried two TODO(after #3681) markers left by the author of the
blob-show path (#3265), both waiting on the ValueSpec factorization that landed with #3681.

1. flag_token_consumes_next — the short-cluster walk. show_positionals found a
git show's object arguments with a hand-rolled scan: position(|a| a == "--") for the
boundary, starts_with('-') for flags, and a per-char walk of each short cluster that
re-queried the flag table by rebuilding format!("-{c}") for every character. The semantics
it encoded — the first value-taking short flag in a cluster takes the remainder as an inline
value, or the next argument when it is the cluster's last char — is exactly Attachment plus
the solo_only rule the tokenizer already models. It is now:

let tokens = tokenize_git_diff_args(args);
arg_tokenizer::before_dashdash(&tokens)
    .iter()
    .filter(|t| t.is_free_positional())
    .map(|t| t.text)
    .collect()

Three functions and a per-char allocation go away.

One grammar per subcommand. This reads git show under diff_takes_value, not
log_takes_value, matching commit_or_stat_route's existing tokenize_git_diff_args for the
same subcommand. Measured against real git, not assumed:

invocation real git
git show -wl 100 HEAD ok — -l is the rename limit and clusters
git show -wl HEAD error: switch `l' expects an integer value
git show -n 1 HEAD ok
git show -wn 1 HEAD / -pn 1 HEAD fatal: ambiguous argument '1'

So -l is value() and -n is solo_only() — the diff table. Under log's table -l is
solo-only, which would leave the 100 looking like a second object and silently drop the
blob window.

2. probe_is_blob — dropped, deliberately. The suggestion was a flag pre-filter to avoid
the cat-file subprocess on the common path. There is nothing conservative left to skip: the
probe only runs once a free positional already looks like rev:path, and after this change
that set is strictly smaller than before. An ordinary commit show never reaches it, and on the
blob path rtk git show HEAD:file.txt measures 3.7 ms ± 0.4 ms end to end — probe included,
well inside the 10 ms budget and unchanged from the merge-base. Any pre-filter would trade a
non-problem for a risk to the one decision only git can make, so the marker is removed rather
than acted on.

Also fixes the doc link on log_wants_raw_shape, which pointed at requests_raw_log_output —
a #[cfg(test)] helper rustdoc cannot resolve outside a test build. Other broken links in the
tree are pre-existing and left alone.

Differential verification

A binary built from the merge-base and one from this branch, run over a matrix of git show
invocations in a fixture repo under LC_ALL=C, with stdout, stderr and exit code compared
byte for byte
.

  • Matrix 1 (55 cases): plain commit show, rev:path blob show, :0:path index blob,
    -wG a:b HEAD:blob, -pS a:b, -S 'url:1' HEAD, -L 1,2:file, -- <pathspec>,
    --textconv, --filters, --ext-diff, --no-textconv, the magic pathspecs
    :(exclude) / :! / :^ / :/text, :: / :, --stat / --numstat / --name-only /
    --pretty / --format, --word-diff / --color-words / --diff-merges, -s,
    --grep -p, -20, bare -, and multi-object shows.
  • Matrix 2 (39 cases): every short flag whose log and diff spellings disagree, each carrying a
    colon operand next to a real blob so grammar drift shows up as a routing change —
    -wl a:b, -pn a:b, -cl a:b, -wG/-wS/-wI/-wO/-wL, -U 3 vs -U3, -M50,
    -C/-B, -Gfoo, -wGa:b, -lw 100, -wnl 1, a -- as a flag's would-be value, and
    -- combined with each.

Result: 94/94 identical, 0 differences. The runner was sanity-checked against itself first
(same binary twice) to confirm it is deterministic.

tests/git_show_blob_differential_test.rs, the seeded 600-iteration fuzzer that already guards
this path against git cat-file -t ground truth, also stays green.

Regression tests

Behavioural, CI-run (not #[ignore]), against the real binary and real git, in
tests/guard_integration_test.rs:

  • git_show_cluster_flag_value_is_not_mistaken_for_the_blob_object — -wG a:b HEAD:big.txt
    must still window the blob. Red when show_positionals is reduced to a
    starts_with('-') scan.
  • git_show_rename_limit_clusters_under_diffs_grammar_not_logs — -wl 100 HEAD:big.txt must
    still window the blob. Red under log_takes_value, green under diff_takes_value. This
    is the test that pins the one-grammar-per-subcommand choice.
  • git_show_blob_spec_after_double_dash_is_a_pathspec_not_an_object — a blob spec past --
    must print the commit, not the file. This one is a pin rather than a fail-before test: the
    cat-file probe and the can_window gate already make it unobservable on their own, which
    is the defence-in-depth working as designed.

Both fail-before results were confirmed by temporarily reverting, running red, and restoring.

Performance

hyperfine --warmup 5 -N, merge-base vs this branch:

invocation merge-base this branch
git show HEAD 6.6 ms ± 0.9 6.7 ms ± 0.5
git show HEAD:file.txt 3.8 ms ± 0.3 3.7 ms ± 0.4
git show -wG a:b HEAD:file.txt 3.6 ms ± 0.4 3.7 ms ± 0.4

Within noise, inside the <10 ms budget.

Out of scope

The output-shape routing refactor proposed in #3910 — log_wants_raw_shape,
diff_wants_raw_shape, show_wants_raw_shape and body_is_suppressed are untouched here so
that work can land cleanly.

Related issue

Refs #3954 — that issue targets flag_token_consumes_next's hand-maintained value-taking table,
which this PR deletes in favour of the shared diff_takes_value grammar, so the two-tables-drift
root cause it describes is closed here. Not Fixes, because its specific claims no longer hold:
its one reproducing example (--ignore-matching-lines a:b HEAD:big.txt) already windows on
develop (36090 B raw -> 8243 B), and git 2.53.0 rejects the three flags it names in separated
form (git show --notes x HEAD:big.txt -> fatal: ambiguous argument 'x'; same for
--conflict-marker-size and --submodule), so adding them to any table would make RTK consume
a token git treats as a revision.

@KuSh KuSh changed the title refactor: placeholder title, patched below refactor(git): tokenize git show's objects instead of walking clusters Sep 12, 2026
@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

♻️ Type refactor
🟡 Risk medium

Summary

Refactors git show's argument scanning to reuse the arg_tokenizer's Attachment/ValueSpec grammar instead of a hand-rolled short-cluster walk, resolving two TODOs left pending on the prior ValueSpec factorization. It removes the now-unnecessary probe_is_blob pre-filter TODO deliberately, and adds differential and regression testing showing behavior parity with real git.

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

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

`show_positionals` hand-rolled git's short-flag cluster grammar to find which
`git show` arguments are objects: a per-char walk that re-queried the flag table
through a `format!("-{c}")` allocation, plus its own `--`-boundary slice. The
tokenizer's `ValueSpec` table models exactly that — attached value, separate
value, and the solo-only cluster rule — so the walk is now `tokenize_grammar` +
`before_dashdash` + `Token::is_free_positional`.

It reads `git show` under diff's grammar rather than log's, matching the route
classifier beside it: `git show -wl 100` consumes the 100 as the rename limit,
where log's `-l` is solo-only and would leave it looking like a second object and
silently drop the blob window.

Verified byte-identical (stdout, stderr and exit code) against a binary built
from the merge-base over 94 `git show` invocations: blob and commit shows, the
`-wG a:b HEAD:blob` cluster, `-pS a:b`, `-S 'url:1' HEAD`, `-L 1,2:file`,
`-- <pathspec>`, `--textconv`/`--filters`/`--ext-diff`, the `:(exclude)`/`:!`/
`:^`/`:/` magic pathspecs, and every short flag whose log and diff spellings
disagree.

The `cat-file` probe keeps its subprocess: it only runs once a free positional
already looks like `rev:path`, which is the one case where nothing but git can
answer, so no flag pre-filter can skip it without risking the blob path.

Also drops a doc link on `log_wants_raw_shape` pointing at a `#[cfg(test)]`
helper, which rustdoc cannot resolve outside a test build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@KuSh
KuSh force-pushed the fix/show-arg-tokenizer branch from 624cdd4 to a6f170b Compare September 17, 2026 22:37

@aeppling aeppling left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@KuSh
KuSh merged commit 0b1d52f into rtk-ai:develop Oct 5, 2026
11 checks passed
@KuSh
KuSh deleted the fix/show-arg-tokenizer branch October 5, 2026 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants