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::{git, stack};
11
12/// Create or update a remote review request for a branch.
13#[derive(Debug, clap::Args)]
14pub struct Submit {
15    #[arg(add = ArgValueCompleter::new(completions::branch_candidates))]
16    branch: Option<String>,
17    /// Print what would change without creating or updating reviews.
18    #[arg(long, action = ArgAction::SetTrue)]
19    dry_run: bool,
20    /// Submit the whole stack parent-first, from anywhere in it.
21    #[arg(long, conflicts_with = "branch")]
22    stack: bool,
23    /// Submit only the current branch, overriding stk.submitStack.
24    #[arg(long, action = ArgAction::SetTrue, conflicts_with = "stack")]
25    no_stack: bool,
26    /// Push branches (-u --force-with-lease) before submitting.
27    #[arg(long, action = ArgAction::SetTrue, conflicts_with = "no_push")]
28    push: bool,
29    /// Do not push branches, overriding stk.pushOnSubmit.
30    #[arg(long, action = ArgAction::SetTrue)]
31    no_push: bool,
32}
33
34impl Run for Submit {
35    fn run(self) -> Result<()> {
36        // Stack mode: --stack forces it on; --no-stack or an explicit branch
37        // forces it off; otherwise stk.submitStack decides.
38        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        // The whole stack containing the current branch, from anywhere in it:
67        // walk to the root, then take its descendants. The root is excluded
68        // only when it is the trunk (the base everything sits on); an
69        // unanchored root stays in so validation can point at the missing
70        // parent instead of silently skipping it.
71        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    // Push after stack validation but before any provider calls: creating a
86    // review requires the branch to exist remotely, and -u --force-with-lease
87    // covers both first pushes and safely updating rebased branches.
88    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    // After every review exists, link any issue the branch name references,
113    // then (in stack mode) write the stack overview into each body.
114    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}