diff --git a/server/src/worktree.rs b/server/src/worktree.rs index 52147c5..4348c0a 100644 --- a/server/src/worktree.rs +++ b/server/src/worktree.rs @@ -253,7 +253,23 @@ fn remove_worktree_sync(project_root: &Path, wt_path: &Path, branch: &str) -> Re if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); - slog!("[worktree] remove warning: {stderr}"); + if stderr.contains("not a working tree") { + // Orphaned directory: git doesn't recognise it as a worktree. + // Remove the directory directly and prune stale git metadata. + slog!( + "[worktree] orphaned worktree detected, removing directory: {}", + wt_path.display() + ); + if let Err(e) = std::fs::remove_dir_all(wt_path) { + slog!("[worktree] failed to remove orphaned directory: {e}"); + } + let _ = Command::new("git") + .args(["worktree", "prune"]) + .current_dir(project_root) + .output(); + } else { + slog!("[worktree] remove warning: {stderr}"); + } } // Delete branch (best effort) @@ -630,6 +646,28 @@ mod tests { ); } + #[test] + fn remove_worktree_sync_removes_orphaned_directory() { + let tmp = TempDir::new().unwrap(); + let project_root = tmp.path().join("my-project"); + fs::create_dir_all(&project_root).unwrap(); + init_git_repo(&project_root); + + // Create a directory that looks like a worktree but isn't registered with git + let wt_path = project_root + .join(".storkit") + .join("worktrees") + .join("orphan"); + fs::create_dir_all(&wt_path).unwrap(); + fs::write(wt_path.join("some_file.txt"), "stale").unwrap(); + assert!(wt_path.exists()); + + // git worktree remove will fail with "not a working tree", + // but the fallback should rm -rf the directory + remove_worktree_sync(&project_root, &wt_path, "feature/orphan").unwrap(); + assert!(!wt_path.exists()); + } + #[test] fn remove_worktree_sync_cleans_up_directory() { let tmp = TempDir::new().unwrap();