use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use super::status::RepoStatusInfo;
#[cfg(feature = "telemetry")]
use crate::telemetry::metrics::GLOBAL_METRICS;
#[cfg(feature = "telemetry")]
use tracing::trace;
struct CacheEntry {
status: RepoStatusInfo,
timestamp: Instant,
}
pub struct GitStatusCache {
cache: Mutex<HashMap<PathBuf, CacheEntry>>,
ttl: Duration,
}
impl GitStatusCache {
pub fn new(ttl: Duration) -> Self {
Self {
cache: Mutex::new(HashMap::new()),
ttl,
}
}
fn is_expired(&self, entry: &CacheEntry) -> bool {
entry.timestamp.elapsed() > self.ttl
}
pub fn get(&self, repo_path: &PathBuf) -> Option<RepoStatusInfo> {
let cache = self.cache.lock().expect("mutex poisoned");
if let Some(entry) = cache.get(repo_path) {
if !self.is_expired(entry) {
#[cfg(feature = "telemetry")]
{
GLOBAL_METRICS.record_cache(true);
trace!(path = %repo_path.display(), "Cache hit");
}
return Some(entry.status.clone());
}
}
#[cfg(feature = "telemetry")]
{
GLOBAL_METRICS.record_cache(false);
trace!(path = %repo_path.display(), "Cache miss");
}
None
}
pub fn set(&self, repo_path: PathBuf, status: RepoStatusInfo) {
let mut cache = self.cache.lock().expect("mutex poisoned");
cache.insert(
repo_path,
CacheEntry {
status,
timestamp: Instant::now(),
},
);
}
pub fn invalidate(&self, repo_path: &PathBuf) {
let mut cache = self.cache.lock().expect("mutex poisoned");
cache.remove(repo_path);
}
pub fn clear(&self) {
let mut cache = self.cache.lock().expect("mutex poisoned");
cache.clear();
}
}
impl Default for GitStatusCache {
fn default() -> Self {
Self::new(Duration::from_millis(5000))
}
}
pub static STATUS_CACHE: Lazy<GitStatusCache> = Lazy::new(GitStatusCache::default);
pub fn invalidate_status_cache(repo_path: &PathBuf) {
STATUS_CACHE.invalidate(repo_path);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_set_get() {
let cache = GitStatusCache::new(Duration::from_secs(60));
let path = PathBuf::from("/test/repo");
let status = RepoStatusInfo {
current_branch: "main".to_string(),
is_clean: true,
staged: vec![],
modified: vec![],
untracked: vec![],
ahead: 0,
behind: 0,
};
cache.set(path.clone(), status.clone());
let cached = cache.get(&path).unwrap();
assert_eq!(cached.current_branch, "main");
assert!(cached.is_clean);
}
#[test]
fn test_cache_invalidate() {
let cache = GitStatusCache::new(Duration::from_secs(60));
let path = PathBuf::from("/test/repo");
let status = RepoStatusInfo {
current_branch: "main".to_string(),
is_clean: true,
staged: vec![],
modified: vec![],
untracked: vec![],
ahead: 0,
behind: 0,
};
cache.set(path.clone(), status);
assert!(cache.get(&path).is_some());
cache.invalidate(&path);
assert!(cache.get(&path).is_none());
}
#[test]
fn test_cache_expiry() {
let cache = GitStatusCache::new(Duration::from_millis(10));
let path = PathBuf::from("/test/repo");
let status = RepoStatusInfo {
current_branch: "main".to_string(),
is_clean: true,
staged: vec![],
modified: vec![],
untracked: vec![],
ahead: 0,
behind: 0,
};
cache.set(path.clone(), status);
assert!(cache.get(&path).is_some());
std::thread::sleep(Duration::from_millis(20));
assert!(cache.get(&path).is_none());
}
}