1use std::collections::{BTreeSet, HashSet};
30use std::fs;
31use std::io;
32use std::path::PathBuf;
33
34use crate::hash::{self, HEX_LEN, Hash, ZERO};
35use crate::layout::RepoLayout;
36use crate::object::Object;
37use crate::store::ObjectStore;
38
39pub const BISECT_FILE: &str = "bisect";
41
42const MAX_BISECT_FILE_BYTES: u64 = 1024 * 1024;
43
44const MAX_ANCESTORS: usize = 10_000;
46
47#[derive(Debug, thiserror::Error)]
49pub enum BisectError {
50 #[error("no bisect in progress")]
51 NoBisectInProgress,
52 #[error("bisect state on disk is malformed")]
53 InvalidBisectState,
54 #[error(transparent)]
55 Io(#[from] io::Error),
56 #[error(transparent)]
57 Object(#[from] crate::object::MkitError),
58 #[error(transparent)]
59 Store(#[from] crate::store::StoreError),
60}
61
62pub type BisectResult<T> = Result<T, BisectError>;
64
65#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct BisectState {
71 pub orig_head: Hash,
73 pub orig_branch: Option<String>,
75 pub bad_hash: Option<Hash>,
77 pub good_hashes: Vec<Hash>,
79 pub skipped: BTreeSet<Hash>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum BisectStep {
88 Testing { hash: Hash, remaining: usize },
91 Found(Hash),
93 Ambiguous { bad: Hash, skipped: Vec<Hash> },
99 NeedMore,
101}
102
103#[must_use]
105pub fn is_bisect_in_progress(layout: &RepoLayout) -> bool {
106 layout.bisect_file().is_file()
107}
108
109pub fn read_state(layout: &RepoLayout) -> BisectResult<BisectState> {
115 let path = layout.bisect_file();
116 let meta = match fs::metadata(&path) {
117 Ok(m) => m,
118 Err(e) if e.kind() == io::ErrorKind::NotFound => {
119 return Err(BisectError::NoBisectInProgress);
120 }
121 Err(e) => return Err(BisectError::Io(e)),
122 };
123 if meta.len() == 0 {
124 return Err(BisectError::InvalidBisectState);
125 }
126 if meta.len() > MAX_BISECT_FILE_BYTES {
127 return Err(BisectError::InvalidBisectState);
128 }
129 let raw = fs::read_to_string(&path)?;
130
131 let mut lines = raw.split('\n');
132
133 let orig_head_line = lines.next().ok_or(BisectError::InvalidBisectState)?;
134 let orig_head_trim = orig_head_line.trim_end_matches(['\r', ' ']);
135 if orig_head_trim.is_empty() {
136 return Err(BisectError::InvalidBisectState);
137 }
138 let orig_head = hash::from_hex(orig_head_trim).map_err(|_| BisectError::InvalidBisectState)?;
139
140 let branch_line = lines.next().ok_or(BisectError::InvalidBisectState)?;
141 let branch_trim = branch_line.trim_end_matches(['\r', ' ']);
142 let orig_branch = if branch_trim.is_empty() {
143 None
144 } else {
145 Some(branch_trim.to_string())
146 };
147
148 let bad_line = lines.next().ok_or(BisectError::InvalidBisectState)?;
149 let bad_trim = bad_line.trim_end_matches(['\r', ' ']);
150 let bad_hash = if bad_trim.is_empty() {
151 None
152 } else {
153 Some(hash::from_hex(bad_trim).map_err(|_| BisectError::InvalidBisectState)?)
154 };
155
156 let mut good_hashes = Vec::new();
157 let mut skipped = BTreeSet::new();
158 for line in lines {
159 let line = line.trim_end_matches(['\r', ' ']);
160 if line.is_empty() {
161 continue;
162 }
163 if let Some(rest) = line.strip_prefix("skip:") {
164 if rest.len() != HEX_LEN {
167 return Err(BisectError::InvalidBisectState);
168 }
169 let h = hash::from_hex(rest).map_err(|_| BisectError::InvalidBisectState)?;
170 skipped.insert(h);
171 } else {
172 if line.len() != HEX_LEN {
173 return Err(BisectError::InvalidBisectState);
174 }
175 let h = hash::from_hex(line).map_err(|_| BisectError::InvalidBisectState)?;
176 good_hashes.push(h);
177 }
178 }
179
180 Ok(BisectState {
181 orig_head,
182 orig_branch,
183 bad_hash,
184 good_hashes,
185 skipped,
186 })
187}
188
189pub fn write_state(layout: &RepoLayout, state: &BisectState) -> BisectResult<()> {
194 let mut buf = String::new();
195 buf.push_str(&hash::to_hex(&state.orig_head));
196 buf.push('\n');
197 if let Some(b) = &state.orig_branch {
198 buf.push_str(b);
199 }
200 buf.push('\n');
201 if let Some(bad) = &state.bad_hash {
202 buf.push_str(&hash::to_hex(bad));
203 }
204 buf.push('\n');
205 for g in &state.good_hashes {
206 buf.push_str(&hash::to_hex(g));
207 buf.push('\n');
208 }
209 for s in &state.skipped {
210 buf.push_str("skip:");
211 buf.push_str(&hash::to_hex(s));
212 buf.push('\n');
213 }
214 crate::atomic::write_atomic(&layout.bisect_file(), buf.as_bytes(), true)?;
219 Ok(())
220}
221
222pub fn cleanup_bisect(layout: &RepoLayout) -> BisectResult<()> {
227 let path = layout.bisect_file();
228 match fs::remove_file(&path) {
229 Ok(()) => Ok(()),
230 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
231 Err(e) => Err(BisectError::Io(e)),
232 }
233}
234
235#[must_use]
237pub fn bisect_file_path(layout: &RepoLayout) -> PathBuf {
238 layout.bisect_file()
239}
240
241pub fn enumerate_range(
247 store: &ObjectStore,
248 bad: Hash,
249 good_hashes: &[Hash],
250) -> BisectResult<Vec<Hash>> {
251 let mut good_set: HashSet<Hash> = HashSet::new();
252 for g in good_hashes {
253 collect_ancestor_set(store, *g, &mut good_set);
254 }
255
256 let mut visited: HashSet<Hash> = HashSet::new();
257 let mut candidates: Vec<Hash> = Vec::new();
258 let mut queue: Vec<Hash> = vec![bad];
259 let mut head = 0usize;
260 while head < queue.len() {
261 let current = queue[head];
262 head += 1;
263 if !visited.insert(current) {
264 continue;
265 }
266 if good_set.contains(¤t) {
267 continue;
268 }
269 candidates.push(current);
270 let Ok(obj) = store.read_object(¤t) else {
271 continue;
272 };
273 if let Object::Commit(commit) = obj {
274 for p in commit.parents {
275 queue.push(p);
276 }
277 }
278 }
279 Ok(candidates)
280}
281
282#[must_use]
285pub fn pick_midpoint(candidates: &[Hash]) -> Hash {
286 if candidates.is_empty() {
287 return ZERO;
288 }
289 candidates[candidates.len() / 2]
290}
291
292#[must_use]
301pub fn pick_midpoint_skip(candidates: &[Hash], skipped: &BTreeSet<Hash>) -> Hash {
302 if candidates.is_empty() {
303 return ZERO;
304 }
305 let mid = candidates.len() / 2;
306 for delta in 0..=candidates.len() {
308 if let Some(idx) = mid.checked_add(delta).filter(|&i| i < candidates.len()) {
310 let h = candidates[idx];
311 if !skipped.contains(&h) {
312 return h;
313 }
314 }
315 if let (true, Some(idx)) = (delta > 0, mid.checked_sub(delta)) {
317 let h = candidates[idx];
318 if !skipped.contains(&h) {
319 return h;
320 }
321 }
322 }
323 ZERO
325}
326
327pub fn next_step(store: &ObjectStore, state: &BisectState) -> BisectResult<BisectStep> {
341 let Some(bad) = state.bad_hash else {
342 return Ok(BisectStep::NeedMore);
343 };
344 if state.good_hashes.is_empty() {
345 return Ok(BisectStep::NeedMore);
346 }
347 let candidates = enumerate_range(store, bad, &state.good_hashes)?;
348 if candidates.is_empty() {
349 return Ok(BisectStep::Found(bad));
350 }
351 let non_skipped: Vec<Hash> = candidates
354 .iter()
355 .copied()
356 .filter(|h| !state.skipped.contains(h))
357 .collect();
358 let skipped_in_range: Vec<Hash> = candidates
362 .iter()
363 .copied()
364 .filter(|h| state.skipped.contains(h))
365 .collect();
366 if non_skipped.len() == 1 {
367 let only = non_skipped[0];
368 if only == bad && !skipped_in_range.is_empty() {
374 return Ok(BisectStep::Ambiguous {
375 bad,
376 skipped: skipped_in_range,
377 });
378 }
379 return Ok(BisectStep::Found(only));
380 }
381 if non_skipped.is_empty() {
382 return Ok(BisectStep::Ambiguous {
384 bad,
385 skipped: skipped_in_range,
386 });
387 }
388 let mid = pick_midpoint_skip(&candidates, &state.skipped);
389 Ok(BisectStep::Testing {
390 hash: mid,
391 remaining: candidates.len(),
392 })
393}
394
395fn collect_ancestor_set(store: &ObjectStore, start: Hash, set: &mut HashSet<Hash>) {
398 let mut stack: Vec<Hash> = vec![start];
399 let mut count = 0usize;
400 while let Some(current) = stack.pop() {
401 if count >= MAX_ANCESTORS {
402 break;
403 }
404 if !set.insert(current) {
405 continue;
406 }
407 count += 1;
408 let Ok(obj) = store.read_object(¤t) else {
409 continue;
410 };
411 if let Object::Commit(commit) = obj {
412 for p in commit.parents {
413 stack.push(p);
414 }
415 }
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::object::{Commit, Identity, Tree, TreeEntry};
423 use crate::serialize;
424 use tempfile::TempDir;
425
426 fn fresh_store() -> (TempDir, ObjectStore) {
427 let dir = TempDir::new().unwrap();
428 let store = ObjectStore::init(&RepoLayout::single(dir.path())).unwrap();
429 (dir, store)
430 }
431
432 fn put_blob(store: &ObjectStore, data: &[u8]) -> Hash {
433 let bytes = serialize::serialize(&Object::Blob(crate::object::Blob {
434 data: data.to_vec(),
435 }))
436 .unwrap();
437 store.write(&bytes).unwrap()
438 }
439
440 fn put_tree(store: &ObjectStore, name: &str, blob_h: Hash) -> Hash {
441 let tree = Object::Tree(Tree {
442 entries: vec![TreeEntry {
443 name: name.as_bytes().to_vec(),
444 mode: crate::object::EntryMode::Blob,
445 object_hash: blob_h,
446 }],
447 });
448 store.write(&serialize::serialize(&tree).unwrap()).unwrap()
449 }
450
451 fn put_commit(store: &ObjectStore, tree_h: Hash, parents: Vec<Hash>, ts: u64) -> Hash {
452 let commit = Object::Commit(Commit::new_unannotated(
453 tree_h,
454 parents,
455 Identity::ed25519([0u8; 32]),
456 [0u8; 32],
457 b"msg".to_vec(),
458 ts,
459 [0u8; 64],
460 ));
461 store
462 .write(&serialize::serialize(&commit).unwrap())
463 .unwrap()
464 }
465
466 #[test]
467 fn state_roundtrip_with_branch_and_bad() {
468 let tmp = TempDir::new().unwrap();
469 let mkit = RepoLayout::single(tmp.path());
470 fs::create_dir_all(mkit.worktree_state_dir()).unwrap();
471 let state = BisectState {
472 orig_head: hash::hash(b"orig-head"),
473 orig_branch: Some("main".to_string()),
474 bad_hash: Some(hash::hash(b"bad")),
475 good_hashes: vec![hash::hash(b"g1"), hash::hash(b"g2")],
476 skipped: BTreeSet::new(),
477 };
478 write_state(&mkit, &state).unwrap();
479 let read = read_state(&mkit).unwrap();
480 assert_eq!(read, state);
481 }
482
483 #[test]
484 fn state_roundtrip_with_no_branch_no_bad() {
485 let tmp = TempDir::new().unwrap();
486 let mkit = RepoLayout::single(tmp.path());
487 fs::create_dir_all(mkit.worktree_state_dir()).unwrap();
488 let state = BisectState {
489 orig_head: hash::hash(b"head"),
490 orig_branch: None,
491 bad_hash: None,
492 good_hashes: Vec::new(),
493 skipped: BTreeSet::new(),
494 };
495 write_state(&mkit, &state).unwrap();
496 let read = read_state(&mkit).unwrap();
497 assert_eq!(read, state);
498 }
499
500 #[test]
501 fn state_roundtrip_with_skipped_hashes() {
502 let tmp = TempDir::new().unwrap();
503 let mkit = RepoLayout::single(tmp.path());
504 fs::create_dir_all(mkit.worktree_state_dir()).unwrap();
505 let s1 = hash::hash(b"skip1");
506 let s2 = hash::hash(b"skip2");
507 let mut skipped = BTreeSet::new();
508 skipped.insert(s1);
509 skipped.insert(s2);
510 let state = BisectState {
511 orig_head: hash::hash(b"head"),
512 orig_branch: Some("main".to_string()),
513 bad_hash: Some(hash::hash(b"bad")),
514 good_hashes: vec![hash::hash(b"good")],
515 skipped,
516 };
517 write_state(&mkit, &state).unwrap();
518 let read = read_state(&mkit).unwrap();
519 assert_eq!(read, state);
520 assert_eq!(read.skipped.len(), 2);
521 assert!(read.skipped.contains(&s1));
522 assert!(read.skipped.contains(&s2));
523 }
524
525 #[test]
526 fn write_state_creates_missing_dir_and_overwrites_atomically() {
527 let tmp = TempDir::new().unwrap();
533 let mkit = RepoLayout::single(tmp.path());
534 let first = BisectState {
536 orig_head: hash::hash(b"head"),
537 orig_branch: Some("main".to_string()),
538 bad_hash: Some(hash::hash(b"bad")),
539 good_hashes: vec![hash::hash(b"g1")],
540 skipped: BTreeSet::new(),
541 };
542 write_state(&mkit, &first).unwrap();
543 assert_eq!(read_state(&mkit).unwrap(), first);
544
545 let second = BisectState {
546 orig_head: hash::hash(b"head2"),
547 orig_branch: None,
548 bad_hash: None,
549 good_hashes: Vec::new(),
550 skipped: BTreeSet::new(),
551 };
552 write_state(&mkit, &second).unwrap();
553 assert_eq!(read_state(&mkit).unwrap(), second);
554
555 let stray: Vec<_> = fs::read_dir(mkit.worktree_state_dir())
557 .unwrap()
558 .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
559 .filter(|n| n != BISECT_FILE)
560 .collect();
561 assert!(stray.is_empty(), "stray temp files left behind: {stray:?}");
562 }
563
564 #[test]
565 fn old_state_file_without_skip_deserializes_with_empty_skipped() {
566 let tmp = TempDir::new().unwrap();
568 let mkit = RepoLayout::single(tmp.path());
569 fs::create_dir_all(mkit.worktree_state_dir()).unwrap();
570 let orig_head = hash::hash(b"orig");
571 let bad = hash::hash(b"bad");
572 let good = hash::hash(b"good");
573 let content = format!(
575 "{}\n\n{}\n{}\n",
576 hash::to_hex(&orig_head),
577 hash::to_hex(&bad),
578 hash::to_hex(&good)
579 );
580 fs::write(mkit.bisect_file(), content.as_bytes()).unwrap();
581 let read = read_state(&mkit).unwrap();
582 assert_eq!(read.orig_head, orig_head);
583 assert_eq!(read.bad_hash, Some(bad));
584 assert_eq!(read.good_hashes, vec![good]);
585 assert!(
586 read.skipped.is_empty(),
587 "old files must deserialize with empty skipped"
588 );
589 }
590
591 #[test]
592 fn read_state_when_no_bisect_returns_error() {
593 let tmp = TempDir::new().unwrap();
594 let mkit = RepoLayout::single(tmp.path());
595 fs::create_dir_all(mkit.worktree_state_dir()).unwrap();
596 let err = read_state(&mkit).unwrap_err();
597 assert!(matches!(err, BisectError::NoBisectInProgress));
598 }
599
600 #[test]
601 fn read_state_rejects_empty_file() {
602 let tmp = TempDir::new().unwrap();
603 let mkit = RepoLayout::single(tmp.path());
604 fs::create_dir_all(mkit.worktree_state_dir()).unwrap();
605 fs::write(mkit.bisect_file(), b"").unwrap();
606 let err = read_state(&mkit).unwrap_err();
607 assert!(matches!(err, BisectError::InvalidBisectState));
608 }
609
610 #[test]
611 fn is_bisect_in_progress_detection() {
612 let tmp = TempDir::new().unwrap();
613 let mkit = RepoLayout::single(tmp.path());
614 fs::create_dir_all(mkit.worktree_state_dir()).unwrap();
615 assert!(!is_bisect_in_progress(&mkit));
616 fs::write(mkit.bisect_file(), b"placeholder").unwrap();
617 assert!(is_bisect_in_progress(&mkit));
618 }
619
620 #[test]
621 fn cleanup_removes_state_file() {
622 let tmp = TempDir::new().unwrap();
623 let mkit = RepoLayout::single(tmp.path());
624 fs::create_dir_all(mkit.worktree_state_dir()).unwrap();
625 fs::write(mkit.bisect_file(), b"x\n").unwrap();
626 cleanup_bisect(&mkit).unwrap();
627 assert!(!is_bisect_in_progress(&mkit));
628 }
629
630 #[test]
631 fn cleanup_on_missing_file_is_noop() {
632 let tmp = TempDir::new().unwrap();
633 let mkit = RepoLayout::single(tmp.path());
634 fs::create_dir_all(mkit.worktree_state_dir()).unwrap();
635 cleanup_bisect(&mkit).unwrap();
636 }
637
638 #[test]
639 fn pick_midpoint_returns_middle_element() {
640 let c1 = hash::hash(b"c1");
641 let c2 = hash::hash(b"c2");
642 let c3 = hash::hash(b"c3");
643 let c4 = hash::hash(b"c4");
644 let c5 = hash::hash(b"c5");
645 assert_eq!(pick_midpoint(&[c1, c2, c3, c4, c5]), c3);
647 assert_eq!(pick_midpoint(&[c1, c2, c3, c4]), c3);
649 assert_eq!(pick_midpoint(&[c1]), c1);
650 }
651
652 #[test]
653 fn pick_midpoint_empty_returns_zero() {
654 assert_eq!(pick_midpoint(&[]), ZERO);
655 }
656
657 #[test]
658 fn enumerate_range_on_linear_chain() {
659 let (_d, store) = fresh_store();
660 let blob = put_blob(&store, b"data");
661 let tree = put_tree(&store, "f.txt", blob);
662 let mut commits = Vec::new();
663 commits.push(put_commit(&store, tree, vec![], 1));
664 for i in 1..8 {
665 let parent = commits[i - 1];
666 commits.push(put_commit(&store, tree, vec![parent], (i + 1) as u64));
667 }
668 let cands = enumerate_range(&store, commits[7], &[commits[1]]).unwrap();
671 assert_eq!(cands.len(), 6);
672 assert_eq!(cands[0], commits[7]);
673 }
674
675 #[test]
676 fn enumerate_range_with_diamond_merge() {
677 let (_d, store) = fresh_store();
678 let blob = put_blob(&store, b"d");
679 let tree = put_tree(&store, "f.txt", blob);
680 let c1 = put_commit(&store, tree, vec![], 1);
681 let c2 = put_commit(&store, tree, vec![c1], 2);
682 let c3 = put_commit(&store, tree, vec![c1], 3);
683 let c4 = put_commit(&store, tree, vec![c2, c3], 4);
684 let cands = enumerate_range(&store, c4, &[c1]).unwrap();
685 assert_eq!(cands.len(), 3);
686 assert_eq!(cands[0], c4);
687 }
688
689 #[test]
690 fn enumerate_range_same_good_and_bad_yields_empty() {
691 let (_d, store) = fresh_store();
692 let blob = put_blob(&store, b"d");
693 let tree = put_tree(&store, "f.txt", blob);
694 let c1 = put_commit(&store, tree, vec![], 1);
695 let cands = enumerate_range(&store, c1, &[c1]).unwrap();
696 assert!(cands.is_empty());
697 }
698
699 #[test]
700 fn next_step_need_more_when_no_bad() {
701 let (_d, store) = fresh_store();
702 let state = BisectState {
703 orig_head: ZERO,
704 orig_branch: None,
705 bad_hash: None,
706 good_hashes: vec![hash::hash(b"x")],
707 skipped: BTreeSet::new(),
708 };
709 assert_eq!(next_step(&store, &state).unwrap(), BisectStep::NeedMore);
710 }
711
712 #[test]
713 fn next_step_drives_bisect_to_completion() {
714 let (_d, store) = fresh_store();
716 let blob = put_blob(&store, b"data");
717 let tree = put_tree(&store, "f.txt", blob);
718 let mut commits = Vec::new();
719 commits.push(put_commit(&store, tree, vec![], 1));
720 for i in 1..6 {
721 let parent = commits[i - 1];
722 commits.push(put_commit(&store, tree, vec![parent], (i + 1) as u64));
723 }
724 let mut state = BisectState {
726 orig_head: commits[5],
727 orig_branch: None,
728 bad_hash: Some(commits[5]),
729 good_hashes: vec![commits[0]],
730 skipped: BTreeSet::new(),
731 };
732 let truth = commits[3];
735
736 let mut iters = 0;
737 loop {
738 assert!(iters < 8, "bisect should converge within 8 iterations");
739 iters += 1;
740 match next_step(&store, &state).unwrap() {
741 BisectStep::Found(h) => {
742 assert_eq!(h, truth);
743 break;
744 }
745 BisectStep::Testing { hash, .. } => {
746 let idx = commits.iter().position(|c| *c == hash).unwrap();
749 let truth_idx = commits.iter().position(|c| *c == truth).unwrap();
750 if idx < truth_idx {
751 state.good_hashes.push(hash);
752 } else {
753 state.bad_hash = Some(hash);
754 }
755 }
756 BisectStep::NeedMore => panic!("unexpected NeedMore"),
757 BisectStep::Ambiguous { .. } => panic!("unexpected ambiguous"),
758 }
759 }
760 }
761
762 #[test]
763 fn skip_advances_midpoint_to_neighbor() {
764 let (_d, store) = fresh_store();
768 let blob = put_blob(&store, b"data");
769 let tree = put_tree(&store, "f.txt", blob);
770 let mut commits = Vec::new();
771 commits.push(put_commit(&store, tree, vec![], 1)); for i in 1..6 {
773 let parent = commits[i - 1];
774 commits.push(put_commit(&store, tree, vec![parent], (i + 1) as u64));
775 }
776 let cands = enumerate_range(&store, commits[5], &[commits[0]]).unwrap();
778 let natural_mid = pick_midpoint(&cands);
780 assert_ne!(natural_mid, ZERO, "should have a natural midpoint");
781
782 let mut skipped = BTreeSet::new();
784 skipped.insert(natural_mid);
785
786 let skipped_mid = pick_midpoint_skip(&cands, &skipped);
787 assert_ne!(
788 skipped_mid, ZERO,
789 "should find a neighbor when mid is skipped"
790 );
791 assert_ne!(
792 skipped_mid, natural_mid,
793 "skip must advance past the natural midpoint"
794 );
795 assert!(
796 !skipped.contains(&skipped_mid),
797 "returned hash must not be in skipped set"
798 );
799 }
800
801 #[test]
802 fn next_step_skip_advances_then_finds() {
803 let (_d, store) = fresh_store();
809 let blob = put_blob(&store, b"data");
810 let tree = put_tree(&store, "f.txt", blob);
811 let mut commits = Vec::new();
812 commits.push(put_commit(&store, tree, vec![], 1));
813 for i in 1..6 {
814 let p = commits[i - 1];
815 commits.push(put_commit(&store, tree, vec![p], (i + 1) as u64));
816 }
817 let truth = commits[3]; let mut skipped = BTreeSet::new();
820 skipped.insert(commits[1]); let mut state = BisectState {
823 orig_head: commits[5],
824 orig_branch: None,
825 bad_hash: Some(commits[5]),
826 good_hashes: vec![commits[0]],
827 skipped,
828 };
829
830 let mut iters = 0;
831 loop {
832 assert!(iters < 10, "bisect with skip should converge");
833 iters += 1;
834 match next_step(&store, &state).unwrap() {
835 BisectStep::Found(_) => break,
836 BisectStep::Testing { hash, .. } => {
837 let idx = commits.iter().position(|c| *c == hash).unwrap();
838 let truth_idx = commits.iter().position(|c| *c == truth).unwrap();
839 if idx < truth_idx {
840 state.good_hashes.push(hash);
841 } else {
842 state.bad_hash = Some(hash);
843 }
844 }
845 BisectStep::NeedMore => panic!("unexpected NeedMore"),
846 BisectStep::Ambiguous { .. } => panic!("unexpected ambiguous"),
847 }
848 }
849 }
850
851 #[test]
852 fn pick_midpoint_skip_all_skipped_returns_zero() {
853 let c1 = hash::hash(b"c1");
854 let c2 = hash::hash(b"c2");
855 let c3 = hash::hash(b"c3");
856 let mut skipped = BTreeSet::new();
857 skipped.insert(c1);
858 skipped.insert(c2);
859 skipped.insert(c3);
860 assert_eq!(pick_midpoint_skip(&[c1, c2, c3], &skipped), ZERO);
861 }
862}