diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index 8ab541e675..665c4b138f 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -826,7 +826,7 @@ it.layer(TestLayer)("git integration", (it) => { }), ); - it.effect("shares upstream refreshes across worktrees that use the same git common dir", () => + it.effect("coalesces upstream refreshes across sibling worktrees on the same remote", () => Effect.gen(function* () { const ok = (stdout = "") => Effect.succeed({ @@ -845,7 +845,9 @@ it.layer(TestLayer)("git integration", (it) => { input.args[2] === "--symbolic-full-name" && input.args[3] === "@{upstream}" ) { - return ok("origin/main\n"); + return ok( + input.cwd === "/repo/worktrees/pr-123" ? "origin/feature/pr-123\n" : "origin/main\n", + ); } if (input.args[0] === "remote") { return ok("origin\n"); @@ -856,10 +858,22 @@ it.layer(TestLayer)("git integration", (it) => { if (input.args[0] === "--git-dir" && input.args[2] === "fetch") { fetchCount += 1; expect(input.cwd).toBe("/repo"); + expect(input.args).toEqual([ + "--git-dir", + "/repo/.git", + "fetch", + "--quiet", + "--no-tags", + "origin", + ]); return ok(); } if (input.operation === "GitCore.statusDetails.status") { - return ok("# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n"); + return ok( + input.cwd === "/repo/worktrees/pr-123" + ? "# branch.head feature/pr-123\n# branch.upstream origin/feature/pr-123\n# branch.ab +0 -0\n" + : "# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n", + ); } if ( input.operation === "GitCore.statusDetails.unstagedNumstat" || @@ -886,70 +900,80 @@ it.layer(TestLayer)("git integration", (it) => { }), ); - it.effect("briefly backs off failed upstream refreshes across sibling worktrees", () => - Effect.gen(function* () { - const ok = (stdout = "") => - Effect.succeed({ - code: 0, - stdout, - stderr: "", - stdoutTruncated: false, - stderrTruncated: false, - }); + it.effect( + "briefly backs off failed upstream refreshes across sibling worktrees on one remote", + () => + Effect.gen(function* () { + const ok = (stdout = "") => + Effect.succeed({ + code: 0, + stdout, + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + }); - let fetchCount = 0; - const core = yield* makeIsolatedGitCore((input) => { - if ( - input.args[0] === "rev-parse" && - input.args[1] === "--abbrev-ref" && - input.args[2] === "--symbolic-full-name" && - input.args[3] === "@{upstream}" - ) { - return ok("origin/main\n"); - } - if (input.args[0] === "remote") { - return ok("origin\n"); - } - if (input.args[0] === "rev-parse" && input.args[1] === "--git-common-dir") { - return ok("/repo/.git\n"); - } - if (input.args[0] === "--git-dir" && input.args[2] === "fetch") { - fetchCount += 1; + let fetchCount = 0; + const core = yield* makeIsolatedGitCore((input) => { + if ( + input.args[0] === "rev-parse" && + input.args[1] === "--abbrev-ref" && + input.args[2] === "--symbolic-full-name" && + input.args[3] === "@{upstream}" + ) { + return ok( + input.cwd === "/repo/worktrees/pr-123" + ? "origin/feature/pr-123\n" + : "origin/main\n", + ); + } + if (input.args[0] === "remote") { + return ok("origin\n"); + } + if (input.args[0] === "rev-parse" && input.args[1] === "--git-common-dir") { + return ok("/repo/.git\n"); + } + if (input.args[0] === "--git-dir" && input.args[2] === "fetch") { + fetchCount += 1; + return Effect.fail( + new GitCommandError({ + operation: input.operation, + command: `git ${input.args.join(" ")}`, + cwd: input.cwd, + detail: "simulated fetch timeout", + }), + ); + } + if (input.operation === "GitCore.statusDetails.status") { + return ok( + input.cwd === "/repo/worktrees/pr-123" + ? "# branch.head feature/pr-123\n# branch.upstream origin/feature/pr-123\n# branch.ab +0 -0\n" + : "# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n", + ); + } + if ( + input.operation === "GitCore.statusDetails.unstagedNumstat" || + input.operation === "GitCore.statusDetails.stagedNumstat" + ) { + return ok(); + } + if (input.operation === "GitCore.statusDetails.defaultRef") { + return ok("refs/remotes/origin/main\n"); + } return Effect.fail( new GitCommandError({ operation: input.operation, command: `git ${input.args.join(" ")}`, cwd: input.cwd, - detail: "simulated fetch timeout", + detail: "Unexpected git command in refresh failure cooldown test.", }), ); - } - if (input.operation === "GitCore.statusDetails.status") { - return ok("# branch.head main\n# branch.upstream origin/main\n# branch.ab +0 -0\n"); - } - if ( - input.operation === "GitCore.statusDetails.unstagedNumstat" || - input.operation === "GitCore.statusDetails.stagedNumstat" - ) { - return ok(); - } - if (input.operation === "GitCore.statusDetails.defaultRef") { - return ok("refs/remotes/origin/main\n"); - } - return Effect.fail( - new GitCommandError({ - operation: input.operation, - command: `git ${input.args.join(" ")}`, - cwd: input.cwd, - detail: "Unexpected git command in refresh failure cooldown test.", - }), - ); - }); + }); - yield* core.statusDetails("/repo/worktrees/main"); - yield* core.statusDetails("/repo/worktrees/pr-123"); - expect(fetchCount).toBe(1); - }), + yield* core.statusDetails("/repo/worktrees/main"); + yield* core.statusDetails("/repo/worktrees/pr-123"); + expect(fetchCount).toBe(1); + }), ); it.effect("throws when branch does not exist", () => @@ -1047,7 +1071,6 @@ it.layer(TestLayer)("git integration", (it) => { "--quiet", "--no-tags", remoteName, - `+refs/heads/${featureBranch}:refs/remotes/${remoteName}/${featureBranch}`, ]); }), ); diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index fb5d908575..3e9df316f1 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -78,11 +78,9 @@ type TraceTailState = { remainder: string; }; -class StatusUpstreamRefreshCacheKey extends Data.Class<{ +class StatusRemoteRefreshCacheKey extends Data.Class<{ gitCommonDir: string; - upstreamRef: string; remoteName: string; - upstreamBranch: string; }> {} interface ExecuteGitOptions { @@ -919,17 +917,16 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { ); }); - const fetchUpstreamRefForStatus = ( + const fetchRemoteForStatus = ( gitCommonDir: string, - upstream: { upstreamRef: string; remoteName: string; upstreamBranch: string }, + remoteName: string, ): Effect.Effect => { - const refspec = `+refs/heads/${upstream.upstreamBranch}:refs/remotes/${upstream.upstreamRef}`; const fetchCwd = path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir; return executeGit( - "GitCore.fetchUpstreamRefForStatus", + "GitCore.fetchRemoteForStatus", fetchCwd, - ["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", upstream.remoteName, refspec], + ["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", remoteName], { allowNonZeroExit: true, timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT), @@ -945,18 +942,14 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir); }); - const refreshStatusUpstreamCacheEntry = Effect.fn("refreshStatusUpstreamCacheEntry")(function* ( - cacheKey: StatusUpstreamRefreshCacheKey, + const refreshStatusRemoteCacheEntry = Effect.fn("refreshStatusRemoteCacheEntry")(function* ( + cacheKey: StatusRemoteRefreshCacheKey, ) { - yield* fetchUpstreamRefForStatus(cacheKey.gitCommonDir, { - upstreamRef: cacheKey.upstreamRef, - remoteName: cacheKey.remoteName, - upstreamBranch: cacheKey.upstreamBranch, - }); + yield* fetchRemoteForStatus(cacheKey.gitCommonDir, cacheKey.remoteName); return true as const; }); - const statusUpstreamRefreshCache = yield* Cache.makeWith(refreshStatusUpstreamCacheEntry, { + const statusRemoteRefreshCache = yield* Cache.makeWith(refreshStatusRemoteCacheEntry, { capacity: STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY, // Keep successful refreshes warm and briefly back off failed refreshes to avoid retry storms. timeToLive: (exit) => @@ -972,12 +965,10 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { if (!upstream) return; const gitCommonDir = yield* resolveGitCommonDir(cwd); yield* Cache.get( - statusUpstreamRefreshCache, - new StatusUpstreamRefreshCacheKey({ + statusRemoteRefreshCache, + new StatusRemoteRefreshCacheKey({ gitCommonDir, - upstreamRef: upstream.upstreamRef, remoteName: upstream.remoteName, - upstreamBranch: upstream.upstreamBranch, }), ); });