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::{git, stack};
9
10#[derive(Debug, clap::Args)]
13pub struct Cleanup {
14 #[arg(add = ArgValueCompleter::new(completions::branch_candidates))]
15 branch: Option<String>,
16 #[arg(long, action = ArgAction::SetTrue)]
18 dry_run: bool,
19 #[arg(long, action = ArgAction::SetTrue)]
21 keep_branch: bool,
22}
23
24impl Run for Cleanup {
25 fn run(self) -> Result<()> {
26 cleanup(self.branch.as_deref(), self.dry_run, self.keep_branch)
27 }
28}
29
30pub fn cleanup(branch: Option<&str>, dry_run: bool, keep_branch: bool) -> Result<()> {
31 let branch = branch
32 .map(str::to_owned)
33 .map_or_else(git::current_branch, Ok)?;
34 let branches = stack::branch_and_descendants(&branch)?;
35 let current_branch = git::current_branch()?;
36 let provider = detect_provider()?;
37 let review_provider = review_provider(provider.kind);
38 let mut cleaned = 0;
39 let mut skipped = 0;
40
41 for branch in branches {
42 let Some(review) = review_provider.review_for_branch(&branch)? else {
43 println!("skipped {branch}: no {} review found", provider.kind);
44 skipped += 1;
45 continue;
46 };
47
48 if review.state != ReviewState::Merged {
49 println!("skipped {branch}: review {} is {}", review.id, review.state);
50 skipped += 1;
51 continue;
52 }
53
54 cleanup_merged_branch(review_provider.as_ref(), &branch, dry_run)?;
55 cleanup_branch_deletion(&branch, ¤t_branch, dry_run, !keep_branch)?;
56 cleaned += 1;
57 }
58
59 println!("cleanup complete: {cleaned} cleaned, {skipped} skipped");
60 Ok(())
61}
62
63pub(crate) fn cleanup_merged_branch(
64 review_provider: &dyn ReviewProvider,
65 branch: &str,
66 dry_run: bool,
67) -> Result<()> {
68 let parent = stack::parent_for_branch(branch)?;
69 let descendants = stack::branch_and_descendants(branch)?;
70 let direct_children: Vec<_> = descendants
71 .into_iter()
72 .skip(1)
73 .filter_map(|child| match stack::parent_for_branch(&child) {
74 Ok(Some(child_parent)) if child_parent == branch => Some(Ok(child)),
75 Ok(_) => None,
76 Err(error) => Some(Err(error)),
77 })
78 .collect::<Result<_>>()?;
79
80 for child in direct_children {
81 match parent.as_deref() {
82 Some(parent) => {
83 println!(
84 "{} retarget {child} -> {parent}",
85 if dry_run { "would" } else { "will" }
86 );
87 update_child_review_base(review_provider, &child, parent, dry_run)?;
88 if !dry_run {
89 if let Ok(base) = git::merge_base(branch, &child) {
93 stack::set_base_for_branch(&child, &base)?;
94 }
95 stack::set_parent_for_branch(&child, parent)?;
96 }
97 }
98 None => {
99 println!("{} detach {child}", if dry_run { "would" } else { "will" });
100 if !dry_run {
101 stack::unset_parent_for_branch(&child)?;
102 stack::unset_base_for_branch(&child)?;
103 }
104 }
105 }
106 }
107
108 println!("{} detach {branch}", if dry_run { "would" } else { "will" });
109 if !dry_run {
110 stack::unset_parent_for_branch(branch)?;
111 stack::unset_base_for_branch(branch)?;
112 }
113
114 Ok(())
115}
116
117pub(crate) fn cleanup_branch_deletion(
118 branch: &str,
119 current_branch: &str,
120 dry_run: bool,
121 delete_branch: bool,
122) -> Result<()> {
123 if !delete_branch {
124 return Ok(());
125 }
126
127 if branch == current_branch {
130 println!("kept {branch}: cannot delete the checked out branch");
131 return Ok(());
132 }
133
134 println!(
135 "{} delete branch {branch}",
136 if dry_run { "would" } else { "will" }
137 );
138 if !dry_run {
139 git::delete_branch(branch)?;
140 }
141
142 Ok(())
143}
144
145fn update_child_review_base(
146 review_provider: &dyn ReviewProvider,
147 child: &str,
148 parent: &str,
149 dry_run: bool,
150) -> Result<()> {
151 let Some(review) = review_provider.review_for_branch(child)? else {
152 return Ok(());
153 };
154
155 if review.state == ReviewState::Merged || review.base == parent {
156 return Ok(());
157 }
158
159 println!(
160 "{} update review {} -> {} ({})",
161 if dry_run { "would" } else { "will" },
162 review.branch,
163 parent,
164 review.id
165 );
166 if !dry_run {
167 let output = review_provider.update_review_base(&review, parent)?;
168 if !output.is_empty() {
169 println!("{output}");
170 }
171 }
172
173 Ok(())
174}