git_stk/commands/
submit.rs1use 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}
33
34impl Run for Submit {
35 fn run(self) -> Result<()> {
36 let submit_stack = if self.stack {
39 true
40 } else if self.no_stack || self.branch.is_some() {
41 false
42 } else {
43 settings::bool_setting(settings::SUBMIT_STACK_KEY)?
44 };
45
46 submit(
47 self.branch.as_deref(),
48 submit_stack,
49 self.dry_run,
50 PushMode::from_flags(self.push, self.no_push),
51 )
52 }
53}
54
55pub fn submit(
56 branch: Option<&str>,
57 submit_stack: bool,
58 dry_run: bool,
59 push_mode: crate::cli::PushMode,
60) -> Result<()> {
61 let branch = branch
62 .map(str::to_owned)
63 .map_or_else(git::current_branch, Ok)?;
64
65 let branches = if submit_stack {
66 let root = stack::stack_root(&branch)?;
72 let trunk = stack::trunk_branch(&git::local_branches()?);
73 let full = stack::branch_and_descendants(&root)?;
74 if Some(root) == trunk {
75 full.into_iter().skip(1).collect()
76 } else {
77 full
78 }
79 } else {
80 vec![branch]
81 };
82
83 let branch_parents = branch_parents(&branches)?;
84
85 let push = settings::push_enabled(push_mode, settings::PUSH_ON_SUBMIT_KEY)?;
89 if push {
90 let remote = settings::remote()?;
91 if dry_run {
92 println!("would push {} to {remote}", branches.join(" "));
93 } else {
94 git::push_set_upstream_force_with_lease(&remote, &branches)?;
95 println!("pushed {} to {remote}", branches.join(" "));
96 }
97 }
98
99 let provider = detect_provider()?;
100 let review_provider = review_provider(provider.kind);
101 let mut summary = SubmitSummary::default();
102
103 for (branch, parent) in &branch_parents {
104 summary.record(submit_branch(
105 review_provider.as_ref(),
106 branch,
107 parent,
108 dry_run,
109 )?);
110 }
111
112 crate::notes::update_closes_notes(review_provider.as_ref(), &branches, dry_run)?;
115 if submit_stack {
116 crate::notes::update_stack_notes(review_provider.as_ref(), &branch_parents, dry_run)?;
117 }
118
119 println!(
120 "submit complete: {} created, {} updated, {} skipped",
121 summary.created, summary.updated, summary.skipped
122 );
123 Ok(())
124}
125
126fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
127 let mut branch_parents = Vec::new();
128 for branch in branches {
129 let Some(parent) = stack::parent_for_branch(branch)? else {
130 bail!("{branch} has no stack parent; run `git stk adopt` or `git stk sync` first");
131 };
132 branch_parents.push((branch.to_owned(), parent));
133 }
134 Ok(branch_parents)
135}
136
137fn submit_branch(
138 review_provider: &dyn ReviewProvider,
139 branch: &str,
140 parent: &str,
141 dry_run: bool,
142) -> Result<SubmitAction> {
143 if let Some(review) = review_provider.review_for_branch(branch)? {
144 if review.base == parent {
145 if dry_run {
146 println!(
147 "would skip {} -> {} ({})",
148 review.branch, review.base, review.id
149 );
150 } else {
151 println!(
152 "{} already targets {} ({})",
153 review.branch, review.base, review.id
154 );
155 }
156 return Ok(SubmitAction::Skipped);
157 }
158
159 let output = if dry_run {
160 String::new()
161 } else {
162 review_provider.update_review_base(&review, parent)?
163 };
164 println!(
165 "{} {} -> {} ({})",
166 if dry_run { "would update" } else { "updated" },
167 review.branch,
168 parent,
169 review.id
170 );
171 if !output.is_empty() {
172 println!("{output}");
173 }
174 } else {
175 let output = if dry_run {
176 String::new()
177 } else {
178 review_provider.create_review(branch, parent)?
179 };
180 println!(
181 "{} {branch} -> {parent}",
182 if dry_run { "would create" } else { "created" }
183 );
184 if !output.is_empty() {
185 println!("{output}");
186 }
187 return Ok(SubmitAction::Created);
188 }
189
190 Ok(SubmitAction::Updated)
191}
192
193#[derive(Debug, Default)]
194struct SubmitSummary {
195 created: usize,
196 updated: usize,
197 skipped: usize,
198}
199
200impl SubmitSummary {
201 fn record(&mut self, action: SubmitAction) {
202 match action {
203 SubmitAction::Created => self.created += 1,
204 SubmitAction::Updated => self.updated += 1,
205 SubmitAction::Skipped => self.skipped += 1,
206 }
207 }
208}
209
210#[derive(Debug, Clone, Copy, Eq, PartialEq)]
211enum SubmitAction {
212 Created,
213 Updated,
214 Skipped,
215}