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