Merge pull request #1971 from trek-e/fix/doctor-orphan-worktree-cwd-1946
fix(doctor): chdir out of orphaned worktree before removal
This commit is contained in:
commit
d861352aac
2 changed files with 63 additions and 6 deletions
|
|
@ -80,18 +80,25 @@ export async function checkGitHealth(
|
|||
});
|
||||
|
||||
if (shouldFix("orphaned_auto_worktree")) {
|
||||
// Never remove a worktree matching current working directory
|
||||
// If cwd is inside the worktree, chdir out first — matching the
|
||||
// pattern in removeWorktree() (#1946). Without this, git cannot
|
||||
// remove the worktree and the doctor enters a deadlock where it
|
||||
// detects the orphan every run but never cleans it up.
|
||||
const cwd = process.cwd();
|
||||
if (wt.path === cwd || cwd.startsWith(wt.path + sep)) {
|
||||
fixesApplied.push(`skipped removing worktree at ${wt.path} (is cwd)`);
|
||||
} else {
|
||||
try {
|
||||
nativeWorktreeRemove(basePath, wt.path, true);
|
||||
fixesApplied.push(`removed orphaned worktree ${wt.path}`);
|
||||
process.chdir(basePath);
|
||||
} catch {
|
||||
fixesApplied.push(`failed to remove worktree ${wt.path}`);
|
||||
fixesApplied.push(`skipped removing worktree at ${wt.path} (cannot chdir to basePath)`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try {
|
||||
nativeWorktreeRemove(basePath, wt.path, true);
|
||||
fixesApplied.push(`removed orphaned worktree ${wt.path}`);
|
||||
} catch {
|
||||
fixesApplied.push(`failed to remove worktree ${wt.path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,56 @@ describe('doctor-git', async () => {
|
|||
} else {
|
||||
}
|
||||
|
||||
// ─── Test 1b: Orphaned worktree fix when cwd is inside worktree (#1946) ──
|
||||
// Reproduces the deadlock: if process.cwd() is inside the orphaned worktree,
|
||||
// the doctor must chdir out before removing it — not skip the removal.
|
||||
if (process.platform !== "win32") {
|
||||
console.log("\n=== orphaned_auto_worktree (cwd inside worktree) ===");
|
||||
{
|
||||
const dir = createRepoWithCompletedMilestone();
|
||||
cleanups.push(dir);
|
||||
|
||||
// Create worktree with milestone/M001 branch under .gsd/worktrees/
|
||||
mkdirSync(join(dir, ".gsd", "worktrees"), { recursive: true });
|
||||
run("git worktree add -b milestone/M001 .gsd/worktrees/M001", dir);
|
||||
|
||||
const wtPath = realpathSync(join(dir, ".gsd", "worktrees", "M001"));
|
||||
|
||||
// Simulate the deadlock: set cwd inside the orphaned worktree
|
||||
const previousCwd = process.cwd();
|
||||
process.chdir(wtPath);
|
||||
try {
|
||||
const fixed = await runGSDDoctor(dir, { fix: true, isolationMode: "worktree" });
|
||||
|
||||
// The fix must NOT skip removal — it should chdir out and remove
|
||||
assertTrue(
|
||||
!fixed.fixesApplied.some(f => f.includes("skipped removing worktree")),
|
||||
"does NOT skip removal when cwd is inside worktree",
|
||||
);
|
||||
assertTrue(
|
||||
fixed.fixesApplied.some(f => f.includes("removed orphaned worktree")),
|
||||
"removes orphaned worktree even when cwd was inside it",
|
||||
);
|
||||
|
||||
// Verify worktree is gone
|
||||
const wtList = run("git worktree list", dir);
|
||||
assertTrue(!wtList.includes("milestone/M001"), "worktree removed after fix with cwd inside");
|
||||
|
||||
// Verify cwd was moved out (should be basePath, not still inside worktree)
|
||||
const newCwd = process.cwd();
|
||||
assertTrue(
|
||||
!newCwd.startsWith(wtPath),
|
||||
"cwd moved out of worktree after fix",
|
||||
);
|
||||
} finally {
|
||||
// Restore cwd — the worktree dir may be gone, so chdir to previousCwd
|
||||
try { process.chdir(previousCwd); } catch { process.chdir(dir); }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("\n=== orphaned_auto_worktree (cwd inside worktree — skipped on Windows) ===");
|
||||
}
|
||||
|
||||
// ─── Test 2: Stale milestone branch detection & fix ────────────────
|
||||
// Skip on Windows: git branch glob matching and path resolution
|
||||
// behave differently in Windows temp dirs.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue