From e3ed586691eff7e1c23329ce7376cf573d01c5fd Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 31 Mar 2026 13:44:01 -0400 Subject: [PATCH] Prune stale git worktrees before branch creation in WorktreeSandbox Stale worktree references from deleted temp directories kept branches locked, causing "cannot force update the branch" errors on subsequent runs with the same branch name. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/crates/fabro-sandbox/src/worktree.rs | 25 +++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/crates/fabro-sandbox/src/worktree.rs b/lib/crates/fabro-sandbox/src/worktree.rs index 9e0fa517e..198bd1e0a 100644 --- a/lib/crates/fabro-sandbox/src/worktree.rs +++ b/lib/crates/fabro-sandbox/src/worktree.rs @@ -123,6 +123,15 @@ impl Sandbox for WorktreeSandbox { .exec_command(&rm_cmd, 30_000, None, None, None) .await; + // Prune all stale worktree references whose directories no longer exist. + // Without this, a branch may remain locked by a worktree in a deleted + // temp directory from a previous run. + let prune_cmd = format!("{GIT} worktree prune"); + let _ = self + .inner + .exec_command(&prune_cmd, 30_000, None, None, None) + .await; + if !self.config.skip_branch_creation { let cmd = format!("{GIT} branch --force {branch} {sha}"); let result = self @@ -386,15 +395,16 @@ mod tests { wt.initialize().await.unwrap(); let cmds = mock.captured_commands.lock().unwrap().clone(); - // worktree remove (best-effort), branch --force, worktree add - assert_eq!(cmds.len(), 3, "expected 3 git commands, got: {cmds:?}"); + // worktree remove (best-effort), worktree prune, branch --force, worktree add + assert_eq!(cmds.len(), 4, "expected 4 git commands, got: {cmds:?}"); assert!( cmds[0].contains("worktree remove --force"), "cmd[0]: {}", cmds[0] ); - assert!(cmds[1].contains("branch --force"), "cmd[1]: {}", cmds[1]); - assert!(cmds[2].contains("worktree add"), "cmd[2]: {}", cmds[2]); + assert!(cmds[1].contains("worktree prune"), "cmd[1]: {}", cmds[1]); + assert!(cmds[2].contains("branch --force"), "cmd[2]: {}", cmds[2]); + assert!(cmds[3].contains("worktree add"), "cmd[3]: {}", cmds[3]); } #[tokio::test] @@ -453,14 +463,15 @@ mod tests { wt.initialize().await.unwrap(); let cmds = mock.captured_commands.lock().unwrap().clone(); - // Only worktree remove (best-effort) and worktree add - assert_eq!(cmds.len(), 2, "expected 2 git commands, got: {cmds:?}"); + // worktree remove (best-effort), worktree prune, worktree add + assert_eq!(cmds.len(), 3, "expected 3 git commands, got: {cmds:?}"); assert!( cmds[0].contains("worktree remove --force"), "cmd[0]: {}", cmds[0] ); - assert!(cmds[1].contains("worktree add"), "cmd[1]: {}", cmds[1]); + assert!(cmds[1].contains("worktree prune"), "cmd[1]: {}", cmds[1]); + assert!(cmds[2].contains("worktree add"), "cmd[2]: {}", cmds[2]); } #[tokio::test]