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,
worktree_path: &Path,
) -> 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
let head = repo.head()?;
let head_commit = head.peel_to_commit()?;
@ -358,7 +365,7 @@ pub fn create_worktree(
// Create the worktree
let reference = branch.into_reference();
repo.worktree(
branch_name,
worktree_name,
worktree_path,
Some(git2::WorktreeAddOptions::new().reference(Some(&reference))),
)?;
@ -366,9 +373,17 @@ pub fn create_worktree(
Ok(())
}
/// Remove a worktree
pub fn remove_worktree(repo: &git2::Repository, name: &str) -> Result<(), RepoError> {
let worktree = repo.find_worktree(name)?;
/// Remove a worktree by path
///
/// 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)
worktree.prune(Some(

View file

@ -701,6 +701,13 @@ fn create_git_worktree(
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
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))?;
@ -720,7 +727,7 @@ fn create_git_worktree(
// Create the worktree
repo.worktree(
branch_name,
worktree_name,
worktree_path,
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
let worktree_removed = if worktree.is_some() {
blue_core::repo::remove_worktree(&repo, &branch_name).is_ok()
let worktree_removed = if let Some(ref wt) = worktree {
let wt_path = Path::new(&wt.worktree_path);
blue_core::repo::remove_worktree(&repo, wt_path).is_ok()
} else {
false
};
@ -465,8 +466,9 @@ pub fn handle_remove(state: &ProjectState, args: &Value) -> Result<Value, Server
// Remove from git
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 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!({
"status": "error",
"message": blue_core::voice::error(