use crate::BisectAction;
use crate::ref_utils::resolve_ref;
pub(crate) async fn cmd_bisect(
action: &crate::BisectAction,
) -> Result<(), Box<dyn std::error::Error>> {
match action {
BisectAction::Start {
good: good_ref,
bad: bad_ref,
} => {
let repo = suture_core::repository::Repository::open(std::path::Path::new("."))?;
let all_patches = repo.all_patches();
let good_patch = resolve_ref(&repo, good_ref, &all_patches)?;
let bad_patch = resolve_ref(&repo, bad_ref, &all_patches)?;
let log = repo.log(None)?;
let good_idx = log
.iter()
.position(|p| p.id == good_patch.id)
.ok_or_else(|| format!("'{}' not found in history", good_ref))?;
let bad_idx = log
.iter()
.position(|p| p.id == bad_patch.id)
.ok_or_else(|| format!("'{}' not found in history", bad_ref))?;
let bad_ancestors = repo.dag().ancestors(&bad_patch.id);
if !bad_ancestors.contains(&good_patch.id) && good_patch.id != bad_patch.id {
return Err("'good' must be an ancestor of 'bad'".into());
}
let (older_idx, newer_idx) = if good_idx > bad_idx {
(good_idx, bad_idx) } else {
(bad_idx, good_idx) };
let remaining = older_idx - newer_idx - 1;
if remaining == 0 {
println!("Only one commit between good and bad:");
println!(
" {} {}",
&log[newer_idx + 1].id.to_hex()[..8],
log[newer_idx + 1].message.lines().next().unwrap_or("")
);
println!(" This is the first bad commit.");
return Ok(());
}
let midpoint_idx = (older_idx + newer_idx) / 2;
let midpoint = &log[midpoint_idx];
println!(
"Bisecting: {} commit(s) remaining between good ({}) and bad ({})",
remaining,
&good_patch.id.to_hex()[..8],
&bad_patch.id.to_hex()[..8]
);
println!();
println!(" Step: test commit {}", midpoint.id.to_hex());
println!(" {}", midpoint.message.lines().next().unwrap_or(""));
println!();
println!("To test this commit:");
println!(" suture reset {} --hard", midpoint.id.to_hex());
println!();
println!("Then mark as:");
if midpoint_idx > newer_idx + 1 {
println!(
" suture bisect start {} {} (if this commit is GOOD)",
good_ref,
&midpoint.id.to_hex()[..8]
);
} else {
println!(" First bad commit found: {}", midpoint.id.to_hex());
}
if midpoint_idx < older_idx - 1 {
println!(
" suture bisect start {} {} (if this commit is BAD)",
&midpoint.id.to_hex()[..8],
bad_ref
);
} else {
println!(" First bad commit found: {}", midpoint.id.to_hex());
}
}
BisectAction::Reset => {
let repo = suture_core::repository::Repository::open(std::path::Path::new("."))?;
let (branch_name, _) = repo.head().map_err(|e| e.to_string())?;
println!("Bisect reset. You are on branch '{}'.", branch_name);
}
BisectAction::Run {
good: good_ref,
bad: bad_ref,
cmd,
} => {
if cmd.is_empty() {
return Err("bisect run requires a command to execute".into());
}
let repo_path = std::path::Path::new(".");
let (original_branch, original_head) = {
let repo = suture_core::repository::Repository::open(repo_path)?;
repo.head()?
};
let (ordered_log, good_idx, bad_idx) = {
let repo = suture_core::repository::Repository::open(repo_path)?;
let all_patches = repo.all_patches();
let good_patch = resolve_ref(&repo, good_ref, &all_patches)?;
let bad_patch = resolve_ref(&repo, bad_ref, &all_patches)?;
let log = repo.log(None)?;
let good_idx = log
.iter()
.position(|p| p.id == good_patch.id)
.ok_or_else(|| format!("'{}' not found in history", good_ref))?;
let bad_idx = log
.iter()
.position(|p| p.id == bad_patch.id)
.ok_or_else(|| format!("'{}' not found in history", bad_ref))?;
let bad_ancestors = repo.dag().ancestors(&bad_patch.id);
if !bad_ancestors.contains(&good_patch.id) && good_patch.id != bad_patch.id {
return Err("'good' must be an ancestor of 'bad'".into());
}
(log, good_idx, bad_idx)
};
let (older_idx, newer_idx) = if good_idx > bad_idx {
(good_idx, bad_idx) } else {
(bad_idx, good_idx) };
let program = &cmd[0];
let args = &cmd[1..];
println!(
"bisect run '{}' with good={} bad={}",
cmd.join(" "),
&ordered_log[older_idx].id.to_hex()[..8],
&ordered_log[newer_idx].id.to_hex()[..8]
);
println!();
let mut current_good = older_idx;
let mut current_bad = newer_idx;
let mut step = 0u32;
loop {
step += 1;
let remaining = current_good.saturating_sub(current_bad + 1);
if remaining == 0 {
let first_bad = &ordered_log[current_good - 1];
println!("First bad commit found after {} step(s):", step);
println!(
" {} {}",
first_bad.id.to_hex(),
first_bad.message.lines().next().unwrap_or("(no message)")
);
break;
}
let midpoint_idx = (current_good + current_bad) / 2;
let midpoint = &ordered_log[midpoint_idx];
{
let mut repo = suture_core::repository::Repository::open(repo_path)?;
repo.reset(
&midpoint.id.to_hex(),
suture_core::repository::ResetMode::Hard,
)?;
}
println!(
"[step {}] Testing {} ({} remaining)...",
step,
&midpoint.id.to_hex()[..8],
remaining
);
let result = std::process::Command::new(program)
.args(args)
.current_dir(repo_path)
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status();
match result {
Ok(status) => {
let is_good = status.success();
if is_good {
println!(" -> GOOD (exit 0)");
current_good = midpoint_idx - 1;
} else {
let code = status.code().unwrap_or(1);
println!(" -> BAD (exit {})", code);
current_bad = midpoint_idx + 1;
}
}
Err(e) => {
eprintln!(" -> Command failed to execute: {}", e);
eprintln!(" Aborting bisect run.");
break;
}
}
println!();
}
let mut repo = suture_core::repository::Repository::open(repo_path)?;
repo.reset(
&original_head.to_hex(),
suture_core::repository::ResetMode::Hard,
)?;
let _ = repo.checkout(&original_branch);
}
}
Ok(())
}