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