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