#![allow(dead_code)]
use lru::LruCache;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::time::{Duration, Instant, SystemTime};
pub struct DiffCache {
entries: LruCache<CacheKey, CacheEntry>,
ttl: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
pub path: PathBuf,
pub staged: bool,
}
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub content: String,
pub computed_at: Instant,
pub file_mtime: Option<SystemTime>,
pub truncated: bool,
pub total_lines: usize,
}
impl DiffCache {
pub fn new(max_entries: usize, ttl_secs: u64) -> Self {
let capacity = NonZeroUsize::new(max_entries.max(1))
.expect("max_entries must be at least 1");
Self {
entries: LruCache::new(capacity),
ttl: Duration::from_secs(ttl_secs),
}
}
pub fn get(&mut self, path: &PathBuf, staged: bool) -> Option<&CacheEntry> {
let key = CacheKey { path: path.clone(), staged };
let is_valid = self.entries.peek(&key).map(|entry| {
if entry.computed_at.elapsed() > self.ttl {
return false;
}
if !staged {
if let Ok(metadata) = std::fs::metadata(path) {
if let Ok(mtime) = metadata.modified() {
if entry.file_mtime != Some(mtime) {
return false;
}
}
}
}
true
}).unwrap_or(false);
if is_valid {
self.entries.get(&key)
} else {
if self.entries.peek(&key).is_some() {
self.entries.pop(&key);
}
None
}
}
pub fn put(
&mut self,
path: PathBuf,
staged: bool,
content: String,
truncated: bool,
total_lines: usize,
) {
let key = CacheKey { path: path.clone(), staged };
let file_mtime = std::fs::metadata(&path)
.ok()
.and_then(|m| m.modified().ok());
let entry = CacheEntry {
content,
computed_at: Instant::now(),
file_mtime,
truncated,
total_lines,
};
self.entries.put(key, entry);
}
pub fn remove(&mut self, key: &CacheKey) {
self.entries.pop(key);
}
pub fn invalidate(&mut self, path: &PathBuf) {
let staged_key = CacheKey { path: path.clone(), staged: true };
let unstaged_key = CacheKey { path: path.clone(), staged: false };
self.entries.pop(&staged_key);
self.entries.pop(&unstaged_key);
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn stats(&self) -> CacheStats {
CacheStats {
entries: self.entries.len(),
max_entries: self.entries.cap().get(),
}
}
}
impl Default for DiffCache {
fn default() -> Self {
Self::new(50, 30) }
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub entries: usize,
pub max_entries: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_put_get() {
let mut cache = DiffCache::new(10, 60);
let path = PathBuf::from("/tmp/test.txt");
cache.put(path.clone(), false, "test diff".to_string(), false, 10);
let entry = cache.get(&path, false);
assert!(entry.is_some());
assert_eq!(entry.unwrap().content, "test diff");
}
#[test]
fn test_cache_eviction() {
let mut cache = DiffCache::new(2, 60);
cache.put(PathBuf::from("a.txt"), false, "a".to_string(), false, 1);
cache.put(PathBuf::from("b.txt"), false, "b".to_string(), false, 1);
cache.put(PathBuf::from("c.txt"), false, "c".to_string(), false, 1);
assert!(cache.get(&PathBuf::from("a.txt"), false).is_none());
assert!(cache.get(&PathBuf::from("b.txt"), false).is_some());
assert!(cache.get(&PathBuf::from("c.txt"), false).is_some());
}
#[test]
fn test_cache_invalidate() {
let mut cache = DiffCache::new(10, 60);
let path = PathBuf::from("/tmp/test.txt");
cache.put(path.clone(), false, "unstaged".to_string(), false, 1);
cache.put(path.clone(), true, "staged".to_string(), false, 1);
cache.invalidate(&path);
assert!(cache.get(&path, false).is_none());
assert!(cache.get(&path, true).is_none());
}
}