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