1use 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 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
50pub fn insert_branch(branch: &str) -> Result<()> {
55 ensure_absent(branch)?;
56 let current = git::current_branch()?;
57 let children = children_of(¤t)?;
58
59 snapshot::take("new --insert");
60 git::create_branch(branch)?; set_parent(branch, ¤t)?;
62 record_base(branch, ¤t);
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(¤t)
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
83pub fn prepend_branch(branch: &str) -> Result<()> {
87 ensure_absent(branch)?;
88 let current = git::current_branch()?;
89 let parent =
90 parent_of(¤t)?.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)?; set_parent(branch, &parent)?;
101 record_base(branch, &parent);
102 set_parent(¤t, branch)?;
103 record_base(¤t, branch);
104
105 anstream::println!(
106 "inserted {} between {} and {}",
107 style::branch(branch),
108 style::branch(&parent),
109 style::branch(¤t)
110 );
111 anstream::println!(
112 "retargeted {} -> {}",
113 style::branch(¤t),
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
129pub 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
176pub 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
239pub 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
247pub 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
261pub 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
281pub 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}