From 1a331e285b7ded48913fa23e1710fb4c80aab811 Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:13:09 -0700 Subject: [PATCH 1/3] fix(server): stop reporting a separate git dir as a branch's worktree Motivation: With a separate git dir that has no core.worktree back-link (e.g. `git init --separate-git-dir`), `git --git-dir= worktree list` names the git dir itself as the main worktree. listRefs kept that path because it exists on disk, so the branch's worktreePath pointed at the git directory (objects/, refs/, HEAD) instead of the folder the user opened, and it could be picked up as an existing worktree to reuse. Approach: in readGitRefsSnapshot, drop worktree entries whose path equals the git common dir. Normal repos (common dir is /.git), bare repos and linked worktrees are unaffected. Validation: added a real-git test using `git init --separate-git-dir`. Ran `vp test run src/vcs` in apps/server (7 files, 214 tests pass). The new test fails without the source change (worktreePath equals the git dir) and passes with it. Also ran vp fmt and vp lint on the touched files. Scope: this is a partial fix for the report. It only changes the worktree paths returned by listRefs; project-root detection elsewhere (resolveRepositoryPaths, --show-toplevel) is unchanged, and I did not exercise the desktop or mobile clients. Report: https://github.com/pingdotgg/t3code/issues/12204 Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Assisted-by: claude-sonnet-5-5 (via Claude Code) --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 28 ++++++++++++++++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 4 +++ 2 files changed, 32 insertions(+) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 85c4d1a59d1d..de546ade933c 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -694,6 +694,34 @@ it.effect("ignores worktree metadata for directories that no longer exist", () = ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))), ); +it.effect("does not report a separate git dir as the worktree of its branch", () => + Effect.scoped( + Effect.gen(function* () { + const driver = yield* makeGitVcsDriverCore(); + const cwd = yield* makeTmpDir(); + const gitDir = NodeFS.realpathSync(yield* makeTmpDir()); + yield* git(cwd, ["init", `--separate-git-dir=${gitDir}`]).pipe( + Effect.provideService(GitVcsDriver.GitVcsDriver, driver), + ); + yield* git(cwd, ["config", "user.email", "test@example.com"]).pipe( + Effect.provideService(GitVcsDriver.GitVcsDriver, driver), + ); + yield* git(cwd, ["config", "user.name", "Test"]).pipe( + Effect.provideService(GitVcsDriver.GitVcsDriver, driver), + ); + yield* git(cwd, ["commit", "--allow-empty", "-m", "init"]).pipe( + Effect.provideService(GitVcsDriver.GitVcsDriver, driver), + ); + const refs = yield* driver.listRefs({ cwd, refresh: true }); + + assert.isTrue(refs.isRepo); + const current = refs.refs.find((ref) => ref.current); + assert.isDefined(current); + assert.notEqual(current?.worktreePath, gitDir); + }), + ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))), +); + it.effect("refreshes the current branch after an external checkout", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 0dd73af687f9..48c701039f59 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -2829,9 +2829,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* [branchName, path.normalize(path.resolve(worktreePath))] as const, ) : []; + // With a separate git dir lacking `core.worktree`, git lists the git dir itself as the + // main worktree. It is not a checkout, so it must never be offered as one. + const normalizedGitCommonDir = path.normalize(path.resolve(gitCommonDir)); const existingWorktreeEntries = yield* Effect.filter( parsedWorktreeEntries, ([, worktreePath]) => + worktreePath !== normalizedGitCommonDir && fileSystem.stat(worktreePath).pipe( Effect.as(true), Effect.orElseSucceed(() => false), From 4dae5c76e78e80153abc76c0052dac18c88c63ad Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:36:22 -0700 Subject: [PATCH 2/3] fix(server): keep the opened folder as the current branch's worktreePath Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 1 + apps/server/src/vcs/GitVcsDriverCore.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index de546ade933c..eca34f9d994e 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -718,6 +718,7 @@ it.effect("does not report a separate git dir as the worktree of its branch", () const current = refs.refs.find((ref) => ref.current); assert.isDefined(current); assert.notEqual(current?.worktreePath, gitDir); + assert.equal(current?.worktreePath, NodeFS.realpathSync(cwd)); }), ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))), ); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 48c701039f59..afe08ff5c913 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -3030,6 +3030,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* snapshot.localBranches.some((ref) => ref.worktreePath === repositoryPaths.worktreeRoot); const localBranches = snapshot.localBranches.map((ref) => ({ ...ref, + // A separate git dir is filtered out of the worktree list, which leaves the + // current branch without a path; fall back to the folder the user opened. + worktreePath: + ref.worktreePath ?? + (!ref.isRemote && ref.name === repositoryPaths.currentBranch + ? repositoryPaths.worktreeRoot + : null), current: hasCurrentWorktreeBranch ? ref.worktreePath === repositoryPaths.worktreeRoot : ref.name === repositoryPaths.currentBranch, From c44eed3aebc2a6e2616aa5866182449e860ec079 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 05:15:00 +0000 Subject: [PATCH 3/3] fix(server): preserve checkout paths with separate Git directories --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 33 ++++++++------------ apps/server/src/vcs/GitVcsDriverCore.ts | 20 +++++------- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 0e7e6bba7ec2..fe106c660114 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -828,30 +828,23 @@ it.effect("reads worktrees from git that rejects worktree list -z", () => it.effect("does not report a separate git dir as the worktree of its branch", () => Effect.scoped( Effect.gen(function* () { - const driver = yield* makeGitVcsDriverCore(); + const driver = yield* GitVcsDriver.GitVcsDriver; const cwd = yield* makeTmpDir(); - const gitDir = NodeFS.realpathSync(yield* makeTmpDir()); - yield* git(cwd, ["init", `--separate-git-dir=${gitDir}`]).pipe( - Effect.provideService(GitVcsDriver.GitVcsDriver, driver), - ); - yield* git(cwd, ["config", "user.email", "test@example.com"]).pipe( - Effect.provideService(GitVcsDriver.GitVcsDriver, driver), - ); - yield* git(cwd, ["config", "user.name", "Test"]).pipe( - Effect.provideService(GitVcsDriver.GitVcsDriver, driver), - ); - yield* git(cwd, ["commit", "--allow-empty", "-m", "init"]).pipe( - Effect.provideService(GitVcsDriver.GitVcsDriver, driver), - ); + const gitDir = yield* makeTmpDir(); + yield* git(cwd, ["init", `--separate-git-dir=${gitDir}`]); + yield* git(cwd, ["config", "user.email", "test@test.com"]); + yield* git(cwd, ["config", "user.name", "Test"]); + yield* git(cwd, ["commit", "--allow-empty", "-m", "init"]); + const refs = yield* driver.listRefs({ cwd, refresh: true }); + const worktreePath = refs.refs.find((ref) => ref.current)?.worktreePath; - assert.isTrue(refs.isRepo); - const current = refs.refs.find((ref) => ref.current); - assert.isDefined(current); - assert.notEqual(current?.worktreePath, gitDir); - assert.equal(current?.worktreePath, NodeFS.realpathSync(cwd)); + assert.equal( + worktreePath && NodeFS.realpathSync.native(worktreePath), + NodeFS.realpathSync.native(cwd), + ); }), - ).pipe(Effect.provide(ServerConfigLayer.pipe(Layer.provideMerge(NodeServices.layer)))), + ).pipe(Effect.provide(layerTest)), ); it.effect("refreshes the current branch after an external checkout", () => diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index addac9dbeb2b..7adc060833f9 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -3236,17 +3236,17 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* [branchName, path.normalize(path.resolve(worktreePath))] as const, ) : []; - // With a separate git dir lacking `core.worktree`, git lists the git dir itself as the - // main worktree. It is not a checkout, so it must never be offered as one. + // Without `core.worktree`, Git reports a separate git dir as the main checkout. const normalizedGitCommonDir = path.normalize(path.resolve(gitCommonDir)); const existingWorktreeEntries = yield* Effect.filter( parsedWorktreeEntries, ([, worktreePath]) => - worktreePath !== normalizedGitCommonDir && - fileSystem.stat(worktreePath).pipe( - Effect.as(true), - Effect.orElseSucceed(() => false), - ), + worktreePath === normalizedGitCommonDir + ? Effect.succeed(false) + : fileSystem.stat(worktreePath).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + ), { concurrency: 16 }, ); const worktreeMap = new Map(existingWorktreeEntries); @@ -3437,13 +3437,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* snapshot.localBranches.some((ref) => ref.worktreePath === repositoryPaths.worktreeRoot); const localBranches = snapshot.localBranches.map((ref) => ({ ...ref, - // A separate git dir is filtered out of the worktree list, which leaves the - // current branch without a path; fall back to the folder the user opened. worktreePath: ref.worktreePath ?? - (!ref.isRemote && ref.name === repositoryPaths.currentBranch - ? repositoryPaths.worktreeRoot - : null), + (ref.name === repositoryPaths.currentBranch ? repositoryPaths.worktreeRoot : null), current: hasCurrentWorktreeBranch ? ref.worktreePath === repositoryPaths.worktreeRoot : ref.name === repositoryPaths.currentBranch,