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