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}
55
56impl Run for Submit {
57 fn run(self) -> Result<()> {
58 let submit_stack = if self.stack {
61 true
62 } else if self.no_stack || self.branch.is_some() {
63 false
64 } else {
65 settings::bool_setting(settings::SUBMIT_STACK_KEY)?
66 };
67
68 let draft = if self.draft {
71 true
72 } else if self.no_draft {
73 false
74 } else {
75 settings::bool_setting(settings::SUBMIT_DRAFT_KEY)?
76 };
77
78 submit(
79 self.branch.as_deref(),
80 submit_stack,
81 self.downstack,
82 self.dry_run,
83 PushMode::from_flags(self.push, self.no_push),
84 self.desc.as_deref(),
85 draft,
86 self.ready,
87 )
88 }
89}
90
91#[allow(clippy::too_many_arguments)]
92pub fn submit(
93 branch: Option<&str>,
94 submit_stack: bool,
95 downstack: bool,
96 dry_run: bool,
97 push_mode: crate::cli::PushMode,
98 desc: Option<&str>,
99 draft: bool,
100 ready: bool,
101) -> Result<()> {
102 let branch = branch
103 .map(str::to_owned)
104 .map_or_else(git::current_branch, Ok)?;
105 let desc_branch = branch.clone();
107
108 let branches = if downstack {
109 stack::path_from_root(&branch)?
112 } else if submit_stack {
113 let root = stack::stack_root(&branch)?;
119 let trunk = stack::trunk_branch(&git::local_branches()?);
120 let full = stack::branch_and_descendants(&root)?;
121 if Some(root) == trunk {
122 full.into_iter().skip(1).collect()
123 } else {
124 full
125 }
126 } else {
127 vec![branch]
128 };
129
130 let branch_parents = branch_parents(&branches)?;
131
132 let push = settings::push_enabled(push_mode, settings::PUSH_ON_SUBMIT_KEY)?;
136 if push {
137 let remote = settings::remote()?;
138 if dry_run {
139 anstream::println!(
140 "would push {} to {remote}",
141 style::branch(&branches.join(" "))
142 );
143 } else {
144 git::push_set_upstream_force_with_lease(&remote, &branches)?;
145 anstream::println!("pushed {} to {remote}", style::branch(&branches.join(" ")));
146 }
147 }
148
149 let provider = detect_provider()?;
150 let review_provider = review_provider(provider.kind);
151 let mut summary = SubmitSummary::default();
152
153 for (branch, parent) in &branch_parents {
154 summary.record(submit_branch(
155 review_provider.as_ref(),
156 branch,
157 parent,
158 dry_run,
159 draft,
160 )?);
161 }
162
163 if ready {
166 for branch in &branches {
167 let Some(review) = review_provider.review_for_branch(branch)? else {
168 continue;
169 };
170 if review.branch != *branch || !review.draft {
171 continue;
172 }
173 if dry_run {
174 println!("would mark {} ready", review.id);
175 continue;
176 }
177 let output = review_provider.mark_ready(&review)?;
178 anstream::println!("marked {} ready", review.id);
179 if !output.is_empty() {
180 println!("{output}");
181 }
182 }
183 }
184
185 if let Some(desc) = desc {
189 crate::notes::update_description_note(
190 review_provider.as_ref(),
191 &desc_branch,
192 desc,
193 dry_run,
194 )?;
195 }
196 crate::notes::update_closes_notes(review_provider.as_ref(), &branches, dry_run)?;
197 if submit_stack || downstack {
198 crate::notes::update_stack_notes(review_provider.as_ref(), &branch_parents, dry_run)?;
199 }
200
201 anstream::println!(
202 "{}",
203 style::success(&format!(
204 "submit complete: {} created, {} updated, {} skipped",
205 summary.created, summary.updated, summary.skipped
206 ))
207 );
208 Ok(())
209}
210
211fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
212 let mut branch_parents = Vec::new();
213 for branch in branches {
214 let Some(parent) = stack::parent_for_branch(branch)? else {
215 bail!("{branch} has no stack parent; run `git stk adopt` or `git stk sync` first");
216 };
217 branch_parents.push((branch.to_owned(), parent));
218 }
219 Ok(branch_parents)
220}
221
222fn submit_branch(
223 review_provider: &dyn ReviewProvider,
224 branch: &str,
225 parent: &str,
226 dry_run: bool,
227 draft: bool,
228) -> Result<SubmitAction> {
229 if let Some(review) = review_provider.review_for_branch(branch)? {
230 if review.base == parent {
231 if dry_run {
232 println!(
233 "would skip {} -> {} ({})",
234 review.branch, review.base, review.id
235 );
236 } else {
237 anstream::println!(
238 "{}",
239 style::dim(&format!(
240 "{} already targets {} ({})",
241 review.branch, review.base, review.id
242 ))
243 );
244 }
245 return Ok(SubmitAction::Skipped);
246 }
247
248 let output = if dry_run {
249 String::new()
250 } else {
251 review_provider.update_review_base(&review, parent)?
252 };
253 anstream::println!(
254 "{} {} -> {} {}",
255 if dry_run { "would update" } else { "updated" },
256 style::branch(&review.branch),
257 style::branch(parent),
258 style::dim(&format!("({})", review.id))
259 );
260 if !output.is_empty() {
261 println!("{output}");
262 }
263 } else {
264 let output = if dry_run {
265 String::new()
266 } else {
267 review_provider.create_review(branch, parent, draft)?
268 };
269 anstream::println!(
270 "{} {} -> {}",
271 if dry_run { "would create" } else { "created" },
272 style::branch(branch),
273 style::branch(parent)
274 );
275 if !output.is_empty() {
276 println!("{output}");
277 }
278 return Ok(SubmitAction::Created);
279 }
280
281 Ok(SubmitAction::Updated)
282}
283
284#[derive(Debug, Default)]
285struct SubmitSummary {
286 created: usize,
287 updated: usize,
288 skipped: usize,
289}
290
291impl SubmitSummary {
292 fn record(&mut self, action: SubmitAction) {
293 match action {
294 SubmitAction::Created => self.created += 1,
295 SubmitAction::Updated => self.updated += 1,
296 SubmitAction::Skipped => self.skipped += 1,
297 }
298 }
299}
300
301#[derive(Debug, Clone, Copy, Eq, PartialEq)]
302enum SubmitAction {
303 Created,
304 Updated,
305 Skipped,
306}