bcore_mutation/
git_changes.rs1use crate::error::{MutationError, Result};
2use regex::Regex;
3use std::process::Command;
4use std::str;
5
6pub async fn run_git_command(args: &[&str]) -> Result<Vec<String>> {
7 let output = Command::new("git")
8 .args(args)
9 .output()
10 .map_err(|e| MutationError::Git(format!("Failed to execute git command: {}", e)))?;
11
12 if !output.status.success() {
13 let stderr = str::from_utf8(&output.stderr).unwrap_or("Unknown error");
14 return Err(MutationError::Git(format!(
15 "Git command failed: {}",
16 stderr
17 )));
18 }
19
20 let stdout = str::from_utf8(&output.stdout)
21 .map_err(|e| MutationError::Git(format!("Invalid UTF-8 in git output: {}", e)))?;
22
23 Ok(stdout.lines().map(|s| s.to_string()).collect())
24}
25
26pub async fn get_commit_hash() -> Result<String> {
27 let lines = run_git_command(&["rev-parse", "HEAD"]).await?;
28 Ok(lines.into_iter().next().unwrap_or_default())
29}
30
31pub async fn get_changed_files(pr_number: Option<u32>) -> Result<Vec<String>> {
32 let mut used_remote = "upstream"; if let Some(pr) = pr_number {
35 let fetch_upstream_args = &["fetch", "upstream", &format!("pull/{}/head:pr/{}", pr, pr)];
37 match run_git_command(fetch_upstream_args).await {
38 Ok(_) => {
39 println!("Successfully fetched from upstream");
40 println!("Checking out...");
41 let checkout_args = &["checkout", &format!("pr/{}", pr)];
42 run_git_command(checkout_args).await?;
43 }
44 Err(upstream_err) => {
45 println!("Failed to fetch from upstream: {:?}", upstream_err);
46 println!("Trying to fetch from origin...");
47
48 let fetch_origin_args =
50 &["fetch", "origin", &format!("pull/{}/head:pr/{}", pr, pr)];
51 match run_git_command(fetch_origin_args).await {
52 Ok(_) => {
53 println!("Successfully fetched from origin");
54 used_remote = "origin";
55 println!("Checking out...");
56 let checkout_args = &["checkout", &format!("pr/{}", pr)];
57 run_git_command(checkout_args).await?;
58 }
59 Err(origin_err) => {
60 println!("Failed to fetch from origin: {:?}", origin_err);
61 println!("Attempting to rebase existing pr/{} branch...", pr);
62 let rebase_args = &["rebase", &format!("pr/{}", pr)];
63 run_git_command(rebase_args).await?;
64 }
67 }
68 }
69 }
70 }
71
72 let diff_args = &[
74 "diff",
75 "--name-only",
76 "--diff-filter=d",
77 &format!("{}/master...HEAD", used_remote),
78 ];
79 match run_git_command(diff_args).await {
80 Ok(result) => Ok(result),
81 Err(_) if used_remote == "upstream" => {
82 println!("Diff with upstream/master failed, trying origin/master...");
84 let diff_args_origin = &["diff", "--name-only", "origin/master...HEAD"];
85 run_git_command(diff_args_origin).await
86 }
87 Err(e) => Err(e),
88 }
89}
90
91pub async fn get_lines_touched(file_path: &str) -> Result<Vec<usize>> {
92 let diff_args_upstream = &[
94 "diff",
95 "--unified=0",
96 "upstream/master...HEAD",
97 "--",
98 file_path,
99 ];
100
101 let diff_output = match run_git_command(diff_args_upstream).await {
102 Ok(output) => output,
103 Err(_) => {
104 println!("Diff with upstream/master failed, trying origin/master...");
106 let diff_args_origin = &[
107 "diff",
108 "--unified=0",
109 "origin/master...HEAD",
110 "--",
111 file_path,
112 ];
113 run_git_command(diff_args_origin).await?
114 }
115 };
116
117 let mut lines = Vec::new();
118 let line_range_regex = Regex::new(r"@@.*\+(\d+)(?:,(\d+))?.*@@")?;
119 for line in diff_output {
120 if line.starts_with("@@") {
121 if let Some(captures) = line_range_regex.captures(&line) {
122 let start_line: usize = captures[1]
123 .parse()
124 .map_err(|_| MutationError::Git("Invalid line number in diff".to_string()))?;
125 let num_lines = if let Some(count_match) = captures.get(2) {
126 count_match
127 .as_str()
128 .parse::<usize>()
129 .map_err(|_| MutationError::Git("Invalid line count in diff".to_string()))?
130 } else {
131 1
132 };
133 lines.extend(start_line..start_line + num_lines);
134 }
135 }
136 }
137 Ok(lines)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[tokio::test]
145 async fn test_get_lines_touched_parsing() {
146 let line_range_regex = Regex::new(r"@@.*\+(\d+)(?:,(\d+))?.*@@").unwrap();
148
149 let single_line = "@@ -10,0 +11 @@ some context";
151 if let Some(captures) = line_range_regex.captures(single_line) {
152 let start_line: usize = captures[1].parse().unwrap();
153 let num_lines = if let Some(count_match) = captures.get(2) {
154 count_match.as_str().parse::<usize>().unwrap()
155 } else {
156 1
157 };
158 assert_eq!(start_line, 11);
159 assert_eq!(num_lines, 1);
160 }
161
162 let multi_line = "@@ -10,3 +11,5 @@ some context";
164 if let Some(captures) = line_range_regex.captures(multi_line) {
165 let start_line: usize = captures[1].parse().unwrap();
166 let num_lines = captures.get(2).unwrap().as_str().parse::<usize>().unwrap();
167 assert_eq!(start_line, 11);
168 assert_eq!(num_lines, 5);
169 }
170 }
171}