fix: derive worktree name from path, not branch name

Git worktree names are stored in .git/worktrees/<name> and cannot
contain slashes. The code was using branch names like "feature/slug"
or "rfc/name" as worktree names, which git2 rejects silently.

Now the worktree name is derived from the path's directory name (the
slug), which is always a simple identifier without slashes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Garcia 2026-01-30 19:09:13 -05:00
parent 71c3d3caa9
commit ddce9e8b03
3 changed files with 32 additions and 8 deletions

View file

@ -346,6 +346,13 @@ pub fn create_worktree(
branch_name: &str, branch_name: &str,
worktree_path: &Path, worktree_path: &Path,
) -> Result<(), RepoError> { ) -> Result<(), RepoError> {
// Derive worktree name from path (directory name = slug, no slashes)
// Git worktree names are stored in .git/worktrees/<name> and cannot contain slashes
let worktree_name = worktree_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| RepoError::Git(git2::Error::from_str("Invalid worktree path")))?;
// Create the branch if it doesn't exist // Create the branch if it doesn't exist
let head = repo.head()?; let head = repo.head()?;
let head_commit = head.peel_to_commit()?; let head_commit = head.peel_to_commit()?;
@ -358,7 +365,7 @@ pub fn create_worktree(
// Create the worktree // Create the worktree
let reference = branch.into_reference(); let reference = branch.into_reference();
repo.worktree( repo.worktree(
branch_name, worktree_name,
worktree_path, worktree_path,
Some(git2::WorktreeAddOptions::new().reference(Some(&reference))), Some(git2::WorktreeAddOptions::new().reference(Some(&reference))),
)?; )?;
@ -366,9 +373,17 @@ pub fn create_worktree(
Ok(()) Ok(())
} }
/// Remove a worktree /// Remove a worktree by path
pub fn remove_worktree(repo: &git2::Repository, name: &str) -> Result<(), RepoError> { ///
let worktree = repo.find_worktree(name)?; /// Derives the worktree name from the path's directory name.
pub fn remove_worktree(repo: &git2::Repository, worktree_path: &Path) -> Result<(), RepoError> {
// Derive worktree name from path (same as create_worktree)
let worktree_name = worktree_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| RepoError::Git(git2::Error::from_str("Invalid worktree path")))?;
let worktree = repo.find_worktree(worktree_name)?;
// Prune the worktree (this removes the worktree but keeps the branch) // Prune the worktree (this removes the worktree but keeps the branch)
worktree.prune(Some( worktree.prune(Some(

View file

@ -701,6 +701,13 @@ fn create_git_worktree(
return Err("Worktree path already exists".to_string()); return Err("Worktree path already exists".to_string());
} }
// Derive worktree name from path (directory name = slug, no slashes)
// Git worktree names are stored in .git/worktrees/<name> and cannot contain slashes
let worktree_name = worktree_path
.file_name()
.and_then(|n| n.to_str())
.ok_or("Invalid worktree path")?;
// Get HEAD commit to branch from // Get HEAD commit to branch from
let head = repo.head().map_err(|e| format!("Failed to get HEAD: {}", e))?; let head = repo.head().map_err(|e| format!("Failed to get HEAD: {}", e))?;
let commit = head.peel_to_commit().map_err(|e| format!("Failed to get commit: {}", e))?; let commit = head.peel_to_commit().map_err(|e| format!("Failed to get commit: {}", e))?;
@ -720,7 +727,7 @@ fn create_git_worktree(
// Create the worktree // Create the worktree
repo.worktree( repo.worktree(
branch_name, worktree_name,
worktree_path, worktree_path,
Some(git2::WorktreeAddOptions::new().reference(Some(&reference))), Some(git2::WorktreeAddOptions::new().reference(Some(&reference))),
) )

View file

@ -380,8 +380,9 @@ pub fn handle_cleanup(state: &ProjectState, args: &Value) -> Result<Value, Serve
} }
// Remove worktree from git // Remove worktree from git
let worktree_removed = if worktree.is_some() { let worktree_removed = if let Some(ref wt) = worktree {
blue_core::repo::remove_worktree(&repo, &branch_name).is_ok() let wt_path = Path::new(&wt.worktree_path);
blue_core::repo::remove_worktree(&repo, wt_path).is_ok()
} else { } else {
false false
}; };
@ -465,8 +466,9 @@ pub fn handle_remove(state: &ProjectState, args: &Value) -> Result<Value, Server
// Remove from git // Remove from git
let repo_path = state.home.root.clone(); let repo_path = state.home.root.clone();
let wt_path = Path::new(&worktree.worktree_path);
if let Ok(repo) = git2::Repository::open(&repo_path) { if let Ok(repo) = git2::Repository::open(&repo_path) {
if let Err(e) = blue_core::repo::remove_worktree(&repo, &worktree.branch_name) { if let Err(e) = blue_core::repo::remove_worktree(&repo, wt_path) {
return Ok(json!({ return Ok(json!({
"status": "error", "status": "error",
"message": blue_core::voice::error( "message": blue_core::voice::error(