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_changed_files(pr_number: Option<u32>) -> Result<Vec<String>> {
27    if let Some(pr) = pr_number {
28        // Fetch the PR
29        let fetch_args = &["fetch", "upstream", &format!("pull/{}/head:pr/{}", pr, pr)];
30        match run_git_command(fetch_args).await {
31            Ok(_) => {
32                println!("Checking out...");
33                let checkout_args = &["checkout", &format!("pr/{}", pr)];
34                run_git_command(checkout_args).await?;
35            }
36            Err(_) => {
37                println!("Fetching and updating branch...");
38                let rebase_args = &["rebase", &format!("pr/{}", pr)];
39                run_git_command(rebase_args).await?;
40            }
41        }
42    }
43
44    let diff_args = &["diff", "--name-only", "upstream/master...HEAD"];
45    run_git_command(diff_args).await
46}
47
48pub async fn get_lines_touched(file_path: &str) -> Result<Vec<usize>> {
49    let diff_args = &[
50        "diff",
51        "--unified=0",
52        "upstream/master...HEAD",
53        "--",
54        file_path,
55    ];
56    let diff_output = run_git_command(diff_args).await?;
57
58    let mut lines = Vec::new();
59    let line_range_regex = Regex::new(r"@@.*\+(\d+)(?:,(\d+))?.*@@")?;
60
61    for line in diff_output {
62        if line.starts_with("@@") {
63            if let Some(captures) = line_range_regex.captures(&line) {
64                let start_line: usize = captures[1]
65                    .parse()
66                    .map_err(|_| MutationError::Git("Invalid line number in diff".to_string()))?;
67
68                let num_lines = if let Some(count_match) = captures.get(2) {
69                    count_match
70                        .as_str()
71                        .parse::<usize>()
72                        .map_err(|_| MutationError::Git("Invalid line count in diff".to_string()))?
73                } else {
74                    1
75                };
76
77                lines.extend(start_line..start_line + num_lines);
78            }
79        }
80    }
81
82    Ok(lines)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[tokio::test]
90    async fn test_get_lines_touched_parsing() {
91        // This would require a git repository setup, so we'll test the regex parsing logic
92        let line_range_regex = Regex::new(r"@@.*\+(\d+)(?:,(\d+))?.*@@").unwrap();
93
94        // Test single line change
95        let single_line = "@@ -10,0 +11 @@ some context";
96        if let Some(captures) = line_range_regex.captures(single_line) {
97            let start_line: usize = captures[1].parse().unwrap();
98            let num_lines = if let Some(count_match) = captures.get(2) {
99                count_match.as_str().parse::<usize>().unwrap()
100            } else {
101                1
102            };
103            assert_eq!(start_line, 11);
104            assert_eq!(num_lines, 1);
105        }
106
107        // Test multiple line change
108        let multi_line = "@@ -10,3 +11,5 @@ some context";
109        if let Some(captures) = line_range_regex.captures(multi_line) {
110            let start_line: usize = captures[1].parse().unwrap();
111            let num_lines = captures.get(2).unwrap().as_str().parse::<usize>().unwrap();
112            assert_eq!(start_line, 11);
113            assert_eq!(num_lines, 5);
114        }
115    }
116}