vibe_workspace/worktree/
cache.rs1use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::time::{Duration, SystemTime};
8
9use crate::worktree::status::WorktreeInfo;
10
11const CACHE_TTL_SECONDS: u64 = 300; #[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct WorktreeStatusCache {
15 entries: HashMap<PathBuf, CacheEntry>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19struct CacheEntry {
20 worktree_info: WorktreeInfo,
21 last_updated: SystemTime,
22 file_mtime: SystemTime,
23}
24
25impl WorktreeStatusCache {
26 pub fn new() -> Self {
27 Self {
28 entries: HashMap::new(),
29 }
30 }
31
32 pub fn get(&self, path: &Path) -> Option<&WorktreeInfo> {
34 if let Some(entry) = self.entries.get(path) {
35 if self.is_entry_valid(entry, path).unwrap_or(false) {
37 return Some(&entry.worktree_info);
38 }
39 }
40
41 None
42 }
43
44 pub fn insert(&mut self, path: PathBuf, info: WorktreeInfo) -> Result<()> {
46 let file_mtime = std::fs::metadata(&path)
47 .and_then(|m| m.modified())
48 .unwrap_or_else(|_| SystemTime::now());
49
50 let entry = CacheEntry {
51 worktree_info: info,
52 last_updated: SystemTime::now(),
53 file_mtime,
54 };
55
56 self.entries.insert(path, entry);
57 Ok(())
58 }
59
60 pub fn cleanup_stale_entries(&mut self) {
62 let now = SystemTime::now();
63 let ttl = Duration::from_secs(CACHE_TTL_SECONDS);
64
65 self.entries.retain(|path, entry| {
66 if let Ok(age) = now.duration_since(entry.last_updated) {
68 age < ttl && path.exists()
69 } else {
70 false
71 }
72 });
73 }
74
75 fn is_entry_valid(&self, entry: &CacheEntry, path: &Path) -> Result<bool> {
77 let now = SystemTime::now();
78 let ttl = Duration::from_secs(CACHE_TTL_SECONDS);
79
80 if now.duration_since(entry.last_updated)? > ttl {
82 return Ok(false);
83 }
84
85 if let Ok(metadata) = std::fs::metadata(path) {
87 if let Ok(current_mtime) = metadata.modified() {
88 if current_mtime > entry.file_mtime {
89 return Ok(false);
90 }
91 }
92 }
93
94 Ok(true)
95 }
96
97 pub fn stats(&self) -> CacheStats {
99 let total_entries = self.entries.len();
100 let now = SystemTime::now();
101
102 let valid_entries = self
103 .entries
104 .values()
105 .filter(|entry| {
106 now.duration_since(entry.last_updated)
107 .map(|age| age.as_secs() < CACHE_TTL_SECONDS)
108 .unwrap_or(false)
109 })
110 .count();
111
112 CacheStats {
113 total_entries,
114 valid_entries,
115 hit_ratio: if total_entries > 0 {
116 valid_entries as f64 / total_entries as f64
117 } else {
118 0.0
119 },
120 }
121 }
122}
123
124#[derive(Debug)]
125pub struct CacheStats {
126 pub total_entries: usize,
127 pub valid_entries: usize,
128 pub hit_ratio: f64,
129}
130
131impl Default for WorktreeStatusCache {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::worktree::status::{StatusSeverity, WorktreeStatus};
141 use std::time::Duration;
142 use tempfile::TempDir;
143
144 fn create_test_worktree_info(path: PathBuf) -> WorktreeInfo {
145 WorktreeInfo {
146 path,
147 branch: "test-branch".to_string(),
148 head: "abc1234".to_string(),
149 task_id: None,
150 status: WorktreeStatus {
151 is_clean: true,
152 severity: StatusSeverity::Clean,
153 uncommitted_changes: Vec::new(),
154 untracked_files: Vec::new(),
155 unpushed_commits: Vec::new(),
156 remote_status: crate::worktree::status::RemoteStatus::UpToDate,
157 merge_info: None,
158 ahead_count: 0,
159 behind_count: 0,
160 },
161 age: Duration::from_secs(3600),
162 is_detached: false,
163 }
164 }
165
166 #[test]
167 fn test_cache_basic_operations() {
168 let mut cache = WorktreeStatusCache::new();
169 let temp_dir = TempDir::new().unwrap();
170 let path = temp_dir.path().to_path_buf();
171
172 let worktree_info = create_test_worktree_info(path.clone());
173
174 assert!(cache.get(&path).is_none());
176
177 cache.insert(path.clone(), worktree_info).unwrap();
179 assert!(cache.get(&path).is_some());
180
181 let cached_info = cache.get(&path).unwrap();
182 assert_eq!(cached_info.branch, "test-branch");
183 assert_eq!(cached_info.head, "abc1234");
184 }
185
186 #[test]
187 fn test_cache_cleanup() {
188 let mut cache = WorktreeStatusCache::new();
189 let temp_dir = TempDir::new().unwrap();
190 let path = temp_dir.path().to_path_buf();
191
192 let worktree_info = create_test_worktree_info(path.clone());
193 cache.insert(path.clone(), worktree_info).unwrap();
194
195 assert_eq!(cache.entries.len(), 1);
196
197 cache.cleanup_stale_entries();
199 }
201
202 #[test]
203 fn test_cache_stats() {
204 let mut cache = WorktreeStatusCache::new();
205 let temp_dir = TempDir::new().unwrap();
206 let path = temp_dir.path().to_path_buf();
207
208 let stats = cache.stats();
210 assert_eq!(stats.total_entries, 0);
211 assert_eq!(stats.valid_entries, 0);
212 assert_eq!(stats.hit_ratio, 0.0);
213
214 let worktree_info = create_test_worktree_info(path);
216 cache
217 .insert(temp_dir.path().to_path_buf(), worktree_info)
218 .unwrap();
219
220 let stats = cache.stats();
221 assert_eq!(stats.total_entries, 1);
222 assert_eq!(stats.valid_entries, 1);
223 assert_eq!(stats.hit_ratio, 1.0);
224 }
225}