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(SubmitOptions {
83 branch: self.branch,
84 submit_stack,
85 downstack: self.downstack,
86 dry_run: self.dry_run,
87 push_mode: PushMode::from_flags(self.push, self.no_push),
88 desc: self.desc,
89 draft,
90 ready: self.ready,
91 rebuild_overview: self.rebuild_overview,
92 })
93 }
94}
95
96pub struct SubmitOptions {
99 pub branch: Option<String>,
100 pub submit_stack: bool,
101 pub downstack: bool,
102 pub dry_run: bool,
103 pub push_mode: crate::cli::PushMode,
104 pub desc: Option<String>,
105 pub draft: bool,
106 pub ready: bool,
107 pub rebuild_overview: bool,
108}
109
110pub fn submit(options: SubmitOptions) -> Result<()> {
111 let SubmitOptions {
112 branch,
113 submit_stack,
114 downstack,
115 dry_run,
116 push_mode,
117 desc,
118 draft,
119 ready,
120 rebuild_overview,
121 } = options;
122
123 let branch = branch.map_or_else(git::current_branch, Ok)?;
124 let desc_branch = branch.clone();
126
127 let branches = if downstack {
128 stack::path_from_root(&branch)?
131 } else if submit_stack {
132 stack::stack_line(&branch)?
136 } else {
137 vec![branch.clone()]
138 };
139
140 if submit_stack || downstack {
144 let trunk = stack::trunk_branch(&git::local_branches()?);
145 if Some(&branch) == trunk.as_ref() {
146 if stack::children_of(&branch)?.is_empty() {
147 bail!("no stacked branches to submit");
148 }
149 bail!("you are on the trunk ({branch}); check out a stacked branch first");
150 }
151 }
152
153 let branch_parents = branch_parents(&branches)?;
154
155 let push = settings::push_enabled(push_mode, settings::PUSH_ON_SUBMIT_KEY)?;
159 if push {
160 let remote = settings::remote()?;
161 if dry_run {
162 anstream::println!(
163 "would push {} to {remote}",
164 style::branch(&branches.join(" "))
165 );
166 } else {
167 git::push_set_upstream_force_with_lease(&remote, &branches)?;
168 anstream::println!("pushed {} to {remote}", style::branch(&branches.join(" ")));
169 stack::publish_metadata(&remote);
172 }
173 }
174
175 let (_, review_provider) = detect_review_provider()?;
176 let mut summary = SubmitSummary::default();
177
178 for (branch, parent) in &branch_parents {
179 summary.record(submit_branch(
180 review_provider.as_ref(),
181 branch,
182 parent,
183 dry_run,
184 draft,
185 )?);
186 }
187
188 if ready {
191 for branch in &branches {
192 let Some(review) = review_provider.review_for_branch(branch)? else {
193 continue;
194 };
195 if review.branch != *branch || !review.draft {
196 continue;
197 }
198 if dry_run {
199 anstream::println!("would mark {} ready", review.id);
200 continue;
201 }
202 let output = review_provider.mark_ready(&review)?;
203 anstream::println!("marked {} ready", review.id);
204 if !output.is_empty() {
205 println!("{output}");
206 }
207 }
208 }
209
210 let renamed: Vec<(String, String)> = if submit_stack || downstack {
217 branch_parents
218 .iter()
219 .filter_map(|(branch, _)| {
220 stack::renamed_from(branch)
221 .ok()
222 .flatten()
223 .map(|old| (branch.clone(), old))
224 })
225 .collect()
226 } else {
227 Vec::new()
228 };
229 for (_, old) in &renamed {
230 close_superseded_review(review_provider.as_ref(), old, dry_run)?;
231 }
232
233 if let Some(desc) = desc {
237 crate::notes::update_description_note(
238 review_provider.as_ref(),
239 &desc_branch,
240 &desc,
241 dry_run,
242 )?;
243 }
244 crate::notes::update_closes_notes(review_provider.as_ref(), &branches, dry_run)?;
245 if submit_stack || downstack {
246 crate::notes::update_stack_notes(
247 review_provider.as_ref(),
248 &branch_parents,
249 dry_run,
250 rebuild_overview,
251 )?;
252 }
253
254 if !dry_run {
256 for (branch, _) in &renamed {
257 stack::clear_renamed_from(branch)?;
258 }
259 }
260
261 anstream::println!(
262 "{}",
263 style::success(&format!(
264 "submit complete: {} created, {} updated, {} skipped",
265 summary.created, summary.updated, summary.skipped
266 ))
267 );
268 Ok(())
269}
270
271fn close_superseded_review(
275 review_provider: &dyn ReviewProvider,
276 old: &str,
277 dry_run: bool,
278) -> Result<()> {
279 let Some(review) = review_provider.review_for_branch(old)? else {
280 return Ok(());
281 };
282 if review.branch != *old {
283 return Ok(());
284 }
285
286 if dry_run {
287 anstream::println!("would close superseded review {} for {old}", review.id);
288 return Ok(());
289 }
290 if !crate::prompt::confirm_default_yes(&format!(
291 "close the replaced review {} for {old} and delete its branch? [Y/n] ",
292 review.id
293 ))? {
294 anstream::println!("kept review {} for {old}", review.id);
295 return Ok(());
296 }
297
298 review_provider.close_review(&review, true)?;
299 anstream::println!("closed superseded review {} for {old}", review.id);
300 Ok(())
301}
302
303fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
304 let mut branch_parents = Vec::new();
305 for branch in branches {
306 let Some(parent) = stack::parent_of(branch)? else {
307 bail!("{branch} has no stack parent; run `git stk adopt` or `git stk sync` first");
308 };
309 branch_parents.push((branch.to_owned(), parent));
310 }
311 Ok(branch_parents)
312}
313
314fn submit_branch(
315 review_provider: &dyn ReviewProvider,
316 branch: &str,
317 parent: &str,
318 dry_run: bool,
319 draft: bool,
320) -> Result<SubmitAction> {
321 if let Some(review) = review_provider.review_for_branch(branch)? {
322 if review.base == parent {
323 if dry_run {
324 anstream::println!(
325 "would skip {} -> {} ({})",
326 review.branch,
327 review.base,
328 review.id
329 );
330 } else {
331 anstream::println!(
332 "{}",
333 style::dim(&format!(
334 "{} already targets {} ({})",
335 review.branch, review.base, review.id
336 ))
337 );
338 }
339 return Ok(SubmitAction::Skipped);
340 }
341
342 let output = if dry_run {
343 String::new()
344 } else {
345 review_provider.update_review_base(&review, parent)?
346 };
347 anstream::println!(
348 "{} {} -> {} {}",
349 if dry_run { "would update" } else { "updated" },
350 style::branch(&review.branch),
351 style::branch(parent),
352 style::dim(&format!("({})", review.id))
353 );
354 if !output.is_empty() {
355 println!("{output}");
356 }
357 } else {
358 let output = if dry_run {
359 String::new()
360 } else {
361 review_provider.create_review(branch, parent, draft)?
362 };
363 anstream::println!(
364 "{} {} -> {}",
365 if dry_run { "would create" } else { "created" },
366 style::branch(branch),
367 style::branch(parent)
368 );
369 if !output.is_empty() {
370 println!("{output}");
371 }
372 return Ok(SubmitAction::Created);
373 }
374
375 Ok(SubmitAction::Updated)
376}
377
378#[derive(Debug, Default)]
379struct SubmitSummary {
380 created: usize,
381 updated: usize,
382 skipped: usize,
383}
384
385impl SubmitSummary {
386 fn record(&mut self, action: SubmitAction) {
387 match action {
388 SubmitAction::Created => self.created += 1,
389 SubmitAction::Updated => self.updated += 1,
390 SubmitAction::Skipped => self.skipped += 1,
391 }
392 }
393}
394
395#[derive(Debug, Clone, Copy, Eq, PartialEq)]
396enum SubmitAction {
397 Created,
398 Updated,
399 Skipped,
400}