1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fs,
4 path::PathBuf,
5};
6
7use anyhow::{Context, Result, bail};
8
9use crate::cli::UpdateRefsMode;
10use crate::git;
11
12const PARENT_KEY: &str = "stackParent";
13const STATE_FILE: &str = "stack-state";
14
15pub fn create_branch(branch: &str) -> Result<()> {
16 let parent = git::current_branch()?;
17 git::create_branch(branch)?;
18 set_parent(branch, &parent)?;
19 println!("created {branch} with parent {parent}");
20 Ok(())
21}
22
23pub fn print_parent(branch: Option<&str>) -> Result<()> {
24 let branch = branch
25 .map(str::to_owned)
26 .map_or_else(git::current_branch, Ok)?;
27 match parent_of(&branch)? {
28 Some(parent) => println!("{parent}"),
29 None => bail!("{branch} has no stack parent"),
30 }
31 Ok(())
32}
33
34pub fn print_children(branch: Option<&str>) -> Result<()> {
35 let branch = branch
36 .map(str::to_owned)
37 .map_or_else(git::current_branch, Ok)?;
38 for child in children_of(&branch)? {
39 println!("{child}");
40 }
41 Ok(())
42}
43
44pub fn checkout_parent() -> Result<()> {
45 let current = git::current_branch()?;
46 let Some(parent) = parent_of(¤t)? else {
47 bail!("{current} has no stack parent");
48 };
49
50 git::checkout(&parent)
51}
52
53pub fn checkout_child(branch: Option<&str>) -> Result<()> {
54 let current = git::current_branch()?;
55 let children = children_of(¤t)?;
56 let child = match (branch, children.as_slice()) {
57 (Some(branch), _) => {
58 if children.iter().any(|child| child == branch) {
59 branch.to_owned()
60 } else {
61 bail!("{branch} is not a stack child of {current}");
62 }
63 }
64 (None, [child]) => child.to_owned(),
65 (None, []) => bail!("{current} has no stack children"),
66 (None, _) => {
67 eprintln!("{current} has multiple stack children:");
68 for child in children {
69 eprintln!(" {child}");
70 }
71 bail!("choose one with `git stk down <branch>`");
72 }
73 };
74
75 git::checkout(&child)
76}
77
78pub fn print_stack() -> Result<()> {
79 let current = git::current_branch()?;
80 let parents = parent_map()?;
81 let root = root_for(¤t, &parents);
82 let children = children_map(&parents);
83 print_tree(&root, ¤t, &children, 0, &mut BTreeSet::new());
84 Ok(())
85}
86
87pub fn adopt_branch(branch: &str, parent: &str) -> Result<()> {
88 if branch == parent {
89 bail!("a branch cannot be its own stack parent");
90 }
91
92 let branches: BTreeSet<_> = git::local_branches()?.into_iter().collect();
93 if !branches.contains(branch) {
94 bail!("branch {branch} does not exist");
95 }
96 if !branches.contains(parent) {
97 bail!("parent branch {parent} does not exist");
98 }
99
100 set_parent(branch, parent)?;
101 println!("attached {branch} to {parent}");
102 Ok(())
103}
104
105pub fn detach_branch(branch: Option<&str>) -> Result<()> {
106 let branch = branch
107 .map(str::to_owned)
108 .map_or_else(git::current_branch, Ok)?;
109 unset_parent(&branch)?;
110 println!("detached {branch}");
111 Ok(())
112}
113
114pub fn restack(update_refs_mode: UpdateRefsMode) -> Result<()> {
115 let current = git::current_branch()?;
116 let parents = parent_map()?;
117 let branches = restack_order(¤t, &parents);
118
119 if branches.is_empty() {
120 println!("nothing to restack");
121 return Ok(());
122 }
123
124 let update_refs = resolve_update_refs(update_refs_mode)?;
125
126 clear_state()?;
127 restack_branches(branches, &parents, update_refs)
128}
129
130pub fn continue_restack() -> Result<()> {
131 let Some(state) = RestackState::read()? else {
132 bail!("no interrupted restack found");
133 };
134
135 if let Err(error) = git::rebase_continue() {
136 eprintln!("restack still has conflicts");
137 eprintln!("resolve conflicts, then run `git stk continue`");
138 eprintln!("or run `git stk abort`");
139 return Err(error);
140 }
141
142 if state.remaining.is_empty() {
143 clear_state()?;
144 println!("restack complete");
145 return Ok(());
146 }
147
148 let parents = parent_map()?;
149 restack_branches(state.remaining, &parents, state.update_refs)
150}
151
152pub fn abort_restack() -> Result<()> {
153 git::rebase_abort()?;
154 clear_state()?;
155 println!("restack aborted");
156 Ok(())
157}
158
159pub fn parent_for_branch(branch: &str) -> Result<Option<String>> {
160 parent_of(branch)
161}
162
163pub fn children_for_branch(branch: &str) -> Result<Vec<String>> {
164 children_of(branch)
165}
166
167pub fn set_parent_for_branch(branch: &str, parent: &str) -> Result<()> {
168 set_parent(branch, parent)
169}
170
171pub fn unset_parent_for_branch(branch: &str) -> Result<()> {
172 unset_parent(branch)
173}
174
175pub fn branch_and_descendants(branch: &str) -> Result<Vec<String>> {
176 let parents = parent_map()?;
177 let children = children_map(&parents);
178 let mut branches = vec![branch.to_owned()];
179 collect_descendants(branch, &children, &mut branches);
180 Ok(branches)
181}
182
183fn parent_map() -> Result<BTreeMap<String, String>> {
184 let mut parents = BTreeMap::new();
185 for branch in git::local_branches()? {
186 if let Some(parent) = parent_of(&branch)? {
187 parents.insert(branch, parent);
188 }
189 }
190 Ok(parents)
191}
192
193fn restack_order(current: &str, parents: &BTreeMap<String, String>) -> Vec<String> {
194 let children = children_map(parents);
195 let mut branches = Vec::new();
196
197 if parents.contains_key(current) {
198 branches.push(current.to_owned());
199 }
200
201 collect_descendants(current, &children, &mut branches);
202 branches
203}
204
205fn collect_descendants(
206 branch: &str,
207 children: &BTreeMap<String, Vec<String>>,
208 branches: &mut Vec<String>,
209) {
210 if let Some(branch_children) = children.get(branch) {
211 for child in branch_children {
212 branches.push(child.to_owned());
213 collect_descendants(child, children, branches);
214 }
215 }
216}
217
218fn restack_branches(
219 branches: Vec<String>,
220 parents: &BTreeMap<String, String>,
221 update_refs: bool,
222) -> Result<()> {
223 for (index, branch) in branches.iter().enumerate() {
224 let Some(parent) = parents.get(branch) else {
225 bail!("{branch} has no stack parent");
226 };
227
228 if update_refs {
229 println!("rebasing {branch} onto {parent} with --update-refs");
230 } else {
231 println!("rebasing {branch} onto {parent}");
232 }
233
234 if let Err(error) = git::rebase(parent, branch, update_refs) {
235 let remaining = branches[index + 1..].to_vec();
236 RestackState {
237 branch: branch.to_owned(),
238 parent: parent.to_owned(),
239 remaining,
240 update_refs,
241 }
242 .write()?;
243
244 eprintln!("conflict while rebasing {branch} onto {parent}");
245 eprintln!("resolve conflicts, then run `git stk continue`");
246 eprintln!("or run `git stk abort`");
247 return Err(error);
248 }
249 }
250
251 clear_state()?;
252 println!("restack complete");
253 Ok(())
254}
255
256fn resolve_update_refs(mode: UpdateRefsMode) -> Result<bool> {
257 match mode {
258 UpdateRefsMode::Config => {
259 let configured = git::config_get_bool("rebase.updateRefs")?.unwrap_or(false);
260 if configured && !git::supports_rebase_update_refs()? {
261 eprintln!("rebase.updateRefs is true, but this Git does not support --update-refs");
262 return Ok(false);
263 }
264 Ok(configured)
265 }
266 UpdateRefsMode::Enabled => {
267 if !git::supports_rebase_update_refs()? {
268 bail!("--update-refs was requested, but this Git does not support it");
269 }
270 Ok(true)
271 }
272 UpdateRefsMode::Disabled => Ok(false),
273 }
274}
275
276fn children_of(parent: &str) -> Result<Vec<String>> {
277 Ok(parent_map()?
278 .into_iter()
279 .filter_map(|(branch, branch_parent)| (branch_parent == parent).then_some(branch))
280 .collect())
281}
282
283fn children_map(parents: &BTreeMap<String, String>) -> BTreeMap<String, Vec<String>> {
284 let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
285 for (branch, parent) in parents {
286 children
287 .entry(parent.to_owned())
288 .or_default()
289 .push(branch.to_owned());
290 }
291 children
292}
293
294fn root_for(branch: &str, parents: &BTreeMap<String, String>) -> String {
295 let mut root = branch.to_owned();
296 let mut seen = BTreeSet::new();
297
298 while let Some(parent) = parents.get(&root) {
299 if !seen.insert(root.clone()) {
300 break;
301 }
302 root = parent.to_owned();
303 }
304
305 root
306}
307
308fn print_tree(
309 branch: &str,
310 current: &str,
311 children: &BTreeMap<String, Vec<String>>,
312 depth: usize,
313 seen: &mut BTreeSet<String>,
314) {
315 let marker = if branch == current { " *" } else { "" };
316 println!("{}{}{}", " ".repeat(depth), branch, marker);
317
318 if !seen.insert(branch.to_owned()) {
319 println!("{}<cycle detected>", " ".repeat(depth + 1));
320 return;
321 }
322
323 if let Some(branch_children) = children.get(branch) {
324 for child in branch_children {
325 print_tree(child, current, children, depth + 1, seen);
326 }
327 }
328}
329
330fn parent_of(branch: &str) -> Result<Option<String>> {
331 git::config_get(&parent_key(branch))
332}
333
334fn set_parent(branch: &str, parent: &str) -> Result<()> {
335 git::config_set(&parent_key(branch), parent)
336}
337
338fn unset_parent(branch: &str) -> Result<()> {
339 git::config_unset(&parent_key(branch))
340}
341
342fn parent_key(branch: &str) -> String {
343 format!("branch.{branch}.{PARENT_KEY}")
344}
345
346#[derive(Debug, Eq, PartialEq)]
347struct RestackState {
348 branch: String,
349 parent: String,
350 remaining: Vec<String>,
351 update_refs: bool,
352}
353
354impl RestackState {
355 fn read() -> Result<Option<Self>> {
356 let path = state_path()?;
357 if !path.exists() {
358 return Ok(None);
359 }
360
361 let contents = fs::read_to_string(&path)
362 .with_context(|| format!("failed to read {}", path.display()))?;
363 let mut branch = None;
364 let mut parent = None;
365 let mut remaining = Vec::new();
366 let mut update_refs = false;
367
368 for line in contents.lines() {
369 if let Some(value) = line.strip_prefix("branch=") {
370 branch = Some(value.to_owned());
371 } else if let Some(value) = line.strip_prefix("parent=") {
372 parent = Some(value.to_owned());
373 } else if let Some(value) = line.strip_prefix("updateRefs=") {
374 update_refs = value == "true";
375 } else if let Some(value) = line.strip_prefix("remaining=") {
376 remaining = value
377 .split('\t')
378 .filter(|branch| !branch.is_empty())
379 .map(str::to_owned)
380 .collect();
381 }
382 }
383
384 let Some(branch) = branch else {
385 bail!("restack state is missing current branch");
386 };
387 let Some(parent) = parent else {
388 bail!("restack state is missing parent branch");
389 };
390
391 Ok(Some(Self {
392 branch,
393 parent,
394 remaining,
395 update_refs,
396 }))
397 }
398
399 fn write(&self) -> Result<()> {
400 let path = state_path()?;
401 let contents = format!(
402 "branch={}\nparent={}\nupdateRefs={}\nremaining={}\n",
403 self.branch,
404 self.parent,
405 self.update_refs,
406 self.remaining.join("\t")
407 );
408 fs::write(&path, contents).with_context(|| format!("failed to write {}", path.display()))
409 }
410}
411
412fn clear_state() -> Result<()> {
413 let path = state_path()?;
414 if path.exists() {
415 fs::remove_file(&path).with_context(|| format!("failed to remove {}", path.display()))?;
416 }
417 Ok(())
418}
419
420fn state_path() -> Result<PathBuf> {
421 Ok(PathBuf::from(git::git_path(STATE_FILE)?))
422}