reth_bench_compare/
git.rs1use eyre::{eyre, Result, WrapErr};
4use std::process::Command;
5use tracing::{info, warn};
6
7#[derive(Debug, Clone)]
9pub(crate) struct GitManager {
10 repo_root: String,
11}
12
13impl GitManager {
14 pub(crate) fn new() -> Result<Self> {
16 let output = Command::new("git")
17 .args(["rev-parse", "--show-toplevel"])
18 .output()
19 .wrap_err("Failed to execute git command - is git installed?")?;
20
21 if !output.status.success() {
22 return Err(eyre!("Not in a git repository or git command failed"));
23 }
24
25 let repo_root = String::from_utf8(output.stdout)
26 .wrap_err("Git output is not valid UTF-8")?
27 .trim()
28 .to_string();
29
30 let manager = Self { repo_root };
31 info!(
32 "Detected git repository at: {}, current reference: {}",
33 manager.repo_root(),
34 manager.get_current_ref()?
35 );
36
37 Ok(manager)
38 }
39
40 pub(crate) fn get_current_branch(&self) -> Result<String> {
42 let output = Command::new("git")
43 .args(["branch", "--show-current"])
44 .current_dir(&self.repo_root)
45 .output()
46 .wrap_err("Failed to get current branch")?;
47
48 if !output.status.success() {
49 return Err(eyre!("Failed to determine current branch"));
50 }
51
52 let branch = String::from_utf8(output.stdout)
53 .wrap_err("Branch name is not valid UTF-8")?
54 .trim()
55 .to_string();
56
57 if branch.is_empty() {
58 return Err(eyre!("Not on a named branch (detached HEAD?)"));
59 }
60
61 Ok(branch)
62 }
63
64 pub(crate) fn get_current_ref(&self) -> Result<String> {
66 if let Ok(branch) = self.get_current_branch() {
68 return Ok(branch);
69 }
70
71 let tag_output = Command::new("git")
73 .args(["describe", "--exact-match", "--tags", "HEAD"])
74 .current_dir(&self.repo_root)
75 .output()
76 .wrap_err("Failed to check for tag")?;
77
78 if tag_output.status.success() {
79 let tag = String::from_utf8(tag_output.stdout)
80 .wrap_err("Tag name is not valid UTF-8")?
81 .trim()
82 .to_string();
83 return Ok(tag);
84 }
85
86 let commit_output = Command::new("git")
88 .args(["rev-parse", "HEAD"])
89 .current_dir(&self.repo_root)
90 .output()
91 .wrap_err("Failed to get current commit")?;
92
93 if !commit_output.status.success() {
94 return Err(eyre!("Failed to get current commit hash"));
95 }
96
97 let commit_hash = String::from_utf8(commit_output.stdout)
98 .wrap_err("Commit hash is not valid UTF-8")?
99 .trim()
100 .to_string();
101
102 Ok(commit_hash)
103 }
104
105 pub(crate) fn validate_clean_state(&self) -> Result<()> {
107 let output = Command::new("git")
108 .args(["status", "--porcelain"])
109 .current_dir(&self.repo_root)
110 .output()
111 .wrap_err("Failed to check git status")?;
112
113 if !output.status.success() {
114 return Err(eyre!("Git status command failed"));
115 }
116
117 let status_output =
118 String::from_utf8(output.stdout).wrap_err("Git status output is not valid UTF-8")?;
119
120 let has_uncommitted_changes = status_output.lines().any(|line| {
124 if line.len() >= 2 {
125 let status = &line[0..2];
126 !matches!(status, "??" | "!!")
128 } else {
129 false
130 }
131 });
132
133 if has_uncommitted_changes {
134 warn!("Git working directory has uncommitted changes to tracked files:");
135 for line in status_output.lines() {
136 if line.len() >= 2 && !matches!(&line[0..2], "??" | "!!") {
137 warn!(" {}", line);
138 }
139 }
140 return Err(eyre!(
141 "Git working directory has uncommitted changes to tracked files. Please commit or stash changes before running benchmark comparison."
142 ));
143 }
144
145 let untracked_files: Vec<&str> =
147 status_output.lines().filter(|line| line.starts_with("??")).collect();
148
149 if !untracked_files.is_empty() {
150 info!(
151 "Git working directory has {} untracked files (this is OK)",
152 untracked_files.len()
153 );
154 }
155
156 info!("Git working directory is clean (no uncommitted changes to tracked files)");
157 Ok(())
158 }
159
160 pub(crate) fn fetch_all(&self) -> Result<()> {
162 let output = Command::new("git")
163 .args(["fetch", "--all", "--tags", "--quiet", "--force"])
164 .current_dir(&self.repo_root)
165 .output()
166 .wrap_err("Failed to fetch latest refs")?;
167
168 if output.status.success() {
169 info!("Fetched latest refs");
170 } else {
171 let stderr = String::from_utf8_lossy(&output.stderr);
172 if !stderr.trim().is_empty() && !stderr.contains("-> origin/") {
174 warn!("Git fetch encountered issues (continuing anyway): {}", stderr);
175 }
176 }
177
178 Ok(())
179 }
180
181 pub(crate) fn validate_refs(&self, refs: &[&str]) -> Result<()> {
183 for &git_ref in refs {
184 let as_is = format!("{git_ref}^{{commit}}");
187 let ref_check = Command::new("git")
188 .args(["rev-parse", "--verify", &as_is])
189 .current_dir(&self.repo_root)
190 .output();
191
192 let found = if let Ok(output) = ref_check &&
193 output.status.success()
194 {
195 info!("Validated reference exists: {}", git_ref);
196 true
197 } else {
198 let origin_ref = format!("origin/{git_ref}^{{commit}}");
200 let origin_check = Command::new("git")
201 .args(["rev-parse", "--verify", &origin_ref])
202 .current_dir(&self.repo_root)
203 .output();
204
205 if let Ok(output) = origin_check &&
206 output.status.success()
207 {
208 info!("Validated remote reference exists: origin/{}", git_ref);
209 true
210 } else {
211 false
212 }
213 };
214
215 if !found {
216 return Err(eyre!(
217 "Git reference '{}' does not exist as branch, tag, or commit (tried '{}' and 'origin/{}^{{commit}}')",
218 git_ref,
219 format!("{git_ref}^{{commit}}"),
220 git_ref,
221 ));
222 }
223 }
224
225 Ok(())
226 }
227
228 pub(crate) fn switch_ref(&self, git_ref: &str) -> Result<()> {
230 let output = Command::new("git")
232 .args(["checkout", git_ref])
233 .current_dir(&self.repo_root)
234 .output()
235 .wrap_err_with(|| format!("Failed to switch to reference '{git_ref}'"))?;
236
237 if !output.status.success() {
238 let stderr = String::from_utf8_lossy(&output.stderr);
239 return Err(eyre!("Failed to switch to reference '{}': {}", git_ref, stderr));
240 }
241
242 let is_branch = Command::new("git")
244 .args(["show-ref", "--verify", "--quiet", &format!("refs/heads/{git_ref}")])
245 .current_dir(&self.repo_root)
246 .status()
247 .map(|s| s.success())
248 .unwrap_or(false);
249
250 if is_branch {
251 let tracking_output = Command::new("git")
253 .args([
254 "rev-parse",
255 "--abbrev-ref",
256 "--symbolic-full-name",
257 &format!("{git_ref}@{{upstream}}"),
258 ])
259 .current_dir(&self.repo_root)
260 .output();
261
262 if let Ok(output) = tracking_output &&
263 output.status.success()
264 {
265 let upstream = String::from_utf8_lossy(&output.stdout).trim().to_string();
266 if !upstream.is_empty() && upstream != format!("{git_ref}@{{upstream}}") {
267 info!("Pulling latest changes for branch: {}", git_ref);
269
270 let pull_output = Command::new("git")
271 .args(["pull", "--ff-only"])
272 .current_dir(&self.repo_root)
273 .output()
274 .wrap_err_with(|| {
275 format!("Failed to pull latest changes for branch '{git_ref}'")
276 })?;
277
278 if pull_output.status.success() {
279 info!("Successfully pulled latest changes for branch: {}", git_ref);
280 } else {
281 let stderr = String::from_utf8_lossy(&pull_output.stderr);
282 warn!("Failed to pull latest changes for branch '{}': {}", git_ref, stderr);
283 }
285 }
286 }
287 }
288
289 let current_commit_output = Command::new("git")
291 .args(["rev-parse", "HEAD"])
292 .current_dir(&self.repo_root)
293 .output()
294 .wrap_err("Failed to get current commit")?;
295
296 if !current_commit_output.status.success() {
297 return Err(eyre!("Failed to verify git checkout"));
298 }
299
300 info!("Switched to reference: {}", git_ref);
301 Ok(())
302 }
303
304 pub(crate) fn get_current_commit(&self) -> Result<String> {
306 let output = Command::new("git")
307 .args(["rev-parse", "HEAD"])
308 .current_dir(&self.repo_root)
309 .output()
310 .wrap_err("Failed to get current commit")?;
311
312 if !output.status.success() {
313 return Err(eyre!("Failed to get current commit hash"));
314 }
315
316 let commit_hash = String::from_utf8(output.stdout)
317 .wrap_err("Commit hash is not valid UTF-8")?
318 .trim()
319 .to_string();
320
321 Ok(commit_hash)
322 }
323
324 pub(crate) fn repo_root(&self) -> &str {
326 &self.repo_root
327 }
328}