use std::io::{self, BufRead, Write};
use std::path::Path;
use anyhow::Result;
use crate::cli::CleanArgs;
use crate::context::Context;
use crate::git;
use crate::worktree::{self, Worktree};
pub fn run(ctx: &Context, args: &CleanArgs) -> Result<()> {
let repo = ctx.repo()?;
let trees = worktree::list(&repo)?;
let candidates: Vec<Worktree> = trees
.into_iter()
.filter(|w| !w.bare)
.filter(|w| {
w.branch
.as_deref()
.is_some_and(|b| is_upstream_gone(&w.path, b))
})
.collect();
if candidates.is_empty() {
if ctx.json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"candidates": [],
"removed": [],
}))?
);
} else {
eprintln!("no stale worktrees (all upstreams alive)");
}
return Ok(());
}
if !ctx.json {
eprintln!("worktrees whose upstream branch is gone:");
for c in &candidates {
eprintln!(
" {} (branch: {})",
c.name,
c.branch.as_deref().unwrap_or("?")
);
}
}
if args.dry_run {
if ctx.json {
let summary = serde_json::json!({
"candidates": candidates.iter().map(|c| &c.name).collect::<Vec<_>>(),
"removed": Vec::<String>::new(),
"dry_run": true,
});
println!("{}", serde_json::to_string_pretty(&summary)?);
} else {
eprintln!("dry-run; nothing removed");
}
return Ok(());
}
if !ctx.yes && !prompt_confirm(candidates.len())? {
eprintln!("cancelled");
return Ok(());
}
let mut removed: Vec<String> = Vec::new();
let mut failed: Vec<(String, String)> = Vec::new();
for c in &candidates {
match worktree::remove(&repo, &c.name, false, ctx.quiet) {
Ok(()) => {
removed.push(c.name.clone());
if !ctx.json {
eprintln!("removed: {}", c.name);
}
}
Err(e) => failed.push((c.name.clone(), format!("{e:#}"))),
}
}
if ctx.json {
let summary = serde_json::json!({
"candidates": candidates.iter().map(|c| &c.name).collect::<Vec<_>>(),
"removed": removed,
"failed": failed.iter().map(|(n, e)| serde_json::json!({"name": n, "error": e})).collect::<Vec<_>>(),
});
println!("{}", serde_json::to_string_pretty(&summary)?);
} else {
for (name, err) in &failed {
eprintln!("failed: {name}: {err}");
}
eprintln!("cleaned {}/{}", removed.len(), candidates.len());
}
Ok(())
}
fn is_upstream_gone(worktree_path: &Path, branch: &str) -> bool {
let ref_name = format!("refs/heads/{branch}");
git::capture(
worktree_path,
&["for-each-ref", "--format=%(upstream:track)", &ref_name],
)
.is_ok_and(|s| s.contains("gone"))
}
fn prompt_confirm(n: usize) -> Result<bool> {
let mut stderr = io::stderr().lock();
write!(stderr, "proceed with removing {n} worktree(s)? [y/N] ")?;
stderr.flush()?;
drop(stderr);
let stdin = io::stdin();
let mut line = String::new();
stdin.lock().read_line(&mut line)?;
Ok(line.trim().eq_ignore_ascii_case("y"))
}