1use std::collections::HashSet;
18use std::collections::VecDeque;
19use std::fmt;
20use std::iter;
21use std::pin::pin;
22use std::sync::Arc;
23
24use futures::TryStreamExt as _;
25use thiserror::Error;
26
27use crate::backend::BackendError;
28use crate::backend::CommitId;
29use crate::commit::Commit;
30use crate::repo::Repo;
31use crate::revset::ResolvedRevsetExpression;
32use crate::revset::Revset;
33use crate::revset::RevsetEvaluationError;
34use crate::revset::RevsetExpression;
35use crate::revset::RevsetStreamExt as _;
36
37#[derive(Error, Debug)]
39pub enum BisectionError {
40 #[error("Failed to read data from the backend involved in bisection")]
42 BackendError(#[from] BackendError),
43 #[error("Failed to evaluate a revset involved in bisection")]
45 RevsetEvaluationError(#[from] RevsetEvaluationError),
46}
47
48#[derive(Debug, PartialEq, Eq, Clone)]
51pub enum Evaluation {
52 Good,
54 Bad,
56 Skip,
58 Abort,
60}
61
62impl Evaluation {
63 pub fn invert(self) -> Self {
67 use Evaluation::*;
68 match self {
69 Good => Bad,
70 Bad => Good,
71 Skip => Skip,
72 Abort => Abort,
73 }
74 }
75
76 pub fn invert_if(self, condition: bool) -> Self {
78 if condition { self.invert() } else { self }
79 }
80}
81impl fmt::Display for Evaluation {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 write!(
84 f,
85 "{}",
86 match self {
87 Self::Good => "good",
88 Self::Bad => "bad",
89 Self::Skip => "skipped",
90 Self::Abort => "aborted",
91 }
92 )
93 }
94}
95
96pub struct Bisector<'repo> {
98 repo: &'repo dyn Repo,
99 input_range: Arc<ResolvedRevsetExpression>,
100 good_commits: HashSet<CommitId>,
101 bad_commits: HashSet<CommitId>,
102 skipped_commits: HashSet<CommitId>,
103 aborted: bool,
104 verifications: Vec<(CommitId, Evaluation)>,
105}
106
107#[derive(Debug, PartialEq, Eq, Clone)]
109pub enum BisectionResult {
110 Found(Vec<Commit>),
113 FoundDespiteSkips {
115 bad_commits: Vec<Commit>,
117 possibly_bad: Vec<Commit>,
120 },
121 Indeterminate,
124 Abort,
126 VerificationFailed,
128}
129
130#[derive(Debug, PartialEq, Eq, Clone)]
132pub enum NextStep {
133 Verify {
135 commit: Commit,
137 expected_evaluation: Evaluation,
139 },
140 Evaluate(Commit),
142 Done(BisectionResult),
144}
145
146impl<'repo> Bisector<'repo> {
147 pub async fn new(
151 repo: &'repo dyn Repo,
152 input_range: Arc<ResolvedRevsetExpression>,
153 verify_preconditions: bool,
154 ) -> Result<Self, BisectionError> {
155 let bad_commits: Vec<CommitId> = input_range
156 .heads()
157 .evaluate(repo)?
158 .stream()
159 .try_collect()
160 .await?;
161 let good_commits: Vec<CommitId> = input_range
162 .connected()
163 .parents()
164 .minus(&input_range.connected())
165 .evaluate(repo)?
166 .stream()
167 .try_collect()
168 .await?;
169
170 let verifications: Vec<(CommitId, Evaluation)> = if verify_preconditions {
171 iter::empty()
173 .chain(
174 good_commits
175 .iter()
176 .map(|commit_id| (commit_id.clone(), Evaluation::Good)),
177 )
178 .chain(
179 bad_commits
180 .iter()
181 .map(|commit_id| (commit_id.clone(), Evaluation::Bad)),
182 )
183 .collect()
184 } else {
185 vec![]
186 };
187
188 Ok(Self {
189 repo,
190 input_range,
191 verifications,
192 bad_commits: HashSet::from_iter(bad_commits),
193 good_commits: HashSet::new(),
194 skipped_commits: HashSet::new(),
195 aborted: false,
196 })
197 }
198
199 pub fn mark_good(&mut self, id: CommitId) {
201 assert!(!self.bad_commits.contains(&id));
202 assert!(!self.skipped_commits.contains(&id));
203 assert!(!self.aborted);
204 self.good_commits.insert(id);
205 }
206
207 pub fn mark_bad(&mut self, id: CommitId) {
209 assert!(!self.good_commits.contains(&id));
210 assert!(!self.skipped_commits.contains(&id));
211 assert!(!self.aborted);
212 self.bad_commits.insert(id);
213 }
214
215 pub fn mark_skipped(&mut self, id: CommitId) {
217 assert!(!self.good_commits.contains(&id));
218 assert!(!self.bad_commits.contains(&id));
219 assert!(!self.aborted);
220 self.skipped_commits.insert(id);
221 }
222
223 pub fn mark_abort(&mut self, id: CommitId) {
225 assert!(!self.good_commits.contains(&id));
230 assert!(!self.bad_commits.contains(&id));
231 assert!(!self.skipped_commits.contains(&id));
232 self.aborted = true;
233 }
234
235 pub fn mark(&mut self, id: CommitId, evaluation: Evaluation) {
238 match evaluation {
239 Evaluation::Good => self.mark_good(id),
240 Evaluation::Bad => self.mark_bad(id),
241 Evaluation::Skip => self.mark_skipped(id),
242 Evaluation::Abort => self.mark_abort(id),
243 }
244 }
245
246 pub fn good_commits(&self) -> &HashSet<CommitId> {
248 &self.good_commits
249 }
250
251 pub fn bad_commits(&self) -> &HashSet<CommitId> {
253 &self.bad_commits
254 }
255
256 pub fn skipped_commits(&self) -> &HashSet<CommitId> {
258 &self.skipped_commits
259 }
260
261 fn candidates(&self) -> Arc<ResolvedRevsetExpression> {
262 let good_expr = RevsetExpression::commits(self.good_commits.iter().cloned().collect());
263 let bad_expr = RevsetExpression::commits(self.bad_commits.iter().cloned().collect());
264 let skipped_expr =
265 RevsetExpression::commits(self.skipped_commits.iter().cloned().collect());
266
267 self.input_range
268 .intersection(&good_expr.heads().range(&bad_expr.roots()))
269 .minus(&bad_expr)
270 .minus(&skipped_expr)
271 }
272
273 pub async fn remaining_revset(&self) -> Result<Box<dyn Revset>, BisectionError> {
277 Ok(self.candidates().evaluate(self.repo)?)
278 }
279
280 pub async fn next_step(&mut self) -> Result<NextStep, BisectionError> {
283 if self.aborted {
284 return Ok(NextStep::Done(BisectionResult::Abort));
285 }
286 let to_evaluate_expr = self.candidates().bisect().latest(1);
291 let to_evaluate_set = to_evaluate_expr.evaluate(self.repo)?;
292 if let Some(verification) = self.verifications.pop() {
293 let commit = self.repo.store().get_commit_async(&verification.0).await?;
294 Ok(NextStep::Verify {
295 commit,
296 expected_evaluation: verification.1,
297 })
298 } else if let Some(commit_id) = pin!(to_evaluate_set.stream()).try_next().await? {
299 let commit = self.repo.store().get_commit_async(&commit_id).await?;
300 Ok(NextStep::Evaluate(commit))
301 } else {
302 let bad_expr = RevsetExpression::commits(self.bad_commits.iter().cloned().collect());
303 let bad_roots = bad_expr.roots().evaluate(self.repo)?;
304 let bad_commits: Vec<_> = bad_roots
305 .stream()
306 .commits(self.repo.store())
307 .try_collect()
308 .await?;
309 if bad_commits.is_empty() {
310 Ok(NextStep::Done(BisectionResult::Indeterminate))
311 } else {
312 let mut todo: VecDeque<Commit> = VecDeque::from(bad_commits.clone());
314 let mut possibly_bad: Vec<Commit> = Vec::new();
315 while let Some(commit) = todo.pop_front() {
316 for parent in commit.parents().await? {
317 if self.skipped_commits.contains(parent.id()) {
318 possibly_bad.push(parent.clone());
319 todo.push_back(parent);
320 }
321 }
322 }
323 if possibly_bad.is_empty() {
324 Ok(NextStep::Done(BisectionResult::Found(bad_commits)))
325 } else {
326 Ok(NextStep::Done(BisectionResult::FoundDespiteSkips {
327 bad_commits,
328 possibly_bad,
329 }))
330 }
331 }
332 }
333 }
334}