git_stk/commands/
cleanup.rs1use anyhow::Result;
2use clap::ArgAction;
3use clap_complete::engine::ArgValueCompleter;
4
5use crate::commands::Run;
6use crate::completions;
7use crate::providers::{ReviewProvider, ReviewState, detect_provider, review_provider};
8use crate::style;
9use crate::{git, stack};
10
11#[derive(Debug, clap::Args)]
14pub struct Cleanup {
15 #[arg(add = ArgValueCompleter::new(completions::branch_candidates))]
16 branch: Option<String>,
17 #[arg(long, action = ArgAction::SetTrue)]
19 dry_run: bool,
20 #[arg(long, action = ArgAction::SetTrue)]
22 keep_branch: bool,
23}
24
25impl Run for Cleanup {
26 fn run(self) -> Result<()> {
27 cleanup(self.branch.as_deref(), self.dry_run, self.keep_branch)
28 }
29}
30
31pub fn cleanup(branch: Option<&str>, dry_run: bool, keep_branch: bool) -> Result<()> {
32 let branch = branch
33 .map(str::to_owned)
34 .map_or_else(git::current_branch, Ok)?;
35 let branches = stack::branch_and_descendants(&branch)?;
36 let current_branch = git::current_branch()?;
37 let local_branches = git::local_branches()?;
38 let provider = detect_provider()?;
39 let review_provider = review_provider(provider.kind);
40 let mut cleaned = 0;
41 let mut skipped = 0;
42 let mut retargeted = 0;
43
44 let branch_parents = stack::branch_parents(&branches)?;
48 crate::notes::update_stack_notes(review_provider.as_ref(), &branch_parents, dry_run)?;
49
50 for branch in branches {
51 retargeted +=
52 recover_deleted_parent(review_provider.as_ref(), &branch, &local_branches, dry_run)?;
53 let Some(review) = review_provider.review_for_branch_including_closed(&branch)? else {
57 anstream::println!(
58 "{}",
59 style::dim(&format!(
60 "skipped {branch}: no {} review found",
61 provider.kind
62 ))
63 );
64 skipped += 1;
65 continue;
66 };
67
68 if review.state != ReviewState::Merged {
69 anstream::println!(
70 "{}",
71 style::dim(&format!(
72 "skipped {branch}: review {} is {}",
73 review.id, review.state
74 ))
75 );
76 skipped += 1;
77 continue;
78 }
79
80 cleanup_merged_branch(review_provider.as_ref(), &branch, dry_run)?;
81 cleanup_branch_deletion(&branch, ¤t_branch, dry_run, !keep_branch)?;
82 cleaned += 1;
83 }
84
85 let retargeted_note = if retargeted > 0 {
86 format!(", {retargeted} retargeted")
87 } else {
88 String::new()
89 };
90 anstream::println!(
91 "{}",
92 style::success(&format!(
93 "cleanup complete: {cleaned} cleaned, {skipped} skipped{retargeted_note}"
94 ))
95 );
96 Ok(())
97}
98
99fn recover_deleted_parent(
104 review_provider: &dyn ReviewProvider,
105 branch: &str,
106 local_branches: &[String],
107 dry_run: bool,
108) -> Result<usize> {
109 let Some(parent) = stack::parent_for_branch(branch)? else {
110 return Ok(0);
111 };
112 if local_branches.contains(&parent) {
113 return Ok(0);
114 }
115
116 let Ok(Some(review)) = review_provider.review_for_branch(&parent) else {
119 return Ok(0);
120 };
121 if review.branch != parent
122 || review.state != ReviewState::Merged
123 || review.base == *branch
124 || !local_branches.contains(&review.base)
125 {
126 return Ok(0);
127 }
128
129 anstream::println!(
130 "{}: parent {} is gone, but review {} merged into {}",
131 style::branch(branch),
132 style::branch(&parent),
133 review.id,
134 style::branch(&review.base)
135 );
136 anstream::println!(
137 "{} retarget {} -> {}",
138 if dry_run { "would" } else { "will" },
139 style::branch(branch),
140 style::branch(&review.base)
141 );
142 update_child_review_base(review_provider, branch, &review.base, dry_run)?;
143 if !dry_run {
144 stack::set_parent_for_branch(branch, &review.base)?;
145 }
146 Ok(1)
147}
148
149pub(crate) fn cleanup_merged_branch(
150 review_provider: &dyn ReviewProvider,
151 branch: &str,
152 dry_run: bool,
153) -> Result<()> {
154 let parent = stack::parent_for_branch(branch)?;
155 let descendants = stack::branch_and_descendants(branch)?;
156 let direct_children: Vec<_> = descendants
157 .into_iter()
158 .skip(1)
159 .filter_map(|child| match stack::parent_for_branch(&child) {
160 Ok(Some(child_parent)) if child_parent == branch => Some(Ok(child)),
161 Ok(_) => None,
162 Err(error) => Some(Err(error)),
163 })
164 .collect::<Result<_>>()?;
165
166 for child in direct_children {
167 match parent.as_deref() {
168 Some(parent) => {
169 anstream::println!(
170 "{} retarget {} -> {}",
171 if dry_run { "would" } else { "will" },
172 style::branch(&child),
173 style::branch(parent)
174 );
175 update_child_review_base(review_provider, &child, parent, dry_run)?;
176 if !dry_run {
177 if let Ok(base) = git::merge_base(branch, &child) {
181 stack::set_base_for_branch(&child, &base)?;
182 }
183 stack::set_parent_for_branch(&child, parent)?;
184 }
185 }
186 None => {
187 anstream::println!(
188 "{} detach {}",
189 if dry_run { "would" } else { "will" },
190 style::branch(&child)
191 );
192 if !dry_run {
193 stack::unset_parent_for_branch(&child)?;
194 stack::unset_base_for_branch(&child)?;
195 }
196 }
197 }
198 }
199
200 anstream::println!(
201 "{} detach {}",
202 if dry_run { "would" } else { "will" },
203 style::branch(branch)
204 );
205 if !dry_run {
206 stack::unset_parent_for_branch(branch)?;
207 stack::unset_base_for_branch(branch)?;
208 }
209
210 Ok(())
211}
212
213pub(crate) fn cleanup_branch_deletion(
214 branch: &str,
215 current_branch: &str,
216 dry_run: bool,
217 delete_branch: bool,
218) -> Result<()> {
219 if !delete_branch {
220 return Ok(());
221 }
222
223 if branch == current_branch {
226 anstream::println!(
227 "{}",
228 style::dim(&format!(
229 "kept {branch}: cannot delete the checked out branch"
230 ))
231 );
232 return Ok(());
233 }
234
235 anstream::println!(
236 "{} delete branch {}",
237 if dry_run { "would" } else { "will" },
238 style::branch(branch)
239 );
240 if !dry_run {
241 git::delete_branch(branch)?;
242 }
243
244 Ok(())
245}
246
247fn update_child_review_base(
248 review_provider: &dyn ReviewProvider,
249 child: &str,
250 parent: &str,
251 dry_run: bool,
252) -> Result<()> {
253 let Some(review) = review_provider.review_for_branch(child)? else {
254 return Ok(());
255 };
256
257 if review.state == ReviewState::Merged || review.base == parent {
258 return Ok(());
259 }
260
261 anstream::println!(
262 "{} update review {} -> {} {}",
263 if dry_run { "would" } else { "will" },
264 style::branch(&review.branch),
265 style::branch(parent),
266 style::dim(&format!("({})", review.id))
267 );
268 if !dry_run {
269 let output = review_provider.update_review_base(&review, parent)?;
270 if !output.is_empty() {
271 println!("{output}");
272 }
273 }
274
275 Ok(())
276}