1use std::{
5 collections::{BTreeMap, BTreeSet},
6 fs,
7 path::PathBuf,
8};
9
10use anyhow::{Context, Result, bail};
11
12use super::{base_of, children_map, collect_descendants, parent_map, record_base, root_for};
13use crate::cli::{PushMode, UpdateRefsMode};
14use crate::git;
15use crate::settings;
16use crate::style;
17
18const STATE_FILE: &str = "stack-state";
19
20pub fn restack(update_refs_mode: UpdateRefsMode, push_mode: PushMode, dry_run: bool) -> Result<()> {
21 let current = git::current_branch()?;
22 let parents = parent_map()?;
23 let root = root_for(¤t, &parents);
26 let branches = restack_order(&root, &parents);
27
28 if branches.is_empty() {
29 anstream::println!("{}", style::dim("nothing to restack"));
30 return Ok(());
31 }
32
33 let update_refs = resolve_update_refs(update_refs_mode)?;
34 let push = settings::push_enabled(push_mode, settings::PUSH_ON_RESTACK_KEY)?;
35
36 if dry_run {
37 return print_restack_plan(&branches, &parents, update_refs, push);
38 }
39
40 super::snapshot("restack");
41 clear_state()?;
42 let all = branches.clone();
43 restack_branches(branches, &parents, update_refs, push, &all)
44}
45
46fn print_restack_plan(
49 branches: &[String],
50 parents: &BTreeMap<String, String>,
51 update_refs: bool,
52 push: bool,
53) -> Result<()> {
54 for branch in branches {
55 let Some(parent) = parents.get(branch) else {
56 bail!("{branch} has no stack parent");
57 };
58
59 if up_to_date(branch, parent)? {
60 anstream::println!(
61 "{} already up to date with {}",
62 style::branch(branch),
63 style::branch(parent)
64 );
65 } else {
66 anstream::println!(
67 "would rebase {} onto {}{}",
68 style::branch(branch),
69 style::branch(parent),
70 if update_refs {
71 " with --update-refs"
72 } else {
73 ""
74 }
75 );
76 }
77 }
78
79 if push {
80 anstream::println!(
81 "would push {} to {}",
82 style::branch(&branches.join(" ")),
83 settings::remote()?
84 );
85 }
86 Ok(())
87}
88
89fn valid_base(branch: &str) -> Result<Option<String>> {
91 Ok(match base_of(branch)? {
92 Some(base) if git::is_ancestor(&base, branch).unwrap_or(false) => Some(base),
93 _ => None,
94 })
95}
96
97fn up_to_date(branch: &str, parent: &str) -> Result<bool> {
99 let parent_tip = git::rev_parse(parent)?;
100 Ok(valid_base(branch)?.as_deref() == Some(parent_tip.as_str())
101 && git::is_ancestor(parent, branch).unwrap_or(false))
102}
103
104pub fn continue_restack() -> Result<()> {
105 let Some(state) = RestackState::read()? else {
106 bail!("no interrupted restack found");
107 };
108
109 if let Err(error) = git::rebase_continue() {
110 anstream::eprintln!("{}", style::warn("restack still has conflicts"));
111 eprintln!("resolve conflicts, then run `git stk continue`");
112 eprintln!("or run `git stk abort`");
113 return Err(error);
114 }
115
116 record_base(&state.branch, &state.parent);
117
118 if state.remaining.is_empty() {
119 clear_state()?;
120 finish_restack(&state.all, state.push)?;
121 return Ok(());
122 }
123
124 let parents = parent_map()?;
125 restack_branches(
126 state.remaining,
127 &parents,
128 state.update_refs,
129 state.push,
130 &state.all,
131 )
132}
133
134pub fn abort_restack() -> Result<()> {
135 git::rebase_abort()?;
136 clear_state()?;
137 println!("restack aborted");
138 Ok(())
139}
140
141fn restack_order(current: &str, parents: &BTreeMap<String, String>) -> Vec<String> {
142 let children = children_map(parents);
143 let mut branches = Vec::new();
144
145 if parents.contains_key(current) {
146 branches.push(current.to_owned());
147 }
148
149 let mut visited = BTreeSet::from([current.to_owned()]);
150 collect_descendants(current, &children, &mut branches, &mut visited);
151 branches
152}
153
154fn restack_branches(
155 branches: Vec<String>,
156 parents: &BTreeMap<String, String>,
157 update_refs: bool,
158 push: bool,
159 all: &[String],
160) -> Result<()> {
161 for (index, branch) in branches.iter().enumerate() {
162 let Some(parent) = parents.get(branch) else {
163 bail!("{branch} has no stack parent");
164 };
165
166 let base = valid_base(branch)?;
171
172 if up_to_date(branch, parent)? {
176 anstream::println!(
177 "{} already up to date with {}",
178 style::branch(branch),
179 style::branch(parent)
180 );
181 continue;
182 }
183
184 if update_refs {
185 anstream::println!(
186 "rebasing {} onto {} with --update-refs",
187 style::branch(branch),
188 style::branch(parent)
189 );
190 } else {
191 anstream::println!(
192 "rebasing {} onto {}",
193 style::branch(branch),
194 style::branch(parent)
195 );
196 }
197 let rebase_result = match &base {
198 Some(base) => git::rebase_onto(parent, base, branch, update_refs),
199 None => git::rebase(parent, branch, update_refs),
200 };
201
202 if let Err(error) = rebase_result {
203 let remaining = branches[index + 1..].to_vec();
204 RestackState {
205 branch: branch.to_owned(),
206 parent: parent.to_owned(),
207 remaining,
208 update_refs,
209 push,
210 all: all.to_vec(),
211 }
212 .write()?;
213
214 anstream::eprintln!(
215 "{}",
216 style::warn(&format!("conflict while rebasing {branch} onto {parent}"))
217 );
218 eprintln!("resolve conflicts, then run `git stk continue`");
219 eprintln!("or run `git stk abort`");
220 return Err(error);
221 }
222
223 record_base(branch, parent);
224 }
225
226 clear_state()?;
227 finish_restack(all, push)
228}
229
230fn finish_restack(branches: &[String], push: bool) -> Result<()> {
233 anstream::println!("{}", style::success("restack complete"));
234
235 let remote = settings::remote()?;
236 if push {
237 git::push_force_with_lease(&remote, branches)?;
238 anstream::println!("pushed {} to {remote}", style::branch(&branches.join(" ")));
239 super::publish_metadata(&remote);
241 } else {
242 println!("remote branches may be stale; push them with:");
243 anstream::println!(
244 "{}",
245 style::dim(&format!(
246 " git push --force-with-lease {remote} {}",
247 branches.join(" ")
248 ))
249 );
250 }
251 Ok(())
252}
253
254fn resolve_update_refs(mode: UpdateRefsMode) -> Result<bool> {
255 match mode {
256 UpdateRefsMode::Config => {
257 let configured = git::config_get_bool(settings::UPDATE_REFS_KEY)?.unwrap_or(false);
258 if configured && !git::supports_rebase_update_refs()? {
259 eprintln!("stk.updateRefs is true, but this Git does not support --update-refs");
260 return Ok(false);
261 }
262 Ok(configured)
263 }
264 UpdateRefsMode::Enabled => {
265 if !git::supports_rebase_update_refs()? {
266 bail!("--update-refs was requested, but this Git does not support it");
267 }
268 Ok(true)
269 }
270 UpdateRefsMode::Disabled => Ok(false),
271 }
272}
273
274#[derive(Debug, Eq, PartialEq)]
275struct RestackState {
276 branch: String,
277 parent: String,
278 remaining: Vec<String>,
279 update_refs: bool,
280 push: bool,
281 all: Vec<String>,
284}
285
286impl RestackState {
287 fn read() -> Result<Option<Self>> {
288 let path = state_path()?;
289 if !path.exists() {
290 return Ok(None);
291 }
292
293 let contents = fs::read_to_string(&path)
294 .with_context(|| format!("failed to read {}", path.display()))?;
295 let mut branch = None;
296 let mut parent = None;
297 let mut remaining = Vec::new();
298 let mut update_refs = false;
299 let mut push = false;
300 let mut all = Vec::new();
301
302 for line in contents.lines() {
303 if let Some(value) = line.strip_prefix("branch=") {
304 branch = Some(value.to_owned());
305 } else if let Some(value) = line.strip_prefix("parent=") {
306 parent = Some(value.to_owned());
307 } else if let Some(value) = line.strip_prefix("updateRefs=") {
308 update_refs = value == "true";
309 } else if let Some(value) = line.strip_prefix("push=") {
310 push = value == "true";
311 } else if let Some(value) = line.strip_prefix("remaining=") {
312 remaining = value
313 .split('\t')
314 .filter(|branch| !branch.is_empty())
315 .map(str::to_owned)
316 .collect();
317 } else if let Some(value) = line.strip_prefix("all=") {
318 all = value
319 .split('\t')
320 .filter(|branch| !branch.is_empty())
321 .map(str::to_owned)
322 .collect();
323 }
324 }
325
326 let Some(branch) = branch else {
327 bail!("restack state is missing current branch");
328 };
329 let Some(parent) = parent else {
330 bail!("restack state is missing parent branch");
331 };
332
333 Ok(Some(Self {
334 branch,
335 parent,
336 remaining,
337 update_refs,
338 push,
339 all,
340 }))
341 }
342
343 fn write(&self) -> Result<()> {
344 let path = state_path()?;
345 let contents = format!(
346 "branch={}\nparent={}\nupdateRefs={}\npush={}\nremaining={}\nall={}\n",
347 self.branch,
348 self.parent,
349 self.update_refs,
350 self.push,
351 self.remaining.join("\t"),
352 self.all.join("\t")
353 );
354 fs::write(&path, contents).with_context(|| format!("failed to write {}", path.display()))
355 }
356}
357
358fn clear_state() -> Result<()> {
359 let path = state_path()?;
360 if path.exists() {
361 fs::remove_file(&path).with_context(|| format!("failed to remove {}", path.display()))?;
362 }
363 Ok(())
364}
365
366fn state_path() -> Result<PathBuf> {
367 Ok(PathBuf::from(git::git_path(STATE_FILE)?))
368}
369
370pub(super) fn in_progress() -> bool {
372 state_path().map(|path| path.exists()).unwrap_or(false)
373}