1use std::collections::{BTreeMap, BTreeSet};
6
7use anyhow::{Context, Result, bail};
8use serde_json::{Value, json};
9
10use crate::git;
11use crate::settings;
12use crate::style;
13
14const METADATA_REF: &str = "refs/stk/metadata";
17const METADATA_FILE: &str = "stack.json";
18
19mod nav;
20mod restack;
21mod snapshot;
22
23pub use nav::{
24 behind_parent_hint, checkout_bottom, checkout_child, checkout_parent, checkout_top,
25 print_all_stacks, print_children, print_parent, print_stack,
26};
27pub use restack::{abort_restack, continue_restack, restack};
28pub use snapshot::{take as snapshot, undo};
29
30const PARENT_KEY: &str = "stkParent";
31const BASE_KEY: &str = "stkBase";
32const RENAMED_FROM_KEY: &str = "stkRenamedFrom";
35
36pub fn create_branch(branch: &str) -> Result<()> {
37 let parent = git::current_branch()?;
38 if git::local_branches()?
40 .iter()
41 .any(|existing| existing == branch)
42 {
43 bail!(
44 "branch {branch} already exists - adopt it onto {parent} \
45 with `git stk adopt {branch} --parent {parent}`"
46 );
47 }
48 git::create_branch(branch)?;
49 set_parent(branch, &parent)?;
50 record_base(branch, &parent);
51 anstream::println!(
52 "created {} with parent {}",
53 style::branch(branch),
54 style::branch(&parent)
55 );
56 Ok(())
57}
58
59pub fn insert_branch(branch: &str) -> Result<()> {
64 ensure_absent(branch)?;
65 let current = git::current_branch()?;
66 let children = children_of(¤t)?;
67
68 snapshot::take("new --insert");
69 git::create_branch(branch)?; set_parent(branch, ¤t)?;
71 record_base(branch, ¤t);
72 for child in &children {
73 set_parent(child, branch)?;
74 record_base(child, branch);
75 }
76
77 anstream::println!(
78 "inserted {} above {}",
79 style::branch(branch),
80 style::branch(¤t)
81 );
82 for child in &children {
83 anstream::println!(
84 "retargeted {} -> {}",
85 style::branch(child),
86 style::branch(branch)
87 );
88 }
89 Ok(())
90}
91
92pub fn prepend_branch(branch: &str) -> Result<()> {
96 ensure_absent(branch)?;
97 let current = git::current_branch()?;
98 let parent =
99 parent_of(¤t)?.context("current branch has no stack parent to prepend below")?;
100 if !git::worktree_is_clean()? {
101 bail!(
102 "working tree has uncommitted changes; commit or stash before `git stk new --prepend`"
103 );
104 }
105
106 snapshot::take("new --prepend");
107 git::checkout(&parent)?;
108 git::create_branch(branch)?; set_parent(branch, &parent)?;
110 record_base(branch, &parent);
111 set_parent(¤t, branch)?;
112 record_base(¤t, branch);
113
114 anstream::println!(
115 "inserted {} between {} and {}",
116 style::branch(branch),
117 style::branch(&parent),
118 style::branch(¤t)
119 );
120 anstream::println!(
121 "retargeted {} -> {}",
122 style::branch(¤t),
123 style::branch(branch)
124 );
125 Ok(())
126}
127
128fn ensure_absent(branch: &str) -> Result<()> {
129 if git::local_branches()?
130 .iter()
131 .any(|existing| existing == branch)
132 {
133 bail!("branch {branch} already exists");
134 }
135 Ok(())
136}
137
138pub fn trunk_branch(branches: &[String]) -> Option<String> {
141 let remote = settings::remote().unwrap_or_else(|_| settings::DEFAULT_REMOTE.to_owned());
142 if let Some(default) = git::remote_default_branch(&remote) {
143 return Some(default);
144 }
145
146 ["main", "master"]
147 .iter()
148 .find(|name| branches.iter().any(|branch| branch == *name))
149 .map(|name| (*name).to_owned())
150}
151
152pub fn adopt_branch(branch: &str, parent: &str) -> Result<()> {
153 if branch == parent {
154 bail!("a branch cannot be its own stack parent");
155 }
156
157 let branches: BTreeSet<_> = git::local_branches()?.into_iter().collect();
158 if !branches.contains(branch) {
159 bail!("branch {branch} does not exist");
160 }
161 if !branches.contains(parent) {
162 bail!("parent branch {parent} does not exist");
163 }
164 if branch_and_descendants(branch)?
165 .iter()
166 .any(|descendant| descendant == parent)
167 {
168 bail!("{parent} is already below {branch} in the stack; that would form a cycle");
169 }
170
171 set_parent(branch, parent)?;
172 record_base(branch, parent);
173 anstream::println!(
174 "attached {} to {}",
175 style::branch(branch),
176 style::branch(parent)
177 );
178 Ok(())
179}
180
181pub fn detach_branch(branch: Option<&str>) -> Result<()> {
182 let branch = branch
183 .map(str::to_owned)
184 .map_or_else(git::current_branch, Ok)?;
185 unset_parent(&branch)?;
186 unset_base(&branch)?;
187 anstream::println!("detached {}", style::branch(&branch));
188 Ok(())
189}
190
191pub fn rename_branch(old: &str, new: &str, dry_run: bool) -> Result<()> {
195 let children = children_for_branch(old)?;
196
197 if !dry_run {
198 snapshot::take("rename");
199 git::rename_branch(old, new)?;
200 }
201 anstream::println!(
202 "{} {} -> {}",
203 if dry_run { "would rename" } else { "renamed" },
204 style::branch(old),
205 style::branch(new)
206 );
207
208 for child in &children {
209 if !dry_run {
210 set_parent_for_branch(child, new)?;
211 }
212 anstream::println!(
213 "{} {} -> {}",
214 if dry_run {
215 "would retarget"
216 } else {
217 "retargeted"
218 },
219 style::branch(child),
220 style::branch(new)
221 );
222 }
223 Ok(())
224}
225
226pub fn parent_for_branch(branch: &str) -> Result<Option<String>> {
227 parent_of(branch)
228}
229
230pub fn children_for_branch(branch: &str) -> Result<Vec<String>> {
231 children_of(branch)
232}
233
234pub fn set_parent_for_branch(branch: &str, parent: &str) -> Result<()> {
235 set_parent(branch, parent)
236}
237
238pub fn unset_parent_for_branch(branch: &str) -> Result<()> {
239 unset_parent(branch)
240}
241
242pub fn base_for_branch(branch: &str) -> Result<Option<String>> {
243 base_of(branch)
244}
245
246pub fn set_base_for_branch(branch: &str, base: &str) -> Result<()> {
247 git::config_set(&base_key(branch), base)
248}
249
250pub fn unset_base_for_branch(branch: &str) -> Result<()> {
251 unset_base(branch)
252}
253
254pub fn set_renamed_from(branch: &str, old: &str) -> Result<()> {
257 git::config_set(&renamed_from_key(branch), old)
258}
259
260pub fn renamed_from(branch: &str) -> Result<Option<String>> {
262 git::config_get(&renamed_from_key(branch))
263}
264
265pub fn clear_renamed_from(branch: &str) -> Result<()> {
267 git::config_unset(&renamed_from_key(branch))
268}
269
270pub fn record_base(branch: &str, parent: &str) {
273 if let Ok(base) = git::merge_base(parent, branch) {
274 let _ = git::config_set(&base_key(branch), &base);
275 }
276}
277
278pub fn stack_root(branch: &str) -> Result<String> {
280 let parents = parent_map()?;
281 Ok(root_for(branch, &parents))
282}
283
284pub fn branch_and_descendants(branch: &str) -> Result<Vec<String>> {
285 let parents = parent_map()?;
286 let children = children_map(&parents);
287 let mut branches = vec![branch.to_owned()];
288 let mut visited = BTreeSet::from([branch.to_owned()]);
289 collect_descendants(branch, &children, &mut branches, &mut visited);
290 Ok(branches)
291}
292
293pub fn stack_line(branch: &str) -> Result<Vec<String>> {
299 let trunk = trunk_branch(&git::local_branches()?);
303 if Some(branch) == trunk.as_deref() {
304 return Ok(Vec::new());
305 }
306
307 let mut line = path_from_root(branch)?; let above = branch_and_descendants(branch)?; line.extend(above.into_iter().skip(1)); line.retain(|candidate| Some(candidate) != trunk.as_ref());
314 Ok(line)
315}
316
317pub fn publish_metadata(remote: &str) {
321 if let Err(error) = try_publish_metadata(remote) {
322 anstream::eprintln!(
323 "{}",
324 style::warn(&format!("could not publish stack metadata: {error:#}"))
325 );
326 }
327}
328
329fn try_publish_metadata(remote: &str) -> Result<()> {
330 let current = git::current_branch()?;
331 let root = stack_root(¤t)?;
332 let trunk = trunk_branch(&git::local_branches()?);
333
334 let mut parents = serde_json::Map::new();
335 for branch in branch_and_descendants(&root)? {
336 if Some(&branch) == trunk.as_ref() {
337 continue;
338 }
339 if let Some(parent) = parent_of(&branch)? {
340 parents.insert(branch, Value::String(parent));
341 }
342 }
343 if parents.is_empty() {
344 return Ok(());
345 }
346
347 let document = json!({ "trunk": trunk, "parents": parents });
348 git::write_blob_ref(METADATA_REF, METADATA_FILE, &document.to_string())?;
349 git::push_ref(remote, METADATA_REF)
350}
351
352pub fn apply_remote_metadata(remote: &str) -> Result<usize> {
355 git::fetch_ref(remote, METADATA_REF)
356 .context("no stack metadata on the remote - push it from the other machine first")?;
357 let Some(content) = git::read_ref_file(METADATA_REF, METADATA_FILE)? else {
358 bail!("the remote stack metadata is empty");
359 };
360
361 let document: Value =
362 serde_json::from_str(&content).context("failed to parse remote stack metadata")?;
363 let parents = document
364 .get("parents")
365 .and_then(Value::as_object)
366 .context("remote stack metadata is malformed")?;
367
368 let local: BTreeSet<String> = git::local_branches()?.into_iter().collect();
371 for branch in parents.keys() {
372 if !local.contains(branch) {
373 git::fetch_branch(remote, branch)
374 .with_context(|| format!("failed to fetch {branch} from {remote}"))?;
375 }
376 }
377
378 let mut attached = 0;
379 for (branch, parent) in parents {
380 let Some(parent) = parent.as_str() else {
381 continue;
382 };
383 set_parent(branch, parent)?;
384 record_base(branch, parent);
385 attached += 1;
386 anstream::println!(
387 "attached {} to {}",
388 style::branch(branch),
389 style::branch(parent)
390 );
391 }
392 Ok(attached)
393}
394
395pub fn path_from_root(branch: &str) -> Result<Vec<String>> {
398 let trunk = trunk_branch(&git::local_branches()?);
399 let mut path = vec![branch.to_owned()];
400 let mut seen = BTreeSet::from([branch.to_owned()]);
401
402 let mut cursor = branch.to_owned();
403 while let Some(parent) = parent_of(&cursor)? {
404 if Some(&parent) == trunk.as_ref() || !seen.insert(parent.clone()) {
405 break;
406 }
407 path.push(parent.clone());
408 cursor = parent;
409 }
410
411 path.reverse();
412 Ok(path)
413}
414
415pub fn branch_parents(branches: &[String]) -> Result<Vec<(String, String)>> {
418 let mut pairs = Vec::new();
419 for branch in branches {
420 if let Some(parent) = parent_of(branch)? {
421 pairs.push((branch.clone(), parent));
422 }
423 }
424 Ok(pairs)
425}
426
427fn parent_map() -> Result<BTreeMap<String, String>> {
428 let mut parents = BTreeMap::new();
429 for branch in git::local_branches()? {
430 if let Some(parent) = parent_of(&branch)? {
431 parents.insert(branch, parent);
432 }
433 }
434 Ok(parents)
435}
436
437fn collect_descendants(
438 branch: &str,
439 children: &BTreeMap<String, Vec<String>>,
440 branches: &mut Vec<String>,
441 visited: &mut BTreeSet<String>,
442) {
443 if let Some(branch_children) = children.get(branch) {
444 for child in branch_children {
445 if !visited.insert(child.to_owned()) {
446 continue; }
448 branches.push(child.to_owned());
449 collect_descendants(child, children, branches, visited);
450 }
451 }
452}
453
454fn children_of(parent: &str) -> Result<Vec<String>> {
455 Ok(parent_map()?
456 .into_iter()
457 .filter_map(|(branch, branch_parent)| (branch_parent == parent).then_some(branch))
458 .collect())
459}
460
461fn children_map(parents: &BTreeMap<String, String>) -> BTreeMap<String, Vec<String>> {
462 let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
463 for (branch, parent) in parents {
464 children
465 .entry(parent.to_owned())
466 .or_default()
467 .push(branch.to_owned());
468 }
469 children
470}
471
472fn root_for(branch: &str, parents: &BTreeMap<String, String>) -> String {
473 let mut root = branch.to_owned();
474 let mut seen = BTreeSet::new();
475
476 while let Some(parent) = parents.get(&root) {
477 if !seen.insert(root.clone()) {
478 break;
479 }
480 root = parent.to_owned();
481 }
482
483 root
484}
485
486fn parent_of(branch: &str) -> Result<Option<String>> {
487 git::config_get(&parent_key(branch))
488}
489
490fn base_of(branch: &str) -> Result<Option<String>> {
491 git::config_get(&base_key(branch))
492}
493
494fn set_parent(branch: &str, parent: &str) -> Result<()> {
495 git::config_set(&parent_key(branch), parent)
496}
497
498fn unset_parent(branch: &str) -> Result<()> {
499 git::config_unset(&parent_key(branch))
500}
501
502fn unset_base(branch: &str) -> Result<()> {
503 git::config_unset(&base_key(branch))
504}
505
506fn parent_key(branch: &str) -> String {
507 format!("branch.{branch}.{PARENT_KEY}")
508}
509
510fn base_key(branch: &str) -> String {
511 format!("branch.{branch}.{BASE_KEY}")
512}
513
514fn renamed_from_key(branch: &str) -> String {
515 format!("branch.{branch}.{RENAMED_FROM_KEY}")
516}