Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ Entering worktree: feature/payments
(wt) $ # Your changes are here
```

Re-running `wt new <name>` 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
Expand Down
4 changes: 3 additions & 1 deletion commands/do.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ curl -s -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
wt new <branch-name> --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

Expand Down
17 changes: 16 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,22 @@ fn cmd_new(config: &RepoConfig, name: Option<String>, 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 {
Expand All @@ -180,7 +196,6 @@ fn cmd_new(config: &RepoConfig, name: Option<String>, 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| {
Expand Down
21 changes: 20 additions & 1 deletion src/worktree_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = None;
Expand Down Expand Up @@ -601,6 +616,10 @@ mod tests {
|_| unreachable!(),
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("already registered"));
}

#[test]
Expand Down
139 changes: 139 additions & 0 deletions tests/new_resume_test.rs
Original file line number Diff line number Diff line change
@@ -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");
}
Loading