use crate::engine::{BranchMetadata, Stack};
use crate::git::{GitRepo, RebaseResult};
use anyhow::{bail, Result};
use colored::Colorize;
use dialoguer::theme::ColorfulTheme;
use dialoguer::FuzzySelect;
pub fn run(target: Option<String>, restack: bool) -> Result<()> {
let repo = GitRepo::open()?;
let stack = Stack::load(&repo)?;
let current = repo.current_branch()?;
let trunk = repo.trunk_branch()?;
if current == trunk {
bail!("Cannot reparent trunk. Checkout a stacked branch first.");
}
let old_meta = BranchMetadata::read(repo.inner(), ¤t)?.ok_or_else(|| {
anyhow::anyhow!(
"Branch '{}' is not tracked by stax. Run `st branch track` first.",
current
)
})?;
let descendants = stack.descendants(¤t);
let new_parent = match target {
Some(t) => {
if repo.branch_commit(&t).is_err() {
bail!("Branch '{}' does not exist", t);
}
t
}
None => pick_parent_interactively(&repo, ¤t, &trunk, &descendants)?,
};
if new_parent == current {
bail!("Cannot reparent a branch onto itself.");
}
if old_meta.parent_branch_name == new_parent {
println!(
"{}",
format!(
"'{}' is already parented onto '{}'. Nothing to do.",
current, new_parent
)
.dimmed()
);
return Ok(());
}
if descendants.contains(&new_parent) {
bail!(
"Cannot reparent '{}' onto '{}': would create circular dependency.\n\
'{}' is a descendant of '{}'.",
current,
new_parent,
new_parent,
current
);
}
let mut subtree = vec![current.clone()];
subtree.extend(descendants.iter().cloned());
let old_parent_name = old_meta.parent_branch_name.clone();
let old_parent_rev = old_meta.parent_branch_revision.clone();
let parent_rev = repo.branch_commit(&new_parent)?;
let merge_base = repo
.merge_base(&new_parent, ¤t)
.unwrap_or_else(|_| parent_rev.clone());
let updated = BranchMetadata {
parent_branch_name: new_parent.clone(),
parent_branch_revision: merge_base.clone(),
..old_meta
};
if !restack {
updated.write(repo.inner(), ¤t)?;
}
println!(
"✓ Reparented '{}' onto '{}'",
current.green(),
new_parent.blue()
);
if subtree.len() > 1 {
println!(
" {} descendant branch(es) moved with it:",
(subtree.len() - 1).to_string().cyan()
);
for desc in &subtree[1..] {
println!(" {}", desc.dimmed());
}
}
if restack {
println!();
println!("{}", "Restacking moved branches...".bold());
let rebase_upstream = resolve_rebase_upstream(
&repo,
&old_parent_name,
&old_parent_rev,
¤t,
&merge_base,
)?;
match repo.rebase_branch_onto_with_provenance(
¤t,
&new_parent,
&rebase_upstream,
false,
)? {
RebaseResult::Success => {
let new_parent_rev = repo.branch_commit(&new_parent)?;
let mut persisted = updated.clone();
persisted.parent_branch_revision = new_parent_rev;
persisted.write(repo.inner(), ¤t)?;
println!(
" {} rebased '{}' onto '{}'",
"✓".green(),
current,
new_parent
);
if subtree.len() > 1 {
super::restack::run(false)?;
}
}
RebaseResult::Conflict => {
bail!(
"Rebase conflict while rebasing '{}' onto '{}'. \
Resolve conflicts, then run `st continue` or `st abort`.",
current,
new_parent
);
}
}
if repo.current_branch()? != current {
let _ = repo.checkout(¤t);
}
} else {
println!(
"{}",
"Run `st restack` to rebase the moved branches onto their new parent.".yellow()
);
}
Ok(())
}
fn resolve_rebase_upstream(
repo: &GitRepo,
old_parent_name: &str,
old_parent_rev: &str,
target: &str,
merge_base: &str,
) -> Result<String> {
if let Ok(tip) = repo.branch_commit(old_parent_name) {
if repo.is_ancestor(&tip, target)? {
return Ok(tip);
}
}
if !old_parent_rev.is_empty() && repo.is_ancestor(old_parent_rev, target)? {
return Ok(old_parent_rev.to_string());
}
Ok(merge_base.to_string())
}
fn pick_parent_interactively(
repo: &GitRepo,
current: &str,
trunk: &str,
descendants: &[String],
) -> Result<String> {
let mut branches = repo.list_branches()?;
branches.retain(|b| b != current && !descendants.contains(b));
branches.sort();
if let Some(pos) = branches.iter().position(|b| b == trunk) {
branches.remove(pos);
branches.insert(0, trunk.to_string());
}
if branches.is_empty() {
bail!("No branches available as a new parent");
}
let selection = FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt(format!(
"Select new parent for '{}' (and all its descendants)",
current
))
.items(&branches)
.default(0)
.interact()?;
Ok(branches[selection].clone())
}