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;
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_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    git::create_branch(branch)?;
30    set_parent(branch, &parent)?;
31    record_base(branch, &parent);
32    anstream::println!(
33        "created {} with parent {}",
34        style::branch(branch),
35        style::branch(&parent)
36    );
37    Ok(())
38}
39
40/// The trunk branch: the remote's default branch when known locally,
41/// otherwise a conventional name that exists.
42pub fn trunk_branch(branches: &[String]) -> Option<String> {
43    let remote = settings::remote().unwrap_or_else(|_| settings::DEFAULT_REMOTE.to_owned());
44    if let Some(default) = git::remote_default_branch(&remote) {
45        return Some(default);
46    }
47
48    ["main", "master"]
49        .iter()
50        .find(|name| branches.iter().any(|branch| branch == *name))
51        .map(|name| (*name).to_owned())
52}
53
54pub fn adopt_branch(branch: &str, parent: &str) -> Result<()> {
55    if branch == parent {
56        bail!("a branch cannot be its own stack parent");
57    }
58
59    let branches: BTreeSet<_> = git::local_branches()?.into_iter().collect();
60    if !branches.contains(branch) {
61        bail!("branch {branch} does not exist");
62    }
63    if !branches.contains(parent) {
64        bail!("parent branch {parent} does not exist");
65    }
66
67    set_parent(branch, parent)?;
68    record_base(branch, parent);
69    anstream::println!(
70        "attached {} to {}",
71        style::branch(branch),
72        style::branch(parent)
73    );
74    Ok(())
75}
76
77pub fn detach_branch(branch: Option<&str>) -> Result<()> {
78    let branch = branch
79        .map(str::to_owned)
80        .map_or_else(git::current_branch, Ok)?;
81    unset_parent(&branch)?;
82    unset_base(&branch)?;
83    anstream::println!("detached {}", style::branch(&branch));
84    Ok(())
85}
86
87/// Rename a branch and keep the stack intact. Git moves the branch's own
88/// metadata with the rename; children pointing at the old name are
89/// retargeted here.
90pub fn rename_branch(old: &str, new: &str, dry_run: bool) -> Result<()> {
91    let children = children_for_branch(old)?;
92
93    if !dry_run {
94        snapshot::take("rename");
95        git::rename_branch(old, new)?;
96    }
97    anstream::println!(
98        "{} {} -> {}",
99        if dry_run { "would rename" } else { "renamed" },
100        style::branch(old),
101        style::branch(new)
102    );
103
104    for child in &children {
105        if !dry_run {
106            set_parent_for_branch(child, new)?;
107        }
108        anstream::println!(
109            "{} {} -> {}",
110            if dry_run {
111                "would retarget"
112            } else {
113                "retargeted"
114            },
115            style::branch(child),
116            style::branch(new)
117        );
118    }
119    Ok(())
120}
121
122pub fn parent_for_branch(branch: &str) -> Result<Option<String>> {
123    parent_of(branch)
124}
125
126pub fn children_for_branch(branch: &str) -> Result<Vec<String>> {
127    children_of(branch)
128}
129
130pub fn set_parent_for_branch(branch: &str, parent: &str) -> Result<()> {
131    set_parent(branch, parent)
132}
133
134pub fn unset_parent_for_branch(branch: &str) -> Result<()> {
135    unset_parent(branch)
136}
137
138pub fn base_for_branch(branch: &str) -> Result<Option<String>> {
139    base_of(branch)
140}
141
142pub fn set_base_for_branch(branch: &str, base: &str) -> Result<()> {
143    git::config_set(&base_key(branch), base)
144}
145
146pub fn unset_base_for_branch(branch: &str) -> Result<()> {
147    unset_base(branch)
148}
149
150/// Record the fork point between a branch and its parent (best effort; e.g.
151/// unrelated histories have no merge base, which is not an error here).
152pub fn record_base(branch: &str, parent: &str) {
153    if let Ok(base) = git::merge_base(parent, branch) {
154        let _ = git::config_set(&base_key(branch), &base);
155    }
156}
157
158/// The root of the stack containing `branch` (the base everything sits on).
159pub fn stack_root(branch: &str) -> Result<String> {
160    let parents = parent_map()?;
161    Ok(root_for(branch, &parents))
162}
163
164pub fn branch_and_descendants(branch: &str) -> Result<Vec<String>> {
165    let parents = parent_map()?;
166    let children = children_map(&parents);
167    let mut branches = vec![branch.to_owned()];
168    collect_descendants(branch, &children, &mut branches);
169    Ok(branches)
170}
171
172/// The stack path from the bottom up to (and including) `branch`,
173/// parent-first; descendants above it are left out.
174pub fn path_from_root(branch: &str) -> Result<Vec<String>> {
175    let trunk = trunk_branch(&git::local_branches()?);
176    let mut path = vec![branch.to_owned()];
177    let mut seen = BTreeSet::from([branch.to_owned()]);
178
179    let mut cursor = branch.to_owned();
180    while let Some(parent) = parent_of(&cursor)? {
181        if Some(&parent) == trunk.as_ref() || !seen.insert(parent.clone()) {
182            break;
183        }
184        path.push(parent.clone());
185        cursor = parent;
186    }
187
188    path.reverse();
189    Ok(path)
190}
191
192/// (branch, parent) pairs for the branches that have a stack parent;
193/// branches without one are skipped.
194pub fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
195    let mut pairs = Vec::new();
196    for branch in branches {
197        if let Some(parent) = parent_of(branch)? {
198            pairs.push((branch.clone(), parent));
199        }
200    }
201    Ok(pairs)
202}
203
204fn parent_map() -> Result<BTreeMap<String, String>> {
205    let mut parents = BTreeMap::new();
206    for branch in git::local_branches()? {
207        if let Some(parent) = parent_of(&branch)? {
208            parents.insert(branch, parent);
209        }
210    }
211    Ok(parents)
212}
213
214fn collect_descendants(
215    branch: &str,
216    children: &BTreeMap<String, Vec<String>>,
217    branches: &mut Vec<String>,
218) {
219    if let Some(branch_children) = children.get(branch) {
220        for child in branch_children {
221            branches.push(child.to_owned());
222            collect_descendants(child, children, branches);
223        }
224    }
225}
226
227fn children_of(parent: &str) -> Result<Vec<String>> {
228    Ok(parent_map()?
229        .into_iter()
230        .filter_map(|(branch, branch_parent)| (branch_parent == parent).then_some(branch))
231        .collect())
232}
233
234fn children_map(parents: &BTreeMap<String, String>) -> BTreeMap<String, Vec<String>> {
235    let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
236    for (branch, parent) in parents {
237        children
238            .entry(parent.to_owned())
239            .or_default()
240            .push(branch.to_owned());
241    }
242    children
243}
244
245fn root_for(branch: &str, parents: &BTreeMap<String, String>) -> String {
246    let mut root = branch.to_owned();
247    let mut seen = BTreeSet::new();
248
249    while let Some(parent) = parents.get(&root) {
250        if !seen.insert(root.clone()) {
251            break;
252        }
253        root = parent.to_owned();
254    }
255
256    root
257}
258
259fn parent_of(branch: &str) -> Result<Option<String>> {
260    git::config_get(&parent_key(branch))
261}
262
263fn base_of(branch: &str) -> Result<Option<String>> {
264    git::config_get(&base_key(branch))
265}
266
267fn set_parent(branch: &str, parent: &str) -> Result<()> {
268    git::config_set(&parent_key(branch), parent)
269}
270
271fn unset_parent(branch: &str) -> Result<()> {
272    git::config_unset(&parent_key(branch))
273}
274
275fn unset_base(branch: &str) -> Result<()> {
276    git::config_unset(&base_key(branch))
277}
278
279fn parent_key(branch: &str) -> String {
280    format!("branch.{branch}.{PARENT_KEY}")
281}
282
283fn base_key(branch: &str) -> String {
284    format!("branch.{branch}.{BASE_KEY}")
285}