use crate::dag;
use crate::error::VcsError;
use crate::hash::ObjectId;
use crate::store::Store;
#[derive(Clone, Debug)]
pub struct BisectState {
path: Vec<ObjectId>,
lo: usize,
hi: usize,
}
#[derive(Clone, Debug)]
pub enum BisectStep {
Test(ObjectId),
Found(ObjectId),
}
pub fn bisect_start(
store: &dyn Store,
good: ObjectId,
bad: ObjectId,
) -> Result<(BisectState, BisectStep), VcsError> {
let path = dag::find_path(store, good, bad)?;
if path.len() <= 2 {
let state = BisectState {
path: path.clone(),
lo: 0,
hi: path.len().saturating_sub(1),
};
return Ok((state, BisectStep::Found(bad)));
}
let state = BisectState {
lo: 0,
hi: path.len() - 1,
path,
};
let mid = usize::midpoint(state.lo, state.hi);
Ok((state.clone(), BisectStep::Test(state.path[mid])))
}
pub fn bisect_step(state: &mut BisectState, is_good: bool) -> BisectStep {
let mid = usize::midpoint(state.lo, state.hi);
if is_good {
state.lo = mid;
} else {
state.hi = mid;
}
if state.hi - state.lo <= 1 {
return BisectStep::Found(state.path[state.hi]);
}
let new_mid = usize::midpoint(state.lo, state.hi);
BisectStep::Test(state.path[new_mid])
}
#[must_use]
pub const fn bisect_remaining(state: &BisectState) -> usize {
let range = state.hi - state.lo;
if range <= 1 {
0
} else {
(usize::BITS - range.leading_zeros()) as usize
}
}
#[cfg(test)]
#[allow(clippy::cast_possible_truncation)]
mod tests {
use super::*;
use crate::MemStore;
use crate::object::{CommitObject, Object};
fn build_linear(n: usize) -> Result<(MemStore, Vec<ObjectId>), Box<dyn std::error::Error>> {
let mut store = MemStore::new();
let mut ids = Vec::new();
for i in 0..n {
let parents = if i == 0 { vec![] } else { vec![ids[i - 1]] };
let commit = CommitObject::builder(
ObjectId::from_bytes([i as u8; 32]),
"test",
"test",
format!("commit {i}"),
)
.parents(parents)
.timestamp(i as u64 * 100)
.build();
let id = store.put(&Object::Commit(commit))?;
ids.push(id);
}
Ok((store, ids))
}
#[test]
fn bisect_finds_in_linear_history() -> Result<(), Box<dyn std::error::Error>> {
let (store, ids) = build_linear(8)?;
let (mut state, step) = bisect_start(&store, ids[0], ids[7])?;
let breaking_index = 5;
let mut steps = 0;
let mut current_step = step;
loop {
match current_step {
BisectStep::Found(id) => {
assert_eq!(id, ids[breaking_index]);
break;
}
BisectStep::Test(id) => {
let idx = ids
.iter()
.position(|i| *i == id)
.ok_or("commit not found in ids")?;
let is_good = idx < breaking_index;
current_step = bisect_step(&mut state, is_good);
steps += 1;
assert!(steps <= 10, "bisect should converge");
}
}
}
Ok(())
}
#[test]
fn bisect_adjacent_commits() -> Result<(), Box<dyn std::error::Error>> {
let (store, ids) = build_linear(2)?;
let (_state, step) = bisect_start(&store, ids[0], ids[1])?;
assert!(matches!(step, BisectStep::Found(id) if id == ids[1]));
Ok(())
}
#[test]
fn bisect_remaining_count() {
let state = BisectState {
path: vec![ObjectId::ZERO; 16],
lo: 0,
hi: 15,
};
assert!(bisect_remaining(&state) <= 4);
}
}