1use anyhow::{Result, bail};
2use clap::ArgAction;
3use clap_complete::engine::ArgValueCompleter;
4
5use crate::cli::PushMode;
6use crate::commands::Run;
7use crate::completions;
8use crate::providers::{ReviewProvider, detect_provider, review_provider};
9use crate::settings;
10use crate::style;
11use crate::{git, stack};
12
13#[derive(Debug, clap::Args)]
15pub struct Submit {
16 #[arg(add = ArgValueCompleter::new(completions::branch_candidates))]
17 branch: Option<String>,
18 #[arg(long, action = ArgAction::SetTrue)]
20 dry_run: bool,
21 #[arg(long, conflicts_with = "branch")]
23 stack: bool,
24 #[arg(long, action = ArgAction::SetTrue, conflicts_with = "stack")]
26 no_stack: bool,
27 #[arg(
30 long,
31 action = ArgAction::SetTrue,
32 conflicts_with_all = ["branch", "stack", "no_stack"],
33 )]
34 downstack: bool,
35 #[arg(long, action = ArgAction::SetTrue, conflicts_with = "no_push")]
37 push: bool,
38 #[arg(long, action = ArgAction::SetTrue)]
40 no_push: bool,
41 #[arg(long, short = 'd')]
44 desc: Option<String>,
45 #[arg(long, action = ArgAction::SetTrue, conflicts_with = "no_draft")]
47 draft: bool,
48 #[arg(long, action = ArgAction::SetTrue)]
50 no_draft: bool,
51 #[arg(long, action = ArgAction::SetTrue, conflicts_with = "draft")]
53 ready: bool,
54 #[arg(long, action = ArgAction::SetTrue)]
57 rebuild_overview: bool,
58}
59
60impl Run for Submit {
61 fn run(self) -> Result<()> {
62 let submit_stack = if self.stack {
65 true
66 } else if self.no_stack || self.branch.is_some() {
67 false
68 } else {
69 settings::bool_setting(settings::SUBMIT_STACK_KEY)?
70 };
71
72 let draft = if self.draft {
75 true
76 } else if self.no_draft {
77 false
78 } else {
79 settings::bool_setting(settings::SUBMIT_DRAFT_KEY)?
80 };
81
82 submit(
83 self.branch.as_deref(),
84 submit_stack,
85 self.downstack,
86 self.dry_run,
87 PushMode::from_flags(self.push, self.no_push),
88 self.desc.as_deref(),
89 draft,
90 self.ready,
91 self.rebuild_overview,
92 )
93 }
94}
95
96#[allow(clippy::too_many_arguments)]
97pub fn submit(
98 branch: Option<&str>,
99 submit_stack: bool,
100 downstack: bool,
101 dry_run: bool,
102 push_mode: crate::cli::PushMode,
103 desc: Option<&str>,
104 draft: bool,
105 ready: bool,
106 rebuild_overview: bool,
107) -> Result<()> {
108 let branch = branch
109 .map(str::to_owned)
110 .map_or_else(git::current_branch, Ok)?;
111 let desc_branch = branch.clone();
113
114 let branches = if downstack {
115 stack::path_from_root(&branch)?
118 } else if submit_stack {
119 stack::stack_line(&branch)?
123 } else {
124 vec![branch]
125 };
126
127 let branch_parents = branch_parents(&branches)?;
128
129 let push = settings::push_enabled(push_mode, settings::PUSH_ON_SUBMIT_KEY)?;
133 if push {
134 let remote = settings::remote()?;
135 if dry_run {
136 anstream::println!(
137 "would push {} to {remote}",
138 style::branch(&branches.join(" "))
139 );
140 } else {
141 git::push_set_upstream_force_with_lease(&remote, &branches)?;
142 anstream::println!("pushed {} to {remote}", style::branch(&branches.join(" ")));
143 }
144 }
145
146 let provider = detect_provider()?;
147 let review_provider = review_provider(provider.kind);
148 let mut summary = SubmitSummary::default();
149
150 for (branch, parent) in &branch_parents {
151 summary.record(submit_branch(
152 review_provider.as_ref(),
153 branch,
154 parent,
155 dry_run,
156 draft,
157 )?);
158 }
159
160 if ready {
163 for branch in &branches {
164 let Some(review) = review_provider.review_for_branch(branch)? else {
165 continue;
166 };
167 if review.branch != *branch || !review.draft {
168 continue;
169 }
170 if dry_run {
171 println!("would mark {} ready", review.id);
172 continue;
173 }
174 let output = review_provider.mark_ready(&review)?;
175 anstream::println!("marked {} ready", review.id);
176 if !output.is_empty() {
177 println!("{output}");
178 }
179 }
180 }
181
182 let renamed: Vec<(String, String)> = branch_parents
186 .iter()
187 .filter_map(|(branch, _)| {
188 stack::renamed_from(branch)
189 .ok()
190 .flatten()
191 .map(|old| (branch.clone(), old))
192 })
193 .collect();
194 for (_, old) in &renamed {
195 close_superseded_review(review_provider.as_ref(), old, dry_run)?;
196 }
197
198 if let Some(desc) = desc {
202 crate::notes::update_description_note(
203 review_provider.as_ref(),
204 &desc_branch,
205 desc,
206 dry_run,
207 )?;
208 }
209 crate::notes::update_closes_notes(review_provider.as_ref(), &branches, dry_run)?;
210 if submit_stack || downstack {
211 crate::notes::update_stack_notes(
212 review_provider.as_ref(),
213 &branch_parents,
214 dry_run,
215 rebuild_overview,
216 )?;
217 }
218
219 if !dry_run {
221 for (branch, _) in &renamed {
222 stack::clear_renamed_from(branch)?;
223 }
224 }
225
226 anstream::println!(
227 "{}",
228 style::success(&format!(
229 "submit complete: {} created, {} updated, {} skipped",
230 summary.created, summary.updated, summary.skipped
231 ))
232 );
233 Ok(())
234}
235
236fn close_superseded_review(
240 review_provider: &dyn ReviewProvider,
241 old: &str,
242 dry_run: bool,
243) -> Result<()> {
244 let Some(review) = review_provider.review_for_branch(old)? else {
245 return Ok(());
246 };
247 if review.branch != *old {
248 return Ok(());
249 }
250
251 if dry_run {
252 println!("would close superseded review {} for {old}", review.id);
253 return Ok(());
254 }
255 if !crate::prompt::confirm_default_yes(&format!(
256 "close the replaced review {} for {old} and delete its branch? [Y/n] ",
257 review.id
258 ))? {
259 println!("kept review {} for {old}", review.id);
260 return Ok(());
261 }
262
263 review_provider.close_review(&review, true)?;
264 anstream::println!("closed superseded review {} for {old}", review.id);
265 Ok(())
266}
267
268fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
269 let mut branch_parents = Vec::new();
270 for branch in branches {
271 let Some(parent) = stack::parent_for_branch(branch)? else {
272 bail!("{branch} has no stack parent; run `git stk adopt` or `git stk sync` first");
273 };
274 branch_parents.push((branch.to_owned(), parent));
275 }
276 Ok(branch_parents)
277}
278
279fn submit_branch(
280 review_provider: &dyn ReviewProvider,
281 branch: &str,
282 parent: &str,
283 dry_run: bool,
284 draft: bool,
285) -> Result<SubmitAction> {
286 if let Some(review) = review_provider.review_for_branch(branch)? {
287 if review.base == parent {
288 if dry_run {
289 println!(
290 "would skip {} -> {} ({})",
291 review.branch, review.base, review.id
292 );
293 } else {
294 anstream::println!(
295 "{}",
296 style::dim(&format!(
297 "{} already targets {} ({})",
298 review.branch, review.base, review.id
299 ))
300 );
301 }
302 return Ok(SubmitAction::Skipped);
303 }
304
305 let output = if dry_run {
306 String::new()
307 } else {
308 review_provider.update_review_base(&review, parent)?
309 };
310 anstream::println!(
311 "{} {} -> {} {}",
312 if dry_run { "would update" } else { "updated" },
313 style::branch(&review.branch),
314 style::branch(parent),
315 style::dim(&format!("({})", review.id))
316 );
317 if !output.is_empty() {
318 println!("{output}");
319 }
320 } else {
321 let output = if dry_run {
322 String::new()
323 } else {
324 review_provider.create_review(branch, parent, draft)?
325 };
326 anstream::println!(
327 "{} {} -> {}",
328 if dry_run { "would create" } else { "created" },
329 style::branch(branch),
330 style::branch(parent)
331 );
332 if !output.is_empty() {
333 println!("{output}");
334 }
335 return Ok(SubmitAction::Created);
336 }
337
338 Ok(SubmitAction::Updated)
339}
340
341#[derive(Debug, Default)]
342struct SubmitSummary {
343 created: usize,
344 updated: usize,
345 skipped: usize,
346}
347
348impl SubmitSummary {
349 fn record(&mut self, action: SubmitAction) {
350 match action {
351 SubmitAction::Created => self.created += 1,
352 SubmitAction::Updated => self.updated += 1,
353 SubmitAction::Skipped => self.skipped += 1,
354 }
355 }
356}
357
358#[derive(Debug, Clone, Copy, Eq, PartialEq)]
359enum SubmitAction {
360 Created,
361 Updated,
362 Skipped,
363}