Repository navigation
perf(server): status refreshes reuse git config until a file changes - #30838
navidnadali wants to merge 3 commits into
Conversation
Each status refresh ran about seven git config reads and git remote twice, for values that only change when a file changes. One `git config --list -z` snapshot per repository now answers those reads until a config file it read, an include or the git dir changes. Git older than 2.42 falls back to one command per read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| return; | ||
| } | ||
| if (dependency.kind === "presence" || dependency.kind === "absent") { | ||
| resolve(stats.isDirectory() ? "directory" : "file"); |
There was a problem hiding this comment.
🟡 Medium vcs/gitConfigSnapshot.ts:179
When .git/objects or .git/refs loses search permission, this fingerprint remains directory, so isCurrent keeps cached config-derived results valid even though Git no longer recognizes the repository. presence dependencies discard stats.mode; include the permission bits in the fingerprint so a permission change invalidates the snapshot.
- resolve(stats.isDirectory() ? "directory" : "file");
+ resolve(`${stats.isDirectory() ? "directory" : "file"}:${stats.mode}`);🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/vcs/gitConfigSnapshot.ts around line 179:
When `.git/objects` or `.git/refs` loses search permission, this fingerprint remains `directory`, so `isCurrent` keeps cached config-derived results valid even though Git no longer recognizes the repository. `presence` dependencies discard `stats.mode`; include the permission bits in the fingerprint so a permission change invalidates the snapshot.
| // Discovery: the directory, what would make Git stop below the top level (a `.git`, or HEAD of a | ||
| // bare repository), the `.git` it found, and what keeps that a repository. The prefix comes from | ||
| // Git, cased as on disk like the top level, whatever case the directory was given in. | ||
| const dependencies: GitConfigDependency[] = [{ path: cwd, kind: "identity" }]; |
There was a problem hiding this comment.
🟡 Medium vcs/gitConfigSnapshot.ts:368
Changing the owner of the worktree root or the actual Git directory leaves the cached snapshot current, so read() continues serving repository config and cached remote results after Git would reject the repository as dubiously owned. The dependency list tracks cwd and toplevel/.git, but not toplevel or the directory returned by --absolute-git-dir when .git is a gitfile; track identity and ownership of both directories as well.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/vcs/gitConfigSnapshot.ts around line 368:
Changing the owner of the worktree root or the actual Git directory leaves the cached snapshot current, so `read()` continues serving repository config and cached remote results after Git would reject the repository as dubiously owned. The dependency list tracks `cwd` and `toplevel/.git`, but not `toplevel` or the directory returned by `--absolute-git-dir` when `.git` is a gitfile; track identity and ownership of both directories as well.
ApprovabilityVerdict: Needs Human Approval Macroscope's review found this PR requires human approval — The change caches Git configuration and config-derived remote results until tracked dependencies change. It also introduces a static-analysis suppression requiring human review; two Medium findings identify invalidation gaps around repository permission and ownership changes. Needs human approval because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
A config snapshot stayed current when the top level of the work tree or a linked worktree's git dir changed owner, or when .git/objects or .git/refs lost search permission. Git then refuses or no longer finds the repository while the snapshot kept answering. Fingerprints now carry the mode and owners, and the top level and git dir are tracked, without another git command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note Written by Verified in a real dev environmentverified unchanged head used the repo's playwright and opening git action options sends the real these are counts across the menu-triggered refresh work, not one driver invocation. other app paths still run remote commands. no wall-clock speedup claim. a real provider turn configured an included local remote. after warming the cache, editing only that included file changed the next observed refresh's comparison base from neighbor checks passed: branch picker switched away and back; a provider edit appeared as blacksmith: focused vcs checks passed (274 passed, 1 skipped). Not verifiednative desktop, ios/android, windows/macos filesystem behavior, a live git older than 2.42, live ownership/permission transitions, acl-only changes, remote/relay/tunnel clients, and external forge services. the filesystem case-insensitivity test was skipped on this linux filesystem. uploaded video playback and seeking were checked. |
|
Note Written by Claude Opus 5.5 (Claude Code) on behalf of Navid Thanks for checking this end to end, and for the trace2 counts. The relative include falling back is expected: git reports the repository's own config with a relative origin ( |




Problem
Each status refresh runs about seven
git configreads andgit remotetwice, for values that cannot change unless a file changes. That is about 9 git processes per turn for nothing. On Linux each one forks the server.Change
git config --list -zsnapshot per repository answers those reads until a file changes.safe.directorycheck read. It is guarded againstGIT_*environment and work-tree overrides, and it sees HEAD by content and the on-disk case of the path.node:fsstat is behind anodeBuiltinImportsuppression, as inantigravityUsageReader.ts, because Effect'sFile.Infohas no ctime or nanosecond mtime.Server only (
apps/server/src/vcs/); no client or contract change; provider adapters are not involved.Scope and approval
No product behaviour, default, setting or contract change. The cache is invalidated by file changes, not by time, and returns what git would return. Complementary to #29721, which stops a different layer re-spawning
git remote. A separate follow-up PR builds on this file. Open PRs #29155 and #28577 also editGitVcsDriverCore.ts, for different reads.Background and measurements: #31002 (context, not an approval request).
Verification
Failing first (
vp test run apps/server/src/vcs/GitVcsDriverCore.test.ts), counting the git commands the driver spawns:-t "runs no config reads on a refresh while the config is unchanged": the second refresh spawns 0git configand 0git remote.-t "on the next read": a table of changes the snapshot must notice (a relative include, a branch switch under anonbranchinclude, a repository losing its HEAD, a nested repository's HEAD becoming invalid or empty, a half-initialised repository completing,objectsorrefslosing search permission, a new owner of the top level or of a linked worktree's git dir); each is seen on the next read. Without root a test cannot chown, so the owner rows have stat report another owner and run git withGIT_TEST_ASSUME_DIFFERENT_OWNER=1.-t "reuses config read through a differently cased path"and-t "sees a repository initialised below a directory outside GIT_WORK_TREE".-t "reads what git config --get and git remote read": the snapshot's answers equal git's.gitConfigSnapshot.test.ts: parsing, include dependencies (a Windows origin too) and the environment signature.On
main@8b44f42b1without the change, 18 of these driver cases fail by assertion; theGIT_WORK_TREEcase passes there, as a guard, andgitConfigSnapshot.test.tsneeds the new module. With the change: 274 pass. One upstream test fails on this macOS machine without the change too ("keeps generated worktree folders short…", a path-spelling case).vp run check:changed: format, lint, knip and typecheck (3 packages) pass; in the 3 affected test files 276 tests pass and 1 fails, the same upstream worktree-folder test named above, which fails onmainon this macOS machine too. On macOS with Node 26.12.1. The server integration suites (vp test run apps/server/integration) also pass, 300 tests in 26 files, and so do the provider replay fixture suites (vp test run apps/server/src/orchestration-v2/testkit), 234 tests in 16 files.Local run, macOS, Node 26.12.1, Apple Git 2.50.1; upstream vs this change; 60 fake agent sessions (20 Claude, 20 Codex, 20 ACP), provider-only, synthetic database; median of 3 interleaved runs (min–max), 90 s window:
git configper turngit remoteper turnThe command counts separate in every run; the spawn-time ranges overlap slightly. A warm status refresh runs 7 git commands instead of 15. The stat checks that replace the reads cost about 1 ms/s of main-thread time.
How this was measured
Local numbers come from one developer machine, before vs after the change on the same tree, interleaved. They show direction and mechanism; the magnitudes are not bench-machine numbers. One shared macOS machine (load average 37–93), so treat timings as indicative; the counts are deterministic. With no spawn worker, every git command starts on the main thread.
Not checked
main@8b44f42b1: the runs above were against an older upstream base.Made with Claude Opus 5.5 via Claude Code. Reviewed by GPT-6.1 Sol via Codex and, independently, by Claude Opus 5.5 via Claude Code.
🤖 Generated with Claude Code