use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;
use clap_complete::ArgValueCompleter;
use color_eyre::eyre::{Result, WrapErr as _, eyre};
use girt::bisect::{Bisect, Mark, ResetTo, Start, Step, Terms, Verdict};
use girt::display::DateFormat;
use girt::{ObjectId, Repository, Signature};
use crate::common::{self, display_bytes};
use crate::{Context, complete};
#[derive(Debug, clap::Subcommand)]
pub enum Command {
Start {
#[arg(long, alias = "term-bad", value_name = "TERM")]
term_new: Option<String>,
#[arg(long, alias = "term-good", value_name = "TERM")]
term_old: Option<String>,
#[arg(long)]
no_checkout: bool,
#[arg(long)]
first_parent: bool,
#[arg(add = ArgValueCompleter::new(complete::revisions))]
revisions: Vec<String>,
#[arg(last = true)]
paths: Vec<String>,
},
#[command(visible_alias = "bad")]
New {
#[arg(add = ArgValueCompleter::new(complete::revisions))]
revisions: Vec<String>,
},
#[command(visible_alias = "good")]
Old {
#[arg(add = ArgValueCompleter::new(complete::revisions))]
revisions: Vec<String>,
},
Skip {
#[arg(add = ArgValueCompleter::new(complete::revisions))]
revisions: Vec<String>,
},
Next,
Reset {
#[arg(add = ArgValueCompleter::new(complete::revisions))]
commit: Option<String>,
},
Log,
Replay {
file: PathBuf,
},
Visualize,
Terms,
Run {
#[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
}
pub fn run(command: Command, context: &Context) -> Result<ExitCode> {
let repository = context.repository()?;
let read_only = matches!(command, Command::Log | Command::Visualize | Command::Terms);
let committer = match Signature::committer(repository.config(), &context.environment) {
Ok(committer) => committer,
Err(_) if read_only => Signature {
name: Vec::new(),
email: Vec::new(),
seconds: 0,
offset_minutes: 0,
},
Err(error) => return Err(error).wrap_err("reading the committer identity"),
};
let open = || -> Result<Bisect<'_>> {
Bisect::open(&repository, committer.clone())?
.ok_or_else(|| eyre!("no bisection in progress; start one with `girt bisect start`"))
};
let step = match command {
Command::Start {
term_new,
term_old,
no_checkout,
first_parent,
revisions,
paths,
} => {
let terms = match (term_new, term_old) {
(None, None) => Terms::default(),
(new, old) => Terms::new(
new.unwrap_or_else(|| "bad".into()),
old.unwrap_or_else(|| "good".into()),
)?,
};
let ids = resolve_all(&repository, &revisions)?;
let mut start = Start::default()
.with_terms(terms)
.with_no_checkout(no_checkout)
.with_first_parent(first_parent)
.with_paths(paths.into_iter().map(String::into_bytes).collect());
if let Some((&new, olds)) = ids.split_first() {
start = start.with_new(new).with_olds(olds.to_vec());
}
let (bisect, step) = Bisect::start(&repository, committer.clone(), start)?;
report(&repository, &bisect, step)?
}
Command::New { revisions } => mark(&repository, open()?, Mark::New, &revisions)?,
Command::Old { revisions } => mark(&repository, open()?, Mark::Old, &revisions)?,
Command::Skip { revisions } => {
let bisect = open()?;
let ids = if revisions.is_empty() {
vec![bisect.current()?]
} else {
skip_ids(&repository, &revisions)?
};
let mut bisect = bisect;
let step = bisect.mark(Mark::Skip, &ids)?;
report(&repository, &bisect, step)?
}
Command::Next => {
let mut bisect = open()?;
let step = bisect.next_step()?;
report(&repository, &bisect, step)?
}
Command::Reset { commit } => {
let to = match commit {
Some(commit) => ResetTo::Commit(common::resolve_commit(
&repository,
&repository.objects()?,
&commit,
)?),
None => ResetTo::Original,
};
match Bisect::open(&repository, committer.clone())? {
Some(bisect) => bisect.reset(to)?,
None => println!("We are not bisecting."),
}
ExitCode::SUCCESS
}
Command::Log => {
let mut out = common::stdout();
out.write_all(&open()?.log()?)?;
out.flush()?;
ExitCode::SUCCESS
}
Command::Replay { file } => {
let log =
std::fs::read(&file).wrap_err_with(|| format!("reading {}", file.display()))?;
let (bisect, step) = Bisect::replay(&repository, committer.clone(), &log)?;
report(&repository, &bisect, step)?
}
Command::Visualize => {
let bisect = open()?;
let candidates = bisect.candidates()?;
let objects = repository.objects()?;
let mut out = common::stdout();
for id in candidates.testable() {
let commit = objects.read_commit(id)?;
writeln!(out, "{id} {}", display_bytes(&subject(commit.message())))?;
}
out.flush()?;
ExitCode::SUCCESS
}
Command::Terms => {
let bisect = open()?;
let terms = bisect.terms();
println!(
"Your current terms are '{}' for the old state\nand '{}' for the new state.",
terms.old_term(),
terms.new_term()
);
ExitCode::SUCCESS
}
Command::Run { command } => {
let root = repository
.worktree()
.ok_or_else(|| eyre!("bisect run needs a working tree"))?
.to_path_buf();
let mut bisect = open()?;
let step = bisect.run(|commit| {
println!("running {}", command.join(" "));
let status = std::process::Command::new(&command[0])
.args(&command[1..])
.current_dir(&root)
.status();
let verdict = match status {
Ok(status) => Verdict::from_exit_code(status.code()),
Err(error) => {
eprintln!("bisect run failed to start {}: {error}", command[0]);
Verdict::Abort
}
};
if verdict == Verdict::Abort {
eprintln!("bisect run failed at {commit}");
}
verdict
});
match step {
Ok(step) => report(&repository, &bisect, step)?,
Err(girt::bisect::Error::Aborted(_)) => ExitCode::FAILURE,
Err(error) => return Err(error.into()),
}
}
};
Ok(step)
}
fn mark(
repository: &Repository,
mut bisect: Bisect<'_>,
mark: Mark,
revisions: &[String],
) -> Result<ExitCode> {
let ids = if revisions.is_empty() {
vec![bisect.current()?]
} else {
resolve_all(repository, revisions)?
};
let step = bisect.mark(mark, &ids)?;
report(repository, &bisect, step)
}
fn resolve_all(repository: &Repository, revisions: &[String]) -> Result<Vec<ObjectId>> {
let objects = repository.objects()?;
revisions
.iter()
.map(|revision| common::resolve_commit(repository, &objects, revision))
.collect()
}
fn skip_ids(repository: &Repository, revisions: &[String]) -> Result<Vec<ObjectId>> {
let objects = repository.objects()?;
let mut ids = Vec::new();
for revision in revisions {
if revision.contains("..") {
let range: girt::revision::Range = revision
.parse()
.map_err(|error| eyre!("parsing `{revision}`: {error}"))?;
let selection =
girt::revision::Resolver::new(repository, &objects)?.select(&[range])?;
ids.extend(objects.walk_range(&selection.include, &selection.exclude)?);
} else {
ids.push(common::resolve_commit(repository, &objects, revision)?);
}
}
Ok(ids)
}
fn report(repository: &Repository, bisect: &Bisect<'_>, step: Step) -> Result<ExitCode> {
let objects = repository.objects()?;
let terms = bisect.terms();
let mut out = common::stdout();
let subject_of = |id: ObjectId| -> Result<String> {
Ok(display_bytes(&subject(objects.read_commit(id)?.message())).into_owned())
};
let plural = |n: usize, word: &str| {
if n == 1 {
format!("{n} {word}")
} else {
format!("{n} {word}s")
}
};
let code = match step {
Step::Waiting { new, olds } => {
let status = match (new, olds) {
(false, 0) => format!(
"waiting for both '{}' and '{}' commits",
terms.old_term(),
terms.new_term()
),
(false, olds) => format!(
"waiting for '{}' commit, {olds} '{}' commit known",
terms.new_term(),
terms.old_term()
),
(true, _) => format!(
"waiting for '{}' commit(s), '{}' commit known",
terms.old_term(),
terms.new_term()
),
};
writeln!(out, "status: {status}")?;
ExitCode::SUCCESS
}
Step::MergeBase(id) => {
writeln!(out, "Bisecting: a merge base must be tested")?;
writeln!(out, "[{id}] {}", subject_of(id)?)?;
ExitCode::SUCCESS
}
Step::Test {
commit,
remaining,
steps,
} => {
writeln!(
out,
"Bisecting: {} left to test after this (roughly {})",
plural(remaining, "revision"),
plural(steps, "step")
)?;
writeln!(out, "[{commit}] {}", subject_of(commit)?)?;
ExitCode::SUCCESS
}
Step::Found(id) => {
writeln!(out, "{id} is the first '{}' commit", terms.new_term())?;
let commit = objects.read_commit(id)?;
writeln!(out, "commit {id}")?;
if let Some(author) = commit.author()? {
writeln!(
out,
"Author: {} <{}>",
display_bytes(author.name),
display_bytes(author.email)
)?;
writeln!(out, "Date: {}", author.format_date(DateFormat::Default)?)?;
}
writeln!(out)?;
for line in commit.message().trim_ascii_end().split(|&b| b == b'\n') {
writeln!(out, " {}", display_bytes(line))?;
}
ExitCode::SUCCESS
}
Step::OnlySkipped(possible) => {
writeln!(out, "There are only 'skip'ped commits left to test.")?;
writeln!(
out,
"The first '{}' commit could be any of:",
terms.new_term()
)?;
for id in possible {
writeln!(out, "{id}")?;
}
writeln!(out, "We cannot bisect more!")?;
ExitCode::from(2)
}
other => return Err(eyre!("unexpected bisect step {other:?}")),
};
out.flush()?;
Ok(code)
}
fn subject(message: &[u8]) -> Vec<u8> {
let mut lines = message
.split(|&b| b == b'\n')
.map(<[u8]>::trim_ascii)
.skip_while(|line| line.is_empty())
.take_while(|line| !line.is_empty());
let mut subject = lines.next().unwrap_or_default().to_vec();
for line in lines {
subject.push(b' ');
subject.extend_from_slice(line);
}
subject
}