diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 5cf3b7c4b4b9..9f4c344c7b8b 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -718,6 +718,67 @@ it.effect("refreshes the current branch after an external checkout", () => ).pipe(Effect.provide(TestLayer)), ); +for (const configureWorktree of [false, true]) { + it.effect( + `keeps separate Git metadata out of worktree paths (core.worktree: ${configureWorktree})`, + () => + Effect.scoped( + Effect.gen(function* () { + const driver = yield* GitVcsDriver.GitVcsDriver; + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const cwd = yield* makeTmpDir(); + const metadataRoot = yield* makeTmpDir("git-vcs-driver-metadata-"); + const gitDir = path.join(metadataRoot, "metadata"); + const { initialBranch } = yield* initRepoWithCommit(cwd); + yield* git(cwd, ["init", "--separate-git-dir", gitDir]); + if (configureWorktree) { + yield* git(cwd, ["config", "core.worktree", cwd]); + } + const nestedCwd = path.join(cwd, "nested"); + yield* fileSystem.makeDirectory(nestedCwd); + const linkedPath = path.join(metadataRoot, "linked"); + yield* git(cwd, ["worktree", "add", "-b", "linked", linkedPath]); + + // Fill the shared ref cache from a linked checkout before reading the + // main checkout, whose root cannot be inferred from the metadata path. + const linkedRefs = yield* driver.listRefs({ cwd: linkedPath, refresh: true }); + assert.equal( + linkedRefs.refs.find((ref) => ref.name === initialBranch)?.worktreePath, + null, + ); + assert.equal( + NodeFS.realpathSync.native( + linkedRefs.refs.find((ref) => ref.name === "linked")!.worktreePath!, + ), + NodeFS.realpathSync.native(linkedPath), + ); + + const mainRefs = yield* driver.listRefs({ cwd: nestedCwd }); + const mainRef = mainRefs.refs.find((ref) => ref.name === initialBranch); + assert.isTrue(mainRef?.current); + assert.equal( + NodeFS.realpathSync.native(mainRef!.worktreePath!), + NodeFS.realpathSync.native(cwd), + ); + assert.equal( + NodeFS.realpathSync.native( + mainRefs.refs.find((ref) => ref.name === "linked")!.worktreePath!, + ), + NodeFS.realpathSync.native(linkedPath), + ); + + const linkedAgain = yield* driver.listRefs({ cwd: linkedPath }); + assert.isTrue(linkedAgain.refs.find((ref) => ref.name === "linked")?.current); + assert.equal( + linkedAgain.refs.find((ref) => ref.name === initialBranch)?.worktreePath, + null, + ); + }), + ).pipe(Effect.provide(TestLayer)), + ); +} + it.effect("backs off failed upstream refreshes across linked worktrees", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 9367d027a210..392e7bcdd696 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2772,10 +2772,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const existingWorktreeEntries = yield* Effect.filter( parsedWorktreeEntries, ([, worktreePath]) => - fileSystem.stat(worktreePath).pipe( - Effect.as(true), - Effect.orElseSucceed(() => false), - ), + // With --separate-git-dir, Git can report the metadata directory as + // the main worktree. It must never be offered as a checkout to reuse. + worktreePath === gitCommonDir + ? Effect.succeed(false) + : fileSystem.stat(worktreePath).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + ), { concurrency: 16 }, ); const worktreeMap = new Map(existingWorktreeEntries); @@ -2964,12 +2968,18 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* const hasCurrentWorktreeBranch = repositoryPaths.worktreeRoot !== null && snapshot.localBranches.some((ref) => ref.worktreePath === repositoryPaths.worktreeRoot); - const localBranches = snapshot.localBranches.map((ref) => ({ - ...ref, - current: hasCurrentWorktreeBranch + const localBranches = snapshot.localBranches.map((ref) => { + const current = hasCurrentWorktreeBranch ? ref.worktreePath === repositoryPaths.worktreeRoot - : ref.name === repositoryPaths.currentBranch, - })); + : ref.name === repositoryPaths.currentBranch; + return { + ...ref, + current, + // The caller's --show-toplevel is authoritative for its checkout. + // Keep this out of the snapshot shared with linked worktrees. + worktreePath: current ? repositoryPaths.worktreeRoot : ref.worktreePath, + }; + }); const combinedBranches = input.includeMatchingRemoteRefs ? [...localBranches, ...snapshot.remoteBranches] : dedupeRemoteBranchesWithLocalMatches([...localBranches, ...snapshot.remoteBranches]);