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";
26const RENAMED_FROM_KEY: &str = "stkRenamedFrom";
29
30pub fn create_branch(branch: &str) -> Result<()> {
31 let parent = git::current_branch()?;
32 if git::local_branches()?
34 .iter()
35 .any(|existing| existing == branch)
36 {
37 bail!(
38 "branch {branch} already exists - adopt it onto {parent} \
39 with `git stk adopt {branch} --parent {parent}`"
40 );
41 }
42 git::create_branch(branch)?;
43 set_parent(branch, &parent)?;
44 record_base(branch, &parent);
45 anstream::println!(
46 "created {} with parent {}",
47 style::branch(branch),
48 style::branch(&parent)
49 );
50 Ok(())
51}
52
53pub fn insert_branch(branch: &str) -> Result<()> {
58 ensure_absent(branch)?;
59 let current = git::current_branch()?;
60 let children = children_of(¤t)?;
61
62 snapshot::take("new --insert");
63 git::create_branch(branch)?; set_parent(branch, ¤t)?;
65 record_base(branch, ¤t);
66 for child in &children {
67 set_parent(child, branch)?;
68 record_base(child, branch);
69 }
70
71 anstream::println!(
72 "inserted {} above {}",
73 style::branch(branch),
74 style::branch(¤t)
75 );
76 for child in &children {
77 anstream::println!(
78 "retargeted {} -> {}",
79 style::branch(child),
80 style::branch(branch)
81 );
82 }
83 Ok(())
84}
85
86pub fn prepend_branch(branch: &str) -> Result<()> {
90 ensure_absent(branch)?;
91 let current = git::current_branch()?;
92 let parent =
93 parent_of(¤t)?.context("current branch has no stack parent to prepend below")?;
94 if !git::worktree_is_clean()? {
95 bail!(
96 "working tree has uncommitted changes; commit or stash before `git stk new --prepend`"
97 );
98 }
99
100 snapshot::take("new --prepend");
101 git::checkout(&parent)?;
102 git::create_branch(branch)?; set_parent(branch, &parent)?;
104 record_base(branch, &parent);
105 set_parent(¤t, branch)?;
106 record_base(¤t, branch);
107
108 anstream::println!(
109 "inserted {} between {} and {}",
110 style::branch(branch),
111 style::branch(&parent),
112 style::branch(¤t)
113 );
114 anstream::println!(
115 "retargeted {} -> {}",
116 style::branch(¤t),
117 style::branch(branch)
118 );
119 Ok(())
120}
121
122fn ensure_absent(branch: &str) -> Result<()> {
123 if git::local_branches()?
124 .iter()
125 .any(|existing| existing == branch)
126 {
127 bail!("branch {branch} already exists");
128 }
129 Ok(())
130}
131
132pub fn trunk_branch(branches: &[String]) -> Option<String> {
135 let remote = settings::remote().unwrap_or_else(|_| settings::DEFAULT_REMOTE.to_owned());
136 if let Some(default) = git::remote_default_branch(&remote) {
137 return Some(default);
138 }
139
140 ["main", "master"]
141 .iter()
142 .find(|name| branches.iter().any(|branch| branch == *name))
143 .map(|name| (*name).to_owned())
144}
145
146pub fn adopt_branch(branch: &str, parent: &str) -> Result<()> {
147 if branch == parent {
148 bail!("a branch cannot be its own stack parent");
149 }
150
151 let branches: BTreeSet<_> = git::local_branches()?.into_iter().collect();
152 if !branches.contains(branch) {
153 bail!("branch {branch} does not exist");
154 }
155 if !branches.contains(parent) {
156 bail!("parent branch {parent} does not exist");
157 }
158
159 set_parent(branch, parent)?;
160 record_base(branch, parent);
161 anstream::println!(
162 "attached {} to {}",
163 style::branch(branch),
164 style::branch(parent)
165 );
166 Ok(())
167}
168
169pub fn detach_branch(branch: Option<&str>) -> Result<()> {
170 let branch = branch
171 .map(str::to_owned)
172 .map_or_else(git::current_branch, Ok)?;
173 unset_parent(&branch)?;
174 unset_base(&branch)?;
175 anstream::println!("detached {}", style::branch(&branch));
176 Ok(())
177}
178
179pub fn rename_branch(old: &str, new: &str, dry_run: bool) -> Result<()> {
183 let children = children_for_branch(old)?;
184
185 if !dry_run {
186 snapshot::take("rename");
187 git::rename_branch(old, new)?;
188 }
189 anstream::println!(
190 "{} {} -> {}",
191 if dry_run { "would rename" } else { "renamed" },
192 style::branch(old),
193 style::branch(new)
194 );
195
196 for child in &children {
197 if !dry_run {
198 set_parent_for_branch(child, new)?;
199 }
200 anstream::println!(
201 "{} {} -> {}",
202 if dry_run {
203 "would retarget"
204 } else {
205 "retargeted"
206 },
207 style::branch(child),
208 style::branch(new)
209 );
210 }
211 Ok(())
212}
213
214pub fn parent_for_branch(branch: &str) -> Result<Option<String>> {
215 parent_of(branch)
216}
217
218pub fn children_for_branch(branch: &str) -> Result<Vec<String>> {
219 children_of(branch)
220}
221
222pub fn set_parent_for_branch(branch: &str, parent: &str) -> Result<()> {
223 set_parent(branch, parent)
224}
225
226pub fn unset_parent_for_branch(branch: &str) -> Result<()> {
227 unset_parent(branch)
228}
229
230pub fn base_for_branch(branch: &str) -> Result<Option<String>> {
231 base_of(branch)
232}
233
234pub fn set_base_for_branch(branch: &str, base: &str) -> Result<()> {
235 git::config_set(&base_key(branch), base)
236}
237
238pub fn unset_base_for_branch(branch: &str) -> Result<()> {
239 unset_base(branch)
240}
241
242pub fn set_renamed_from(branch: &str, old: &str) -> Result<()> {
245 git::config_set(&renamed_from_key(branch), old)
246}
247
248pub fn renamed_from(branch: &str) -> Result<Option<String>> {
250 git::config_get(&renamed_from_key(branch))
251}
252
253pub fn clear_renamed_from(branch: &str) -> Result<()> {
255 git::config_unset(&renamed_from_key(branch))
256}
257
258pub fn record_base(branch: &str, parent: &str) {
261 if let Ok(base) = git::merge_base(parent, branch) {
262 let _ = git::config_set(&base_key(branch), &base);
263 }
264}
265
266pub fn stack_root(branch: &str) -> Result<String> {
268 let parents = parent_map()?;
269 Ok(root_for(branch, &parents))
270}
271
272pub fn branch_and_descendants(branch: &str) -> Result<Vec<String>> {
273 let parents = parent_map()?;
274 let children = children_map(&parents);
275 let mut branches = vec![branch.to_owned()];
276 collect_descendants(branch, &children, &mut branches);
277 Ok(branches)
278}
279
280pub fn stack_line(branch: &str) -> Result<Vec<String>> {
286 let mut line = path_from_root(branch)?; let above = branch_and_descendants(branch)?; line.extend(above.into_iter().skip(1)); let trunk = trunk_branch(&git::local_branches()?);
293 line.retain(|candidate| Some(candidate) != trunk.as_ref());
294 Ok(line)
295}
296
297pub fn path_from_root(branch: &str) -> Result<Vec<String>> {
300 let trunk = trunk_branch(&git::local_branches()?);
301 let mut path = vec![branch.to_owned()];
302 let mut seen = BTreeSet::from([branch.to_owned()]);
303
304 let mut cursor = branch.to_owned();
305 while let Some(parent) = parent_of(&cursor)? {
306 if Some(&parent) == trunk.as_ref() || !seen.insert(parent.clone()) {
307 break;
308 }
309 path.push(parent.clone());
310 cursor = parent;
311 }
312
313 path.reverse();
314 Ok(path)
315}
316
317pub fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
320 let mut pairs = Vec::new();
321 for branch in branches {
322 if let Some(parent) = parent_of(branch)? {
323 pairs.push((branch.clone(), parent));
324 }
325 }
326 Ok(pairs)
327}
328
329fn parent_map() -> Result<BTreeMap<String, String>> {
330 let mut parents = BTreeMap::new();
331 for branch in git::local_branches()? {
332 if let Some(parent) = parent_of(&branch)? {
333 parents.insert(branch, parent);
334 }
335 }
336 Ok(parents)
337}
338
339fn collect_descendants(
340 branch: &str,
341 children: &BTreeMap<String, Vec<String>>,
342 branches: &mut Vec<String>,
343) {
344 if let Some(branch_children) = children.get(branch) {
345 for child in branch_children {
346 branches.push(child.to_owned());
347 collect_descendants(child, children, branches);
348 }
349 }
350}
351
352fn children_of(parent: &str) -> Result<Vec<String>> {
353 Ok(parent_map()?
354 .into_iter()
355 .filter_map(|(branch, branch_parent)| (branch_parent == parent).then_some(branch))
356 .collect())
357}
358
359fn children_map(parents: &BTreeMap<String, String>) -> BTreeMap<String, Vec<String>> {
360 let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
361 for (branch, parent) in parents {
362 children
363 .entry(parent.to_owned())
364 .or_default()
365 .push(branch.to_owned());
366 }
367 children
368}
369
370fn root_for(branch: &str, parents: &BTreeMap<String, String>) -> String {
371 let mut root = branch.to_owned();
372 let mut seen = BTreeSet::new();
373
374 while let Some(parent) = parents.get(&root) {
375 if !seen.insert(root.clone()) {
376 break;
377 }
378 root = parent.to_owned();
379 }
380
381 root
382}
383
384fn parent_of(branch: &str) -> Result<Option<String>> {
385 git::config_get(&parent_key(branch))
386}
387
388fn base_of(branch: &str) -> Result<Option<String>> {
389 git::config_get(&base_key(branch))
390}
391
392fn set_parent(branch: &str, parent: &str) -> Result<()> {
393 git::config_set(&parent_key(branch), parent)
394}
395
396fn unset_parent(branch: &str) -> Result<()> {
397 git::config_unset(&parent_key(branch))
398}
399
400fn unset_base(branch: &str) -> Result<()> {
401 git::config_unset(&base_key(branch))
402}
403
404fn parent_key(branch: &str) -> String {
405 format!("branch.{branch}.{PARENT_KEY}")
406}
407
408fn base_key(branch: &str) -> String {
409 format!("branch.{branch}.{BASE_KEY}")
410}
411
412fn renamed_from_key(branch: &str) -> String {
413 format!("branch.{branch}.{RENAMED_FROM_KEY}")
414}