Repository navigation
fix(core): diff path selections through a private index to avoid ENAMETOOLONG - #53855
Closed
opencode-agent[bot] wants to merge 2 commits into
Closed
opencode-agent[bot] wants to merge 2 commits into
opencode-agent[bot] wants to merge 2 commits into
Conversation
opencode-agent
Bot
force-pushed
the
diff-pathspec-batches
branch
from
October 8, 2026 04:55
f721cce to
d8305fa
Compare
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Git.tree.diffspreads an explicit path selection into thegit diffargv. Since #47821 batched the per-file diffs into three calls, a revert that touches many files (SessionRevertpasses every file changed since the reverted message) can build a command line past Windows' 32,767-character limit and fail withENAMETOOLONG(E2BIGon Linux once pastARG_MAX).#22560 fixed the same class for snapshot staging with
--pathspec-from-file=- --pathspec-file-nul, butgit diff,diff-tree, anddiff-indexhave no--pathspec-from-fileor stdin pathspec input.Fix
No path ever goes on the command line, so the selection size is unbounded:
git diff --raw -z --no-abbrev --no-renames <from> <to>lists every changed entry with its new mode and object ID.update-index --index-inforecords. Selection follows literal pathspec semantics: the path or a parent directory is selected;./aanda//bnormalize toaanda/b; a trailing slash selects only directories and submodules.GIT_INDEX_FILE:read-tree <from>, thenupdate-index -z --index-infowith the records on stdin. The index is nowfromplus only the selected changes.diff --cached <from>against that index, so output, patch cap, and parsing are unchanged.The private index lives in the snapshot Git directory and is removed afterwards; the repository's own index is untouched. Output is filtered to the selected entries, because staging a path over a file/directory conflict drops the other side from the index. Diffs without a selection are unchanged.
Selections are now matched literally (path or parent directory). Previously a selected path containing
*,?, or[was interpreted as a glob pathspec.A comment on
repositoryArgsrecords the rule for all Git spawns: never pass an unbounded path list as arguments; use stdin.Audit of other Git spawns on
v2Git.index.refresh(add/rm) andGit.index.ignored(check-ignore) already pass paths on stdin.Git.tree.restore,hasEntry, and the VCS plugin patch helpers pass one path per process; the VCS plugin otherwise uses-- ..Differential fuzz
A seeded harness compared this implementation with the
origin/v2implementation across random repositories and histories: text/binary/CRLF/empty/large files, executable-bit changes, symlinks, file↔directory swaps, renames, nested repositories (gitlinks), and names with spaces, Unicode, tabs, newlines, quotes, backslashes, leading-/:/!/#, and glob characters. Selections covered exact changed paths, supersets with unchanged/missing paths, parent directories,., and malformed forms (./, trailing/, case and backslash variants, glob and magic strings), with random context sizes.GIT_LITERAL_PATHSPECS=1): 22,310 comparisons (93,648 diff entries), 0 errors, 3 differences, all the same niche: a trailing-slash selection of a path that changed between a regular file and a submodule.origin/v2reports only the submodule side asadded; this reports the type change asmodified.origin/v2as-is: the remaining differences are all selections containing glob or magic characters (*,?,[,],\, leading:), whichorigin/v2interpreted as globs; for example selecting a file named[br]also returnedb../and trailing-slash normalization, and trailing-slash submodule matching. A deterministic test covers the first two.Verification
origin/v2withE2BIG: argument list too long, posix_spawn 'git'and passes here; it also checks the private index is removed and the real index is untouched.bun test test/git.test.ts test/snapshot.test.ts test/session-revert.test.ts test/session-diff.test.tsandbun typecheckpass inpackages/core.Requested by: @Hona (Hona via Slack)