1use anyhow::Result;
2use apm_core::clean;
3use std::path::Path;
4use crate::ctx::CmdContext;
5
6#[allow(clippy::too_many_arguments)]
7pub fn run(
8 root: &Path,
9 dry_run: bool,
10 _yes: bool,
11 force: bool,
12 branches: bool,
13 older_than: Option<String>,
14 untracked: bool,
15 epics: bool,
16) -> Result<()> {
17 let config = CmdContext::load_config_only(root)?;
18 let (mut candidates, dirty, candidate_warnings) = clean::candidates(root, &config, force, untracked, dry_run)?;
19 for w in &candidate_warnings {
20 eprintln!("{w}");
21 }
22
23 if branches {
28 let local_branch_set: std::collections::HashSet<String> =
29 candidates.iter().map(|c| c.branch.clone()).collect();
30 candidates.extend(clean::remote_only_candidates(root, &config, &local_branch_set)?);
31 }
32
33 if let Some(threshold_str) = older_than.as_deref() {
36 let threshold = clean::parse_older_than(threshold_str)?;
37 candidates.retain(|c| match c.updated_at {
38 Some(ts) => ts < threshold,
39 None => false,
40 });
41 }
42
43 let cwd = std::env::current_dir().unwrap_or_default();
47 let canonical_cwd = cwd.canonicalize().unwrap_or_else(|_| cwd.clone());
48 for candidate in &candidates {
49 if let Some(ref wt_path) = candidate.worktree {
50 let canonical_wt = wt_path.canonicalize().unwrap_or_else(|_| wt_path.clone());
51 if canonical_cwd.starts_with(&canonical_wt) {
52 eprintln!(
53 "refusing to remove worktree containing the current working directory: {}",
54 wt_path.display()
55 );
56 anyhow::bail!(
57 "refusing to remove worktree containing the current working directory: {}",
58 wt_path.display()
59 );
60 }
61 }
62 }
63 for dw in &dirty {
64 let canonical_wt = dw.path.canonicalize().unwrap_or_else(|_| dw.path.clone());
65 if canonical_cwd.starts_with(&canonical_wt) {
66 eprintln!(
67 "refusing to remove worktree containing the current working directory: {}",
68 dw.path.display()
69 );
70 anyhow::bail!(
71 "refusing to remove worktree containing the current working directory: {}",
72 dw.path.display()
73 );
74 }
75 }
76
77 if candidates.is_empty() && dirty.is_empty() && !epics {
78 println!("Nothing to clean.");
79 return Ok(());
80 }
81
82 for dw in &dirty {
84 if !dw.modified_tracked.is_empty() {
85 for f in &dw.modified_tracked {
86 eprintln!(" M {}", f.display());
87 }
88 eprintln!(
89 "warning: {} has modified tracked files — manual cleanup required — skipping",
90 dw.branch
91 );
92 } else {
93 for f in &dw.other_untracked {
94 eprintln!(" ? {}", f.display());
95 }
96 eprintln!(
97 "warning: {} has untracked files — re-run with --untracked to remove — skipping",
98 dw.branch
99 );
100 }
101 }
102
103 for candidate in &candidates {
104 let scope = match (candidate.local_branch_exists, candidate.remote_branch_exists) {
105 (true, true) => "local + remote",
106 (true, false) => "local",
107 (false, true) => "remote",
108 (false, false) => "registry only",
109 };
110 if dry_run {
111 if let Some(ref path) = candidate.worktree {
112 println!(
113 "would remove worktree {} (ticket #{}, state: {})",
114 path.display(),
115 candidate.ticket_id,
116 candidate.reason
117 );
118 }
119 if branches {
120 println!(
121 "would remove branch {} ({}, state: {})",
122 candidate.branch, scope, candidate.reason
123 );
124 }
125 } else if force {
126 if crate::util::prompt_yes_no(&format!("Remove {}? [y/N] ", candidate.branch))? {
127 if let Some(ref path) = candidate.worktree {
128 println!("removed worktree {}", path.display());
129 }
130 if branches {
131 println!("removed branch {} ({})", candidate.branch, scope);
132 }
133 let remove_out = clean::remove(root, candidate, true, branches)?;
134 for w in &remove_out.warnings {
135 eprintln!("{w}");
136 }
137 } else {
138 eprintln!("skipping {}", candidate.branch);
139 }
140 } else {
141 if let Some(ref path) = candidate.worktree {
142 println!("removed worktree {}", path.display());
143 }
144 if branches {
145 println!("removed branch {} ({})", candidate.branch, scope);
146 }
147 let remove_out = clean::remove(root, candidate, false, branches)?;
148 for w in &remove_out.warnings {
149 eprintln!("{w}");
150 }
151 }
152 }
153
154 if epics {
155 crate::cmd::epic::run_epic_clean(root, &config, dry_run, _yes)?;
156 }
157
158 Ok(())
159}