use anyhow::Result;
use clap::ArgAction;
use crate::commands::Run;
use crate::providers::{detect_provider, review_provider};
use crate::style;
use crate::{git, stack};
#[derive(Debug, clap::Args)]
pub struct Repair {
#[arg(long, action = ArgAction::SetTrue)]
dry_run: bool,
}
impl Run for Repair {
fn run(self) -> Result<()> {
repair(self.dry_run)
}
}
pub fn repair(dry_run: bool) -> Result<()> {
let branches = git::local_branches()?;
let trunk = stack::trunk_branch(&branches);
let provider = detect_provider()
.ok()
.map(|provider| (provider.kind, review_provider(provider.kind)));
let mut repaired = 0;
let mut verified = 0;
let mut unresolved = 0;
for branch in &branches {
if Some(branch.as_str()) == trunk.as_deref() {
continue;
}
if let Some(parent) = stack::parent_for_branch(branch)? {
if !branches.contains(&parent) {
anstream::println!(
"{}",
style::warn(&format!(
"{branch}: parent {parent} does not exist locally; \
fix with `git stk adopt` or `git stk detach {branch}`"
))
);
unresolved += 1;
continue;
}
let base_valid = matches!(
stack::base_for_branch(branch)?,
Some(base) if git::is_ancestor(&base, branch).unwrap_or(false)
);
if base_valid {
verified += 1;
} else {
anstream::println!(
"{}: {} fork point from {}",
style::branch(branch),
if dry_run {
"would re-record"
} else {
"re-recorded"
},
style::branch(&parent)
);
if !dry_run {
stack::record_base(branch, &parent);
}
repaired += 1;
}
continue;
}
let mut found: Option<(String, String)> = None;
if let Some((kind, review_provider)) = &provider
&& let Ok(Some(review)) = review_provider.review_for_branch(branch)
&& review.branch == *branch
&& review.base != *branch
{
if branches.contains(&review.base) {
found = Some((review.base.clone(), format!("{kind} review {}", review.id)));
} else {
anstream::println!(
"{}",
style::warn(&format!(
"{branch}: review {} targets {}, which is not a local branch",
review.id, review.base
))
);
}
}
if found.is_none() {
match nearest_ancestor_branch(branch, &branches)? {
Ancestry::One(parent) => found = Some((parent, "ancestry".to_owned())),
Ancestry::None => {
anstream::println!(
"{}",
style::warn(&format!(
"{branch}: no parent found; attach manually with \
`git stk adopt {branch} --parent <parent>`"
))
);
}
Ancestry::Ambiguous(candidates) => {
anstream::println!(
"{}",
style::warn(&format!(
"{branch}: ambiguous parent candidates ({}); attach manually with \
`git stk adopt`",
candidates.join(", ")
))
);
}
}
}
match found {
Some((parent, source)) => {
anstream::println!(
"{}: {} parent {} {}",
style::branch(branch),
if dry_run { "would set" } else { "set" },
style::branch(&parent),
style::dim(&format!("(from {source})"))
);
if !dry_run {
stack::set_parent_for_branch(branch, &parent)?;
stack::record_base(branch, &parent);
}
repaired += 1;
}
None => unresolved += 1,
}
}
anstream::println!(
"{}",
style::success(&format!(
"repair complete: {repaired} {}repaired, {verified} verified, {unresolved} unresolved",
if dry_run { "would be " } else { "" }
))
);
Ok(())
}
enum Ancestry {
One(String),
None,
Ambiguous(Vec<String>),
}
fn nearest_ancestor_branch(branch: &str, branches: &[String]) -> Result<Ancestry> {
let tip = git::rev_parse(branch)?;
let mut candidates: Vec<(String, String)> = Vec::new();
for other in branches {
if other == branch {
continue;
}
let other_tip = git::rev_parse(other)?;
if other_tip != tip && git::is_ancestor(other, branch)? {
candidates.push((other.clone(), other_tip));
}
}
let nearest: Vec<String> = candidates
.iter()
.filter(|(candidate, candidate_tip)| {
!candidates.iter().any(|(other, other_tip)| {
other != candidate
&& other_tip != candidate_tip
&& git::is_ancestor(candidate, other).unwrap_or(false)
})
})
.map(|(candidate, _)| candidate.clone())
.collect();
Ok(match nearest.len() {
0 => Ancestry::None,
1 => Ancestry::One(nearest.into_iter().next().expect("one candidate")),
_ => Ancestry::Ambiguous(nearest),
})
}