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::{Result, bail};
8
9use crate::git;
10use crate::settings;
11
12mod nav;
13mod restack;
14
15pub use nav::{
16    behind_parent_hint, checkout_bottom, checkout_child, checkout_parent, checkout_top,
17    print_children, print_parent, print_stack,
18};
19pub use restack::{abort_restack, continue_restack, restack};
20
21const PARENT_KEY: &str = "stkParent";
22const BASE_KEY: &str = "stkBase";
23
24pub fn create_branch(branch: &str) -> Result<()> {
25    let parent = git::current_branch()?;
26    git::create_branch(branch)?;
27    set_parent(branch, &parent)?;
28    record_base(branch, &parent);
29    println!("created {branch} with parent {parent}");
30    Ok(())
31}
32
33/// The trunk branch: the remote's default branch when known locally,
34/// otherwise a conventional name that exists.
35pub fn trunk_branch(branches: &[String]) -> Option<String> {
36    let remote = settings::remote().unwrap_or_else(|_| settings::DEFAULT_REMOTE.to_owned());
37    if let Some(default) = git::remote_default_branch(&remote) {
38        return Some(default);
39    }
40
41    ["main", "master"]
42        .iter()
43        .find(|name| branches.iter().any(|branch| branch == *name))
44        .map(|name| (*name).to_owned())
45}
46
47pub fn adopt_branch(branch: &str, parent: &str) -> Result<()> {
48    if branch == parent {
49        bail!("a branch cannot be its own stack parent");
50    }
51
52    let branches: BTreeSet<_> = git::local_branches()?.into_iter().collect();
53    if !branches.contains(branch) {
54        bail!("branch {branch} does not exist");
55    }
56    if !branches.contains(parent) {
57        bail!("parent branch {parent} does not exist");
58    }
59
60    set_parent(branch, parent)?;
61    record_base(branch, parent);
62    println!("attached {branch} to {parent}");
63    Ok(())
64}
65
66pub fn detach_branch(branch: Option<&str>) -> Result<()> {
67    let branch = branch
68        .map(str::to_owned)
69        .map_or_else(git::current_branch, Ok)?;
70    unset_parent(&branch)?;
71    unset_base(&branch)?;
72    println!("detached {branch}");
73    Ok(())
74}
75
76/// Rename a branch and keep the stack intact. Git moves the branch's own
77/// metadata with the rename; children pointing at the old name are
78/// retargeted here.
79pub fn rename_branch(old: &str, new: &str) -> Result<()> {
80    let children = children_for_branch(old)?;
81    git::rename_branch(old, new)?;
82    println!("renamed {old} -> {new}");
83
84    for child in &children {
85        set_parent_for_branch(child, new)?;
86        println!("retargeted {child} -> {new}");
87    }
88    Ok(())
89}
90
91pub fn parent_for_branch(branch: &str) -> Result<Option<String>> {
92    parent_of(branch)
93}
94
95pub fn children_for_branch(branch: &str) -> Result<Vec<String>> {
96    children_of(branch)
97}
98
99pub fn set_parent_for_branch(branch: &str, parent: &str) -> Result<()> {
100    set_parent(branch, parent)
101}
102
103pub fn unset_parent_for_branch(branch: &str) -> Result<()> {
104    unset_parent(branch)
105}
106
107pub fn base_for_branch(branch: &str) -> Result<Option<String>> {
108    base_of(branch)
109}
110
111pub fn set_base_for_branch(branch: &str, base: &str) -> Result<()> {
112    git::config_set(&base_key(branch), base)
113}
114
115pub fn unset_base_for_branch(branch: &str) -> Result<()> {
116    unset_base(branch)
117}
118
119/// Record the fork point between a branch and its parent (best effort; e.g.
120/// unrelated histories have no merge base, which is not an error here).
121pub fn record_base(branch: &str, parent: &str) {
122    if let Ok(base) = git::merge_base(parent, branch) {
123        let _ = git::config_set(&base_key(branch), &base);
124    }
125}
126
127/// The root of the stack containing `branch` (the base everything sits on).
128pub fn stack_root(branch: &str) -> Result<String> {
129    let parents = parent_map()?;
130    Ok(root_for(branch, &parents))
131}
132
133pub fn branch_and_descendants(branch: &str) -> Result<Vec<String>> {
134    let parents = parent_map()?;
135    let children = children_map(&parents);
136    let mut branches = vec![branch.to_owned()];
137    collect_descendants(branch, &children, &mut branches);
138    Ok(branches)
139}
140
141/// (branch, parent) pairs for the branches that have a stack parent;
142/// branches without one are skipped.
143pub fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
144    let mut pairs = Vec::new();
145    for branch in branches {
146        if let Some(parent) = parent_of(branch)? {
147            pairs.push((branch.clone(), parent));
148        }
149    }
150    Ok(pairs)
151}
152
153fn parent_map() -> Result<BTreeMap<String, String>> {
154    let mut parents = BTreeMap::new();
155    for branch in git::local_branches()? {
156        if let Some(parent) = parent_of(&branch)? {
157            parents.insert(branch, parent);
158        }
159    }
160    Ok(parents)
161}
162
163fn collect_descendants(
164    branch: &str,
165    children: &BTreeMap<String, Vec<String>>,
166    branches: &mut Vec<String>,
167) {
168    if let Some(branch_children) = children.get(branch) {
169        for child in branch_children {
170            branches.push(child.to_owned());
171            collect_descendants(child, children, branches);
172        }
173    }
174}
175
176fn children_of(parent: &str) -> Result<Vec<String>> {
177    Ok(parent_map()?
178        .into_iter()
179        .filter_map(|(branch, branch_parent)| (branch_parent == parent).then_some(branch))
180        .collect())
181}
182
183fn children_map(parents: &BTreeMap<String, String>) -> BTreeMap<String, Vec<String>> {
184    let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
185    for (branch, parent) in parents {
186        children
187            .entry(parent.to_owned())
188            .or_default()
189            .push(branch.to_owned());
190    }
191    children
192}
193
194fn root_for(branch: &str, parents: &BTreeMap<String, String>) -> String {
195    let mut root = branch.to_owned();
196    let mut seen = BTreeSet::new();
197
198    while let Some(parent) = parents.get(&root) {
199        if !seen.insert(root.clone()) {
200            break;
201        }
202        root = parent.to_owned();
203    }
204
205    root
206}
207
208fn parent_of(branch: &str) -> Result<Option<String>> {
209    git::config_get(&parent_key(branch))
210}
211
212fn base_of(branch: &str) -> Result<Option<String>> {
213    git::config_get(&base_key(branch))
214}
215
216fn set_parent(branch: &str, parent: &str) -> Result<()> {
217    git::config_set(&parent_key(branch), parent)
218}
219
220fn unset_parent(branch: &str) -> Result<()> {
221    git::config_unset(&parent_key(branch))
222}
223
224fn unset_base(branch: &str) -> Result<()> {
225    git::config_unset(&base_key(branch))
226}
227
228fn parent_key(branch: &str) -> String {
229    format!("branch.{branch}.{PARENT_KEY}")
230}
231
232fn base_key(branch: &str) -> String {
233    format!("branch.{branch}.{BASE_KEY}")
234}