use crate::error::{MutationError, Result};
use crate::project::Project;
use regex::Regex;
use std::process::Command;
use std::str;
const SECP256K1_BASE_REF: &str = "secp256k1-master";
pub async fn run_git_command(args: &[&str]) -> Result<Vec<String>> {
let output = Command::new("git")
.args(args)
.output()
.map_err(|e| MutationError::Git(format!("Failed to execute git command: {}", e)))?;
if !output.status.success() {
let stderr = str::from_utf8(&output.stderr).unwrap_or("Unknown error");
return Err(MutationError::Git(format!(
"Git command failed: {}",
stderr
)));
}
let stdout = str::from_utf8(&output.stdout)
.map_err(|e| MutationError::Git(format!("Invalid UTF-8 in git output: {}", e)))?;
Ok(stdout.lines().map(|s| s.to_string()).collect())
}
pub async fn get_commit_hash() -> Result<String> {
let lines = run_git_command(&["rev-parse", "HEAD"]).await?;
Ok(lines.into_iter().next().unwrap_or_default())
}
pub async fn get_changed_files(pr_number: Option<u32>, project: Project) -> Result<Vec<String>> {
match project {
Project::BitcoinCore => get_changed_files_bitcoin_core(pr_number).await,
Project::Secp256k1 => get_changed_files_secp256k1(pr_number).await,
}
}
async fn get_changed_files_secp256k1(pr_number: Option<u32>) -> Result<Vec<String>> {
let url = Project::Secp256k1.repository_url();
let fetch_master_args = &["fetch", url, &format!("+master:{}", SECP256K1_BASE_REF)];
run_git_command(fetch_master_args).await?;
if let Some(pr) = pr_number {
println!("Fetching secp256k1 PR #{} from {}", pr, url);
let fetch_pr_args = &["fetch", url, &format!("pull/{}/head:pr/{}", pr, pr)];
run_git_command(fetch_pr_args).await?;
println!("Checking out pr/{}...", pr);
run_git_command(&["checkout", &format!("pr/{}", pr)]).await?;
}
let diff_args = &[
"diff",
"--name-only",
"--diff-filter=d",
&format!("{}...HEAD", SECP256K1_BASE_REF),
];
run_git_command(diff_args).await
}
async fn get_changed_files_bitcoin_core(pr_number: Option<u32>) -> Result<Vec<String>> {
let mut used_remote = "upstream";
if let Some(pr) = pr_number {
let fetch_upstream_args = &["fetch", "upstream", &format!("pull/{}/head:pr/{}", pr, pr)];
match run_git_command(fetch_upstream_args).await {
Ok(_) => {
println!("Successfully fetched from upstream");
println!("Checking out...");
let checkout_args = &["checkout", &format!("pr/{}", pr)];
run_git_command(checkout_args).await?;
}
Err(upstream_err) => {
println!("Failed to fetch from upstream: {:?}", upstream_err);
println!("Trying to fetch from origin...");
let fetch_origin_args =
&["fetch", "origin", &format!("pull/{}/head:pr/{}", pr, pr)];
match run_git_command(fetch_origin_args).await {
Ok(_) => {
println!("Successfully fetched from origin");
used_remote = "origin";
println!("Checking out...");
let checkout_args = &["checkout", &format!("pr/{}", pr)];
run_git_command(checkout_args).await?;
}
Err(origin_err) => {
println!("Failed to fetch from origin: {:?}", origin_err);
println!("Attempting to rebase existing pr/{} branch...", pr);
let rebase_args = &["rebase", &format!("pr/{}", pr)];
run_git_command(rebase_args).await?;
}
}
}
}
}
let diff_args = &[
"diff",
"--name-only",
"--diff-filter=d",
&format!("{}/master...HEAD", used_remote),
];
match run_git_command(diff_args).await {
Ok(result) => Ok(result),
Err(_) if used_remote == "upstream" => {
println!("Diff with upstream/master failed, trying origin/master...");
let diff_args_origin = &["diff", "--name-only", "origin/master...HEAD"];
run_git_command(diff_args_origin).await
}
Err(e) => Err(e),
}
}
pub async fn get_lines_touched(file_path: &str, project: Project) -> Result<Vec<usize>> {
let diff_output = match project {
Project::Secp256k1 => {
let diff_args = &[
"diff",
"--unified=0",
&format!("{}...HEAD", SECP256K1_BASE_REF),
"--",
file_path,
];
run_git_command(diff_args).await?
}
Project::BitcoinCore => {
let diff_args_upstream = &[
"diff",
"--unified=0",
"upstream/master...HEAD",
"--",
file_path,
];
match run_git_command(diff_args_upstream).await {
Ok(output) => output,
Err(_) => {
println!("Diff with upstream/master failed, trying origin/master...");
let diff_args_origin = &[
"diff",
"--unified=0",
"origin/master...HEAD",
"--",
file_path,
];
run_git_command(diff_args_origin).await?
}
}
}
};
let mut lines = Vec::new();
let line_range_regex = Regex::new(r"@@.*\+(\d+)(?:,(\d+))?.*@@")?;
for line in diff_output {
if line.starts_with("@@") {
if let Some(captures) = line_range_regex.captures(&line) {
let start_line: usize = captures[1]
.parse()
.map_err(|_| MutationError::Git("Invalid line number in diff".to_string()))?;
let num_lines = if let Some(count_match) = captures.get(2) {
count_match
.as_str()
.parse::<usize>()
.map_err(|_| MutationError::Git("Invalid line count in diff".to_string()))?
} else {
1
};
lines.extend(start_line..start_line + num_lines);
}
}
}
Ok(lines)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_get_lines_touched_parsing() {
let line_range_regex = Regex::new(r"@@.*\+(\d+)(?:,(\d+))?.*@@").unwrap();
let single_line = "@@ -10,0 +11 @@ some context";
if let Some(captures) = line_range_regex.captures(single_line) {
let start_line: usize = captures[1].parse().unwrap();
let num_lines = if let Some(count_match) = captures.get(2) {
count_match.as_str().parse::<usize>().unwrap()
} else {
1
};
assert_eq!(start_line, 11);
assert_eq!(num_lines, 1);
}
let multi_line = "@@ -10,3 +11,5 @@ some context";
if let Some(captures) = line_range_regex.captures(multi_line) {
let start_line: usize = captures[1].parse().unwrap();
let num_lines = captures.get(2).unwrap().as_str().parse::<usize>().unwrap();
assert_eq!(start_line, 11);
assert_eq!(num_lines, 5);
}
}
}