use crate::git::{SEP, display_name, find_repos, git_capture};
use colored::Colorize;
use std::path::PathBuf;
#[derive(clap::Args)]
pub struct Args {
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(short, long, default_value_t = 3)]
pub depth: usize,
#[arg(short, long)]
pub all: bool,
}
struct Branch {
name: String,
age: String,
}
pub fn run(args: Args) {
let repos = find_repos(&args.path, args.depth);
let mut total_repos = 0usize;
let mut total_branches = 0usize;
for repo in &repos {
let repo_str = match repo.to_str() {
Some(s) => s,
None => continue,
};
let name = display_name(repo);
let current = git_capture(repo_str, &["rev-parse", "--abbrev-ref", "HEAD"])
.unwrap_or_else(|| "HEAD".to_string());
let merged = gone_branches(repo_str, ¤t);
if merged.is_empty() {
if args.all {
println!(
"{} {}",
format!("📁 {}", name).bold(),
"nothing to clean up".dimmed()
);
}
continue;
}
total_repos += 1;
total_branches += merged.len();
println!(
"{} {}",
format!("📁 {}", name).bold(),
format!("(on {})", current).dimmed()
);
println!(" {}", "upstream gone - safe to delete:".dimmed());
let width = merged.iter().map(|b| b.name.len()).max().unwrap_or(0);
for b in &merged {
println!(
" {} {}",
format!("{:width$}", b.name).yellow(),
b.age.dimmed()
);
}
let names = merged
.iter()
.map(|b| b.name.as_str())
.collect::<Vec<_>>()
.join(" ");
println!(" {} git branch -d {}\n", "↳".dimmed(), names.dimmed());
}
if total_branches == 0 {
println!(
"{}",
"No branches with a gone upstream to clean up.".green()
);
} else {
println!(
"{}",
format!(
"{} branch(es) across {} repo(s) can be deleted",
total_branches, total_repos
)
.dimmed()
);
}
}
fn gone_branches(repo: &str, current: &str) -> Vec<Branch> {
let fmt = format!("--format=%(refname){SEP}%(upstream:track){SEP}%(committerdate:relative)");
git_capture(
repo,
&["for-each-ref", "--sort=-committerdate", &fmt, "refs/heads"],
)
.map(|out| {
out.lines()
.filter_map(|line| {
let mut f = line.split(SEP);
let refname = f.next()?;
let track = f.next()?;
if !track.contains("gone") {
return None;
}
let name = refname.strip_prefix("refs/heads/").unwrap_or(refname);
if name == current {
return None; }
Some(Branch {
name: name.to_string(),
age: f.next().unwrap_or("").to_string(),
})
})
.collect()
})
.unwrap_or_default()
}