1use anyhow::Context;
4use perfgate_adapters::{CommandSpec, ProcessRunner, StdProcessRunner};
5use std::fs;
6use std::path::PathBuf;
7use std::process::Command;
8
9pub struct BisectRequest {
10 pub good: String,
11 pub bad: String,
12 pub build_cmd: String,
13 pub executable: PathBuf,
14 pub threshold: f64,
15}
16
17pub struct BisectUseCase<R: ProcessRunner> {
18 runner: R,
19}
20
21impl Default for BisectUseCase<StdProcessRunner> {
22 fn default() -> Self {
23 Self::new(StdProcessRunner)
24 }
25}
26
27impl<R: ProcessRunner> BisectUseCase<R> {
28 pub fn new(runner: R) -> Self {
29 Self { runner }
30 }
31
32 pub fn execute(&self, req: BisectRequest) -> anyhow::Result<()> {
33 let original_branch = Self::get_current_branch()?;
34
35 println!("Checking out good commit: {}", req.good);
37 Self::run_git(&["checkout", &req.good])?;
38
39 println!("Building baseline...");
41 self.run_shell(&req.build_cmd)?;
42
43 let baseline_exe = req.executable.with_extension("baseline.exe");
45 fs::copy(&req.executable, &baseline_exe).context("Failed to copy baseline executable")?;
46
47 println!("Starting git bisect...");
49 Self::run_git(&["bisect", "start", &req.bad, &req.good])?;
50
51 loop {
53 println!("\nBuilding current commit...");
54 let build_res = self.run_shell(&req.build_cmd);
55
56 let result = if build_res.is_err() || build_res.unwrap().exit_code != 0 {
57 println!("Build failed, skipping commit...");
58 "skip"
59 } else {
60 println!("Running performance comparison...");
61 let current_exe = std::env::current_exe()?;
62 let mut paired = Command::new(current_exe);
63 paired.args([
64 "paired",
65 "--name",
66 "bisect",
67 "--baseline-cmd",
68 &baseline_exe.to_string_lossy(),
69 "--current-cmd",
70 &req.executable.to_string_lossy(),
71 "--fail-on-regression",
72 &req.threshold.to_string(),
73 "--require-significance",
74 ]);
75
76 let paired_status = paired.status().context("Failed to run perfgate paired")?;
77
78 if paired_status.success() {
79 println!("Performance looks good!");
80 "good"
81 } else {
82 println!("Performance regressed!");
83 "bad"
84 }
85 };
86
87 let out = Command::new("git")
88 .args(["bisect", result])
89 .output()
90 .context("Failed to run git bisect step")?;
91 let stdout = String::from_utf8_lossy(&out.stdout);
92
93 if stdout.contains("is the first bad commit") {
94 println!("\n{}", stdout);
95
96 if let Some(first_word) = stdout.split_whitespace().next() {
98 let author_out = Command::new("git")
99 .args(["show", "-s", "--format=%an <%ae>", first_word])
100 .output()
101 .ok();
102 if let Some(author_out) = author_out
103 && author_out.status.success()
104 {
105 let author = String::from_utf8_lossy(&author_out.stdout)
106 .trim()
107 .to_string();
108 println!("Regression Blame: Likely introduced by {}", author);
109 }
110 }
111
112 break;
113 } else if !out.status.success() {
114 anyhow::bail!(
115 "git bisect failed: {}",
116 String::from_utf8_lossy(&out.stderr)
117 );
118 }
119 }
120
121 println!("Cleaning up...");
123 let _ = Self::run_git(&["bisect", "reset"]);
124 if !original_branch.is_empty() {
125 let _ = Self::run_git(&["checkout", &original_branch]);
126 }
127 let _ = fs::remove_file(&baseline_exe);
128
129 Ok(())
130 }
131
132 fn get_current_branch() -> anyhow::Result<String> {
133 let out = Command::new("git")
134 .args(["rev-parse", "--abbrev-ref", "HEAD"])
135 .output()
136 .context("Failed to get current branch")?;
137 Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
138 }
139
140 fn run_git(args: &[&str]) -> anyhow::Result<()> {
141 let status = Command::new("git").args(args).status()?;
142 if !status.success() {
143 anyhow::bail!("git command failed: {:?}", args);
144 }
145 Ok(())
146 }
147
148 fn run_shell(&self, cmd: &str) -> anyhow::Result<perfgate_adapters::RunResult> {
149 let spec = if cfg!(windows) {
150 CommandSpec {
151 name: "cmd".to_string(),
152 argv: vec!["/C".to_string(), cmd.to_string()],
153 ..Default::default()
154 }
155 } else {
156 CommandSpec {
157 name: "sh".to_string(),
158 argv: vec!["-c".to_string(), cmd.to_string()],
159 ..Default::default()
160 }
161 };
162
163 self.runner.run(&spec).map_err(|e| anyhow::anyhow!(e))
164 }
165}