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_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, short = 'n', 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.clone()]
125 };
126
127 if submit_stack || downstack {
131 let trunk = stack::trunk_branch(&git::local_branches()?);
132 if Some(&branch) == trunk.as_ref() {
133 if stack::children_of(&branch)?.is_empty() {
134 bail!("no stacked branches to submit");
135 }
136 bail!("you are on the trunk ({branch}); check out a stacked branch first");
137 }
138 }
139
140 let branch_parents = branch_parents(&branches)?;
141
142 let push = settings::push_enabled(push_mode, settings::PUSH_ON_SUBMIT_KEY)?;
146 if push {
147 let remote = settings::remote()?;
148 if dry_run {
149 anstream::println!(
150 "would push {} to {remote}",
151 style::branch(&branches.join(" "))
152 );
153 } else {
154 git::push_set_upstream_force_with_lease(&remote, &branches)?;
155 anstream::println!("pushed {} to {remote}", style::branch(&branches.join(" ")));
156 stack::publish_metadata(&remote);
159 }
160 }
161
162 let (_, review_provider) = detect_review_provider()?;
163 let mut summary = SubmitSummary::default();
164
165 for (branch, parent) in &branch_parents {
166 summary.record(submit_branch(
167 review_provider.as_ref(),
168 branch,
169 parent,
170 dry_run,
171 draft,
172 )?);
173 }
174
175 if ready {
178 for branch in &branches {
179 let Some(review) = review_provider.review_for_branch(branch)? else {
180 continue;
181 };
182 if review.branch != *branch || !review.draft {
183 continue;
184 }
185 if dry_run {
186 anstream::println!("would mark {} ready", review.id);
187 continue;
188 }
189 let output = review_provider.mark_ready(&review)?;
190 anstream::println!("marked {} ready", review.id);
191 if !output.is_empty() {
192 println!("{output}");
193 }
194 }
195 }
196
197 let renamed: Vec<(String, String)> = if submit_stack || downstack {
204 branch_parents
205 .iter()
206 .filter_map(|(branch, _)| {
207 stack::renamed_from(branch)
208 .ok()
209 .flatten()
210 .map(|old| (branch.clone(), old))
211 })
212 .collect()
213 } else {
214 Vec::new()
215 };
216 for (_, old) in &renamed {
217 close_superseded_review(review_provider.as_ref(), old, dry_run)?;
218 }
219
220 if let Some(desc) = desc {
224 crate::notes::update_description_note(
225 review_provider.as_ref(),
226 &desc_branch,
227 desc,
228 dry_run,
229 )?;
230 }
231 crate::notes::update_closes_notes(review_provider.as_ref(), &branches, dry_run)?;
232 if submit_stack || downstack {
233 crate::notes::update_stack_notes(
234 review_provider.as_ref(),
235 &branch_parents,
236 dry_run,
237 rebuild_overview,
238 )?;
239 }
240
241 if !dry_run {
243 for (branch, _) in &renamed {
244 stack::clear_renamed_from(branch)?;
245 }
246 }
247
248 anstream::println!(
249 "{}",
250 style::success(&format!(
251 "submit complete: {} created, {} updated, {} skipped",
252 summary.created, summary.updated, summary.skipped
253 ))
254 );
255 Ok(())
256}
257
258fn close_superseded_review(
262 review_provider: &dyn ReviewProvider,
263 old: &str,
264 dry_run: bool,
265) -> Result<()> {
266 let Some(review) = review_provider.review_for_branch(old)? else {
267 return Ok(());
268 };
269 if review.branch != *old {
270 return Ok(());
271 }
272
273 if dry_run {
274 anstream::println!("would close superseded review {} for {old}", review.id);
275 return Ok(());
276 }
277 if !crate::prompt::confirm_default_yes(&format!(
278 "close the replaced review {} for {old} and delete its branch? [Y/n] ",
279 review.id
280 ))? {
281 anstream::println!("kept review {} for {old}", review.id);
282 return Ok(());
283 }
284
285 review_provider.close_review(&review, true)?;
286 anstream::println!("closed superseded review {} for {old}", review.id);
287 Ok(())
288}
289
290fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
291 let mut branch_parents = Vec::new();
292 for branch in branches {
293 let Some(parent) = stack::parent_of(branch)? else {
294 bail!("{branch} has no stack parent; run `git stk adopt` or `git stk sync` first");
295 };
296 branch_parents.push((branch.to_owned(), parent));
297 }
298 Ok(branch_parents)
299}
300
301fn submit_branch(
302 review_provider: &dyn ReviewProvider,
303 branch: &str,
304 parent: &str,
305 dry_run: bool,
306 draft: bool,
307) -> Result<SubmitAction> {
308 if let Some(review) = review_provider.review_for_branch(branch)? {
309 if review.base == parent {
310 if dry_run {
311 anstream::println!(
312 "would skip {} -> {} ({})",
313 review.branch,
314 review.base,
315 review.id
316 );
317 } else {
318 anstream::println!(
319 "{}",
320 style::dim(&format!(
321 "{} already targets {} ({})",
322 review.branch, review.base, review.id
323 ))
324 );
325 }
326 return Ok(SubmitAction::Skipped);
327 }
328
329 let output = if dry_run {
330 String::new()
331 } else {
332 review_provider.update_review_base(&review, parent)?
333 };
334 anstream::println!(
335 "{} {} -> {} {}",
336 if dry_run { "would update" } else { "updated" },
337 style::branch(&review.branch),
338 style::branch(parent),
339 style::dim(&format!("({})", review.id))
340 );
341 if !output.is_empty() {
342 println!("{output}");
343 }
344 } else {
345 let output = if dry_run {
346 String::new()
347 } else {
348 review_provider.create_review(branch, parent, draft)?
349 };
350 anstream::println!(
351 "{} {} -> {}",
352 if dry_run { "would create" } else { "created" },
353 style::branch(branch),
354 style::branch(parent)
355 );
356 if !output.is_empty() {
357 println!("{output}");
358 }
359 return Ok(SubmitAction::Created);
360 }
361
362 Ok(SubmitAction::Updated)
363}
364
365#[derive(Debug, Default)]
366struct SubmitSummary {
367 created: usize,
368 updated: usize,
369 skipped: usize,
370}
371
372impl SubmitSummary {
373 fn record(&mut self, action: SubmitAction) {
374 match action {
375 SubmitAction::Created => self.created += 1,
376 SubmitAction::Updated => self.updated += 1,
377 SubmitAction::Skipped => self.skipped += 1,
378 }
379 }
380}
381
382#[derive(Debug, Clone, Copy, Eq, PartialEq)]
383enum SubmitAction {
384 Created,
385 Updated,
386 Skipped,
387}