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