1use std::collections::{BTreeMap, BTreeSet};
4
5use anyhow::{Result, bail};
6
7use super::{
8 branch_and_descendants, children_map, children_of, parent_map, parent_of, root_for,
9 trunk_branch,
10};
11use crate::git;
12use crate::prompt;
13use crate::style;
14
15fn pick_child(title: &str, children: &[String]) -> anyhow::Result<Option<String>> {
18 let painted: Vec<String> = children
19 .iter()
20 .map(|child| style::paint(style::BRANCH, child))
21 .collect();
22 Ok(prompt::pick(title, &painted)?.map(|index| children[index].clone()))
23}
24
25pub fn print_parent(branch: Option<&str>) -> Result<()> {
26 let branch = branch
27 .map(str::to_owned)
28 .map_or_else(git::current_branch, Ok)?;
29 match parent_of(&branch)? {
30 Some(parent) => println!("{parent}"),
31 None => bail!("{branch} has no stack parent"),
32 }
33 Ok(())
34}
35
36pub fn print_children(branch: Option<&str>) -> Result<()> {
37 let branch = branch
38 .map(str::to_owned)
39 .map_or_else(git::current_branch, Ok)?;
40 for child in children_of(&branch)? {
41 println!("{child}");
42 }
43 Ok(())
44}
45
46pub fn checkout_parent() -> Result<()> {
47 let current = git::current_branch()?;
48 let Some(parent) = parent_of(¤t)? else {
49 bail!("{current} has no stack parent");
50 };
51
52 git::checkout(&parent)
53}
54
55pub fn checkout_child(branch: Option<&str>) -> Result<()> {
56 let current = git::current_branch()?;
57 let children = children_of(¤t)?;
58 let child = match (branch, children.as_slice()) {
59 (Some(branch), _) => {
60 if children.iter().any(|child| child == branch) {
61 branch.to_owned()
62 } else {
63 bail!("{branch} is not a stack child of {current}");
64 }
65 }
66 (None, [child]) => child.to_owned(),
67 (None, []) => bail!("{current} has no stack children"),
68 (None, _) => {
69 match pick_child(
70 &format!("{current} has multiple stack children:"),
71 &children,
72 )? {
73 Some(child) => child,
74 None => bail!("choose one with `git stk up <branch>`"),
75 }
76 }
77 };
78
79 git::checkout(&child)
80}
81
82pub fn checkout_top() -> Result<()> {
85 let current = git::current_branch()?;
86 let mut top = current.clone();
87 loop {
88 let children = children_of(&top)?;
89 match children.as_slice() {
90 [] => break,
91 [child] => top = child.clone(),
92 _ => match pick_child(&format!("{top} has multiple stack children:"), &children)? {
94 Some(child) => top = child,
95 None => bail!("walk up from {top} with `git stk up <branch>`"),
96 },
97 }
98 }
99
100 if top == current {
101 if children_of(¤t)?.is_empty() && parent_of(¤t)?.is_none() {
102 bail!("{current} is not in a stack");
103 }
104 println!("{current} is already at the top of the stack");
105 return Ok(());
106 }
107 git::checkout(&top)
108}
109
110pub fn checkout_bottom() -> Result<()> {
113 let current = git::current_branch()?;
114 let trunk = trunk_branch(&git::local_branches()?);
115
116 let bottom = if Some(¤t) == trunk.as_ref() {
117 let children = children_of(¤t)?;
118 match children.as_slice() {
119 [child] => child.clone(),
120 [] => bail!("{current} has no stacked branches"),
121 _ => {
122 match pick_child(
123 &format!("{current} has multiple stack children:"),
124 &children,
125 )? {
126 Some(child) => child,
127 None => bail!("choose one with `git stk up <branch>`"),
128 }
129 }
130 }
131 } else {
132 let mut bottom = current.clone();
133 while let Some(parent) = parent_of(&bottom)? {
134 if Some(&parent) == trunk.as_ref() {
135 break;
136 }
137 bottom = parent;
138 }
139 bottom
140 };
141
142 if bottom == current {
143 if parent_of(¤t)?.is_none() && children_of(¤t)?.is_empty() {
144 bail!("{current} is not in a stack");
145 }
146 println!("{current} is already at the bottom of the stack");
147 return Ok(());
148 }
149 git::checkout(&bottom)
150}
151
152pub fn print_stack(reviews: &BTreeMap<String, String>) -> Result<()> {
153 let current = git::current_branch()?;
154 let parents = parent_map()?;
155 let root = root_for(¤t, &parents);
156 let children = children_map(&parents);
157 let trunk = trunk_branch(&git::local_branches()?);
158
159 let mut lines = Vec::new();
160 collect_tree_lines(
161 &root,
162 ¤t,
163 trunk.as_deref(),
164 &children,
165 reviews,
166 0,
167 &mut BTreeSet::new(),
168 &mut lines,
169 );
170
171 for line in lines.iter().rev() {
174 anstream::println!("{line}");
175 }
176
177 for branch in branch_and_descendants(&root)? {
178 if let Some(parent) = parents.get(&branch)
179 && let Some(hint) = behind_parent_hint(&branch, parent)
180 {
181 anstream::println!("{} {hint}", style::paint(style::HINT, "hint:"));
182 }
183 }
184 Ok(())
185}
186
187pub fn print_all_stacks(reviews: &BTreeMap<String, String>) -> Result<()> {
191 let current = git::current_branch()?;
192 let parents = parent_map()?;
193 let children = children_map(&parents);
194 let trunk = trunk_branch(&git::local_branches()?);
195
196 let mut roots = Vec::new();
201 let mut seen = BTreeSet::new();
202 for branch in parents
203 .iter()
204 .flat_map(|(child, parent)| [child.clone(), parent.clone()])
205 {
206 let root = root_for(&branch, &parents);
207 if seen.insert(root.clone()) {
208 roots.push(root);
209 }
210 }
211
212 if roots.is_empty() {
213 println!("no stacked branches");
214 return Ok(());
215 }
216
217 roots.sort_by_key(|root| Some(root.as_str()) == trunk.as_deref());
220
221 for (index, root) in roots.iter().enumerate() {
222 if index > 0 {
223 anstream::println!();
224 }
225 let mut lines = Vec::new();
226 collect_tree_lines(
227 root,
228 ¤t,
229 trunk.as_deref(),
230 &children,
231 reviews,
232 0,
233 &mut BTreeSet::new(),
234 &mut lines,
235 );
236 for line in lines.iter().rev() {
237 anstream::println!("{line}");
238 }
239 }
240
241 for (branch, parent) in &parents {
243 if let Some(hint) = behind_parent_hint(branch, parent) {
244 anstream::println!("{} {hint}", style::paint(style::HINT, "hint:"));
245 }
246 }
247 Ok(())
248}
249
250pub fn behind_parent_hint(branch: &str, parent: &str) -> Option<String> {
253 let behind = git::commits_behind(branch, parent)
254 .ok()
255 .filter(|count| *count > 0)?;
256 Some(format!(
257 "{branch} is {behind} commit{} behind {parent} - run `git stk restack`",
258 if behind == 1 { "" } else { "s" }
259 ))
260}
261
262#[allow(clippy::too_many_arguments)]
263fn collect_tree_lines(
264 branch: &str,
265 current: &str,
266 trunk: Option<&str>,
267 children: &BTreeMap<String, Vec<String>>,
268 reviews: &BTreeMap<String, String>,
269 depth: usize,
270 seen: &mut BTreeSet<String>,
271 lines: &mut Vec<String>,
272) {
273 let mut line = " ".repeat(depth);
275 if branch == current {
276 line.push_str(&style::paint(style::CURRENT, &format!("\u{25c9} {branch}")));
277 } else {
278 line.push_str("\u{25cb} ");
279 line.push_str(&style::paint(style::BRANCH, branch));
280 }
281 if Some(branch) == trunk {
282 line.push_str(&style::paint(style::DIM, " (trunk)"));
283 }
284 if let Some(id) = reviews.get(branch) {
286 line.push_str(&style::paint(style::DIM, &format!(" ({id})")));
287 }
288 lines.push(line);
289
290 if !seen.insert(branch.to_owned()) {
291 lines.push(format!("{}<cycle detected>", " ".repeat(depth + 1)));
292 return;
293 }
294
295 if let Some(branch_children) = children.get(branch) {
296 for child in branch_children {
297 collect_tree_lines(
298 child,
299 current,
300 trunk,
301 children,
302 reviews,
303 depth + 1,
304 seen,
305 lines,
306 );
307 }
308 }
309}