use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use crate::error::IndexError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileState {
pub mtime: SystemTime,
pub content_hash: String,
pub chunk_ids: Vec<u64>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct IncrementalState {
pub files: HashMap<PathBuf, FileState>,
}
#[derive(Debug)]
pub struct FileChanges {
pub changed: Vec<PathBuf>,
pub deleted: Vec<PathBuf>,
pub unchanged: Vec<PathBuf>,
}
impl IncrementalState {
pub fn load(path: &Path) -> Result<Self, IndexError> {
if !path.exists() {
return Ok(Self::default());
}
let data = std::fs::read(path)?;
serde_json::from_slice(&data).map_err(|e| {
IndexError::Serialization(format!("Failed to load incremental state: {}", e))
})
}
pub fn save(&self, path: &Path) -> Result<(), IndexError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let data = serde_json::to_vec_pretty(self)
.map_err(|e| IndexError::Serialization(e.to_string()))?;
std::fs::write(path, data)?;
Ok(())
}
pub fn detect_changes(&self, current_files: &[PathBuf]) -> FileChanges {
let mut changed = Vec::new();
let mut unchanged = Vec::new();
let current_set: std::collections::HashSet<&PathBuf> = current_files.iter().collect();
for file in current_files {
if let Some(prev_state) = self.files.get(file) {
let mtime = std::fs::metadata(file).and_then(|m| m.modified()).ok();
if mtime != Some(prev_state.mtime) {
if let Ok(content) = std::fs::read(file) {
let hash = blake3::hash(&content).to_hex().to_string();
if hash != prev_state.content_hash {
changed.push(file.clone());
} else {
unchanged.push(file.clone());
}
} else {
changed.push(file.clone());
}
} else {
unchanged.push(file.clone());
}
} else {
changed.push(file.clone());
}
}
let deleted: Vec<PathBuf> = self
.files
.keys()
.filter(|f| !current_set.contains(f))
.cloned()
.collect();
FileChanges {
changed,
deleted,
unchanged,
}
}
pub fn update_file(&mut self, path: PathBuf, content: &[u8], chunk_ids: Vec<u64>) {
let hash = blake3::hash(content).to_hex().to_string();
let mtime = std::fs::metadata(&path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
self.files.insert(
path,
FileState {
mtime,
content_hash: hash,
chunk_ids,
},
);
}
pub fn remove_file(&mut self, path: &Path) -> Option<FileState> {
self.files.remove(path)
}
pub fn chunk_ids_for_file(&self, path: &Path) -> Vec<u64> {
self.files
.get(path)
.map(|state| state.chunk_ids.clone())
.unwrap_or_default()
}
pub fn chunk_ids_to_remove(&self, deleted_files: &[PathBuf]) -> Vec<u64> {
deleted_files
.iter()
.flat_map(|path| self.chunk_ids_for_file(path))
.collect()
}
pub fn apply_deletions(&mut self, deleted_files: &[PathBuf]) -> Vec<u64> {
let mut removed_ids = Vec::new();
for path in deleted_files {
if let Some(state) = self.remove_file(path) {
removed_ids.extend(state.chunk_ids);
}
}
removed_ids
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_incremental_state_save_load() {
let dir = tempfile::tempdir().unwrap();
let state_path = dir.path().join("state.json");
let mut state = IncrementalState::default();
state.update_file(
PathBuf::from("/test/file.rs"),
b"fn main() {}",
vec![1, 2, 3],
);
state.save(&state_path).unwrap();
let loaded = IncrementalState::load(&state_path).unwrap();
assert_eq!(loaded.files.len(), 1);
assert!(loaded.files.contains_key(&PathBuf::from("/test/file.rs")));
}
#[test]
fn test_detect_new_file() {
let state = IncrementalState::default();
let changes = state.detect_changes(&[PathBuf::from("/new/file.rs")]);
assert_eq!(changes.changed.len(), 1);
assert!(changes.deleted.is_empty());
assert!(changes.unchanged.is_empty());
}
}