Skip to main content

git_psect/
candidates.rs

1use std::collections::HashSet;
2
3use git2::{Oid, Repository, Sort};
4use psect_core::{
5    Bernoulli, RegressionProbabilities, TestOutcomeDistributions, next_revision_to_test,
6    regression::Revision,
7};
8
9use crate::{error::Error, state::State};
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub struct GitRevision(pub Oid);
13
14impl PartialOrd for GitRevision {
15    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
16        self.0.as_bytes().partial_cmp(other.0.as_bytes())
17    }
18}
19
20impl Revision for GitRevision {}
21
22/// Walk commits reachable from new_revisions back through (and including)
23/// old_revisions, in oldest-first topological order.
24pub fn build(repo: &Repository, state: &State) -> Result<Vec<GitRevision>, Error> {
25    let mut walk = repo.revwalk()?;
26    for sha in &state.new_revisions {
27        walk.push(repo.revparse_single(sha)?.id())?;
28    }
29    // Hide the *parents* of old_revisions so the walk includes old_revisions
30    // themselves but stops before their ancestors.
31    for sha in &state.old_revisions {
32        let commit = repo.find_commit(repo.revparse_single(sha)?.id())?;
33        for parent in commit.parents() {
34            walk.hide(parent.id())?;
35        }
36    }
37    walk.set_sorting(Sort::TOPOLOGICAL | Sort::REVERSE)?;
38    walk.map(|r| r.map(GitRevision).map_err(Error::from))
39        .collect()
40}
41
42pub fn build_distributions(state: &State) -> TestOutcomeDistributions<bool> {
43    TestOutcomeDistributions {
44        old: Box::new(Bernoulli {
45            prior: state
46                .priors
47                .old_pass_rate
48                .expect("old_pass_rate set by `git psect old`"),
49        }),
50        new: Box::new(Bernoulli {
51            prior: state
52                .priors
53                .new_pass_rate
54                .expect("new_pass_rate set by `git psect new`"),
55        }),
56    }
57}
58
59pub fn reconstruct<'a>(
60    repo: &Repository,
61    state: &State,
62    candidates: &'a Vec<GitRevision>,
63    distributions: &TestOutcomeDistributions<bool>,
64) -> Result<RegressionProbabilities<'a, GitRevision>, Error> {
65    let known_old: HashSet<GitRevision> = state
66        .old_revisions
67        .iter()
68        .map(|sha| {
69            repo.revparse_single(sha)
70                .map(|o| GitRevision(o.id()))
71                .map_err(Error::from)
72        })
73        .collect::<Result<_, _>>()?;
74
75    let mut ps = RegressionProbabilities::initialize(candidates, &known_old);
76    for sample in &state.samples {
77        let oid = repo.revparse_single(&sample.revision)?.id();
78        let idx = candidates.iter().position(|r| r.0 == oid).ok_or_else(|| {
79            Error::Validation(format!(
80                "sample {} not found in candidate list",
81                &sample.revision[..10]
82            ))
83        })?;
84        ps.update_with_sample(distributions, idx, sample.outcome);
85    }
86    Ok(ps)
87}
88
89pub fn checkout(repo: &Repository, oid: Oid) -> Result<(), Error> {
90    let commit = repo.find_commit(oid)?;
91    let tree = commit.tree()?;
92    repo.checkout_tree(
93        tree.as_object(),
94        Some(&mut git2::build::CheckoutBuilder::default()),
95    )?;
96    repo.set_head_detached(oid)?;
97    Ok(())
98}
99
100/// Pick the next revision to test and check it out. Returns its full SHA.
101pub fn checkout_next<'a>(
102    repo: &Repository,
103    distributions: &TestOutcomeDistributions<bool>,
104    ps: &RegressionProbabilities<'a, GitRevision>,
105) -> Result<String, Error> {
106    let next_oid = next_revision_to_test(ps, distributions).0;
107    checkout(repo, next_oid)?;
108    Ok(next_oid.to_string())
109}