use std::collections::{BTreeMap, BTreeSet};
use anyhow::{Result, bail};
use super::{
branch_and_descendants, children_map, children_of, parent_map, parent_of, root_for,
trunk_branch,
};
use crate::git;
use crate::prompt;
use crate::style;
fn pick_child(title: &str, children: &[String]) -> anyhow::Result<Option<String>> {
let painted: Vec<String> = children
.iter()
.map(|child| style::paint(style::BRANCH, child))
.collect();
Ok(prompt::pick(title, &painted)?.map(|index| children[index].clone()))
}
pub fn print_parent(branch: Option<&str>) -> Result<()> {
let branch = branch
.map(str::to_owned)
.map_or_else(git::current_branch, Ok)?;
match parent_of(&branch)? {
Some(parent) => println!("{parent}"),
None => bail!("{branch} has no stack parent"),
}
Ok(())
}
pub fn print_children(branch: Option<&str>) -> Result<()> {
let branch = branch
.map(str::to_owned)
.map_or_else(git::current_branch, Ok)?;
for child in children_of(&branch)? {
println!("{child}");
}
Ok(())
}
pub fn checkout_parent() -> Result<()> {
let current = git::current_branch()?;
let Some(parent) = parent_of(¤t)? else {
bail!("{current} has no stack parent");
};
git::checkout(&parent)
}
pub fn checkout_child(branch: Option<&str>) -> Result<()> {
let current = git::current_branch()?;
let children = children_of(¤t)?;
let child = match (branch, children.as_slice()) {
(Some(branch), _) => {
if children.iter().any(|child| child == branch) {
branch.to_owned()
} else {
bail!("{branch} is not a stack child of {current}");
}
}
(None, [child]) => child.to_owned(),
(None, []) => bail!("{current} has no stack children"),
(None, _) => {
match pick_child(
&format!("{current} has multiple stack children:"),
&children,
)? {
Some(child) => child,
None => bail!("choose one with `git stk up <branch>`"),
}
}
};
git::checkout(&child)
}
pub fn checkout_top() -> Result<()> {
let current = git::current_branch()?;
let mut top = current.clone();
loop {
let children = children_of(&top)?;
match children.as_slice() {
[] => break,
[child] => top = child.clone(),
_ => match pick_child(&format!("{top} has multiple stack children:"), &children)? {
Some(child) => top = child,
None => bail!("walk up from {top} with `git stk up <branch>`"),
},
}
}
if top == current {
if children_of(¤t)?.is_empty() && parent_of(¤t)?.is_none() {
bail!("{current} is not in a stack");
}
println!("{current} is already at the top of the stack");
return Ok(());
}
git::checkout(&top)
}
pub fn checkout_bottom() -> Result<()> {
let current = git::current_branch()?;
let trunk = trunk_branch(&git::local_branches()?);
let bottom = if Some(¤t) == trunk.as_ref() {
let children = children_of(¤t)?;
match children.as_slice() {
[child] => child.clone(),
[] => bail!("{current} has no stacked branches"),
_ => {
match pick_child(
&format!("{current} has multiple stack children:"),
&children,
)? {
Some(child) => child,
None => bail!("choose one with `git stk up <branch>`"),
}
}
}
} else {
let mut bottom = current.clone();
while let Some(parent) = parent_of(&bottom)? {
if Some(&parent) == trunk.as_ref() {
break;
}
bottom = parent;
}
bottom
};
if bottom == current {
if parent_of(¤t)?.is_none() && children_of(¤t)?.is_empty() {
bail!("{current} is not in a stack");
}
println!("{current} is already at the bottom of the stack");
return Ok(());
}
git::checkout(&bottom)
}
pub fn print_stack() -> Result<()> {
let current = git::current_branch()?;
let parents = parent_map()?;
let root = root_for(¤t, &parents);
let children = children_map(&parents);
let trunk = trunk_branch(&git::local_branches()?);
let mut lines = Vec::new();
collect_tree_lines(
&root,
¤t,
trunk.as_deref(),
&children,
0,
&mut BTreeSet::new(),
&mut lines,
);
for line in lines.iter().rev() {
anstream::println!("{line}");
}
for branch in branch_and_descendants(&root)? {
if let Some(parent) = parents.get(&branch)
&& let Some(hint) = behind_parent_hint(&branch, parent)
{
anstream::println!("{} {hint}", style::paint(style::HINT, "hint:"));
}
}
Ok(())
}
pub fn behind_parent_hint(branch: &str, parent: &str) -> Option<String> {
let behind = git::commits_behind(branch, parent)
.ok()
.filter(|count| *count > 0)?;
Some(format!(
"{branch} is {behind} commit{} behind {parent} - run `git stk restack`",
if behind == 1 { "" } else { "s" }
))
}
#[allow(clippy::too_many_arguments)]
fn collect_tree_lines(
branch: &str,
current: &str,
trunk: Option<&str>,
children: &BTreeMap<String, Vec<String>>,
depth: usize,
seen: &mut BTreeSet<String>,
lines: &mut Vec<String>,
) {
let mut line = " ".repeat(depth);
if branch == current {
line.push_str(&style::paint(style::CURRENT, &format!("\u{25c9} {branch}")));
} else {
line.push_str("\u{25cb} ");
line.push_str(&style::paint(style::BRANCH, branch));
}
if Some(branch) == trunk {
line.push_str(&style::paint(style::DIM, " (trunk)"));
}
lines.push(line);
if !seen.insert(branch.to_owned()) {
lines.push(format!("{}<cycle detected>", " ".repeat(depth + 1)));
return;
}
if let Some(branch_children) = children.get(branch) {
for child in branch_children {
collect_tree_lines(child, current, trunk, children, depth + 1, seen, lines);
}
}
}