Description
The diff viewer (GET /vcs/diff, mode: "git") hangs on "Loading diff..." for minutes when the working tree has many untracked files. It does eventually return, but the per-file git subprocess spawn makes it impractically slow. Most severe on Windows where each git subprocess spawn is ~150-165ms.
Root cause is in packages/opencode/src/project/vcs.ts:
Vcs.diff → track(git, cwd, ref, options) (vcs.ts:384). When there is no HEAD yet (freshly git init'd repo with no commits), ref is undefined, so track takes the if (!ref) branch (vcs.ts:230) and passes an empty batch (emptyBatch()) to files().
- In
files() (vcs.ts:178) the for loop iterates every untracked item. For each, patchForItem (vcs.ts:148) finds no batched patch and falls through to nativePatch (vcs.ts:162) → git.patchUntracked (vcs.ts:130), which spawns one git diff --no-index /dev/null <file> subprocess per file.
- The
MAX_TOTAL_PATCH_BYTES cap (vcs.ts:187) does not break the loop — it only swaps the patch for emptyPatch (vcs.ts:157). The stat line (vcs.ts:179) still calls git.statUntracked (another per-file subprocess) for every added item even after cap, since the cap check happens after it.
This is not fixed by having a HEAD either: diffAgainstRef (vcs.ts:201) merges untracked ?? items into the list (vcs.ts:214-217), but batchPatches (vcs.ts:96) only batch-patches tracked changes via git.patchAll (git diff --patch HEAD). Untracked items still miss the batch and fall through to per-file patchUntracked.
So any repo with a large number of untracked files (e.g. node_modules not yet gitignored, generated/synced caches) makes the diff viewer effectively unusable, regardless of HEAD presence.
Steps to reproduce
- In a directory with many untracked files (700+), run
git init but create no commit.
- Start opencode, open the diff viewer (
/diff or command palette → "Open diff viewer").
- It stays on "Loading diff..." for several minutes. With ~700 files on Windows it is ~2+ minutes; with ~4000+ it never practically returns.
Per-file commands Vcs.diff ends up running:
git diff --no-index --numstat -- /dev/null <file> # statUntracked
git diff --no-index --patch --no-ext-diff --no-renames --unified=3 -- /dev/null <file> # patchUntracked
Operating System
Windows 11 (each git subprocess spawn ~150-165ms here, which compounds the problem; less severe but still linear on macOS/Linux)
Suggested fix directions (optional)
- Batch untracked-file patches in a single git invocation instead of per-file
git diff --no-index (e.g. feed multiple paths, or diff the whole untracked set against an empty tree once).
- In
files(), break the loop once capped is true (or at least skip git.statUntracked for capped items).
- Make the
stat line (vcs.ts:179) respect capped before spawning git.statUntracked.
Description
The diff viewer (
GET /vcs/diff,mode: "git") hangs on "Loading diff..." for minutes when the working tree has many untracked files. It does eventually return, but the per-file git subprocess spawn makes it impractically slow. Most severe on Windows where each git subprocess spawn is ~150-165ms.Root cause is in
packages/opencode/src/project/vcs.ts:Vcs.diff→track(git, cwd, ref, options)(vcs.ts:384). When there is noHEADyet (freshlygit init'd repo with no commits),refisundefined, sotracktakes theif (!ref)branch (vcs.ts:230) and passes an empty batch (emptyBatch()) tofiles().files()(vcs.ts:178) theforloop iterates every untracked item. For each,patchForItem(vcs.ts:148) finds no batched patch and falls through tonativePatch(vcs.ts:162) →git.patchUntracked(vcs.ts:130), which spawns onegit diff --no-index /dev/null <file>subprocess per file.MAX_TOTAL_PATCH_BYTEScap (vcs.ts:187) does not break the loop — it only swaps the patch foremptyPatch(vcs.ts:157). Thestatline (vcs.ts:179) still callsgit.statUntracked(another per-file subprocess) for everyaddeditem even after cap, since the cap check happens after it.This is not fixed by having a HEAD either:
diffAgainstRef(vcs.ts:201) merges untracked??items into the list (vcs.ts:214-217), butbatchPatches(vcs.ts:96) only batch-patches tracked changes viagit.patchAll(git diff --patch HEAD). Untracked items still miss the batch and fall through to per-filepatchUntracked.So any repo with a large number of untracked files (e.g.
node_modulesnot yet gitignored, generated/synced caches) makes the diff viewer effectively unusable, regardless of HEAD presence.Steps to reproduce
git initbut create no commit./diffor command palette → "Open diff viewer").Per-file commands
Vcs.diffends up running:Operating System
Windows 11 (each git subprocess spawn ~150-165ms here, which compounds the problem; less severe but still linear on macOS/Linux)
Suggested fix directions (optional)
git diff --no-index(e.g. feed multiple paths, or diff the whole untracked set against an empty tree once).files(),breakthe loop oncecappedis true (or at least skipgit.statUntrackedfor capped items).statline (vcs.ts:179) respectcappedbefore spawninggit.statUntracked.