1use std::collections::BTreeSet;
2
3use anyhow::{Result, bail};
4use clap::ArgAction;
5
6use crate::cli::{FetchMode, PushMode, UpdateRefsMode};
7use crate::commands::Run;
8use crate::commands::cleanup::{
9 Landing, cleanup_branch_deletion, cleanup_finished_branch, landing_for,
10};
11use crate::providers::{ReviewState, detect_review_provider};
12use crate::settings;
13use crate::style;
14use crate::{git, stack};
15
16#[derive(Debug, clap::Args)]
19pub struct Sync {
20 #[arg(long, short = 'n', action = ArgAction::SetTrue)]
22 dry_run: bool,
23 #[arg(long, action = ArgAction::SetTrue, conflicts_with = "no_push")]
25 push: bool,
26 #[arg(long, action = ArgAction::SetTrue)]
28 no_push: bool,
29}
30
31impl Run for Sync {
32 fn run(self) -> Result<()> {
33 sync(self.dry_run, PushMode::from_flags(self.push, self.no_push))
34 }
35}
36
37pub(crate) fn sync(dry_run: bool, push_mode: PushMode) -> Result<()> {
38 let current = git::current_branch()?;
39 let local_branches = git::local_branches()?;
40 let trunk = stack::trunk_branch(&local_branches);
41
42 if !dry_run {
45 stack::snapshot("sync");
46 }
47
48 let remote = settings::remote()?;
50 let has_remote = git::remote_url(&remote)?.is_some();
51 if let Some(trunk) = &trunk {
52 if !has_remote {
53 anstream::println!("no remote {remote}; skipped fetch");
54 } else if stack::trunk_held_elsewhere(trunk)? {
55 } else if dry_run {
58 anstream::println!("would fetch {trunk} from {remote}");
59 } else if current == *trunk {
60 git::pull_ff_only()?;
61 } else {
62 git::fetch_branch(&remote, trunk)?;
63 }
64 }
65
66 let root = stack::stack_root(¤t)?;
69 let branches = stack::current_stack_branches(¤t)?;
70
71 let (provider, review_provider) = match detect_review_provider() {
72 Ok(pair) => pair,
73 Err(_) if !has_remote => {
79 if branches.is_empty() {
80 anstream::println!("no stacked branches to sync");
81 } else {
82 anstream::println!("no remote configured - nothing to sync");
83 anstream::println!(
84 "{}",
85 style::dim("run `git stk restack` to refresh local branches")
86 );
87 }
88 return Ok(());
89 }
90 Err(error) => return Err(error),
91 };
92
93 let clean_closed = settings::bool_setting(settings::CLEAN_CLOSED_KEY)?;
98 let mut finished = Vec::new();
99 let mut closed = BTreeSet::new();
100 let mut synced = 0;
101 let mut skipped = 0;
102
103 for branch in &branches {
104 let Some(review) = review_provider.review_for_branch_including_closed(branch)? else {
107 anstream::println!(
108 "{}",
109 style::dim(&format!(
110 "skipped {branch}: no {} review found",
111 provider.kind
112 ))
113 );
114 skipped += 1;
115 continue;
116 };
117
118 if review.branch != *branch {
119 anstream::println!(
120 "{}",
121 style::dim(&format!(
122 "skipped {branch}: {} review belongs to {}",
123 provider.kind, review.branch
124 ))
125 );
126 skipped += 1;
127 continue;
128 }
129
130 if let Some(landing) = landing_for(&review.state, clean_closed) {
131 anstream::println!(
132 "{}: review {} is {}",
133 style::branch(branch),
134 review.id,
135 style::state(&review.state)
136 );
137 finished.push(branch.clone());
138 if landing == Landing::Closed {
139 closed.insert(branch.clone());
140 }
141 continue;
142 }
143
144 if review.state == ReviewState::Closed {
147 anstream::println!(
148 "{}",
149 style::dim(&format!(
150 "skipped {branch}: review {} was closed without merging",
151 review.id
152 ))
153 );
154 skipped += 1;
155 continue;
156 }
157
158 if let Some(parent) = stack::parent_of(branch)?
165 && finished.contains(&parent)
166 {
167 continue;
168 }
169
170 if review.branch == review.base {
171 bail!("refusing to set {branch} as its own stack parent");
172 }
173
174 if !dry_run {
175 stack::set_parent(branch, &review.base)?;
176 stack::record_base(branch, &review.base);
177 }
178 anstream::println!(
179 "{} {} -> {} {}",
180 if dry_run { "would sync" } else { "synced" },
181 style::branch(&review.branch),
182 style::branch(&review.base),
183 style::dim(&format!("({})", review.id))
184 );
185 synced += 1;
186 }
187
188 anstream::println!(
189 "{}",
190 style::success(&format!(
191 "sync complete: {synced} {}synced, {skipped} skipped",
192 if dry_run { "would be " } else { "" }
193 ))
194 );
195
196 let branch_parents = stack::branch_parents(&branches)?;
200 crate::notes::update_stack_notes(review_provider.as_ref(), &branch_parents, dry_run, false)?;
201
202 let survivors: Vec<String> = branches
203 .iter()
204 .filter(|branch| !finished.contains(branch))
205 .cloned()
206 .collect();
207
208 let mut position = current.clone();
211 if finished.contains(¤t) {
212 let target = survivors
213 .first()
214 .cloned()
215 .or_else(|| trunk.clone())
216 .unwrap_or(root.clone());
217 let held = git::worktree_holding(&target)?;
218 if let Some(path) = held {
219 anstream::println!(
224 "{}",
225 style::warn(&format!(
226 "stayed on {current}: {target} is checked out in the worktree at {}",
227 git::display_path(&path)
228 ))
229 );
230 } else if dry_run {
231 anstream::println!("would switch to {}", style::branch(&target));
232 position = target;
233 } else {
234 git::checkout(&target)?;
235 position = target;
236 }
237 }
238
239 for branch in &finished {
241 let landing = if closed.contains(branch) {
242 Landing::Closed
243 } else {
244 Landing::Merged
245 };
246 cleanup_finished_branch(review_provider.as_ref(), branch, landing, dry_run)?;
247 cleanup_branch_deletion(branch, &position, landing, dry_run, true)?;
248 }
249
250 if dry_run {
252 anstream::println!("would restack the remaining stack");
253 } else if !survivors.is_empty() {
254 stack::restack(
256 FetchMode::Disabled,
257 UpdateRefsMode::Config,
258 push_mode,
259 false,
260 )?;
261 }
262
263 match survivors.first() {
265 Some(bottom) => match review_provider.review_for_branch(bottom)? {
266 Some(review) => anstream::println!(
267 "next up: {} -> {} {}",
268 style::branch(bottom),
269 review.id,
270 style::dim(&review.url)
271 ),
272 None => anstream::println!(
273 "next up: {} {}",
274 style::branch(bottom),
275 style::dim("(no review yet)")
276 ),
277 },
278 None => {
279 let base = trunk.unwrap_or(root);
280 let ending = if closed.is_empty() {
283 format!("stack complete: everything merged into {base}")
284 } else {
285 format!("stack complete: nothing left above {base} - merged or closed")
286 };
287 anstream::println!("{}", style::success(&ending));
288 }
289 }
290
291 Ok(())
292}