git_psect/commands/
session.rs1use 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
12pub 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
83pub 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 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}