use std::path::Path;
use serde::Serialize;
use tracing::warn;
use crate::error::DiffError;
use crate::model::{ExtractedSymbol, FileChangeStatus, ImpactEntry};
use crate::port::{DiffStore, LanguageRegistry, VcsProvider};
use super::change::{
compare_extracted_symbols, symbol_change_from_current, symbol_change_from_old, ChangeType,
SymbolChange,
};
#[derive(Debug, Serialize)]
pub struct SinceResult {
pub reference: String,
pub commit_count: usize,
pub added: Vec<SymbolChange>,
pub modified: Vec<SymbolChange>,
pub removed: Vec<SymbolChange>,
pub downstream_impacts: Vec<ImpactEntry>,
pub summary: SinceSummary,
}
#[derive(Debug, Serialize)]
pub struct SinceSummary {
pub added_count: usize,
pub modified_count: usize,
pub removed_count: usize,
pub impact_count: usize,
pub files_changed: usize,
}
pub fn since_against_ref(
vcs: &dyn VcsProvider,
reference: &str,
db: &dyn DiffStore,
impact_depth: i64,
supported_ext: &[&str],
lang: &dyn LanguageRegistry,
) -> Result<SinceResult, DiffError> {
let vcs_changes = vcs.changes_between_refs(reference, "HEAD")?;
let commit_count = vcs.commit_count(reference, "HEAD")?;
let (mut added, mut modified, mut removed, mut modified_sym_ids) =
(Vec::new(), Vec::new(), Vec::new(), Vec::new());
for change in &vcs_changes {
let primary_path = if change.status == FileChangeStatus::Deleted {
change.old_path.as_deref().unwrap_or(&change.path)
} else {
&change.path
};
let is_supported = Path::new(primary_path)
.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| supported_ext.contains(&ext));
if !is_supported {
continue;
}
match change.status {
FileChangeStatus::Added => {
let current = db.all_symbols_for_file(&change.path).unwrap_or_default();
for sym in current {
added.push(symbol_change_from_current(sym, ChangeType::Added));
}
}
FileChangeStatus::Deleted => {
let old_path = change.old_path.as_deref().unwrap_or(&change.path);
let Some(old_symbols) = parse_old_blob(vcs, reference, old_path, lang) else {
warn!("skipping unparseable deleted file: {}", old_path);
continue;
};
for sym in &old_symbols {
removed.push(symbol_change_from_old(sym, &old_symbols, old_path));
}
}
FileChangeStatus::Modified | FileChangeStatus::Renamed => {
let old_path = change.old_path.as_deref().unwrap_or(&change.path);
let Some(old_symbols) = parse_old_blob(vcs, reference, old_path, lang) else {
warn!("skipping unparseable file: {}", old_path);
continue;
};
let current = db.all_symbols_for_file(&change.path).unwrap_or_default();
let (changes, file_mod_ids) =
compare_extracted_symbols(&old_symbols, ¤t, &change.path);
for c in changes {
match c.change_type {
ChangeType::Added => added.push(c),
ChangeType::Modified => modified.push(c),
ChangeType::Removed => removed.push(c),
}
}
modified_sym_ids.extend(file_mod_ids);
}
}
}
let mut downstream_impacts = Vec::new();
let mut seen_ids = std::collections::HashSet::new();
for sym_id in &modified_sym_ids {
seen_ids.insert(*sym_id);
}
for sym_id in &modified_sym_ids {
if let Ok(entries) = db.impact_analysis(*sym_id, impact_depth) {
for entry in entries {
if seen_ids.insert(entry.symbol_id) {
downstream_impacts.push(entry);
}
}
}
}
let files_changed = vcs_changes
.iter()
.filter(|c| {
let primary_path = if c.status == FileChangeStatus::Deleted {
c.old_path.as_deref().unwrap_or(&c.path)
} else {
&c.path
};
Path::new(primary_path)
.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| supported_ext.contains(&ext))
})
.count();
let summary = SinceSummary {
added_count: added.len(),
modified_count: modified.len(),
removed_count: removed.len(),
impact_count: downstream_impacts.len(),
files_changed,
};
Ok(SinceResult {
reference: reference.to_string(),
commit_count,
added,
modified,
removed,
downstream_impacts,
summary,
})
}
fn parse_old_blob(
vcs: &dyn VcsProvider,
reference: &str,
file_path: &str,
lang: &dyn LanguageRegistry,
) -> Option<Vec<ExtractedSymbol>> {
let ext = Path::new(file_path).extension().and_then(|e| e.to_str());
let parser = match ext.and_then(|e| lang.parser_for(e)) {
Some(p) => p,
None => {
warn!("no parser for old file: {}", file_path);
return None;
}
};
let source = match vcs.read_file_at_ref(file_path, reference) {
Ok(Some(s)) => s,
Ok(None) => {
warn!("old blob not found or binary for {}", file_path);
return None;
}
Err(e) => {
warn!("failed to read old blob for {}: {}", file_path, e);
return None;
}
};
let result = parser.parse(&source, Path::new(file_path));
Some(result.symbols)
}