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(long, action = ArgAction::SetTrue, conflicts_with = "no_push")]
29 push: bool,
30 #[arg(long, action = ArgAction::SetTrue)]
32 no_push: bool,
33 #[arg(long, short = 'd')]
36 desc: Option<String>,
37}
38
39impl Run for Submit {
40 fn run(self) -> Result<()> {
41 let submit_stack = if self.stack {
44 true
45 } else if self.no_stack || self.branch.is_some() {
46 false
47 } else {
48 settings::bool_setting(settings::SUBMIT_STACK_KEY)?
49 };
50
51 submit(
52 self.branch.as_deref(),
53 submit_stack,
54 self.dry_run,
55 PushMode::from_flags(self.push, self.no_push),
56 self.desc.as_deref(),
57 )
58 }
59}
60
61pub fn submit(
62 branch: Option<&str>,
63 submit_stack: bool,
64 dry_run: bool,
65 push_mode: crate::cli::PushMode,
66 desc: Option<&str>,
67) -> Result<()> {
68 let branch = branch
69 .map(str::to_owned)
70 .map_or_else(git::current_branch, Ok)?;
71 let desc_branch = branch.clone();
73
74 let branches = if submit_stack {
75 let root = stack::stack_root(&branch)?;
81 let trunk = stack::trunk_branch(&git::local_branches()?);
82 let full = stack::branch_and_descendants(&root)?;
83 if Some(root) == trunk {
84 full.into_iter().skip(1).collect()
85 } else {
86 full
87 }
88 } else {
89 vec![branch]
90 };
91
92 let branch_parents = branch_parents(&branches)?;
93
94 let push = settings::push_enabled(push_mode, settings::PUSH_ON_SUBMIT_KEY)?;
98 if push {
99 let remote = settings::remote()?;
100 if dry_run {
101 anstream::println!(
102 "would push {} to {remote}",
103 style::branch(&branches.join(" "))
104 );
105 } else {
106 git::push_set_upstream_force_with_lease(&remote, &branches)?;
107 anstream::println!("pushed {} to {remote}", style::branch(&branches.join(" ")));
108 }
109 }
110
111 let provider = detect_provider()?;
112 let review_provider = review_provider(provider.kind);
113 let mut summary = SubmitSummary::default();
114
115 for (branch, parent) in &branch_parents {
116 summary.record(submit_branch(
117 review_provider.as_ref(),
118 branch,
119 parent,
120 dry_run,
121 )?);
122 }
123
124 if let Some(desc) = desc {
128 crate::notes::update_description_note(
129 review_provider.as_ref(),
130 &desc_branch,
131 desc,
132 dry_run,
133 )?;
134 }
135 crate::notes::update_closes_notes(review_provider.as_ref(), &branches, dry_run)?;
136 if submit_stack {
137 crate::notes::update_stack_notes(review_provider.as_ref(), &branch_parents, dry_run)?;
138 }
139
140 anstream::println!(
141 "{}",
142 style::success(&format!(
143 "submit complete: {} created, {} updated, {} skipped",
144 summary.created, summary.updated, summary.skipped
145 ))
146 );
147 Ok(())
148}
149
150fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
151 let mut branch_parents = Vec::new();
152 for branch in branches {
153 let Some(parent) = stack::parent_for_branch(branch)? else {
154 bail!("{branch} has no stack parent; run `git stk adopt` or `git stk sync` first");
155 };
156 branch_parents.push((branch.to_owned(), parent));
157 }
158 Ok(branch_parents)
159}
160
161fn submit_branch(
162 review_provider: &dyn ReviewProvider,
163 branch: &str,
164 parent: &str,
165 dry_run: bool,
166) -> Result<SubmitAction> {
167 if let Some(review) = review_provider.review_for_branch(branch)? {
168 if review.base == parent {
169 if dry_run {
170 println!(
171 "would skip {} -> {} ({})",
172 review.branch, review.base, review.id
173 );
174 } else {
175 anstream::println!(
176 "{}",
177 style::dim(&format!(
178 "{} already targets {} ({})",
179 review.branch, review.base, review.id
180 ))
181 );
182 }
183 return Ok(SubmitAction::Skipped);
184 }
185
186 let output = if dry_run {
187 String::new()
188 } else {
189 review_provider.update_review_base(&review, parent)?
190 };
191 anstream::println!(
192 "{} {} -> {} {}",
193 if dry_run { "would update" } else { "updated" },
194 style::branch(&review.branch),
195 style::branch(parent),
196 style::dim(&format!("({})", review.id))
197 );
198 if !output.is_empty() {
199 println!("{output}");
200 }
201 } else {
202 let output = if dry_run {
203 String::new()
204 } else {
205 review_provider.create_review(branch, parent)?
206 };
207 anstream::println!(
208 "{} {} -> {}",
209 if dry_run { "would create" } else { "created" },
210 style::branch(branch),
211 style::branch(parent)
212 );
213 if !output.is_empty() {
214 println!("{output}");
215 }
216 return Ok(SubmitAction::Created);
217 }
218
219 Ok(SubmitAction::Updated)
220}
221
222#[derive(Debug, Default)]
223struct SubmitSummary {
224 created: usize,
225 updated: usize,
226 skipped: usize,
227}
228
229impl SubmitSummary {
230 fn record(&mut self, action: SubmitAction) {
231 match action {
232 SubmitAction::Created => self.created += 1,
233 SubmitAction::Updated => self.updated += 1,
234 SubmitAction::Skipped => self.skipped += 1,
235 }
236 }
237}
238
239#[derive(Debug, Clone, Copy, Eq, PartialEq)]
240enum SubmitAction {
241 Created,
242 Updated,
243 Skipped,
244}