1use std::collections::HashSet;
18use std::sync::Arc;
19
20use itertools::Itertools as _;
21use thiserror::Error;
22
23use crate::backend::CommitId;
24use crate::commit::Commit;
25use crate::repo::Repo;
26use crate::revset::ResolvedRevsetExpression;
27use crate::revset::RevsetEvaluationError;
28use crate::revset::RevsetExpression;
29use crate::revset::RevsetIteratorExt as _;
30
31#[derive(Error, Debug)]
33pub enum BisectionError {
34 #[error("Failed to evaluate a revset involved in bisection")]
36 RevsetEvaluationError(#[from] RevsetEvaluationError),
37}
38
39#[derive(Debug)]
42pub enum Evaluation {
43 Good,
45 Bad,
47 Skip,
49}
50
51impl Evaluation {
52 pub fn invert(self) -> Self {
56 use Evaluation::*;
57 match self {
58 Good => Bad,
59 Bad => Good,
60 Skip => Skip,
61 }
62 }
63}
64
65pub struct Bisector<'repo> {
67 repo: &'repo dyn Repo,
68 input_range: Arc<ResolvedRevsetExpression>,
69 good_commits: HashSet<CommitId>,
70 bad_commits: HashSet<CommitId>,
71 skipped_commits: HashSet<CommitId>,
72}
73
74#[derive(Debug, PartialEq, Eq, Clone)]
76pub enum BisectionResult {
77 Found(Vec<Commit>),
80 Indeterminate,
83}
84
85#[derive(Debug, PartialEq, Eq, Clone)]
87pub enum NextStep {
88 Evaluate(Commit),
90 Done(BisectionResult),
92}
93
94impl<'repo> Bisector<'repo> {
95 pub fn new(
98 repo: &'repo dyn Repo,
99 input_range: Arc<ResolvedRevsetExpression>,
100 ) -> Result<Self, BisectionError> {
101 let bad_commits = input_range.heads().evaluate(repo)?.iter().try_collect()?;
102 Ok(Self {
103 repo,
104 input_range,
105 bad_commits,
106 good_commits: HashSet::new(),
107 skipped_commits: HashSet::new(),
108 })
109 }
110
111 pub fn mark_good(&mut self, id: CommitId) {
113 assert!(!self.bad_commits.contains(&id));
114 assert!(!self.skipped_commits.contains(&id));
115 self.good_commits.insert(id);
116 }
117
118 pub fn mark_bad(&mut self, id: CommitId) {
120 assert!(!self.good_commits.contains(&id));
121 assert!(!self.skipped_commits.contains(&id));
122 self.bad_commits.insert(id);
123 }
124
125 pub fn mark_skipped(&mut self, id: CommitId) {
127 assert!(!self.good_commits.contains(&id));
128 assert!(!self.bad_commits.contains(&id));
129 self.skipped_commits.insert(id);
130 }
131
132 pub fn mark(&mut self, id: CommitId, evaluation: Evaluation) {
135 match evaluation {
136 Evaluation::Good => self.mark_good(id),
137 Evaluation::Bad => self.mark_bad(id),
138 Evaluation::Skip => self.mark_skipped(id),
139 }
140 }
141
142 pub fn good_commits(&self) -> &HashSet<CommitId> {
144 &self.good_commits
145 }
146
147 pub fn bad_commits(&self) -> &HashSet<CommitId> {
149 &self.bad_commits
150 }
151
152 pub fn skipped_commits(&self) -> &HashSet<CommitId> {
154 &self.skipped_commits
155 }
156
157 pub fn next_step(&mut self) -> Result<NextStep, BisectionError> {
160 let good_expr = RevsetExpression::commits(self.good_commits.iter().cloned().collect());
161 let bad_expr = RevsetExpression::commits(self.bad_commits.iter().cloned().collect());
162 let skipped_expr =
163 RevsetExpression::commits(self.skipped_commits.iter().cloned().collect());
164 let to_evaluate_expr = self
169 .input_range
170 .intersection(&good_expr.heads().range(&bad_expr.roots()))
171 .minus(&bad_expr)
172 .minus(&skipped_expr)
173 .bisect()
174 .latest(1);
175 let to_evaluate_set = to_evaluate_expr.evaluate(self.repo)?;
176 if let Some(commit) = to_evaluate_set
177 .iter()
178 .commits(self.repo.store())
179 .next()
180 .transpose()?
181 {
182 Ok(NextStep::Evaluate(commit))
183 } else {
184 let bad_roots = bad_expr.roots().evaluate(self.repo)?;
185 let bad_commits: Vec<_> = bad_roots.iter().commits(self.repo.store()).try_collect()?;
186 if bad_commits.is_empty() {
187 Ok(NextStep::Done(BisectionResult::Indeterminate))
188 } else {
189 Ok(NextStep::Done(BisectionResult::Found(bad_commits)))
190 }
191 }
192 }
193}