use console::style;
use crate::constants::{format_config_key, path_age_days, CONFIG_KEY_BASE_BRANCH};
use crate::error::Result;
use crate::git;
use crate::messages;
use super::display::get_worktree_status;
pub fn clean_worktrees(
merged: bool,
older_than: Option<u64>,
interactive: bool,
dry_run: bool,
force: bool,
) -> Result<()> {
let repo = git::get_repo_root(None)?;
if !merged && older_than.is_none() && !interactive {
eprintln!(
"Error: Please specify at least one cleanup criterion:\n \
--merged, --older-than, or -i/--interactive"
);
return Ok(());
}
let mut to_delete: Vec<(String, String, String)> = Vec::new();
for (branch_name, path) in git::get_feature_worktrees(Some(&repo))? {
let mut should_delete = false;
let mut reasons = Vec::new();
if merged {
let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, &branch_name);
if let Some(base_branch) = git::get_config(&base_key, Some(&repo)) {
if let Ok(r) = git::git_command(
&[
"branch",
"--merged",
&base_branch,
"--format=%(refname:short)",
],
Some(&repo),
false,
true,
) {
if r.returncode == 0 && r.stdout.lines().any(|l| l.trim() == branch_name) {
should_delete = true;
reasons.push(format!("merged into {}", base_branch));
}
}
}
}
if let Some(days) = older_than {
if let Some(age) = path_age_days(&path) {
let age_days = age as u64;
if age_days >= days {
should_delete = true;
reasons.push(format!("older than {} days ({} days)", days, age_days));
}
}
}
if should_delete {
to_delete.push((
branch_name.clone(),
path.to_string_lossy().to_string(),
reasons.join(", "),
));
}
}
if interactive && to_delete.is_empty() {
println!("{}\n", style("Available worktrees:").cyan().bold());
let mut all_wt = Vec::new();
for (branch_name, path) in git::get_feature_worktrees(Some(&repo))? {
let status = get_worktree_status(&path, &repo, Some(branch_name.as_str()));
println!(" [{:8}] {:<30} {}", status, branch_name, path.display());
all_wt.push((branch_name, path.to_string_lossy().to_string()));
}
println!();
println!("Enter branch names to delete (space-separated), or 'all' for all:");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.eq_ignore_ascii_case("all") {
to_delete = all_wt
.into_iter()
.map(|(b, p)| (b, p, "user selected".to_string()))
.collect();
} else {
let selected: Vec<&str> = input.split_whitespace().collect();
to_delete = all_wt
.into_iter()
.filter(|(b, _)| selected.contains(&b.as_str()))
.map(|(b, p)| (b, p, "user selected".to_string()))
.collect();
}
if to_delete.is_empty() {
println!("{}", style("No worktrees selected for deletion").yellow());
return Ok(());
}
}
let mut busy_skipped: Vec<(String, Vec<crate::operations::busy::BusyInfo>)> = Vec::new();
if !force {
let mut kept: Vec<(String, String, String)> = Vec::with_capacity(to_delete.len());
for (branch, path, reason) in to_delete.into_iter() {
let busy = crate::operations::busy::detect_busy(std::path::Path::new(&path));
if busy.is_empty() {
kept.push((branch, path, reason));
} else {
busy_skipped.push((branch, busy));
}
}
to_delete = kept;
}
if !busy_skipped.is_empty() {
println!(
"{}",
style(format!(
"Skipping {} busy worktree(s) (use --force to override):",
busy_skipped.len()
))
.yellow()
);
for (branch, infos) in &busy_skipped {
let detail = infos
.first()
.map(|b| format!("PID {} {}", b.pid, b.cmd))
.unwrap_or_default();
println!(" - {:<30} (busy: {})", branch, detail);
}
println!();
}
if to_delete.is_empty() {
println!(
"{} No worktrees match the cleanup criteria\n",
style("*").green().bold()
);
return Ok(());
}
let prefix = if dry_run { "DRY RUN: " } else { "" };
println!(
"\n{}\n",
style(format!("{}Worktrees to delete:", prefix))
.yellow()
.bold()
);
for (branch, path, reason) in &to_delete {
println!(" - {:<30} ({})", branch, reason);
println!(" Path: {}", path);
}
println!();
if dry_run {
println!(
"{} Would delete {} worktree(s)",
style("*").cyan().bold(),
to_delete.len()
);
println!("Run without --dry-run to actually delete them");
return Ok(());
}
let mut deleted = 0u32;
for (branch, _, _) in &to_delete {
println!("{}", style(format!("Deleting {}...", branch)).yellow());
match super::worktree::delete_worktree(Some(branch), false, false, true, true, None) {
Ok(()) => {
println!("{} Deleted {}", style("*").green().bold(), branch);
deleted += 1;
}
Err(e) => {
println!(
"{} Failed to delete {}: {}",
style("x").red().bold(),
branch,
e
);
}
}
}
println!(
"\n{}\n",
style(messages::cleanup_complete(deleted)).green().bold()
);
println!("{}", style("Pruning stale worktree metadata...").dim());
let _ = git::git_command(&["worktree", "prune"], Some(&repo), false, false);
println!("{}\n", style("* Prune complete").dim());
Ok(())
}