git_worktree_manager/operations/
clean.rs1use console::style;
4
5use crate::constants::{format_config_key, path_age_days, CONFIG_KEY_BASE_BRANCH};
6use crate::error::Result;
7use crate::git;
8use crate::messages;
9
10use super::display::get_worktree_status;
11
12pub fn clean_worktrees(
14 merged: bool,
15 older_than: Option<u64>,
16 interactive: bool,
17 dry_run: bool,
18) -> Result<()> {
19 let repo = git::get_repo_root(None)?;
20
21 if !merged && older_than.is_none() && !interactive {
23 eprintln!(
24 "Error: Please specify at least one cleanup criterion:\n \
25 --merged, --older-than, or -i/--interactive"
26 );
27 return Ok(());
28 }
29
30 let mut to_delete: Vec<(String, String, String)> = Vec::new(); for (branch_name, path) in git::get_feature_worktrees(Some(&repo))? {
33 let mut should_delete = false;
34 let mut reasons = Vec::new();
35
36 if merged {
38 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, &branch_name);
39 if let Some(base_branch) = git::get_config(&base_key, Some(&repo)) {
40 if let Ok(r) = git::git_command(
41 &[
42 "branch",
43 "--merged",
44 &base_branch,
45 "--format=%(refname:short)",
46 ],
47 Some(&repo),
48 false,
49 true,
50 ) {
51 if r.returncode == 0 && r.stdout.lines().any(|l| l.trim() == branch_name) {
52 should_delete = true;
53 reasons.push(format!("merged into {}", base_branch));
54 }
55 }
56 }
57 }
58
59 if let Some(days) = older_than {
61 if let Some(age) = path_age_days(&path) {
62 let age_days = age as u64;
63 if age_days > days {
64 should_delete = true;
65 reasons.push(format!("older than {} days ({} days)", days, age_days));
66 }
67 }
68 }
69
70 if should_delete {
71 to_delete.push((
72 branch_name.clone(),
73 path.to_string_lossy().to_string(),
74 reasons.join(", "),
75 ));
76 }
77 }
78
79 if interactive && to_delete.is_empty() {
81 println!("{}\n", style("Available worktrees:").cyan().bold());
82 let mut all_wt = Vec::new();
83 for (branch_name, path) in git::get_feature_worktrees(Some(&repo))? {
84 let status = get_worktree_status(&path, &repo);
85 println!(" [{:8}] {:<30} {}", status, branch_name, path.display());
86 all_wt.push((branch_name, path.to_string_lossy().to_string()));
87 }
88 println!();
89 println!("Enter branch names to delete (space-separated), or 'all' for all:");
90
91 let mut input = String::new();
92 std::io::stdin().read_line(&mut input)?;
93 let input = input.trim();
94
95 if input.eq_ignore_ascii_case("all") {
96 to_delete = all_wt
97 .into_iter()
98 .map(|(b, p)| (b, p, "user selected".to_string()))
99 .collect();
100 } else {
101 let selected: Vec<&str> = input.split_whitespace().collect();
102 to_delete = all_wt
103 .into_iter()
104 .filter(|(b, _)| selected.contains(&b.as_str()))
105 .map(|(b, p)| (b, p, "user selected".to_string()))
106 .collect();
107 }
108
109 if to_delete.is_empty() {
110 println!("{}", style("No worktrees selected for deletion").yellow());
111 return Ok(());
112 }
113 }
114
115 if to_delete.is_empty() {
116 println!(
117 "{} No worktrees match the cleanup criteria\n",
118 style("*").green().bold()
119 );
120 return Ok(());
121 }
122
123 let prefix = if dry_run { "DRY RUN: " } else { "" };
125 println!(
126 "\n{}\n",
127 style(format!("{}Worktrees to delete:", prefix))
128 .yellow()
129 .bold()
130 );
131 for (branch, path, reason) in &to_delete {
132 println!(" - {:<30} ({})", branch, reason);
133 println!(" Path: {}", path);
134 }
135 println!();
136
137 if dry_run {
138 println!(
139 "{} Would delete {} worktree(s)",
140 style("*").cyan().bold(),
141 to_delete.len()
142 );
143 println!("Run without --dry-run to actually delete them");
144 return Ok(());
145 }
146
147 let mut deleted = 0u32;
149 for (branch, _, _) in &to_delete {
150 println!("{}", style(format!("Deleting {}...", branch)).yellow());
151 match super::worktree::delete_worktree(Some(branch), false, false, true, None) {
152 Ok(()) => {
153 println!("{} Deleted {}", style("*").green().bold(), branch);
154 deleted += 1;
155 }
156 Err(e) => {
157 println!(
158 "{} Failed to delete {}: {}",
159 style("x").red().bold(),
160 branch,
161 e
162 );
163 }
164 }
165 }
166
167 println!(
168 "\n{}\n",
169 style(messages::cleanup_complete(deleted)).green().bold()
170 );
171
172 println!("{}", style("Pruning stale worktree metadata...").dim());
174 let _ = git::git_command(&["worktree", "prune"], Some(&repo), false, false);
175 println!("{}\n", style("* Prune complete").dim());
176
177 Ok(())
178}