Skip to main content

bcore_mutation/
git_changes.rs

1use 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"; // Track which remote we successfully used
33
34    if let Some(pr) = pr_number {
35        // Try to fetch the PR from upstream first
36        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                // Try to fetch from origin as fallback
49                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                        // In rebase case, we don't know which remote was used originally
65                        // Try upstream first, fall back to origin if it fails
66                    }
67                }
68            }
69        }
70    }
71
72    // Try diff with the appropriate remote
73    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            // If upstream diff failed, try origin
83            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    // Try upstream first
93    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            // Fall back to origin if upstream fails
105            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        // This would require a git repository setup, so we'll test the regex parsing logic
147        let line_range_regex = Regex::new(r"@@.*\+(\d+)(?:,(\d+))?.*@@").unwrap();
148
149        // Test single line change
150        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        // Test multiple line change
163        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}