From 64f4b23ccf011f872eaefc2ce1eed047715d73f5 Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Sat, 9 May 2026 16:34:16 +0300 Subject: [PATCH 1/5] fix(worktree): distinguish orphan paths from registered worktrees in create_worktree --- src/worktree_manager.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/worktree_manager.rs b/src/worktree_manager.rs index 17c8f51..df3ca5b 100644 --- a/src/worktree_manager.rs +++ b/src/worktree_manager.rs @@ -169,7 +169,22 @@ impl WorktreeManager { let worktree_path = worktree_dir.join(&safe_name); if worktree_path.exists() { - anyhow::bail!("Worktree path already exists: {:?}", worktree_path); + let registered = self + .list_worktrees()? + .into_iter() + .any(|w| w.path == worktree_path); + if registered { + anyhow::bail!( + "Worktree '{}' is already registered at {:?}", + task_id, + worktree_path + ); + } + anyhow::bail!( + "Path {:?} exists but is not a registered worktree.\n\ + Run 'git worktree prune' or remove the directory before retrying.", + worktree_path + ); } let mut upstream_branch: Option = None; @@ -601,6 +616,10 @@ mod tests { |_| unreachable!(), ); assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("already registered")); } #[test] From 9940529a89bf8c891603cb838d5dd85240965c70 Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Sat, 9 May 2026 16:34:30 +0300 Subject: [PATCH 2/5] fix(new): resume existing worktree instead of erroring --- src/main.rs | 17 +++++- tests/new_resume_test.rs | 124 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/new_resume_test.rs diff --git a/src/main.rs b/src/main.rs index af8289b..c62142c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -172,6 +172,22 @@ fn cmd_new(config: &RepoConfig, name: Option, base: &str, print_path: bo } }; + let manager = WorktreeManager::new(config.root.clone())?; + + if let Some(info) = manager.get_worktree_info(&name)? { + eprintln!( + "Worktree '{}' already exists at {}, entering it.", + name, + info.path.display() + ); + if print_path { + println!("{}", info.path.display()); + } else { + spawn_wt_shell(&info.path, &info.task_id, &info.branch)?; + } + return Ok(()); + } + // If creating worktree for currently checked out branch, migrate the work let migrating = name == current_branch && current_branch != root_branch; let had_changes = if migrating { @@ -180,7 +196,6 @@ fn cmd_new(config: &RepoConfig, name: Option, base: &str, print_path: bo false }; - let manager = WorktreeManager::new(config.root.clone())?; ensure_worktrees_in_gitignore(&config.root, &config.worktree_dir)?; std::fs::create_dir_all(&config.worktree_dir)?; let path = manager.create_worktree(&name, base, &config.worktree_dir, |remotes| { diff --git a/tests/new_resume_test.rs b/tests/new_resume_test.rs new file mode 100644 index 0000000..f0b2ec6 --- /dev/null +++ b/tests/new_resume_test.rs @@ -0,0 +1,124 @@ +use std::process::Command; +use tempfile::TempDir; +use wt::worktree_manager::WorktreeManager; + +fn setup_git_repo() -> TempDir { + let temp_dir = TempDir::new().unwrap(); + let repo_path = temp_dir.path(); + + Command::new("git") + .args(["init", "-b", "main"]) + .current_dir(repo_path) + .output() + .unwrap(); + + Command::new("git") + .args(["config", "user.email", "test@example.com"]) + .current_dir(repo_path) + .output() + .unwrap(); + + Command::new("git") + .args(["config", "user.name", "Test User"]) + .current_dir(repo_path) + .output() + .unwrap(); + + std::fs::write(repo_path.join("README.md"), "# Test Repo\n").unwrap(); + + Command::new("git") + .args(["add", "."]) + .current_dir(repo_path) + .output() + .unwrap(); + + Command::new("git") + .args(["commit", "-m", "Initial commit"]) + .current_dir(repo_path) + .output() + .unwrap(); + + temp_dir +} + +#[test] +fn test_create_worktree_fresh_succeeds() { + let repo = setup_git_repo(); + let worktree_dir = TempDir::new().unwrap(); + + let manager = WorktreeManager::new(repo.path().to_path_buf()).unwrap(); + let result = manager.create_worktree("fresh-feature", "main", worktree_dir.path(), |_| { + unreachable!() + }); + + assert!(result.is_ok(), "fresh create should succeed: {:?}", result); + let path = result.unwrap(); + assert!(path.exists()); + assert!(manager.worktree_exists("fresh-feature")); +} + +#[test] +fn test_create_worktree_already_registered_errors_at_manager_layer() { + let repo = setup_git_repo(); + let worktree_dir = TempDir::new().unwrap(); + + let manager = WorktreeManager::new(repo.path().to_path_buf()).unwrap(); + manager + .create_worktree("dup-feature", "main", worktree_dir.path(), |_| { + unreachable!() + }) + .unwrap(); + + let result = manager.create_worktree("dup-feature", "main", worktree_dir.path(), |_| { + unreachable!() + }); + + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!( + msg.contains("already registered"), + "expected 'already registered' in error: {msg}" + ); +} + +#[test] +fn test_create_worktree_orphan_directory_errors() { + let repo = setup_git_repo(); + let worktree_dir = TempDir::new().unwrap(); + + let manager = WorktreeManager::new(repo.path().to_path_buf()).unwrap(); + let path = manager + .create_worktree("orphan-feature", "main", worktree_dir.path(), |_| { + unreachable!() + }) + .unwrap(); + + // Deregister from git but leave the directory on disk + Command::new("git") + .args(["worktree", "remove", "--force", path.to_str().unwrap()]) + .current_dir(repo.path()) + .output() + .unwrap(); + + // Write a marker file to prove the directory is not cleaned up on error + let marker = path.join("user_work.txt"); + std::fs::create_dir_all(&path).unwrap(); + std::fs::write(&marker, "precious work").unwrap(); + + let result = manager.create_worktree("orphan-feature", "main", worktree_dir.path(), |_| { + unreachable!() + }); + + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!( + msg.contains("not a registered worktree"), + "expected actionable error, got: {msg}" + ); + assert!( + msg.contains("git worktree prune"), + "expected prune hint in error: {msg}" + ); + // Directory must not be deleted + assert!(marker.exists(), "orphan directory must not be auto-cleaned"); +} From 8a8ca45bdaf9a0713a6fc219c129955de2229d12 Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Sat, 9 May 2026 16:34:46 +0300 Subject: [PATCH 3/5] docs: document wt new idempotent resume behaviour --- README.md | 13 +++++++++++++ commands/do.md | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aff64c8..81f218b 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,19 @@ Entering worktree: feature/payments (wt) $ # Your changes are here ``` +Re-running `wt new ` for an existing worktree drops you back into it — +no error, no stash: + +```bash +$ wt new feature/auth +Worktree 'feature/auth' already exists at /…/.worktrees/feature--auth, entering it. +(wt) $ +``` + +If a directory exists under `.worktrees/` but is not registered with git +(e.g. after a partial `wt rm`), `wt new` exits with an actionable error and +leaves the directory untouched. + ### Switch workspaces ```bash diff --git a/commands/do.md b/commands/do.md index c957ba1..859708f 100644 --- a/commands/do.md +++ b/commands/do.md @@ -46,7 +46,9 @@ curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \ wt new --print-path ``` -This prints the worktree path. Capture it. +This prints the worktree path. Capture it. Re-running this command for an +already-created worktree is safe — it prints the existing path and exits 0 +without spawning a shell or creating a stash. ### 4. Set working context From 0fc500f64208b3a4a39e62300474187a70cdb5c7 Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Sat, 9 May 2026 17:03:37 +0300 Subject: [PATCH 4/5] fix(worktree): drop misleading prune hint from orphan error --- src/worktree_manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worktree_manager.rs b/src/worktree_manager.rs index df3ca5b..a5deda1 100644 --- a/src/worktree_manager.rs +++ b/src/worktree_manager.rs @@ -182,7 +182,7 @@ impl WorktreeManager { } anyhow::bail!( "Path {:?} exists but is not a registered worktree.\n\ - Run 'git worktree prune' or remove the directory before retrying.", + Remove the directory before retrying.", worktree_path ); } From 9f47642121be324da6f2547ec3e1424c4d34006e Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Sat, 9 May 2026 17:03:51 +0300 Subject: [PATCH 5/5] test: fmt and update orphan assertion to match new error --- tests/new_resume_test.rs | 49 ++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/tests/new_resume_test.rs b/tests/new_resume_test.rs index f0b2ec6..ba46b18 100644 --- a/tests/new_resume_test.rs +++ b/tests/new_resume_test.rs @@ -47,9 +47,12 @@ fn test_create_worktree_fresh_succeeds() { let worktree_dir = TempDir::new().unwrap(); let manager = WorktreeManager::new(repo.path().to_path_buf()).unwrap(); - let result = manager.create_worktree("fresh-feature", "main", worktree_dir.path(), |_| { - unreachable!() - }); + let result = manager.create_worktree( + "fresh-feature", + "main", + worktree_dir.path(), + |_| unreachable!(), + ); assert!(result.is_ok(), "fresh create should succeed: {:?}", result); let path = result.unwrap(); @@ -64,14 +67,20 @@ fn test_create_worktree_already_registered_errors_at_manager_layer() { let manager = WorktreeManager::new(repo.path().to_path_buf()).unwrap(); manager - .create_worktree("dup-feature", "main", worktree_dir.path(), |_| { - unreachable!() - }) + .create_worktree( + "dup-feature", + "main", + worktree_dir.path(), + |_| unreachable!(), + ) .unwrap(); - let result = manager.create_worktree("dup-feature", "main", worktree_dir.path(), |_| { - unreachable!() - }); + let result = manager.create_worktree( + "dup-feature", + "main", + worktree_dir.path(), + |_| unreachable!(), + ); assert!(result.is_err()); let msg = result.unwrap_err().to_string(); @@ -88,9 +97,12 @@ fn test_create_worktree_orphan_directory_errors() { let manager = WorktreeManager::new(repo.path().to_path_buf()).unwrap(); let path = manager - .create_worktree("orphan-feature", "main", worktree_dir.path(), |_| { - unreachable!() - }) + .create_worktree( + "orphan-feature", + "main", + worktree_dir.path(), + |_| unreachable!(), + ) .unwrap(); // Deregister from git but leave the directory on disk @@ -105,9 +117,12 @@ fn test_create_worktree_orphan_directory_errors() { std::fs::create_dir_all(&path).unwrap(); std::fs::write(&marker, "precious work").unwrap(); - let result = manager.create_worktree("orphan-feature", "main", worktree_dir.path(), |_| { - unreachable!() - }); + let result = manager.create_worktree( + "orphan-feature", + "main", + worktree_dir.path(), + |_| unreachable!(), + ); assert!(result.is_err()); let msg = result.unwrap_err().to_string(); @@ -116,8 +131,8 @@ fn test_create_worktree_orphan_directory_errors() { "expected actionable error, got: {msg}" ); assert!( - msg.contains("git worktree prune"), - "expected prune hint in error: {msg}" + msg.contains("Remove the directory"), + "expected removal hint in error: {msg}" ); // Directory must not be deleted assert!(marker.exists(), "orphan directory must not be auto-cleaned");