Skip to main content

git_stk/stack/
mod.rs

1//! Stack metadata: the `branch.<name>.stkParent`/`stkBase` annotations and
2//! the structural queries built on them. Navigation lives in [`nav`], the
3//! rebase engine in [`restack`].
4
5use std::collections::{BTreeMap, BTreeSet};
6
7use anyhow::{Context, Result, bail};
8
9use crate::git;
10use crate::settings;
11use crate::style;
12
13mod nav;
14mod restack;
15mod snapshot;
16
17pub use nav::{
18    behind_parent_hint, checkout_bottom, checkout_child, checkout_parent, checkout_top,
19    print_all_stacks, print_children, print_parent, print_stack,
20};
21pub use restack::{abort_restack, continue_restack, restack};
22pub use snapshot::{take as snapshot, undo};
23
24const PARENT_KEY: &str = "stkParent";
25const BASE_KEY: &str = "stkBase";
26/// Marks a branch as the rename of another that still has an open review, so
27/// the next submit can replace and close that review.
28const RENAMED_FROM_KEY: &str = "stkRenamedFrom";
29
30pub fn create_branch(branch: &str) -> Result<()> {
31    let parent = git::current_branch()?;
32    // `new` creates the branch; an existing one is an adopt, not a create.
33    if git::local_branches()?
34        .iter()
35        .any(|existing| existing == branch)
36    {
37        bail!(
38            "branch {branch} already exists - adopt it onto {parent} \
39             with `git stk adopt {branch} --parent {parent}`"
40        );
41    }
42    git::create_branch(branch)?;
43    set_parent(branch, &parent)?;
44    record_base(branch, &parent);
45    anstream::println!(
46        "created {} with parent {}",
47        style::branch(branch),
48        style::branch(&parent)
49    );
50    Ok(())
51}
52
53/// Insert a new empty branch directly above the current one, moving the
54/// current branch's children onto it. The new branch shares the current tip,
55/// so descendants stay correctly based; commit to it, then `restack` to
56/// replay them. Any uncommitted changes ride onto the new branch, like `new`.
57pub fn insert_branch(branch: &str) -> Result<()> {
58    ensure_absent(branch)?;
59    let current = git::current_branch()?;
60    let children = children_of(&current)?;
61
62    snapshot::take("new --insert");
63    git::create_branch(branch)?; // off current; leaves us on the new branch
64    set_parent(branch, &current)?;
65    record_base(branch, &current);
66    for child in &children {
67        set_parent(child, branch)?;
68        record_base(child, branch);
69    }
70
71    anstream::println!(
72        "inserted {} above {}",
73        style::branch(branch),
74        style::branch(&current)
75    );
76    for child in &children {
77        anstream::println!(
78            "retargeted {} -> {}",
79            style::branch(child),
80            style::branch(branch)
81        );
82    }
83    Ok(())
84}
85
86/// Insert a new empty branch directly below the current one, moving the
87/// current branch onto it. Branches from the current branch's parent, so it
88/// requires a clean worktree. Commit to it, then `restack`.
89pub fn prepend_branch(branch: &str) -> Result<()> {
90    ensure_absent(branch)?;
91    let current = git::current_branch()?;
92    let parent =
93        parent_of(&current)?.context("current branch has no stack parent to prepend below")?;
94    if !git::worktree_is_clean()? {
95        bail!(
96            "working tree has uncommitted changes; commit or stash before `git stk new --prepend`"
97        );
98    }
99
100    snapshot::take("new --prepend");
101    git::checkout(&parent)?;
102    git::create_branch(branch)?; // off the parent; leaves us on the new branch
103    set_parent(branch, &parent)?;
104    record_base(branch, &parent);
105    set_parent(&current, branch)?;
106    record_base(&current, branch);
107
108    anstream::println!(
109        "inserted {} between {} and {}",
110        style::branch(branch),
111        style::branch(&parent),
112        style::branch(&current)
113    );
114    anstream::println!(
115        "retargeted {} -> {}",
116        style::branch(&current),
117        style::branch(branch)
118    );
119    Ok(())
120}
121
122fn ensure_absent(branch: &str) -> Result<()> {
123    if git::local_branches()?
124        .iter()
125        .any(|existing| existing == branch)
126    {
127        bail!("branch {branch} already exists");
128    }
129    Ok(())
130}
131
132/// The trunk branch: the remote's default branch when known locally,
133/// otherwise a conventional name that exists.
134pub fn trunk_branch(branches: &[String]) -> Option<String> {
135    let remote = settings::remote().unwrap_or_else(|_| settings::DEFAULT_REMOTE.to_owned());
136    if let Some(default) = git::remote_default_branch(&remote) {
137        return Some(default);
138    }
139
140    ["main", "master"]
141        .iter()
142        .find(|name| branches.iter().any(|branch| branch == *name))
143        .map(|name| (*name).to_owned())
144}
145
146pub fn adopt_branch(branch: &str, parent: &str) -> Result<()> {
147    if branch == parent {
148        bail!("a branch cannot be its own stack parent");
149    }
150
151    let branches: BTreeSet<_> = git::local_branches()?.into_iter().collect();
152    if !branches.contains(branch) {
153        bail!("branch {branch} does not exist");
154    }
155    if !branches.contains(parent) {
156        bail!("parent branch {parent} does not exist");
157    }
158
159    set_parent(branch, parent)?;
160    record_base(branch, parent);
161    anstream::println!(
162        "attached {} to {}",
163        style::branch(branch),
164        style::branch(parent)
165    );
166    Ok(())
167}
168
169pub fn detach_branch(branch: Option<&str>) -> Result<()> {
170    let branch = branch
171        .map(str::to_owned)
172        .map_or_else(git::current_branch, Ok)?;
173    unset_parent(&branch)?;
174    unset_base(&branch)?;
175    anstream::println!("detached {}", style::branch(&branch));
176    Ok(())
177}
178
179/// Rename a branch and keep the stack intact. Git moves the branch's own
180/// metadata with the rename; children pointing at the old name are
181/// retargeted here.
182pub fn rename_branch(old: &str, new: &str, dry_run: bool) -> Result<()> {
183    let children = children_for_branch(old)?;
184
185    if !dry_run {
186        snapshot::take("rename");
187        git::rename_branch(old, new)?;
188    }
189    anstream::println!(
190        "{} {} -> {}",
191        if dry_run { "would rename" } else { "renamed" },
192        style::branch(old),
193        style::branch(new)
194    );
195
196    for child in &children {
197        if !dry_run {
198            set_parent_for_branch(child, new)?;
199        }
200        anstream::println!(
201            "{} {} -> {}",
202            if dry_run {
203                "would retarget"
204            } else {
205                "retargeted"
206            },
207            style::branch(child),
208            style::branch(new)
209        );
210    }
211    Ok(())
212}
213
214pub fn parent_for_branch(branch: &str) -> Result<Option<String>> {
215    parent_of(branch)
216}
217
218pub fn children_for_branch(branch: &str) -> Result<Vec<String>> {
219    children_of(branch)
220}
221
222pub fn set_parent_for_branch(branch: &str, parent: &str) -> Result<()> {
223    set_parent(branch, parent)
224}
225
226pub fn unset_parent_for_branch(branch: &str) -> Result<()> {
227    unset_parent(branch)
228}
229
230pub fn base_for_branch(branch: &str) -> Result<Option<String>> {
231    base_of(branch)
232}
233
234pub fn set_base_for_branch(branch: &str, base: &str) -> Result<()> {
235    git::config_set(&base_key(branch), base)
236}
237
238pub fn unset_base_for_branch(branch: &str) -> Result<()> {
239    unset_base(branch)
240}
241
242/// Record that `branch` is the rename of `old`, whose open review the next
243/// submit should replace and close.
244pub fn set_renamed_from(branch: &str, old: &str) -> Result<()> {
245    git::config_set(&renamed_from_key(branch), old)
246}
247
248/// The branch `branch` was renamed from, if a replaced review is still pending.
249pub fn renamed_from(branch: &str) -> Result<Option<String>> {
250    git::config_get(&renamed_from_key(branch))
251}
252
253/// Drop the rename marker once its review has been handled.
254pub fn clear_renamed_from(branch: &str) -> Result<()> {
255    git::config_unset(&renamed_from_key(branch))
256}
257
258/// Record the fork point between a branch and its parent (best effort; e.g.
259/// unrelated histories have no merge base, which is not an error here).
260pub fn record_base(branch: &str, parent: &str) {
261    if let Ok(base) = git::merge_base(parent, branch) {
262        let _ = git::config_set(&base_key(branch), &base);
263    }
264}
265
266/// The root of the stack containing `branch` (the base everything sits on).
267pub fn stack_root(branch: &str) -> Result<String> {
268    let parents = parent_map()?;
269    Ok(root_for(branch, &parents))
270}
271
272pub fn branch_and_descendants(branch: &str) -> Result<Vec<String>> {
273    let parents = parent_map()?;
274    let children = children_map(&parents);
275    let mut branches = vec![branch.to_owned()];
276    collect_descendants(branch, &children, &mut branches);
277    Ok(branches)
278}
279
280/// Every branch in the stack containing `branch`, parent-first: the line from
281/// the stack bottom up through `branch`, plus everything above it. Sibling
282/// stacks that share only the trunk are left out - they branch off the trunk
283/// separately, not through `branch`. The trunk itself is excluded; an
284/// unanchored root stays in (`path_from_root` keeps it).
285pub fn stack_line(branch: &str) -> Result<Vec<String>> {
286    let mut line = path_from_root(branch)?; // [bottom ..= branch]
287    let above = branch_and_descendants(branch)?; // [branch, ..descendants]
288    line.extend(above.into_iter().skip(1)); // append above-branch, dropping the duplicate
289
290    // `path_from_root` keeps its starting branch even when that is the trunk
291    // (you are standing on it); a trunk is never part of a stack.
292    let trunk = trunk_branch(&git::local_branches()?);
293    line.retain(|candidate| Some(candidate) != trunk.as_ref());
294    Ok(line)
295}
296
297/// The stack path from the bottom up to (and including) `branch`,
298/// parent-first; descendants above it are left out.
299pub fn path_from_root(branch: &str) -> Result<Vec<String>> {
300    let trunk = trunk_branch(&git::local_branches()?);
301    let mut path = vec![branch.to_owned()];
302    let mut seen = BTreeSet::from([branch.to_owned()]);
303
304    let mut cursor = branch.to_owned();
305    while let Some(parent) = parent_of(&cursor)? {
306        if Some(&parent) == trunk.as_ref() || !seen.insert(parent.clone()) {
307            break;
308        }
309        path.push(parent.clone());
310        cursor = parent;
311    }
312
313    path.reverse();
314    Ok(path)
315}
316
317/// (branch, parent) pairs for the branches that have a stack parent;
318/// branches without one are skipped.
319pub fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
320    let mut pairs = Vec::new();
321    for branch in branches {
322        if let Some(parent) = parent_of(branch)? {
323            pairs.push((branch.clone(), parent));
324        }
325    }
326    Ok(pairs)
327}
328
329fn parent_map() -> Result<BTreeMap<String, String>> {
330    let mut parents = BTreeMap::new();
331    for branch in git::local_branches()? {
332        if let Some(parent) = parent_of(&branch)? {
333            parents.insert(branch, parent);
334        }
335    }
336    Ok(parents)
337}
338
339fn collect_descendants(
340    branch: &str,
341    children: &BTreeMap<String, Vec<String>>,
342    branches: &mut Vec<String>,
343) {
344    if let Some(branch_children) = children.get(branch) {
345        for child in branch_children {
346            branches.push(child.to_owned());
347            collect_descendants(child, children, branches);
348        }
349    }
350}
351
352fn children_of(parent: &str) -> Result<Vec<String>> {
353    Ok(parent_map()?
354        .into_iter()
355        .filter_map(|(branch, branch_parent)| (branch_parent == parent).then_some(branch))
356        .collect())
357}
358
359fn children_map(parents: &BTreeMap<String, String>) -> BTreeMap<String, Vec<String>> {
360    let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
361    for (branch, parent) in parents {
362        children
363            .entry(parent.to_owned())
364            .or_default()
365            .push(branch.to_owned());
366    }
367    children
368}
369
370fn root_for(branch: &str, parents: &BTreeMap<String, String>) -> String {
371    let mut root = branch.to_owned();
372    let mut seen = BTreeSet::new();
373
374    while let Some(parent) = parents.get(&root) {
375        if !seen.insert(root.clone()) {
376            break;
377        }
378        root = parent.to_owned();
379    }
380
381    root
382}
383
384fn parent_of(branch: &str) -> Result<Option<String>> {
385    git::config_get(&parent_key(branch))
386}
387
388fn base_of(branch: &str) -> Result<Option<String>> {
389    git::config_get(&base_key(branch))
390}
391
392fn set_parent(branch: &str, parent: &str) -> Result<()> {
393    git::config_set(&parent_key(branch), parent)
394}
395
396fn unset_parent(branch: &str) -> Result<()> {
397    git::config_unset(&parent_key(branch))
398}
399
400fn unset_base(branch: &str) -> Result<()> {
401    git::config_unset(&base_key(branch))
402}
403
404fn parent_key(branch: &str) -> String {
405    format!("branch.{branch}.{PARENT_KEY}")
406}
407
408fn base_key(branch: &str) -> String {
409    format!("branch.{branch}.{BASE_KEY}")
410}
411
412fn renamed_from_key(branch: &str) -> String {
413    format!("branch.{branch}.{RENAMED_FROM_KEY}")
414}