1use anyhow::Result;
2use apm_core::{clean, git};
3use std::io::IsTerminal;
4use std::path::Path;
5use crate::ctx::CmdContext;
6
7pub fn run(
8 root: &Path,
9 dry_run: bool,
10 yes: bool,
11 force: bool,
12 branches: bool,
13 remote: bool,
14 older_than: Option<String>,
15 untracked: bool,
16 epics: bool,
17) -> Result<()> {
18 if remote && older_than.is_none() {
20 anyhow::bail!("--remote requires --older-than <THRESHOLD>");
21 }
22
23 let config = CmdContext::load_config_only(root)?;
24 let (candidates, dirty, candidate_warnings) = clean::candidates(root, &config, force, untracked, dry_run)?;
25 for w in &candidate_warnings {
26 eprintln!("{w}");
27 }
28
29 if candidates.is_empty() && dirty.is_empty() && !remote && !epics {
30 println!("Nothing to clean.");
31 return Ok(());
32 }
33
34 for dw in &dirty {
36 if !dw.modified_tracked.is_empty() {
37 for f in &dw.modified_tracked {
38 eprintln!(" M {}", f.display());
39 }
40 eprintln!(
41 "warning: {} has modified tracked files — manual cleanup required — skipping",
42 dw.branch
43 );
44 } else {
45 for f in &dw.other_untracked {
46 eprintln!(" ? {}", f.display());
47 }
48 eprintln!(
49 "warning: {} has untracked files — re-run with --untracked to remove — skipping",
50 dw.branch
51 );
52 }
53 }
54
55 for candidate in &candidates {
56 if dry_run {
57 if let Some(ref path) = candidate.worktree {
58 println!(
59 "would remove worktree {} (ticket #{}, state: {})",
60 path.display(),
61 candidate.ticket_id,
62 candidate.reason
63 );
64 }
65 if branches && candidate.local_branch_exists && (candidate.branch_merged || force) {
66 println!(
67 "would remove branch {} (state: {})",
68 candidate.branch, candidate.reason
69 );
70 } else if branches && candidate.local_branch_exists && !candidate.branch_merged {
71 println!(
72 "would keep branch {} (not merged into main)",
73 candidate.branch
74 );
75 }
76 } else if force {
77 eprintln!(
78 "warning: force-removing {} — branch may not be merged",
79 candidate.branch
80 );
81 if crate::util::prompt_yes_no(&format!("Force-remove {}? [y/N] ", candidate.branch))? {
82 if let Some(ref path) = candidate.worktree {
83 println!("removed worktree {}", path.display());
84 }
85 if branches && candidate.local_branch_exists {
86 println!("removed branch {}", candidate.branch);
87 }
88 let remove_out = clean::remove(root, candidate, true, branches)?;
89 for w in &remove_out.warnings {
90 eprintln!("{w}");
91 }
92 } else {
93 eprintln!("skipping {}", candidate.branch);
94 }
95 } else {
96 if let Some(ref path) = candidate.worktree {
97 println!("removed worktree {}", path.display());
98 }
99 if branches && candidate.local_branch_exists && candidate.branch_merged {
100 println!("removed branch {}", candidate.branch);
101 } else if branches && candidate.local_branch_exists && !candidate.branch_merged {
102 println!("kept branch {} (not merged into main)", candidate.branch);
103 }
104 let remove_out = clean::remove(root, candidate, false, branches)?;
105 for w in &remove_out.warnings {
106 eprintln!("{w}");
107 }
108 }
109 }
110
111 if remote {
113 let threshold_str = older_than.as_deref().unwrap();
114 let threshold = clean::parse_older_than(threshold_str)?;
115 let remote_candidates = clean::remote_candidates(root, &config, threshold)?;
116
117 if remote_candidates.is_empty() {
118 println!("No remote branches to clean.");
119 }
120
121 for rc in &remote_candidates {
122 if dry_run {
123 println!(
124 "would delete remote branch {} (last commit: {})",
125 rc.branch,
126 rc.last_commit.format("%Y-%m-%d")
127 );
128 continue;
129 }
130 let should_delete = if yes {
131 true
132 } else if std::io::stdout().is_terminal() {
133 crate::util::prompt_yes_no(&format!(
134 "Delete remote branch {} (last commit: {})? [y/N] ",
135 rc.branch,
136 rc.last_commit.format("%Y-%m-%d")
137 ))?
138 } else {
139 eprintln!(
140 "skipping {} — non-interactive (use --yes to auto-confirm)",
141 rc.branch
142 );
143 false
144 };
145 if should_delete {
146 git::delete_remote_branch(root, &rc.branch)?;
147 println!("deleted remote branch {}", rc.branch);
148 }
149 }
150 }
151
152 if epics || remote {
153 crate::cmd::epic::run_epic_clean(root, &config, dry_run, yes)?;
154 }
155
156 Ok(())
157}