Skip to main content

git_stk/commands/
submit.rs

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/// Create or update a remote review request for a branch.
14#[derive(Debug, clap::Args)]
15pub struct Submit {
16    #[arg(add = ArgValueCompleter::new(completions::branch_candidates))]
17    branch: Option<String>,
18    /// Print what would change without creating or updating reviews.
19    #[arg(long, action = ArgAction::SetTrue)]
20    dry_run: bool,
21    /// Submit the whole stack parent-first, from anywhere in it.
22    #[arg(long, conflicts_with = "branch")]
23    stack: bool,
24    /// Submit only the current branch, overriding stk.submitStack.
25    #[arg(long, action = ArgAction::SetTrue, conflicts_with = "stack")]
26    no_stack: bool,
27    /// Push branches (-u --force-with-lease) before submitting.
28    #[arg(long, action = ArgAction::SetTrue, conflicts_with = "no_push")]
29    push: bool,
30    /// Do not push branches, overriding stk.pushOnSubmit.
31    #[arg(long, action = ArgAction::SetTrue)]
32    no_push: bool,
33    /// Set a description block at the top of the review body; an empty
34    /// string clears it. Applies to the current or named branch only.
35    #[arg(long, short = 'd')]
36    desc: Option<String>,
37}
38
39impl Run for Submit {
40    fn run(self) -> Result<()> {
41        // Stack mode: --stack forces it on; --no-stack or an explicit branch
42        // forces it off; otherwise stk.submitStack decides.
43        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    // The description targets this branch's review even in stack mode.
72    let desc_branch = branch.clone();
73
74    let branches = if submit_stack {
75        // The whole stack containing the current branch, from anywhere in it:
76        // walk to the root, then take its descendants. The root is excluded
77        // only when it is the trunk (the base everything sits on); an
78        // unanchored root stays in so validation can point at the missing
79        // parent instead of silently skipping it.
80        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    // Push after stack validation but before any provider calls: creating a
95    // review requires the branch to exist remotely, and -u --force-with-lease
96    // covers both first pushes and safely updating rebased branches.
97    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    // After every review exists, write the description, link any issue the
125    // branch name references, then (in stack mode) write the stack overview
126    // into each body.
127    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}