eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
#![allow(dead_code)]
//! LRU cache for diff operations.
//!
//! Caches computed diffs to avoid re-computing them for files that haven't changed.
//! Uses file modification time to invalidate stale entries.
//!
//! Uses the `lru` crate for O(1) cache operations instead of O(n) manual implementation.

use lru::LruCache;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::time::{Duration, Instant, SystemTime};

/// LRU cache for diff results.
/// 
/// This significantly improves performance when navigating between files,
/// as diffs are expensive to compute (especially for large files).
/// 
/// Uses proper LRU cache implementation for O(1) operations.
pub struct DiffCache {
    entries: LruCache<CacheKey, CacheEntry>,
    ttl: Duration,
}

/// Key for cache lookups.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
    pub path: PathBuf,
    pub staged: bool,
}

/// Cached diff entry with metadata.
#[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 {
    /// Create a new diff cache.
    pub fn new(max_entries: usize, ttl_secs: u64) -> Self {
        // LruCache requires NonZeroUsize, so ensure at least 1 entry
        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),
        }
    }
    
    /// Get a cached diff if it's still valid.
    /// 
    /// This is now O(1) instead of O(n) thanks to proper LRU cache implementation.
    pub fn get(&mut self, path: &PathBuf, staged: bool) -> Option<&CacheEntry> {
        let key = CacheKey { path: path.clone(), staged };
        
        // LruCache::get() automatically promotes to most recently used (O(1))
        // We need to check validity and potentially remove, so we use peek first
        let is_valid = self.entries.peek(&key).map(|entry| {
            // Check TTL
            if entry.computed_at.elapsed() > self.ttl {
                return false;
            }
            
            // Check if file was modified since we cached (for unstaged diffs)
            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 {
            // Entry is valid - get it (this promotes to most recently used)
            self.entries.get(&key)
        } else {
            // Entry is invalid or doesn't exist - remove if present
            if self.entries.peek(&key).is_some() {
                self.entries.pop(&key);
            }
            None
        }
    }
    
    /// Store a diff in the cache.
    /// 
    /// This is now O(1) instead of O(n) thanks to proper LRU cache implementation.
    pub fn put(
        &mut self, 
        path: PathBuf, 
        staged: bool, 
        content: String, 
        truncated: bool,
        total_lines: usize,
    ) {
        let key = CacheKey { path: path.clone(), staged };
        
        // Get file mtime for cache invalidation
        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,
        };
        
        // LruCache::put() automatically evicts oldest if at capacity (O(1))
        self.entries.put(key, entry);
    }
    
    /// Remove an entry from the cache.
    pub fn remove(&mut self, key: &CacheKey) {
        self.entries.pop(key);
    }
    
    /// Invalidate all entries for a path (both staged and unstaged).
    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);
    }
    
    /// Clear the entire cache.
    pub fn clear(&mut self) {
        self.entries.clear();
    }
    
    /// Get cache statistics.
    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) // 50 entries, 30 second TTL
    }
}

/// Cache statistics for monitoring.
#[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);
        
        // Oldest entry should be evicted
        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());
    }
}