bcore_mutation/
git_changes.rs1use crate::error::{MutationError, Result};
2use crate::project::Project;
3use regex::Regex;
4use std::process::Command;
5use std::str;
6
7const SECP256K1_BASE_REF: &str = "secp256k1-master";
9
10pub async fn run_git_command(args: &[&str]) -> Result<Vec<String>> {
11 let output = Command::new("git")
12 .args(args)
13 .output()
14 .map_err(|e| MutationError::Git(format!("Failed to execute git command: {}", e)))?;
15
16 if !output.status.success() {
17 let stderr = str::from_utf8(&output.stderr).unwrap_or("Unknown error");
18 return Err(MutationError::Git(format!(
19 "Git command failed: {}",
20 stderr
21 )));
22 }
23
24 let stdout = str::from_utf8(&output.stdout)
25 .map_err(|e| MutationError::Git(format!("Invalid UTF-8 in git output: {}", e)))?;
26
27 Ok(stdout.lines().map(|s| s.to_string()).collect())
28}
29
30pub async fn get_commit_hash() -> Result<String> {
31 let lines = run_git_command(&["rev-parse", "HEAD"]).await?;
32 Ok(lines.into_iter().next().unwrap_or_default())
33}
34
35pub async fn get_changed_files(pr_number: Option<u32>, project: Project) -> Result<Vec<String>> {
36 match project {
37 Project::BitcoinCore => get_changed_files_bitcoin_core(pr_number).await,
38 Project::Secp256k1 => get_changed_files_secp256k1(pr_number).await,
39 }
40}
41
42async fn get_changed_files_secp256k1(pr_number: Option<u32>) -> Result<Vec<String>> {
48 let url = Project::Secp256k1.repository_url();
49
50 let fetch_master_args = &["fetch", url, &format!("+master:{}", SECP256K1_BASE_REF)];
52 run_git_command(fetch_master_args).await?;
53
54 if let Some(pr) = pr_number {
55 println!("Fetching secp256k1 PR #{} from {}", pr, url);
56 let fetch_pr_args = &["fetch", url, &format!("pull/{}/head:pr/{}", pr, pr)];
57 run_git_command(fetch_pr_args).await?;
58 println!("Checking out pr/{}...", pr);
59 run_git_command(&["checkout", &format!("pr/{}", pr)]).await?;
60 }
61
62 let diff_args = &[
63 "diff",
64 "--name-only",
65 "--diff-filter=d",
66 &format!("{}...HEAD", SECP256K1_BASE_REF),
67 ];
68 run_git_command(diff_args).await
69}
70
71async fn get_changed_files_bitcoin_core(pr_number: Option<u32>) -> Result<Vec<String>> {
72 let mut used_remote = "upstream"; if let Some(pr) = pr_number {
75 let fetch_upstream_args = &["fetch", "upstream", &format!("pull/{}/head:pr/{}", pr, pr)];
77 match run_git_command(fetch_upstream_args).await {
78 Ok(_) => {
79 println!("Successfully fetched from upstream");
80 println!("Checking out...");
81 let checkout_args = &["checkout", &format!("pr/{}", pr)];
82 run_git_command(checkout_args).await?;
83 }
84 Err(upstream_err) => {
85 println!("Failed to fetch from upstream: {:?}", upstream_err);
86 println!("Trying to fetch from origin...");
87
88 let fetch_origin_args =
90 &["fetch", "origin", &format!("pull/{}/head:pr/{}", pr, pr)];
91 match run_git_command(fetch_origin_args).await {
92 Ok(_) => {
93 println!("Successfully fetched from origin");
94 used_remote = "origin";
95 println!("Checking out...");
96 let checkout_args = &["checkout", &format!("pr/{}", pr)];
97 run_git_command(checkout_args).await?;
98 }
99 Err(origin_err) => {
100 println!("Failed to fetch from origin: {:?}", origin_err);
101 println!("Attempting to rebase existing pr/{} branch...", pr);
102 let rebase_args = &["rebase", &format!("pr/{}", pr)];
103 run_git_command(rebase_args).await?;
104 }
107 }
108 }
109 }
110 }
111
112 let diff_args = &[
114 "diff",
115 "--name-only",
116 "--diff-filter=d",
117 &format!("{}/master...HEAD", used_remote),
118 ];
119 match run_git_command(diff_args).await {
120 Ok(result) => Ok(result),
121 Err(_) if used_remote == "upstream" => {
122 println!("Diff with upstream/master failed, trying origin/master...");
124 let diff_args_origin = &["diff", "--name-only", "origin/master...HEAD"];
125 run_git_command(diff_args_origin).await
126 }
127 Err(e) => Err(e),
128 }
129}
130
131pub async fn get_lines_touched(file_path: &str, project: Project) -> Result<Vec<usize>> {
132 let diff_output = match project {
133 Project::Secp256k1 => {
134 let diff_args = &[
136 "diff",
137 "--unified=0",
138 &format!("{}...HEAD", SECP256K1_BASE_REF),
139 "--",
140 file_path,
141 ];
142 run_git_command(diff_args).await?
143 }
144 Project::BitcoinCore => {
145 let diff_args_upstream = &[
147 "diff",
148 "--unified=0",
149 "upstream/master...HEAD",
150 "--",
151 file_path,
152 ];
153
154 match run_git_command(diff_args_upstream).await {
155 Ok(output) => output,
156 Err(_) => {
157 println!("Diff with upstream/master failed, trying origin/master...");
159 let diff_args_origin = &[
160 "diff",
161 "--unified=0",
162 "origin/master...HEAD",
163 "--",
164 file_path,
165 ];
166 run_git_command(diff_args_origin).await?
167 }
168 }
169 }
170 };
171
172 let mut lines = Vec::new();
173 let line_range_regex = Regex::new(r"@@.*\+(\d+)(?:,(\d+))?.*@@")?;
174 for line in diff_output {
175 if line.starts_with("@@") {
176 if let Some(captures) = line_range_regex.captures(&line) {
177 let start_line: usize = captures[1]
178 .parse()
179 .map_err(|_| MutationError::Git("Invalid line number in diff".to_string()))?;
180 let num_lines = if let Some(count_match) = captures.get(2) {
181 count_match
182 .as_str()
183 .parse::<usize>()
184 .map_err(|_| MutationError::Git("Invalid line count in diff".to_string()))?
185 } else {
186 1
187 };
188 lines.extend(start_line..start_line + num_lines);
189 }
190 }
191 }
192 Ok(lines)
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[tokio::test]
200 async fn test_get_lines_touched_parsing() {
201 let line_range_regex = Regex::new(r"@@.*\+(\d+)(?:,(\d+))?.*@@").unwrap();
203
204 let single_line = "@@ -10,0 +11 @@ some context";
206 if let Some(captures) = line_range_regex.captures(single_line) {
207 let start_line: usize = captures[1].parse().unwrap();
208 let num_lines = if let Some(count_match) = captures.get(2) {
209 count_match.as_str().parse::<usize>().unwrap()
210 } else {
211 1
212 };
213 assert_eq!(start_line, 11);
214 assert_eq!(num_lines, 1);
215 }
216
217 let multi_line = "@@ -10,3 +11,5 @@ some context";
219 if let Some(captures) = line_range_regex.captures(multi_line) {
220 let start_line: usize = captures[1].parse().unwrap();
221 let num_lines = captures.get(2).unwrap().as_str().parse::<usize>().unwrap();
222 assert_eq!(start_line, 11);
223 assert_eq!(num_lines, 5);
224 }
225 }
226}