git_stk/commands/
merge.rs1use 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#[derive(Debug, clap::Args)]
14pub struct Merge {
15 #[arg(long, action = ArgAction::SetTrue)]
17 dry_run: bool,
18 #[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(¤t)?;
32 let trunk = stack::trunk_branch(&crate::git::local_branches()?);
33
34 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 sync(false, PushMode::Config)
103}