use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct DiffInfo {
pub changed_files: HashSet<PathBuf>,
pub deleted_files: HashSet<PathBuf>,
}
impl DiffInfo {
pub fn from_git_diff(diff_output: &str) -> Result<Self, String> {
let mut changed_files = HashSet::new();
let mut deleted_files = HashSet::new();
for line in diff_output.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let status = parts[0];
let file_path = if status.starts_with('R') || status.starts_with('C') {
parts.get(2).unwrap_or(&parts[1])
} else {
parts.get(1).unwrap_or(&"")
};
if file_path.is_empty() {
continue;
}
let path = PathBuf::from(file_path);
match status.chars().next() {
Some('D') => {
deleted_files.insert(path);
}
Some('M') | Some('A') | Some('R') | Some('C') => {
changed_files.insert(path);
}
_ => {}
}
}
Ok(Self {
changed_files,
deleted_files,
})
}
pub fn is_changed(&self, file_path: &Path) -> bool {
self.changed_files.contains(&file_path.to_path_buf())
}
pub fn is_deleted(&self, file_path: &Path) -> bool {
self.deleted_files.contains(&file_path.to_path_buf())
}
pub fn changed_file_strings(&self) -> Vec<String> {
self.changed_files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect()
}
}
pub struct DependentAnalyzer {
function_call_sites: HashMap<String, HashSet<String>>,
file_to_functions: HashMap<String, HashSet<String>>,
}
impl Default for DependentAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl DependentAnalyzer {
pub fn new() -> Self {
Self {
function_call_sites: HashMap::new(),
file_to_functions: HashMap::new(),
}
}
pub fn add_function_definition(&mut self, function: String, file: String) {
self.file_to_functions
.entry(file)
.or_default()
.insert(function);
}
pub fn add_function_call(&mut self, function: String, called_in_file: String) {
self.function_call_sites
.entry(function)
.or_default()
.insert(called_in_file);
}
pub fn find_affected_files(&self, changed_files: &HashSet<PathBuf>) -> HashSet<String> {
let mut affected = HashSet::new();
let changed_file_strings: HashSet<String> = changed_files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
for file in &changed_file_strings {
affected.insert(file.clone());
}
for changed_file in &changed_file_strings {
if let Some(functions) = self.file_to_functions.get(changed_file) {
for function in functions {
if let Some(call_sites) = self.function_call_sites.get(function) {
affected.extend(call_sites.clone());
}
}
}
}
affected
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_git_diff_modified() {
let diff = "M\tsrc/file1.rs\nM\tsrc/file2.rs";
let info = DiffInfo::from_git_diff(diff).unwrap();
assert_eq!(info.changed_files.len(), 2);
assert!(info.is_changed(Path::new("src/file1.rs")));
assert!(info.is_changed(Path::new("src/file2.rs")));
}
#[test]
fn test_parse_git_diff_added_deleted() {
let diff = "A\tsrc/new.rs\nD\tsrc/old.rs";
let info = DiffInfo::from_git_diff(diff).unwrap();
assert!(info.is_changed(Path::new("src/new.rs")));
assert!(info.is_deleted(Path::new("src/old.rs")));
}
#[test]
fn test_parse_git_diff_renamed() {
let diff = "R100\tsrc/old.rs\tsrc/new.rs";
let info = DiffInfo::from_git_diff(diff).unwrap();
assert!(info.is_changed(Path::new("src/new.rs")));
}
#[test]
fn test_find_affected_files() {
let mut analyzer = DependentAnalyzer::new();
analyzer.add_function_definition("foo".to_string(), "src/a.rs".to_string());
analyzer.add_function_definition("bar".to_string(), "src/b.rs".to_string());
analyzer.add_function_call("foo".to_string(), "src/c.rs".to_string());
analyzer.add_function_call("bar".to_string(), "src/d.rs".to_string());
let mut changed = HashSet::new();
changed.insert(PathBuf::from("src/a.rs"));
let affected = analyzer.find_affected_files(&changed);
assert!(affected.contains("src/a.rs"));
assert!(affected.contains("src/c.rs"));
assert!(!affected.contains("src/b.rs")); }
#[test]
fn test_empty_diff() {
let diff = "";
let info = DiffInfo::from_git_diff(diff).unwrap();
assert!(info.changed_files.is_empty());
assert!(info.deleted_files.is_empty());
}
}