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
27pub fn create_branch(branch: &str) -> Result<()> {
28    let parent = git::current_branch()?;
29    // `new` creates the branch; an existing one is an adopt, not a create.
30    if git::local_branches()?
31        .iter()
32        .any(|existing| existing == branch)
33    {
34        bail!(
35            "branch {branch} already exists - adopt it onto {parent} \
36             with `git stk adopt {branch} --parent {parent}`"
37        );
38    }
39    git::create_branch(branch)?;
40    set_parent(branch, &parent)?;
41    record_base(branch, &parent);
42    anstream::println!(
43        "created {} with parent {}",
44        style::branch(branch),
45        style::branch(&parent)
46    );
47    Ok(())
48}
49
50/// Insert a new empty branch directly above the current one, moving the
51/// current branch's children onto it. The new branch shares the current tip,
52/// so descendants stay correctly based; commit to it, then `restack` to
53/// replay them. Any uncommitted changes ride onto the new branch, like `new`.
54pub fn insert_branch(branch: &str) -> Result<()> {
55    ensure_absent(branch)?;
56    let current = git::current_branch()?;
57    let children = children_of(&current)?;
58
59    snapshot::take("new --insert");
60    git::create_branch(branch)?; // off current; leaves us on the new branch
61    set_parent(branch, &current)?;
62    record_base(branch, &current);
63    for child in &children {
64        set_parent(child, branch)?;
65        record_base(child, branch);
66    }
67
68    anstream::println!(
69        "inserted {} above {}",
70        style::branch(branch),
71        style::branch(&current)
72    );
73    for child in &children {
74        anstream::println!(
75            "retargeted {} -> {}",
76            style::branch(child),
77            style::branch(branch)
78        );
79    }
80    Ok(())
81}
82
83/// Insert a new empty branch directly below the current one, moving the
84/// current branch onto it. Branches from the current branch's parent, so it
85/// requires a clean worktree. Commit to it, then `restack`.
86pub fn prepend_branch(branch: &str) -> Result<()> {
87    ensure_absent(branch)?;
88    let current = git::current_branch()?;
89    let parent =
90        parent_of(&current)?.context("current branch has no stack parent to prepend below")?;
91    if !git::worktree_is_clean()? {
92        bail!(
93            "working tree has uncommitted changes; commit or stash before `git stk new --prepend`"
94        );
95    }
96
97    snapshot::take("new --prepend");
98    git::checkout(&parent)?;
99    git::create_branch(branch)?; // off the parent; leaves us on the new branch
100    set_parent(branch, &parent)?;
101    record_base(branch, &parent);
102    set_parent(&current, branch)?;
103    record_base(&current, branch);
104
105    anstream::println!(
106        "inserted {} between {} and {}",
107        style::branch(branch),
108        style::branch(&parent),
109        style::branch(&current)
110    );
111    anstream::println!(
112        "retargeted {} -> {}",
113        style::branch(&current),
114        style::branch(branch)
115    );
116    Ok(())
117}
118
119fn ensure_absent(branch: &str) -> Result<()> {
120    if git::local_branches()?
121        .iter()
122        .any(|existing| existing == branch)
123    {
124        bail!("branch {branch} already exists");
125    }
126    Ok(())
127}
128
129/// The trunk branch: the remote's default branch when known locally,
130/// otherwise a conventional name that exists.
131pub fn trunk_branch(branches: &[String]) -> Option<String> {
132    let remote = settings::remote().unwrap_or_else(|_| settings::DEFAULT_REMOTE.to_owned());
133    if let Some(default) = git::remote_default_branch(&remote) {
134        return Some(default);
135    }
136
137    ["main", "master"]
138        .iter()
139        .find(|name| branches.iter().any(|branch| branch == *name))
140        .map(|name| (*name).to_owned())
141}
142
143pub fn adopt_branch(branch: &str, parent: &str) -> Result<()> {
144    if branch == parent {
145        bail!("a branch cannot be its own stack parent");
146    }
147
148    let branches: BTreeSet<_> = git::local_branches()?.into_iter().collect();
149    if !branches.contains(branch) {
150        bail!("branch {branch} does not exist");
151    }
152    if !branches.contains(parent) {
153        bail!("parent branch {parent} does not exist");
154    }
155
156    set_parent(branch, parent)?;
157    record_base(branch, parent);
158    anstream::println!(
159        "attached {} to {}",
160        style::branch(branch),
161        style::branch(parent)
162    );
163    Ok(())
164}
165
166pub fn detach_branch(branch: Option<&str>) -> Result<()> {
167    let branch = branch
168        .map(str::to_owned)
169        .map_or_else(git::current_branch, Ok)?;
170    unset_parent(&branch)?;
171    unset_base(&branch)?;
172    anstream::println!("detached {}", style::branch(&branch));
173    Ok(())
174}
175
176/// Rename a branch and keep the stack intact. Git moves the branch's own
177/// metadata with the rename; children pointing at the old name are
178/// retargeted here.
179pub fn rename_branch(old: &str, new: &str, dry_run: bool) -> Result<()> {
180    let children = children_for_branch(old)?;
181
182    if !dry_run {
183        snapshot::take("rename");
184        git::rename_branch(old, new)?;
185    }
186    anstream::println!(
187        "{} {} -> {}",
188        if dry_run { "would rename" } else { "renamed" },
189        style::branch(old),
190        style::branch(new)
191    );
192
193    for child in &children {
194        if !dry_run {
195            set_parent_for_branch(child, new)?;
196        }
197        anstream::println!(
198            "{} {} -> {}",
199            if dry_run {
200                "would retarget"
201            } else {
202                "retargeted"
203            },
204            style::branch(child),
205            style::branch(new)
206        );
207    }
208    Ok(())
209}
210
211pub fn parent_for_branch(branch: &str) -> Result<Option<String>> {
212    parent_of(branch)
213}
214
215pub fn children_for_branch(branch: &str) -> Result<Vec<String>> {
216    children_of(branch)
217}
218
219pub fn set_parent_for_branch(branch: &str, parent: &str) -> Result<()> {
220    set_parent(branch, parent)
221}
222
223pub fn unset_parent_for_branch(branch: &str) -> Result<()> {
224    unset_parent(branch)
225}
226
227pub fn base_for_branch(branch: &str) -> Result<Option<String>> {
228    base_of(branch)
229}
230
231pub fn set_base_for_branch(branch: &str, base: &str) -> Result<()> {
232    git::config_set(&base_key(branch), base)
233}
234
235pub fn unset_base_for_branch(branch: &str) -> Result<()> {
236    unset_base(branch)
237}
238
239/// Record the fork point between a branch and its parent (best effort; e.g.
240/// unrelated histories have no merge base, which is not an error here).
241pub fn record_base(branch: &str, parent: &str) {
242    if let Ok(base) = git::merge_base(parent, branch) {
243        let _ = git::config_set(&base_key(branch), &base);
244    }
245}
246
247/// The root of the stack containing `branch` (the base everything sits on).
248pub fn stack_root(branch: &str) -> Result<String> {
249    let parents = parent_map()?;
250    Ok(root_for(branch, &parents))
251}
252
253pub fn branch_and_descendants(branch: &str) -> Result<Vec<String>> {
254    let parents = parent_map()?;
255    let children = children_map(&parents);
256    let mut branches = vec![branch.to_owned()];
257    collect_descendants(branch, &children, &mut branches);
258    Ok(branches)
259}
260
261/// The stack path from the bottom up to (and including) `branch`,
262/// parent-first; descendants above it are left out.
263pub fn path_from_root(branch: &str) -> Result<Vec<String>> {
264    let trunk = trunk_branch(&git::local_branches()?);
265    let mut path = vec![branch.to_owned()];
266    let mut seen = BTreeSet::from([branch.to_owned()]);
267
268    let mut cursor = branch.to_owned();
269    while let Some(parent) = parent_of(&cursor)? {
270        if Some(&parent) == trunk.as_ref() || !seen.insert(parent.clone()) {
271            break;
272        }
273        path.push(parent.clone());
274        cursor = parent;
275    }
276
277    path.reverse();
278    Ok(path)
279}
280
281/// (branch, parent) pairs for the branches that have a stack parent;
282/// branches without one are skipped.
283pub fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
284    let mut pairs = Vec::new();
285    for branch in branches {
286        if let Some(parent) = parent_of(branch)? {
287            pairs.push((branch.clone(), parent));
288        }
289    }
290    Ok(pairs)
291}
292
293fn parent_map() -> Result<BTreeMap<String, String>> {
294    let mut parents = BTreeMap::new();
295    for branch in git::local_branches()? {
296        if let Some(parent) = parent_of(&branch)? {
297            parents.insert(branch, parent);
298        }
299    }
300    Ok(parents)
301}
302
303fn collect_descendants(
304    branch: &str,
305    children: &BTreeMap<String, Vec<String>>,
306    branches: &mut Vec<String>,
307) {
308    if let Some(branch_children) = children.get(branch) {
309        for child in branch_children {
310            branches.push(child.to_owned());
311            collect_descendants(child, children, branches);
312        }
313    }
314}
315
316fn children_of(parent: &str) -> Result<Vec<String>> {
317    Ok(parent_map()?
318        .into_iter()
319        .filter_map(|(branch, branch_parent)| (branch_parent == parent).then_some(branch))
320        .collect())
321}
322
323fn children_map(parents: &BTreeMap<String, String>) -> BTreeMap<String, Vec<String>> {
324    let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
325    for (branch, parent) in parents {
326        children
327            .entry(parent.to_owned())
328            .or_default()
329            .push(branch.to_owned());
330    }
331    children
332}
333
334fn root_for(branch: &str, parents: &BTreeMap<String, String>) -> String {
335    let mut root = branch.to_owned();
336    let mut seen = BTreeSet::new();
337
338    while let Some(parent) = parents.get(&root) {
339        if !seen.insert(root.clone()) {
340            break;
341        }
342        root = parent.to_owned();
343    }
344
345    root
346}
347
348fn parent_of(branch: &str) -> Result<Option<String>> {
349    git::config_get(&parent_key(branch))
350}
351
352fn base_of(branch: &str) -> Result<Option<String>> {
353    git::config_get(&base_key(branch))
354}
355
356fn set_parent(branch: &str, parent: &str) -> Result<()> {
357    git::config_set(&parent_key(branch), parent)
358}
359
360fn unset_parent(branch: &str) -> Result<()> {
361    git::config_unset(&parent_key(branch))
362}
363
364fn unset_base(branch: &str) -> Result<()> {
365    git::config_unset(&base_key(branch))
366}
367
368fn parent_key(branch: &str) -> String {
369    format!("branch.{branch}.{PARENT_KEY}")
370}
371
372fn base_key(branch: &str) -> String {
373    format!("branch.{branch}.{BASE_KEY}")
374}