use crate::engine::Stack;
use crate::git::{GitRepo, checkout_branch_in, local_branch_exists_in, refs};
use anyhow::{Result, bail};
use colored::Colorize;
use dialoguer::{FuzzySelect, theme::ColorfulTheme};
use std::collections::VecDeque;
pub fn up(count: Option<usize>) -> Result<()> {
let repo = GitRepo::open()?;
let workdir = repo.workdir()?.to_path_buf();
let mut current = repo.current_branch()?;
let stack = Stack::load(&repo)?;
let steps = count.unwrap_or(1);
if steps == 0 {
return Ok(());
}
for _ in 0..steps {
let children: Vec<String> = stack
.branches
.get(¤t)
.map(|b| b.children.clone())
.unwrap_or_default();
if children.is_empty() {
if current == repo.current_branch()? {
println!(
"{}",
"Already at the top of the stack (no child branches).".dimmed()
);
return Ok(());
} else {
break;
}
}
current = if children.len() == 1 {
children[0].clone()
} else {
let selection = FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt("Multiple child branches - select one")
.items(&children)
.default(0)
.interact()?;
children[selection].clone()
};
}
let previous = repo.current_branch()?;
drop(repo);
switch_branch(&workdir, &previous, ¤t)?;
println!("Switched to branch '{}'", current.bright_cyan());
Ok(())
}
pub fn down(count: Option<usize>) -> Result<()> {
let repo = GitRepo::open()?;
let workdir = repo.workdir()?.to_path_buf();
let mut current = repo.current_branch()?;
let stack = Stack::load(&repo)?;
let steps = count.unwrap_or(1);
if steps == 0 {
return Ok(());
}
for _ in 0..steps {
let parent = stack.branches.get(¤t).and_then(|b| b.parent.clone());
match parent {
Some(p) => {
current = p;
}
None => {
if current == repo.current_branch()? {
if current == stack.trunk {
println!(
"{}",
"Already at the bottom of the stack (on trunk).".dimmed()
);
} else {
bail!("Branch '{}' has no tracked parent.", current);
}
return Ok(());
}
break;
}
}
}
let previous = repo.current_branch()?;
drop(repo);
switch_branch(&workdir, &previous, ¤t)?;
println!("Switched to branch '{}'", current.bright_cyan());
Ok(())
}
pub fn top() -> Result<()> {
let repo = GitRepo::open()?;
let workdir = repo.workdir()?.to_path_buf();
let mut current = repo.current_branch()?;
let stack = Stack::load(&repo)?;
loop {
let children: Vec<String> = stack
.branches
.get(¤t)
.map(|b| b.children.clone())
.unwrap_or_default();
if children.is_empty() {
break;
}
current = if children.len() == 1 {
children[0].clone()
} else {
let selection = FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt("Multiple child branches - select one")
.items(&children)
.default(0)
.interact()?;
children[selection].clone()
};
}
let original = repo.current_branch()?;
if current == original {
println!("{}", "Already at the top of the stack.".dimmed());
return Ok(());
}
drop(repo);
switch_branch(&workdir, &original, ¤t)?;
println!("Switched to branch '{}'", current.bright_cyan());
Ok(())
}
pub fn bottom() -> Result<()> {
let repo = GitRepo::open()?;
let workdir = repo.workdir()?.to_path_buf();
let current = repo.current_branch()?;
let stack = Stack::load(&repo)?;
drop(repo);
bottom_with_loaded_stack(&stack, &workdir, ¤t)
}
pub(crate) fn bottom_with_loaded_stack(
stack: &Stack,
workdir: &std::path::Path,
current: &str,
) -> Result<()> {
let current_stack = stack.current_stack(current);
let bottom_branch = current_stack.iter().find(|b| *b != &stack.trunk);
match bottom_branch {
Some(target) => {
if target == current {
println!("{}", "Already at the bottom of the stack.".dimmed());
return Ok(());
}
switch_branch(workdir, current, target)?;
println!("Switched to branch '{}'", target.bright_cyan());
}
None => {
println!(
"{}",
"No branches above trunk in the current stack.".dimmed()
);
}
}
Ok(())
}
pub fn prev() -> Result<()> {
let repo = GitRepo::open()?;
let workdir = repo.workdir()?.to_path_buf();
let current = repo.current_branch()?;
let prev_branch = refs::read_prev_branch(repo.inner())?;
match prev_branch {
Some(target) => {
if target == current {
println!(
"{}",
"Previous branch is the same as current branch.".dimmed()
);
return Ok(());
}
if !local_branch_exists_in(&workdir, &target) {
bail!("Previous branch '{}' no longer exists.", target);
}
drop(repo);
switch_branch(&workdir, ¤t, &target)?;
println!("Switched to branch '{}'", target.bright_cyan());
}
None => {
println!(
"{}",
"No previous branch recorded. Use checkout, up, down, etc. first.".dimmed()
);
}
}
Ok(())
}
pub fn next() -> Result<()> {
let repo = GitRepo::open()?;
let workdir = repo.workdir()?.to_path_buf();
let current = repo.current_branch()?;
let stack = Stack::load(&repo)?;
let mut queue = VecDeque::new();
enqueue_sorted_children(&stack, ¤t, &mut queue);
while let Some(candidate) = queue.pop_front() {
let is_merged = stack
.branches
.get(&candidate)
.and_then(|branch| branch.pr_state.as_deref())
.is_some_and(|state| state.eq_ignore_ascii_case("merged"));
if !is_merged {
drop(repo);
switch_branch(&workdir, ¤t, &candidate)?;
println!("Switched to branch '{}'", candidate.bright_cyan());
return Ok(());
}
enqueue_sorted_children(&stack, &candidate, &mut queue);
}
println!(
"{}",
"No unmerged branch remains above the current branch.".dimmed()
);
Ok(())
}
fn enqueue_sorted_children(stack: &Stack, branch: &str, queue: &mut VecDeque<String>) {
let mut children = stack
.branches
.get(branch)
.map(|branch| branch.children.clone())
.unwrap_or_default();
children.sort();
queue.extend(children);
}
fn switch_branch(workdir: &std::path::Path, previous: &str, target: &str) -> Result<()> {
let _ = refs::write_prev_branch_at(workdir, previous);
checkout_branch_in(workdir, target)
}