diff --git a/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts b/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts index fc9efe1684..cd9ec71d83 100644 --- a/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts +++ b/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts @@ -493,6 +493,7 @@ async function createRestorableWorkspace(baseRef: string) { commitFile(cwd, "feature.txt", "feature change\n"); const head = runGit(cwd, "rev-parse", "HEAD"); await client.checkoutRefresh(cwd); + expect(await client.getCheckoutStatus(cwd)).toMatchObject({ isPaseoOwnedWorktree: true }); const before = await client.getCheckoutDiff(cwd, { mode: "base", baseRef: "main" }); expect(before.error).toBeNull(); expect(before.files.map((file) => file.path)).toEqual(["feature.txt"]); diff --git a/packages/server/src/utils/worktree.test.ts b/packages/server/src/utils/worktree.test.ts index 848feddf99..605e2b8c20 100644 --- a/packages/server/src/utils/worktree.test.ts +++ b/packages/server/src/utils/worktree.test.ts @@ -5,6 +5,7 @@ import { deletePaseoWorktree, isPaseoOwnedWorktreeCwd, mapWorkspaceCwdToWorktree, + runWorktreeTeardownCommands, slugify, type CreateWorktreeOptions, type WorktreeConfig, @@ -172,6 +173,14 @@ describe("paseo worktree manager", () => { const ownership = await isPaseoOwnedWorktreeCwd(created.worktreePath, { paseoHome }); expect(ownership).toMatchObject({ allowed: true, worktreePath: created.worktreePath }); + // Windows may report the same root through native 8.3 and JS long spellings. + const nativeWorktreePath = realpathSync.native(created.worktreePath); + await expect( + runWorktreeTeardownCommands({ + worktreePath: created.worktreePath, + teardownCwd: nativeWorktreePath, + }), + ).resolves.toEqual([]); await expect( isPaseoOwnedWorktreeCwd(join(created.worktreePath, "packages", "app"), { paseoHome }), ).resolves.toMatchObject({ diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts index 687f8c1e54..faa7ec4065 100644 --- a/packages/server/src/utils/worktree.ts +++ b/packages/server/src/utils/worktree.ts @@ -6,6 +6,7 @@ import { join, basename, dirname, isAbsolute, resolve, sep } from "path"; import net from "node:net"; import { createHash } from "node:crypto"; import stripAnsi from "strip-ansi"; +import pLimit from "p-limit"; import { buildStringCommandShellInvocation, createStringCommandShellEnv, @@ -912,20 +913,32 @@ export function mapWorkspaceRelativeCwdToWorktree(input: { // Keep the former realpathSync spelling on Windows: promises.realpath uses the // native resolver and can return an 8.3 alias instead of the long path. const resolveOwnershipPath = promisify(realpath); +const resolveNativeOwnershipPath = promisify(realpath.native); +const ownershipIdentityIo = pLimit(2); async function normalizePathForOwnership(input: string): Promise { try { - return await resolveOwnershipPath(input); + return await ownershipIdentityIo(() => resolveOwnershipPath(input)); } catch { return resolve(input); } } +async function ownershipPathVariants(input: string): Promise { + const variants = new Set([input, await normalizePathForOwnership(input)]); + try { + variants.add(await ownershipIdentityIo(() => resolveNativeOwnershipPath(input))); + } catch { + // Missing descendants still match a lexical or established root spelling. + } + return [...variants]; +} + // Keep lexical variants for missing paths: archive must still recognize a removed // worktree below a symlinked worktrees root, whose full realpath no longer exists. async function getOwnershipRelativePath(root: string, candidate: string): Promise { - const roots = [root, await normalizePathForOwnership(root)]; - const candidates = [candidate, await normalizePathForOwnership(candidate)]; + const roots = await ownershipPathVariants(root); + const candidates = await ownershipPathVariants(candidate); for (const rootPath of roots) { for (const candidatePath of candidates) { const relative = getRelativePathInsideRoot(rootPath, candidatePath); @@ -1062,7 +1075,7 @@ export async function listPaseoWorktrees({ envOverlay: READ_ONLY_GIT_ENV, }); - const roots = [projectWorktreesRoot, await normalizePathForOwnership(projectWorktreesRoot)]; + const roots = await ownershipPathVariants(projectWorktreesRoot); const worktrees: PaseoWorktreeInfo[] = []; // Inspect foreign paths sequentially; a large worktree list must not consume the FS pool. for (const entry of parseWorktreeList(stdout)) {