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 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/src/worktree_manager.rs b/src/worktree_manager.rs index 17c8f51..a5deda1 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\ + 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] diff --git a/tests/new_resume_test.rs b/tests/new_resume_test.rs new file mode 100644 index 0000000..ba46b18 --- /dev/null +++ b/tests/new_resume_test.rs @@ -0,0 +1,139 @@ +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("Remove the directory"), + "expected removal hint in error: {msg}" + ); + // Directory must not be deleted + assert!(marker.exists(), "orphan directory must not be auto-cleaned"); +}