use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use scryer_db::{ScryerDb, SourceFile};
pub fn hash_bytes(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
pub fn hash_file(path: &Path) -> std::io::Result<String> {
let bytes = fs::read(path)?;
Ok(hash_bytes(&bytes))
}
#[derive(Debug, Clone)]
pub enum FileChange {
Added {
rel_path: PathBuf,
content_hash: String,
content: Vec<u8>,
},
Modified {
file_id: u64,
rel_path: PathBuf,
content_hash: String,
content: Vec<u8>,
},
Unchanged { file_id: u64, rel_path: PathBuf },
Deleted { file_id: u64, rel_path: PathBuf },
}
#[derive(Debug, Clone, Default)]
pub struct ChangeReport {
pub changes: Vec<FileChange>,
pub added_count: usize,
pub modified_count: usize,
pub unchanged_count: usize,
pub deleted_count: usize,
}
impl ChangeReport {
pub fn is_empty(&self) -> bool {
self.added_count == 0 && self.modified_count == 0 && self.deleted_count == 0
}
}
pub struct ChangeDetector;
impl ChangeDetector {
pub async fn detect_changes(
db: &ScryerDb,
project_id: u64,
root: &Path,
scanned_paths: &[PathBuf],
) -> anyhow::Result<ChangeReport> {
let existing_files = {
let mut db_guard = db.lock().await;
SourceFile::filter_by_project_id(project_id)
.exec(&mut *db_guard)
.await?
};
let mut db_map: HashMap<String, (u64, String)> = existing_files
.into_iter()
.map(|f| (f.path, (f.id, f.content_hash)))
.collect();
let mut changes = Vec::new();
let mut added_count = 0;
let mut modified_count = 0;
let mut unchanged_count = 0;
for rel in scanned_paths {
let rel_str = rel.to_string_lossy().to_string();
let full_path = root.join(rel);
let content = match fs::read(&full_path) {
Ok(c) => c,
Err(err) => {
tracing::warn!("Failed to read file {}: {err}", full_path.display());
db_map.remove(&rel_str);
continue;
}
};
let current_hash = hash_bytes(&content);
if let Some((file_id, cached_hash)) = db_map.remove(&rel_str) {
if cached_hash == current_hash {
changes.push(FileChange::Unchanged {
file_id,
rel_path: rel.clone(),
});
unchanged_count += 1;
} else {
changes.push(FileChange::Modified {
file_id,
rel_path: rel.clone(),
content_hash: current_hash,
content,
});
modified_count += 1;
}
} else {
changes.push(FileChange::Added {
rel_path: rel.clone(),
content_hash: current_hash,
content,
});
added_count += 1;
}
}
let mut deleted_count = 0;
for (del_path, (file_id, _)) in db_map {
changes.push(FileChange::Deleted {
file_id,
rel_path: PathBuf::from(del_path),
});
deleted_count += 1;
}
Ok(ChangeReport {
changes,
added_count,
modified_count,
unchanged_count,
deleted_count,
})
}
}