use anyhow::{Context, Result};
use std::fs;
use std::path::PathBuf;
use crate::git::GitRepo;
use crate::selection::{RealSelectionProvider, SelectionProvider};
use crate::storage::{WorktreeStorage, read_worktree_head_branch};
pub fn remove_worktree(
target: Option<&str>,
delete_branch: bool,
interactive: bool,
list_completions: bool,
current_repo_only: bool,
) -> Result<()> {
remove_worktree_with_provider(
target,
delete_branch,
interactive,
list_completions,
current_repo_only,
&RealSelectionProvider,
)
}
pub fn remove_worktree_with_provider(
target: Option<&str>,
delete_branch: bool,
interactive: bool,
list_completions: bool,
current_repo_only: bool,
provider: &dyn SelectionProvider,
) -> Result<()> {
let storage = WorktreeStorage::new()?;
if list_completions {
list_worktree_completions(&storage, current_repo_only)?;
return Ok(());
}
let current_dir = std::env::current_dir()?;
let git_repo = GitRepo::open(¤t_dir)?;
let repo_path = git_repo.get_repo_path();
let repo_name = WorktreeStorage::get_repo_name(repo_path)?;
let (worktree_path, feature_name) = if interactive || target.is_none() {
select_worktree_for_removal(&storage, current_repo_only, provider)?
} else if let Some(target_str) = target {
resolve_target(target_str, &storage, &repo_name)?
} else {
anyhow::bail!("No target specified for worktree removal");
};
if !worktree_path.exists() {
anyhow::bail!("Worktree path does not exist: {}", worktree_path.display());
}
println!(
"Removing worktree '{}': {}",
feature_name,
worktree_path.display()
);
let current_branch = read_worktree_head_branch(&worktree_path);
let worktree_name = worktree_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&feature_name);
if worktree_path.exists() {
fs::remove_dir_all(&worktree_path).context("Failed to remove worktree directory")?;
}
git_repo
.remove_worktree(worktree_name)
.context("Failed to remove worktree from git")?;
if let Err(e) = storage.remove_worktree_origin(&repo_name, &feature_name) {
println!("⚠ Warning: Failed to clean up origin information: {}", e);
}
if delete_branch {
if let Some(branch) = ¤t_branch {
println!("Deleting branch: {}", branch);
match git_repo.delete_branch(branch) {
Ok(_) => println!("✓ Branch deleted successfully"),
Err(e) => println!("⚠ Warning: Failed to delete branch: {}", e),
}
} else {
println!("⚠ Warning: Could not determine branch to delete (detached HEAD or error)");
}
} else if let Some(branch) = ¤t_branch {
println!(
"Branch '{}' preserved (use --delete-branch to remove it)",
branch
);
}
println!("✓ Worktree removed successfully!");
Ok(())
}
fn resolve_target(
target: &str,
storage: &WorktreeStorage,
repo_name: &str,
) -> Result<(PathBuf, String)> {
let worktree_path = storage.get_worktree_path(repo_name, target);
if worktree_path.exists() {
return Ok((worktree_path, target.to_string()));
}
let known = storage.list_repo_worktrees(repo_name)?;
let matches: Vec<&String> = known.iter().filter(|name| name.contains(target)).collect();
match matches.len() {
0 => anyhow::bail!("No worktree found matching '{}'", target),
1 => {
let feature_name = matches[0].clone();
let path = storage.get_worktree_path(repo_name, &feature_name);
Ok((path, feature_name))
}
_ => {
eprintln!(
"Multiple worktrees match '{}'. Please be more specific:",
target
);
for name in &matches {
eprintln!(" {}", name);
}
anyhow::bail!("Ambiguous worktree name '{}'", target);
}
}
}
fn list_worktree_completions(storage: &WorktreeStorage, current_repo_only: bool) -> Result<()> {
let worktrees = get_available_worktrees(storage, current_repo_only)?;
for (_, feature_name, _) in worktrees {
println!("{}", feature_name);
}
Ok(())
}
fn select_worktree_for_removal(
storage: &WorktreeStorage,
current_repo_only: bool,
provider: &dyn SelectionProvider,
) -> Result<(PathBuf, String)> {
let worktrees = get_available_worktrees(storage, current_repo_only)?;
if worktrees.is_empty() {
anyhow::bail!("No worktrees found");
}
let options: Vec<String> = worktrees
.iter()
.map(|(repo, feature_name, path)| {
let branch_info = read_worktree_head_branch(path)
.map(|b| format!(" ({})", b))
.unwrap_or_default();
format!(
"{}/{}{} ({})",
repo,
feature_name,
branch_info,
path.display()
)
})
.collect();
let selection = provider.select("Select worktree to remove:", options.clone())?;
let index = options
.iter()
.position(|o| o == &selection)
.ok_or_else(|| anyhow::anyhow!("Selected option not found in list"))?;
let (_, feature_name, path) = &worktrees[index];
Ok((path.clone(), feature_name.clone()))
}
fn get_available_worktrees(
storage: &WorktreeStorage,
current_repo_only: bool,
) -> Result<Vec<(String, String, PathBuf)>> {
let mut worktrees = Vec::new();
if current_repo_only {
let current_dir = std::env::current_dir()?;
if let Ok(git_repo) = GitRepo::open(¤t_dir) {
let repo_path = git_repo.get_repo_path();
let repo_name = WorktreeStorage::get_repo_name(repo_path)?;
let repo_worktrees = storage.list_repo_worktrees(&repo_name)?;
for feature_name in repo_worktrees {
let worktree_path = storage.get_worktree_path(&repo_name, &feature_name);
if worktree_path.exists() {
worktrees.push((repo_name.clone(), feature_name, worktree_path));
}
}
}
} else {
let all_worktrees = storage.list_all_worktrees()?;
for (repo_name, repo_worktrees) in all_worktrees {
for feature_name in repo_worktrees {
let worktree_path = storage.get_worktree_path(&repo_name, &feature_name);
if worktree_path.exists() {
worktrees.push((repo_name.clone(), feature_name, worktree_path));
}
}
}
}
Ok(worktrees)
}