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