Skip to main content

git_stk/commands/
sync.rs

1use anyhow::{Result, bail};
2use clap::ArgAction;
3
4use crate::cli::{PushMode, UpdateRefsMode};
5use crate::commands::Run;
6use crate::commands::cleanup::{cleanup_branch_deletion, cleanup_merged_branch};
7use crate::providers::{ReviewState, detect_provider, review_provider};
8use crate::settings;
9use crate::{git, stack};
10
11/// Sync the stack with remote state: fetch the trunk, refresh metadata from
12/// reviews, clean up merged branches, then restack and push.
13#[derive(Debug, clap::Args)]
14pub struct Sync {
15    /// Print what would change without changing anything.
16    #[arg(long, action = ArgAction::SetTrue)]
17    dry_run: bool,
18    /// Force-push (with lease) rebased branches after the restack.
19    #[arg(long, action = ArgAction::SetTrue, conflicts_with = "no_push")]
20    push: bool,
21    /// Do not push rebased branches, overriding stk.pushOnRestack.
22    #[arg(long, action = ArgAction::SetTrue)]
23    no_push: bool,
24}
25
26impl Run for Sync {
27    fn run(self) -> Result<()> {
28        sync(self.dry_run, PushMode::from_flags(self.push, self.no_push))
29    }
30}
31
32pub(crate) fn sync(dry_run: bool, push_mode: PushMode) -> Result<()> {
33    let current = git::current_branch()?;
34    let local_branches = git::local_branches()?;
35    let trunk = stack::trunk_branch(&local_branches);
36
37    // 1. Fetch the trunk so merged work is visible locally.
38    let remote = settings::remote()?;
39    if let Some(trunk) = &trunk {
40        if git::remote_url(&remote)?.is_none() {
41            println!("no remote {remote}; skipped fetch");
42        } else if dry_run {
43            println!("would fetch {trunk} from {remote}");
44        } else if current == *trunk {
45            git::pull_ff_only()?;
46        } else {
47            git::fetch_branch(&remote, trunk)?;
48        }
49    }
50
51    // 2. The stack containing the current branch (the trunk itself has no
52    //    review and is never synced).
53    let root = stack::stack_root(&current)?;
54    let branches: Vec<String> = stack::branch_and_descendants(&root)?
55        .into_iter()
56        .filter(|branch| Some(branch) != trunk.as_ref())
57        .collect();
58
59    let provider = detect_provider()?;
60    let review_provider = review_provider(provider.kind);
61
62    // 3. Classify every branch: refresh metadata from open reviews, collect
63    //    merged ones for cleanup.
64    let mut merged = Vec::new();
65    let mut synced = 0;
66    let mut skipped = 0;
67
68    for branch in &branches {
69        let Some(review) = review_provider.review_for_branch(branch)? else {
70            println!("skipped {branch}: no {} review found", provider.kind);
71            skipped += 1;
72            continue;
73        };
74
75        if review.branch != *branch {
76            println!(
77                "skipped {branch}: {} review belongs to {}",
78                provider.kind, review.branch
79            );
80            skipped += 1;
81            continue;
82        }
83
84        if review.state == ReviewState::Merged {
85            println!("{branch}: review {} is merged", review.id);
86            merged.push(branch.clone());
87            continue;
88        }
89
90        if review.branch == review.base {
91            bail!("refusing to set {branch} as its own stack parent");
92        }
93
94        if !dry_run {
95            stack::set_parent_for_branch(branch, &review.base)?;
96            stack::record_base(branch, &review.base);
97        }
98        println!(
99            "{} {} -> {} ({})",
100            if dry_run { "would sync" } else { "synced" },
101            review.branch,
102            review.base,
103            review.id
104        );
105        synced += 1;
106    }
107
108    println!(
109        "sync complete: {synced} {}synced, {skipped} skipped",
110        if dry_run { "would be " } else { "" }
111    );
112
113    // 4. Refresh the stack overview ledger in every review body while the
114    //    merged branches and their reviews are still resolvable, so their
115    //    entries get restyled rather than dropped.
116    let mut branch_parents = Vec::new();
117    for branch in &branches {
118        if let Some(parent) = stack::parent_for_branch(branch)? {
119            branch_parents.push((branch.clone(), parent));
120        }
121    }
122    crate::notes::update_stack_notes(review_provider.as_ref(), &branch_parents, dry_run)?;
123
124    let survivors: Vec<String> = branches
125        .iter()
126        .filter(|branch| !merged.contains(branch))
127        .cloned()
128        .collect();
129
130    // 5. Move off any branch that is about to be deleted, onto the first
131    //    survivor (the new stack bottom) or the trunk.
132    let mut position = current.clone();
133    if merged.contains(&current) {
134        let target = survivors
135            .first()
136            .cloned()
137            .or_else(|| trunk.clone())
138            .unwrap_or(root.clone());
139        if dry_run {
140            println!("would switch to {target}");
141        } else {
142            git::checkout(&target)?;
143        }
144        position = target;
145    }
146
147    // 6. Clean up the merged branches: retarget children, then delete.
148    for branch in &merged {
149        cleanup_merged_branch(review_provider.as_ref(), branch, dry_run)?;
150        cleanup_branch_deletion(branch, &position, dry_run, true)?;
151    }
152
153    // 7. Restack the remainder (and push, per flags/config).
154    if dry_run {
155        println!("would restack the remaining stack");
156    } else if !survivors.is_empty() {
157        stack::restack(UpdateRefsMode::Config, push_mode)?;
158    }
159
160    // 8. Where to look next.
161    match survivors.first() {
162        Some(bottom) => match review_provider.review_for_branch(bottom)? {
163            Some(review) => println!("next up: {bottom} -> {} {}", review.id, review.url),
164            None => println!("next up: {bottom} (no review yet)"),
165        },
166        None => {
167            let base = trunk.unwrap_or(root);
168            println!("stack complete: everything merged into {base}");
169        }
170    }
171
172    Ok(())
173}