Skip to main content

git_psect/commands/
session.rs

1use crate::{candidates, error::Error, repo::RepoContext, state, state::State};
2
3const DEFAULT_OLD_PASS_RATE: f64 = 0.95;
4const DEFAULT_NEW_PASS_RATE: f64 = 0.5;
5
6#[derive(Clone, clap::ValueEnum)]
7pub enum Bound {
8    Old,
9    New,
10}
11
12/// Mark a revision as an old (known-good) or new (known-bad) bound, persist
13/// state, then call `advance`.
14pub fn mark(
15    ctx: &RepoContext,
16    state: &mut State,
17    rev: Option<String>,
18    bound: Bound,
19) -> Result<(), Error> {
20    let refspec = rev.as_deref().unwrap_or("HEAD");
21    let sha = ctx.resolve_rev(refspec)?;
22
23    match bound {
24        Bound::Old => {
25            if !state.old_revisions.is_empty() {
26                return Err(Error::Validation(
27                    "only one old revision is supported for now".into(),
28                ));
29            }
30            state.old_revisions.push(sha.clone());
31        }
32        Bound::New => {
33            if !state.new_revisions.is_empty() {
34                return Err(Error::Validation(
35                    "only one new revision is supported for now".into(),
36                ));
37            }
38            state.new_revisions.push(sha.clone());
39        }
40    };
41
42    let rate = match bound {
43        Bound::Old => *state
44            .priors
45            .old_pass_rate
46            .get_or_insert(DEFAULT_OLD_PASS_RATE),
47        Bound::New => *state
48            .priors
49            .new_pass_rate
50            .get_or_insert(DEFAULT_NEW_PASS_RATE),
51    };
52
53    let next_prompt = advance(&ctx.repo, state)?;
54    state::write(&ctx.state_dir, state)?;
55
56    println!(
57        "Marked {} as {}.",
58        &sha[..10],
59        match bound {
60            Bound::Old => "old",
61            Bound::New => "new",
62        }
63    );
64    println!(
65        "Expecting the test to pass {:.0}% of the time {} the regression.",
66        rate * 100.0,
67        match bound {
68            Bound::Old => "before",
69            Bound::New => "after",
70        }
71    );
72    println!(
73        "Change with 'git psect set-prior {} <rate>'.",
74        match bound {
75            Bound::Old => "old",
76            Bound::New => "new",
77        }
78    );
79    println!("{next_prompt}");
80    Ok(())
81}
82
83/// Called after any config mutation. Either prompts the user for the next
84/// required input or, once both bounds are set, kicks off the bisection.
85/// Returns a trailing prompt string to be printed after the caller's own output.
86pub fn advance(repo: &git2::Repository, state: &State) -> Result<String, Error> {
87    if state.old_revisions.is_empty() && state.new_revisions.is_empty() {
88        return Ok(concat!(
89            "Waiting for reference pre-regression and post-regression revisions.\n",
90            "Mark them with 'git psect old <rev>' and 'git psect new <rev>'."
91        )
92        .into());
93    }
94    if state.old_revisions.is_empty() {
95        return Ok(
96            "Now mark a reference pre-regression revision with 'git psect old <rev>'.".into(),
97        );
98    }
99    if state.new_revisions.is_empty() {
100        return Ok(
101            "Now mark a reference post-regression revision with 'git psect new <rev>'.".into(),
102        );
103    }
104
105    // Validate ancestry regardless of the order old/new were set.
106    let old_sha = state.old_revisions.first().unwrap();
107    let new_sha = state.new_revisions.first().unwrap();
108    let old_oid = repo.revparse_single(old_sha)?.id();
109    let new_oid = repo.revparse_single(new_sha)?.id();
110    if !repo.graph_descendant_of(new_oid, old_oid)? {
111        return Err(Error::Validation(format!(
112            "'{}' is not a descendant of '{}'",
113            &new_sha[..10],
114            &old_sha[..10]
115        )));
116    }
117
118    let candidates = candidates::build(repo, state)?;
119    if candidates.is_empty() {
120        return Err(Error::Validation(
121            "no commits found between old and new revisions".into(),
122        ));
123    }
124    let distributions = candidates::build_distributions(state);
125    let ps = candidates::reconstruct(repo, state, &candidates, &distributions)?;
126    let next_sha = candidates::checkout_next(repo, &distributions, &ps)?;
127    let next_summary = repo
128        .find_commit(repo.revparse_single(&next_sha)?.id())?
129        .summary()
130        .unwrap_or("")
131        .to_string();
132    Ok(format!(
133        concat!(
134            "Checking out {} \"{}\".\n",
135            "Now either:\n",
136            "- run your test and call 'git psect pass' or 'git psect fail', or\n",
137            "- use 'git psect run <test>' to run on autopilot."
138        ),
139        &next_sha[..10],
140        next_summary
141    ))
142}