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