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 Abort,
51}
52
53impl Evaluation {
54 pub fn invert(self) -> Self {
58 use Evaluation::*;
59 match self {
60 Good => Bad,
61 Bad => Good,
62 Skip => Skip,
63 Abort => Abort,
64 }
65 }
66}
67
68pub struct Bisector<'repo> {
70 repo: &'repo dyn Repo,
71 input_range: Arc<ResolvedRevsetExpression>,
72 good_commits: HashSet<CommitId>,
73 bad_commits: HashSet<CommitId>,
74 skipped_commits: HashSet<CommitId>,
75 aborted: bool,
76}
77
78#[derive(Debug, PartialEq, Eq, Clone)]
80pub enum BisectionResult {
81 Found(Vec<Commit>),
84 Indeterminate,
87 Abort,
89}
90
91#[derive(Debug, PartialEq, Eq, Clone)]
93pub enum NextStep {
94 Evaluate(Commit),
96 Done(BisectionResult),
98}
99
100impl<'repo> Bisector<'repo> {
101 pub fn new(
104 repo: &'repo dyn Repo,
105 input_range: Arc<ResolvedRevsetExpression>,
106 ) -> Result<Self, BisectionError> {
107 let bad_commits = input_range.heads().evaluate(repo)?.iter().try_collect()?;
108 Ok(Self {
109 repo,
110 input_range,
111 bad_commits,
112 good_commits: HashSet::new(),
113 skipped_commits: HashSet::new(),
114 aborted: false,
115 })
116 }
117
118 pub fn mark_good(&mut self, id: CommitId) {
120 assert!(!self.bad_commits.contains(&id));
121 assert!(!self.skipped_commits.contains(&id));
122 assert!(!self.aborted);
123 self.good_commits.insert(id);
124 }
125
126 pub fn mark_bad(&mut self, id: CommitId) {
128 assert!(!self.good_commits.contains(&id));
129 assert!(!self.skipped_commits.contains(&id));
130 assert!(!self.aborted);
131 self.bad_commits.insert(id);
132 }
133
134 pub fn mark_skipped(&mut self, id: CommitId) {
136 assert!(!self.good_commits.contains(&id));
137 assert!(!self.bad_commits.contains(&id));
138 assert!(!self.aborted);
139 self.skipped_commits.insert(id);
140 }
141
142 pub fn mark_abort(&mut self, id: CommitId) {
144 assert!(!self.good_commits.contains(&id));
149 assert!(!self.bad_commits.contains(&id));
150 assert!(!self.skipped_commits.contains(&id));
151 self.aborted = true;
152 }
153
154 pub fn mark(&mut self, id: CommitId, evaluation: Evaluation) {
157 match evaluation {
158 Evaluation::Good => self.mark_good(id),
159 Evaluation::Bad => self.mark_bad(id),
160 Evaluation::Skip => self.mark_skipped(id),
161 Evaluation::Abort => self.mark_abort(id),
162 }
163 }
164
165 pub fn good_commits(&self) -> &HashSet<CommitId> {
167 &self.good_commits
168 }
169
170 pub fn bad_commits(&self) -> &HashSet<CommitId> {
172 &self.bad_commits
173 }
174
175 pub fn skipped_commits(&self) -> &HashSet<CommitId> {
177 &self.skipped_commits
178 }
179
180 pub fn next_step(&mut self) -> Result<NextStep, BisectionError> {
183 if self.aborted {
184 return Ok(NextStep::Done(BisectionResult::Abort));
185 }
186 let good_expr = RevsetExpression::commits(self.good_commits.iter().cloned().collect());
187 let bad_expr = RevsetExpression::commits(self.bad_commits.iter().cloned().collect());
188 let skipped_expr =
189 RevsetExpression::commits(self.skipped_commits.iter().cloned().collect());
190 let to_evaluate_expr = self
195 .input_range
196 .intersection(&good_expr.heads().range(&bad_expr.roots()))
197 .minus(&bad_expr)
198 .minus(&skipped_expr)
199 .bisect()
200 .latest(1);
201 let to_evaluate_set = to_evaluate_expr.evaluate(self.repo)?;
202 if let Some(commit) = to_evaluate_set
203 .iter()
204 .commits(self.repo.store())
205 .next()
206 .transpose()?
207 {
208 Ok(NextStep::Evaluate(commit))
209 } else {
210 let bad_roots = bad_expr.roots().evaluate(self.repo)?;
211 let bad_commits: Vec<_> = bad_roots.iter().commits(self.repo.store()).try_collect()?;
212 if bad_commits.is_empty() {
213 Ok(NextStep::Done(BisectionResult::Indeterminate))
214 } else {
215 Ok(NextStep::Done(BisectionResult::Found(bad_commits)))
216 }
217 }
218 }
219}