diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index aab69de6a12..24d53cd4846 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -153,6 +153,51 @@ it.effect("uses stable diagnostics for every parsed non-repository command", () }).pipe(Effect.provide(layer)); }); +it.effect("invalidates origin remote cache when a driver mutation adds origin", () => + Effect.gen(function* () { + const driver = yield* GitVcsDriver.GitVcsDriver; + const cwd = yield* makeTmpDir(); + const remote = yield* makeTmpDir("git-vcs-driver-remote-"); + yield* initRepoWithCommit(cwd); + yield* git(remote, ["init", "--bare"]); + + const before = yield* driver.statusDetailsLocal(cwd); + assert.equal(before.hasOriginRemote, false); + + yield* driver.ensureRemote({ cwd, preferredName: "origin", url: remote }); + + const after = yield* driver.statusDetailsLocal(cwd); + assert.equal(after.hasOriginRemote, true); + }).pipe(Effect.provide(TestLayer)), +); + +it.effect("re-reads origin remote status after cache TTL expiry and bypassed invalidation", () => + Effect.gen(function* () { + const driver = yield* GitVcsDriver.GitVcsDriver; + const cwd = yield* makeTmpDir(); + const remote = yield* makeTmpDir("git-vcs-driver-remote-"); + yield* initRepoWithCommit(cwd); + yield* git(remote, ["init", "--bare"]); + + // First call caches hasOriginRemote = false (5-min TTL) + assert.equal((yield* driver.statusDetailsLocal(cwd)).hasOriginRemote, false); + + // Add origin via raw git (bypasses invalidation hook) + yield* git(cwd, ["remote", "add", "origin", remote]); + + // Cache still has the stale false (TTL not yet expired) + const stillCached = yield* driver.statusDetailsLocal(cwd); + assert.equal(stillCached.hasOriginRemote, false); + + // Advance past the 5-minute TTL so the cache entry expires + yield* TestClock.adjust("6 minutes"); + + // After expiry, the next call re-executes and picks up the remote + const afterExpiry = yield* driver.statusDetailsLocal(cwd); + assert.equal(afterExpiry.hasOriginRemote, true); + }).pipe(Effect.provide(TestLayer)), +); + it.effect("coalesces concurrent ref pages into one repository snapshot", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 3aa7575d590..e44dc048634 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -61,6 +61,8 @@ const LIST_REFS_SNAPSHOT_CACHE_CAPACITY = 64; const LIST_REFS_SNAPSHOT_CACHE_TTL = Duration.minutes(2); const LIST_REFS_REFRESH_COALESCE_TTL = Duration.seconds(5); const LIST_REFS_REFRESH_FAILURE_COOLDOWN = Duration.seconds(30); +const STATUS_DEFAULT_BRANCH_CACHE_TTL = Duration.minutes(5); +const STATUS_ORIGIN_EXISTS_CACHE_TTL = Duration.minutes(5); const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({ GCM_INTERACTIVE: "never", GIT_ASKPASS: "", @@ -1119,6 +1121,63 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* return Cache.get(refresh ? repositoryPathsRefreshCache : repositoryPathsCache, cacheKey); }; + const defaultBranchCache = yield* Cache.makeWith( + (gitCommonDir: string) => + Effect.gen(function* () { + const path = yield* Path.Path; + const fetchCwd = + path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir; + return yield* executeGit( + "GitVcsDriver.statusDetails.defaultBranch", + fetchCwd, + ["--git-dir", gitCommonDir, "symbolic-ref", "refs/remotes/origin/HEAD"], + { allowNonZeroExit: true }, + ).pipe( + Effect.map((result) => { + if (result.exitCode !== 0) return null; + return parseDefaultBranchFromRemoteHeadRef(result.stdout, "origin"); + }), + ); + }), + { + capacity: 2_048, + timeToLive: Exit.match({ + onSuccess: () => STATUS_DEFAULT_BRANCH_CACHE_TTL, + onFailure: () => Duration.zero, + }), + }, + ); + const originExistsCache = yield* Cache.makeWith( + (gitCommonDir: string) => + Effect.gen(function* () { + const path = yield* Path.Path; + const fetchCwd = + path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir; + return yield* executeGit( + "GitVcsDriver.statusDetails.originExists", + fetchCwd, + ["--git-dir", gitCommonDir, "remote", "get-url", "origin"], + { allowNonZeroExit: true }, + ).pipe(Effect.map((result) => result.exitCode === 0)); + }), + { + capacity: 2_048, + timeToLive: Exit.match({ + onSuccess: () => STATUS_ORIGIN_EXISTS_CACHE_TTL, + onFailure: () => Duration.zero, + }), + }, + ); + const invalidateStatusStaticCaches = (cwd: string) => + Effect.gen(function* () { + const repositoryPaths = yield* resolveRepositoryPaths(cwd).pipe( + Effect.catchTags({ GitCommandError: () => Effect.succeed(null) }), + ); + const cacheKey = repositoryPaths?.gitCommonDir ?? normalizeRepositoryPathsCacheKey(cwd); + yield* Cache.invalidate(defaultBranchCache, cacheKey); + yield* Cache.invalidate(originExistsCache, cacheKey); + }); + const resolveGitCommonDir = Effect.fn("resolveGitCommonDir")(function* (cwd: string) { const repositoryPaths = yield* resolveRepositoryPaths(cwd); if (repositoryPaths !== null) { @@ -1517,7 +1576,11 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }); } - const [numstatStdout, defaultRefResult, hasPrimaryRemote] = yield* Effect.all( + const repositoryPaths = yield* resolveRepositoryPaths(cwd).pipe( + Effect.catchTags({ GitCommandError: () => Effect.succeed(null) }), + ); + const statusCacheKey = repositoryPaths?.gitCommonDir; + const [numstatStdout, defaultBranch, hasPrimaryRemote] = yield* Effect.all( [ executeGitWithStableDiagnostics( "GitVcsDriver.statusDetails.numstat", @@ -1574,21 +1637,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* ); }), ), - executeGit( - "GitVcsDriver.statusDetails.defaultRef", - cwd, - ["symbolic-ref", "refs/remotes/origin/HEAD"], - { allowNonZeroExit: true }, - ), - originRemoteExists(cwd).pipe(Effect.orElseSucceed(() => false)), + statusCacheKey + ? Cache.get(defaultBranchCache, statusCacheKey).pipe(Effect.orElseSucceed(() => null)) + : resolveDefaultBranchName(cwd, "origin").pipe(Effect.orElseSucceed(() => null)), + statusCacheKey + ? Cache.get(originExistsCache, statusCacheKey).pipe(Effect.orElseSucceed(() => false)) + : originRemoteExists(cwd).pipe(Effect.orElseSucceed(() => false)), ], { concurrency: "unbounded" }, ); const statusStdout = statusResult.stdout; - const defaultBranch = - defaultRefResult.exitCode === 0 - ? defaultRefResult.stdout.trim().replace(/^refs\/remotes\/origin\//, "") - : null; let refName: string | null = null; let upstreamRef: string | null = null; @@ -2809,7 +2867,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* cwd: string, effect: Effect.Effect, ): Effect.Effect => - effect.pipe(Effect.ensuring(invalidateListRefsSnapshot(cwd).pipe(Effect.ignore))); + effect.pipe( + Effect.ensuring( + Effect.all([ + invalidateListRefsSnapshot(cwd).pipe(Effect.ignore), + invalidateStatusStaticCaches(cwd).pipe(Effect.ignore), + ]), + ), + ); const initRepoWithListRefsInvalidation: GitVcsDriver.GitVcsDriver["Service"]["initRepo"] = ( input, ) =>