use anyhow::{Context, Result};
use std::collections::HashSet;
use std::process::Command;
use crate::color;
use crate::commands::common::{get_main_repo_root, parse_all_worktrees, resolve_worktree_target};
use crate::config;
use crate::domain::worktree::display_path;
use crate::hooks;
use crate::integrations;
use crate::integrations::fzf::FzfPicker;
fn remove_worktree_internal(
worktree_path: &std::path::Path,
branch_name: Option<&str>,
config: &config::Config,
repo_root: &std::path::Path,
color_mode: color::ColorMode,
) -> Result<()> {
if worktree_path.exists()
&& (!config.hooks.delete.run.is_empty()
|| !config.hooks.delete.copy.is_empty()
|| !config.hooks.delete.link.is_empty())
{
eprintln!("{}", color::info(color_mode, "Executing delete hooks…"));
hooks::execute_hooks(&config.hooks.delete, worktree_path, repo_root, color_mode)?;
}
let output = Command::new("git")
.args(["worktree", "remove"])
.arg(worktree_path)
.current_dir(repo_root)
.output()
.context("Failed to execute git worktree remove")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git worktree remove failed: {stderr}");
}
eprintln!(
"{}",
color::success(
color_mode,
format!("Removed worktree: {}", display_path(worktree_path))
)
);
if let Some(branch) = branch_name {
let branch_output = Command::new("git")
.args(["branch", "-D", branch])
.current_dir(repo_root)
.output()
.context("Failed to execute git branch -D")?;
if branch_output.status.success() {
eprintln!(
"{}",
color::success(color_mode, format!("Deleted branch: {branch}"))
);
}
}
Ok(())
}
#[allow(clippy::too_many_lines)]
pub fn cmd_rm_many(targets: &[String], color_mode: color::ColorMode) -> Result<()> {
let repo_root = get_main_repo_root()?;
let config = config::Config::load_from_repo_root(&repo_root)?;
let list_output = Command::new("git")
.args(["worktree", "list", "--porcelain"])
.current_dir(&repo_root)
.output()
.context("Failed to execute git worktree list --porcelain")?;
if !list_output.status.success() {
let stderr = String::from_utf8_lossy(&list_output.stderr);
anyhow::bail!("git worktree list failed: {stderr}");
}
let list_stdout = String::from_utf8_lossy(&list_output.stdout);
let targets: Vec<String> = if targets.is_empty() {
if !config.integrations.fzf.enabled {
anyhow::bail!("Provide at least one target or enable fzf in config");
}
if !integrations::fzf::is_fzf_available() {
anyhow::bail!("fzf is not installed. Install it or provide at least one target");
}
let items = integrations::fzf::build_worktree_items(&list_stdout);
if items.is_empty() {
anyhow::bail!("No worktrees found");
}
let picker = integrations::fzf::RealFzfPicker::new(config.integrations.fzf.options.clone());
let selected = picker.pick(&items, true)?;
if selected.is_empty() {
return Ok(());
}
selected
.iter()
.map(std::string::ToString::to_string)
.collect()
} else {
targets.to_vec()
};
let mut non_current_removals = Vec::new();
let mut current_removal: Option<(std::path::PathBuf, std::path::PathBuf, Option<String>)> =
None;
let mut seen_paths = HashSet::new();
for target in &targets {
match resolve_worktree_target(target, &list_stdout, &repo_root) {
Ok((canonical_path, worktree_path, branch_name, is_current)) => {
if is_current {
if seen_paths.contains(&canonical_path) {
non_current_removals.retain(|(path, _, _)| path != &canonical_path);
eprintln!(
"{}",
color::warn(
color_mode,
format!(
"Duplicate target {} (treating as current worktree)",
display_path(&canonical_path)
)
)
);
} else {
seen_paths.insert(canonical_path.clone());
}
current_removal = Some((canonical_path, worktree_path, branch_name));
} else {
if seen_paths.contains(&canonical_path) {
eprintln!(
"{}",
color::warn(
color_mode,
format!(
"Duplicate target {} (skipping)",
display_path(&canonical_path)
)
)
);
continue;
}
seen_paths.insert(canonical_path.clone());
non_current_removals.push((canonical_path, worktree_path, branch_name));
}
}
Err(e) => {
return Err(e);
}
}
}
for (_, worktree_path, branch_name) in non_current_removals {
remove_worktree_internal(
&worktree_path,
branch_name.as_deref(),
&config,
&repo_root,
color_mode,
)?;
}
if let Some((_, worktree_path, branch_name)) = current_removal {
remove_worktree_internal(
&worktree_path,
branch_name.as_deref(),
&config,
&repo_root,
color_mode,
)?;
let (main_path, _) = parse_all_worktrees(&list_stdout);
println!("{main_path}");
}
Ok(())
}