From 9f42901683f3dadff3c4b793e8e6c85eb2301b06 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 4 Nov 2026 22:02:22 -0600 Subject: [PATCH] fix(workspaces): inspect retained indexes before Git probes Read managed Git metadata before starting Git commands. Keep the synthetic orphan index locked so unrelated Git maintenance cannot prune the regression fixture before inspection. Co-Authored-By: Paperclip --- server/src/services/remote-workspace-migration-git.ts | 4 ++-- server/src/services/remote-workspace-migration.test.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/services/remote-workspace-migration-git.ts b/server/src/services/remote-workspace-migration-git.ts index 4ab4bbbd99c..ea66217b354 100644 --- a/server/src/services/remote-workspace-migration-git.ts +++ b/server/src/services/remote-workspace-migration-git.ts @@ -43,7 +43,7 @@ async function git(cwd: string, args: string[]) { async function assertNoRetainedWorktree(sourcePath: string, expected: string, branch: string) { const target = await physicalPath(expected); - const common = await git(sourcePath, ["rev-parse", "--path-format=absolute", "--git-common-dir"]); + const common = path.join(sourcePath, ".git"); const admin = path.join(common, "worktrees"); const entries = await fs.readdir(admin, { withFileTypes: true }).catch((error: NodeJS.ErrnoException) => { if (error.code === "ENOENT") return []; @@ -118,6 +118,7 @@ export async function inspectArchivedGitMigrationSource(db: Pick, // not certify lost untracked files. Any surviving worktree registration // can retain additional index/files authority and must stay blocked. if (workspace.mode !== "isolated_workspace" || !workspace.projectId || !managedSourcePaths(workspace).includes(sourcePath)) return null; + await assertNoRetainedWorktree(sourcePath, path.join(sourcePath, ".paperclip", "worktrees", workspace.branchName), workspace.branchName); await git(sourcePath, ["check-ref-format", `refs/heads/${workspace.branchName}`]); const expected = path.join(sourcePath, ".paperclip", "worktrees", workspace.branchName); if (workspace.cwd !== expected || workspace.providerRef && workspace.providerRef !== expected) return null; @@ -147,7 +148,6 @@ export async function inspectArchivedGitMigrationSource(db: Pick, ne(executionWorkspaces.id, workspace.id), )).limit(1); if (other) throw unavailable(); - await assertNoRetainedWorktree(sourcePath, expected, workspace.branchName); const commit = await git(sourcePath, ["rev-parse", "--verify", `refs/heads/${workspace.branchName}^{commit}`]); if (!/^[a-f0-9]{40}$/.test(commit)) throw unavailable(); return { commit, sourcePath, cleanupOperationId: removalId, recipe: null, retainedBranch: workspace.branchName }; diff --git a/server/src/services/remote-workspace-migration.test.ts b/server/src/services/remote-workspace-migration.test.ts index d4c5be8d31f..9f61f38566d 100644 --- a/server/src/services/remote-workspace-migration.test.ts +++ b/server/src/services/remote-workspace-migration.test.ts @@ -597,6 +597,7 @@ describe.skipIf(!support.supported)("remote workspace migration", () => { if (mode === "orphan-index") { const admin = path.join(f.base, ".git", "worktrees", "orphan"); await fs.mkdir(admin, { recursive: true }); await fs.copyFile(path.join(f.base, ".git", "index"), path.join(admin, "index")); + await fs.writeFile(path.join(admin, "locked"), "retained staged recovery work\n"); } if (mode === "changed-branch") { const put = storage.putFile.bind(storage);