1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fs,
4 path::PathBuf,
5};
6
7use anyhow::{Context, Result, bail};
8
9use crate::cli::{PushMode, UpdateRefsMode};
10use crate::git;
11use crate::settings;
12
13const PARENT_KEY: &str = "stkParent";
14const BASE_KEY: &str = "stkBase";
15const STATE_FILE: &str = "stack-state";
16
17pub fn create_branch(branch: &str) -> Result<()> {
18 let parent = git::current_branch()?;
19 git::create_branch(branch)?;
20 set_parent(branch, &parent)?;
21 record_base(branch, &parent);
22 println!("created {branch} with parent {parent}");
23 Ok(())
24}
25
26pub fn print_parent(branch: Option<&str>) -> Result<()> {
27 let branch = branch
28 .map(str::to_owned)
29 .map_or_else(git::current_branch, Ok)?;
30 match parent_of(&branch)? {
31 Some(parent) => println!("{parent}"),
32 None => bail!("{branch} has no stack parent"),
33 }
34 Ok(())
35}
36
37pub fn print_children(branch: Option<&str>) -> Result<()> {
38 let branch = branch
39 .map(str::to_owned)
40 .map_or_else(git::current_branch, Ok)?;
41 for child in children_of(&branch)? {
42 println!("{child}");
43 }
44 Ok(())
45}
46
47pub fn checkout_parent() -> Result<()> {
48 let current = git::current_branch()?;
49 let Some(parent) = parent_of(¤t)? else {
50 bail!("{current} has no stack parent");
51 };
52
53 git::checkout(&parent)
54}
55
56pub fn checkout_child(branch: Option<&str>) -> Result<()> {
57 let current = git::current_branch()?;
58 let children = children_of(¤t)?;
59 let child = match (branch, children.as_slice()) {
60 (Some(branch), _) => {
61 if children.iter().any(|child| child == branch) {
62 branch.to_owned()
63 } else {
64 bail!("{branch} is not a stack child of {current}");
65 }
66 }
67 (None, [child]) => child.to_owned(),
68 (None, []) => bail!("{current} has no stack children"),
69 (None, _) => {
70 eprintln!("{current} has multiple stack children:");
71 for child in children {
72 eprintln!(" {child}");
73 }
74 bail!("choose one with `git stk up <branch>`");
75 }
76 };
77
78 git::checkout(&child)
79}
80
81pub fn print_stack() -> Result<()> {
82 let current = git::current_branch()?;
83 let parents = parent_map()?;
84 let root = root_for(¤t, &parents);
85 let children = children_map(&parents);
86 let trunk = trunk_branch(&git::local_branches()?);
87
88 let mut lines = Vec::new();
89 collect_tree_lines(
90 &root,
91 ¤t,
92 trunk.as_deref(),
93 &children,
94 0,
95 &mut BTreeSet::new(),
96 &mut lines,
97 );
98
99 for line in lines.iter().rev() {
102 println!("{line}");
103 }
104 Ok(())
105}
106
107pub fn trunk_branch(branches: &[String]) -> Option<String> {
110 let remote = settings::remote().unwrap_or_else(|_| settings::DEFAULT_REMOTE.to_owned());
111 if let Some(default) = git::remote_default_branch(&remote) {
112 return Some(default);
113 }
114
115 ["main", "master"]
116 .iter()
117 .find(|name| branches.iter().any(|branch| branch == *name))
118 .map(|name| (*name).to_owned())
119}
120
121pub fn adopt_branch(branch: &str, parent: &str) -> Result<()> {
122 if branch == parent {
123 bail!("a branch cannot be its own stack parent");
124 }
125
126 let branches: BTreeSet<_> = git::local_branches()?.into_iter().collect();
127 if !branches.contains(branch) {
128 bail!("branch {branch} does not exist");
129 }
130 if !branches.contains(parent) {
131 bail!("parent branch {parent} does not exist");
132 }
133
134 set_parent(branch, parent)?;
135 record_base(branch, parent);
136 println!("attached {branch} to {parent}");
137 Ok(())
138}
139
140pub fn detach_branch(branch: Option<&str>) -> Result<()> {
141 let branch = branch
142 .map(str::to_owned)
143 .map_or_else(git::current_branch, Ok)?;
144 unset_parent(&branch)?;
145 unset_base(&branch)?;
146 println!("detached {branch}");
147 Ok(())
148}
149
150pub fn restack(update_refs_mode: UpdateRefsMode, push_mode: PushMode) -> Result<()> {
151 let current = git::current_branch()?;
152 let parents = parent_map()?;
153 let root = root_for(¤t, &parents);
156 let branches = restack_order(&root, &parents);
157
158 if branches.is_empty() {
159 println!("nothing to restack");
160 return Ok(());
161 }
162
163 let update_refs = resolve_update_refs(update_refs_mode)?;
164 let push = settings::push_enabled(push_mode, settings::PUSH_ON_RESTACK_KEY)?;
165
166 clear_state()?;
167 let all = branches.clone();
168 restack_branches(branches, &parents, update_refs, push, &all)
169}
170
171pub fn continue_restack() -> Result<()> {
172 let Some(state) = RestackState::read()? else {
173 bail!("no interrupted restack found");
174 };
175
176 if let Err(error) = git::rebase_continue() {
177 eprintln!("restack still has conflicts");
178 eprintln!("resolve conflicts, then run `git stk continue`");
179 eprintln!("or run `git stk abort`");
180 return Err(error);
181 }
182
183 record_base(&state.branch, &state.parent);
184
185 if state.remaining.is_empty() {
186 clear_state()?;
187 finish_restack(&state.all, state.push)?;
188 return Ok(());
189 }
190
191 let parents = parent_map()?;
192 restack_branches(
193 state.remaining,
194 &parents,
195 state.update_refs,
196 state.push,
197 &state.all,
198 )
199}
200
201pub fn abort_restack() -> Result<()> {
202 git::rebase_abort()?;
203 clear_state()?;
204 println!("restack aborted");
205 Ok(())
206}
207
208pub fn parent_for_branch(branch: &str) -> Result<Option<String>> {
209 parent_of(branch)
210}
211
212pub fn children_for_branch(branch: &str) -> Result<Vec<String>> {
213 children_of(branch)
214}
215
216pub fn set_parent_for_branch(branch: &str, parent: &str) -> Result<()> {
217 set_parent(branch, parent)
218}
219
220pub fn unset_parent_for_branch(branch: &str) -> Result<()> {
221 unset_parent(branch)
222}
223
224pub fn base_for_branch(branch: &str) -> Result<Option<String>> {
225 base_of(branch)
226}
227
228pub fn set_base_for_branch(branch: &str, base: &str) -> Result<()> {
229 git::config_set(&base_key(branch), base)
230}
231
232pub fn unset_base_for_branch(branch: &str) -> Result<()> {
233 unset_base(branch)
234}
235
236pub fn record_base(branch: &str, parent: &str) {
239 if let Ok(base) = git::merge_base(parent, branch) {
240 let _ = git::config_set(&base_key(branch), &base);
241 }
242}
243
244pub fn stack_root(branch: &str) -> Result<String> {
246 let parents = parent_map()?;
247 Ok(root_for(branch, &parents))
248}
249
250pub fn branch_and_descendants(branch: &str) -> Result<Vec<String>> {
251 let parents = parent_map()?;
252 let children = children_map(&parents);
253 let mut branches = vec![branch.to_owned()];
254 collect_descendants(branch, &children, &mut branches);
255 Ok(branches)
256}
257
258fn parent_map() -> Result<BTreeMap<String, String>> {
259 let mut parents = BTreeMap::new();
260 for branch in git::local_branches()? {
261 if let Some(parent) = parent_of(&branch)? {
262 parents.insert(branch, parent);
263 }
264 }
265 Ok(parents)
266}
267
268fn restack_order(current: &str, parents: &BTreeMap<String, String>) -> Vec<String> {
269 let children = children_map(parents);
270 let mut branches = Vec::new();
271
272 if parents.contains_key(current) {
273 branches.push(current.to_owned());
274 }
275
276 collect_descendants(current, &children, &mut branches);
277 branches
278}
279
280fn collect_descendants(
281 branch: &str,
282 children: &BTreeMap<String, Vec<String>>,
283 branches: &mut Vec<String>,
284) {
285 if let Some(branch_children) = children.get(branch) {
286 for child in branch_children {
287 branches.push(child.to_owned());
288 collect_descendants(child, children, branches);
289 }
290 }
291}
292
293fn restack_branches(
294 branches: Vec<String>,
295 parents: &BTreeMap<String, String>,
296 update_refs: bool,
297 push: bool,
298 all: &[String],
299) -> Result<()> {
300 for (index, branch) in branches.iter().enumerate() {
301 let Some(parent) = parents.get(branch) else {
302 bail!("{branch} has no stack parent");
303 };
304
305 let base = match base_of(branch)? {
310 Some(base) if git::is_ancestor(&base, branch).unwrap_or(false) => Some(base),
311 _ => None,
312 };
313
314 let parent_tip = git::rev_parse(parent)?;
318 if base.as_deref() == Some(parent_tip.as_str())
319 && git::is_ancestor(parent, branch).unwrap_or(false)
320 {
321 println!("{branch} already up to date with {parent}");
322 continue;
323 }
324
325 if update_refs {
326 println!("rebasing {branch} onto {parent} with --update-refs");
327 } else {
328 println!("rebasing {branch} onto {parent}");
329 }
330 let rebase_result = match &base {
331 Some(base) => git::rebase_onto(parent, base, branch, update_refs),
332 None => git::rebase(parent, branch, update_refs),
333 };
334
335 if let Err(error) = rebase_result {
336 let remaining = branches[index + 1..].to_vec();
337 RestackState {
338 branch: branch.to_owned(),
339 parent: parent.to_owned(),
340 remaining,
341 update_refs,
342 push,
343 all: all.to_vec(),
344 }
345 .write()?;
346
347 eprintln!("conflict while rebasing {branch} onto {parent}");
348 eprintln!("resolve conflicts, then run `git stk continue`");
349 eprintln!("or run `git stk abort`");
350 return Err(error);
351 }
352
353 record_base(branch, parent);
354 }
355
356 clear_state()?;
357 finish_restack(all, push)
358}
359
360fn finish_restack(branches: &[String], push: bool) -> Result<()> {
363 println!("restack complete");
364
365 let remote = settings::remote()?;
366 if push {
367 git::push_force_with_lease(&remote, branches)?;
368 println!("pushed {} to {remote}", branches.join(" "));
369 } else {
370 println!("remote branches may be stale; push them with:");
371 println!(
372 " git push --force-with-lease {remote} {}",
373 branches.join(" ")
374 );
375 }
376 Ok(())
377}
378
379fn resolve_update_refs(mode: UpdateRefsMode) -> Result<bool> {
380 match mode {
381 UpdateRefsMode::Config => {
382 let configured = git::config_get_bool(settings::UPDATE_REFS_KEY)?.unwrap_or(false);
383 if configured && !git::supports_rebase_update_refs()? {
384 eprintln!("stk.updateRefs is true, but this Git does not support --update-refs");
385 return Ok(false);
386 }
387 Ok(configured)
388 }
389 UpdateRefsMode::Enabled => {
390 if !git::supports_rebase_update_refs()? {
391 bail!("--update-refs was requested, but this Git does not support it");
392 }
393 Ok(true)
394 }
395 UpdateRefsMode::Disabled => Ok(false),
396 }
397}
398
399fn children_of(parent: &str) -> Result<Vec<String>> {
400 Ok(parent_map()?
401 .into_iter()
402 .filter_map(|(branch, branch_parent)| (branch_parent == parent).then_some(branch))
403 .collect())
404}
405
406fn children_map(parents: &BTreeMap<String, String>) -> BTreeMap<String, Vec<String>> {
407 let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
408 for (branch, parent) in parents {
409 children
410 .entry(parent.to_owned())
411 .or_default()
412 .push(branch.to_owned());
413 }
414 children
415}
416
417fn root_for(branch: &str, parents: &BTreeMap<String, String>) -> String {
418 let mut root = branch.to_owned();
419 let mut seen = BTreeSet::new();
420
421 while let Some(parent) = parents.get(&root) {
422 if !seen.insert(root.clone()) {
423 break;
424 }
425 root = parent.to_owned();
426 }
427
428 root
429}
430
431#[allow(clippy::too_many_arguments)]
432fn collect_tree_lines(
433 branch: &str,
434 current: &str,
435 trunk: Option<&str>,
436 children: &BTreeMap<String, Vec<String>>,
437 depth: usize,
438 seen: &mut BTreeSet<String>,
439 lines: &mut Vec<String>,
440) {
441 let mut line = format!("{}{}", " ".repeat(depth), branch);
442 if Some(branch) == trunk {
443 line.push_str(" (trunk)");
444 }
445 if branch == current {
446 line.push_str(" *");
447 }
448 lines.push(line);
449
450 if !seen.insert(branch.to_owned()) {
451 lines.push(format!("{}<cycle detected>", " ".repeat(depth + 1)));
452 return;
453 }
454
455 if let Some(branch_children) = children.get(branch) {
456 for child in branch_children {
457 collect_tree_lines(child, current, trunk, children, depth + 1, seen, lines);
458 }
459 }
460}
461
462fn parent_of(branch: &str) -> Result<Option<String>> {
463 git::config_get(&parent_key(branch))
464}
465
466fn base_of(branch: &str) -> Result<Option<String>> {
467 git::config_get(&base_key(branch))
468}
469
470fn set_parent(branch: &str, parent: &str) -> Result<()> {
471 git::config_set(&parent_key(branch), parent)
472}
473
474fn unset_parent(branch: &str) -> Result<()> {
475 git::config_unset(&parent_key(branch))
476}
477
478fn unset_base(branch: &str) -> Result<()> {
479 git::config_unset(&base_key(branch))
480}
481
482fn parent_key(branch: &str) -> String {
483 format!("branch.{branch}.{PARENT_KEY}")
484}
485
486fn base_key(branch: &str) -> String {
487 format!("branch.{branch}.{BASE_KEY}")
488}
489
490#[derive(Debug, Eq, PartialEq)]
491struct RestackState {
492 branch: String,
493 parent: String,
494 remaining: Vec<String>,
495 update_refs: bool,
496 push: bool,
497 all: Vec<String>,
500}
501
502impl RestackState {
503 fn read() -> Result<Option<Self>> {
504 let path = state_path()?;
505 if !path.exists() {
506 return Ok(None);
507 }
508
509 let contents = fs::read_to_string(&path)
510 .with_context(|| format!("failed to read {}", path.display()))?;
511 let mut branch = None;
512 let mut parent = None;
513 let mut remaining = Vec::new();
514 let mut update_refs = false;
515 let mut push = false;
516 let mut all = Vec::new();
517
518 for line in contents.lines() {
519 if let Some(value) = line.strip_prefix("branch=") {
520 branch = Some(value.to_owned());
521 } else if let Some(value) = line.strip_prefix("parent=") {
522 parent = Some(value.to_owned());
523 } else if let Some(value) = line.strip_prefix("updateRefs=") {
524 update_refs = value == "true";
525 } else if let Some(value) = line.strip_prefix("push=") {
526 push = value == "true";
527 } else if let Some(value) = line.strip_prefix("remaining=") {
528 remaining = value
529 .split('\t')
530 .filter(|branch| !branch.is_empty())
531 .map(str::to_owned)
532 .collect();
533 } else if let Some(value) = line.strip_prefix("all=") {
534 all = value
535 .split('\t')
536 .filter(|branch| !branch.is_empty())
537 .map(str::to_owned)
538 .collect();
539 }
540 }
541
542 let Some(branch) = branch else {
543 bail!("restack state is missing current branch");
544 };
545 let Some(parent) = parent else {
546 bail!("restack state is missing parent branch");
547 };
548
549 Ok(Some(Self {
550 branch,
551 parent,
552 remaining,
553 update_refs,
554 push,
555 all,
556 }))
557 }
558
559 fn write(&self) -> Result<()> {
560 let path = state_path()?;
561 let contents = format!(
562 "branch={}\nparent={}\nupdateRefs={}\npush={}\nremaining={}\nall={}\n",
563 self.branch,
564 self.parent,
565 self.update_refs,
566 self.push,
567 self.remaining.join("\t"),
568 self.all.join("\t")
569 );
570 fs::write(&path, contents).with_context(|| format!("failed to write {}", path.display()))
571 }
572}
573
574fn clear_state() -> Result<()> {
575 let path = state_path()?;
576 if path.exists() {
577 fs::remove_file(&path).with_context(|| format!("failed to remove {}", path.display()))?;
578 }
579 Ok(())
580}
581
582fn state_path() -> Result<PathBuf> {
583 Ok(PathBuf::from(git::git_path(STATE_FILE)?))
584}