Skip to main content

git_stk/commands/
merge.rs

1use anyhow::{Result, bail};
2use clap::ArgAction;
3
4use crate::cli::PushMode;
5use crate::commands::Run;
6use crate::commands::sync::sync;
7use crate::prompt::confirm;
8use crate::providers::{ReviewState, detect_provider, review_provider};
9use crate::settings;
10use crate::stack;
11
12/// Merge the review at the bottom of the stack, then sync.
13#[derive(Debug, clap::Args)]
14pub struct Merge {
15    /// Print what would happen without merging anything.
16    #[arg(long, action = ArgAction::SetTrue)]
17    dry_run: bool,
18    /// Skip the confirmation prompt.
19    #[arg(long, short = 'y', action = ArgAction::SetTrue)]
20    yes: bool,
21}
22
23impl Run for Merge {
24    fn run(self) -> Result<()> {
25        merge(self.dry_run, self.yes)
26    }
27}
28
29fn merge(dry_run: bool, yes: bool) -> Result<()> {
30    let current = crate::git::current_branch()?;
31    let root = stack::stack_root(&current)?;
32    let trunk = stack::trunk_branch(&crate::git::local_branches()?);
33
34    // The bottom of the stack: the first branch that is not the trunk.
35    // (A rootless fragment's own root is its bottom.)
36    let Some(bottom) = stack::branch_and_descendants(&root)?
37        .into_iter()
38        .find(|branch| Some(branch) != trunk.as_ref())
39    else {
40        bail!("no stacked branches to merge");
41    };
42
43    let provider = detect_provider()?;
44    let review_provider = review_provider(provider.kind);
45
46    let Some(review) = review_provider.review_for_branch(&bottom)? else {
47        bail!(
48            "no {} review found for {bottom}; submit the stack first",
49            provider.kind
50        );
51    };
52    if review.state != ReviewState::Open {
53        bail!(
54            "review {} for {bottom} is {}, not open",
55            review.id,
56            review.state
57        );
58    }
59
60    let expected_base = stack::parent_for_branch(&bottom)?;
61    if let Some(expected) = &expected_base
62        && *expected != review.base
63    {
64        bail!(
65            "review {} targets {}, but {bottom}'s stack parent is {expected}; \
66             run `git stk submit` first",
67            review.id,
68            review.base
69        );
70    }
71
72    let strategy = settings::merge_strategy()?;
73    let label = if review.title.is_empty() {
74        review.id.clone()
75    } else {
76        format!("{} ({})", review.title, review.id)
77    };
78
79    if dry_run {
80        println!("would merge {label} into {} ({strategy})", review.base);
81        println!("would sync afterwards");
82        return Ok(());
83    }
84
85    if !yes
86        && !confirm(&format!(
87            "merge {label} into {} ({strategy})? [y/N] ",
88            review.base
89        ))?
90    {
91        println!("merge cancelled");
92        return Ok(());
93    }
94
95    let output = review_provider.merge_review(&review, &strategy)?;
96    if !output.is_empty() {
97        println!("{output}");
98    }
99    println!("merged {label}");
100
101    // Reconcile everything the merge changed: fetch, clean up, restack, push.
102    sync(false, PushMode::Config)
103}