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