1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::{HashMap, HashSet};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9pub struct GitCache {
10 pub repos: HashMap<String, HashSet<String>>,
12}
13
14impl GitCache {
15 pub fn default_path() -> PathBuf {
17 crate::paths::PlatformPaths::default_git_cache_path()
18 }
19
20 pub fn load(cache_path: &Path) -> Result<Self> {
22 if !cache_path.exists() {
23 tracing::debug!("Git cache file not found, starting with empty cache");
24 return Ok(Self::default());
25 }
26
27 let content = fs::read_to_string(cache_path).context("Failed to read git cache file")?;
28
29 let cache: GitCache =
30 serde_json::from_str(&content).context("Failed to parse git cache file")?;
31
32 tracing::info!(
33 "Loaded git cache with {} indexed repositories",
34 cache.repos.len()
35 );
36 Ok(cache)
37 }
38
39 pub fn save(&self, cache_path: &Path) -> Result<()> {
41 if let Some(parent) = cache_path.parent() {
43 fs::create_dir_all(parent).context("Failed to create git cache directory")?;
44 }
45
46 let content =
47 serde_json::to_string_pretty(self).context("Failed to serialize git cache")?;
48
49 fs::write(cache_path, content).context("Failed to write git cache file")?;
50
51 tracing::debug!("Saved git cache to {:?}", cache_path);
52 Ok(())
53 }
54
55 pub fn has_commit(&self, repo_path: &str, commit_hash: &str) -> bool {
57 self.repos
58 .get(repo_path)
59 .map(|commits| commits.contains(commit_hash))
60 .unwrap_or(false)
61 }
62
63 pub fn get_repo(&self, repo_path: &str) -> Option<&HashSet<String>> {
65 self.repos.get(repo_path)
66 }
67
68 pub fn commit_count(&self, repo_path: &str) -> usize {
70 self.repos
71 .get(repo_path)
72 .map(|commits| commits.len())
73 .unwrap_or(0)
74 }
75
76 pub fn add_commits(&mut self, repo_path: String, commit_hashes: HashSet<String>) {
78 self.repos
79 .entry(repo_path)
80 .or_default()
81 .extend(commit_hashes);
82 }
83
84 pub fn update_repo(&mut self, repo_path: String, commit_hashes: HashSet<String>) {
86 self.repos.insert(repo_path, commit_hashes);
87 }
88
89 pub fn remove_repo(&mut self, repo_path: &str) -> bool {
91 self.repos.remove(repo_path).is_some()
92 }
93
94 pub fn clear(&mut self) {
96 self.repos.clear();
97 }
98
99 pub fn total_commits(&self) -> usize {
101 self.repos.values().map(|commits| commits.len()).sum()
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use tempfile::tempdir;
109
110 #[test]
111 fn test_default() {
112 let cache = GitCache::default();
113 assert_eq!(cache.repos.len(), 0);
114 }
115
116 #[test]
117 fn test_has_commit() {
118 let mut cache = GitCache::default();
119 let mut commits = HashSet::new();
120 commits.insert("abc123".to_string());
121 cache.repos.insert("/repo/path".to_string(), commits);
122
123 assert!(cache.has_commit("/repo/path", "abc123"));
124 assert!(!cache.has_commit("/repo/path", "def456"));
125 assert!(!cache.has_commit("/other/path", "abc123"));
126 }
127
128 #[test]
129 fn test_add_commits() {
130 let mut cache = GitCache::default();
131 let mut commits = HashSet::new();
132 commits.insert("abc123".to_string());
133
134 cache.add_commits("/repo/path".to_string(), commits);
135 assert_eq!(cache.commit_count("/repo/path"), 1);
136
137 let mut more_commits = HashSet::new();
138 more_commits.insert("def456".to_string());
139 cache.add_commits("/repo/path".to_string(), more_commits);
140 assert_eq!(cache.commit_count("/repo/path"), 2);
141 }
142
143 #[test]
144 fn test_update_repo() {
145 let mut cache = GitCache::default();
146 let mut commits1 = HashSet::new();
147 commits1.insert("abc123".to_string());
148 cache.add_commits("/repo/path".to_string(), commits1);
149
150 let mut commits2 = HashSet::new();
151 commits2.insert("def456".to_string());
152 cache.update_repo("/repo/path".to_string(), commits2);
153
154 assert_eq!(cache.commit_count("/repo/path"), 1);
155 assert!(!cache.has_commit("/repo/path", "abc123"));
156 assert!(cache.has_commit("/repo/path", "def456"));
157 }
158
159 #[test]
160 fn test_remove_repo() {
161 let mut cache = GitCache::default();
162 let mut commits = HashSet::new();
163 commits.insert("abc123".to_string());
164 cache.add_commits("/repo/path".to_string(), commits);
165
166 assert!(cache.remove_repo("/repo/path"));
167 assert!(!cache.remove_repo("/repo/path"));
168 assert_eq!(cache.commit_count("/repo/path"), 0);
169 }
170
171 #[test]
172 fn test_clear() {
173 let mut cache = GitCache::default();
174 let mut commits = HashSet::new();
175 commits.insert("abc123".to_string());
176 cache.add_commits("/repo1".to_string(), commits.clone());
177 cache.add_commits("/repo2".to_string(), commits);
178
179 cache.clear();
180 assert_eq!(cache.repos.len(), 0);
181 assert_eq!(cache.total_commits(), 0);
182 }
183
184 #[test]
185 fn test_total_commits() {
186 let mut cache = GitCache::default();
187 let mut commits1 = HashSet::new();
188 commits1.insert("abc123".to_string());
189 commits1.insert("abc124".to_string());
190
191 let mut commits2 = HashSet::new();
192 commits2.insert("def456".to_string());
193
194 cache.add_commits("/repo1".to_string(), commits1);
195 cache.add_commits("/repo2".to_string(), commits2);
196
197 assert_eq!(cache.total_commits(), 3);
198 }
199
200 #[test]
201 fn test_save_load() {
202 let dir = tempdir().unwrap();
203 let cache_path = dir.path().join("git_cache.json");
204
205 let mut cache = GitCache::default();
206 let mut commits = HashSet::new();
207 commits.insert("abc123".to_string());
208 commits.insert("def456".to_string());
209 cache.add_commits("/repo/path".to_string(), commits);
210
211 cache.save(&cache_path).unwrap();
212 assert!(cache_path.exists());
213
214 let loaded = GitCache::load(&cache_path).unwrap();
215 assert_eq!(loaded.commit_count("/repo/path"), 2);
216 assert!(loaded.has_commit("/repo/path", "abc123"));
217 assert!(loaded.has_commit("/repo/path", "def456"));
218 }
219
220 #[test]
221 fn test_load_nonexistent() {
222 let dir = tempdir().unwrap();
223 let cache_path = dir.path().join("nonexistent.json");
224
225 let cache = GitCache::load(&cache_path).unwrap();
226 assert_eq!(cache.repos.len(), 0);
227 }
228
229 #[test]
230 fn test_save_creates_directory() {
231 let dir = tempdir().unwrap();
232 let cache_path = dir.path().join("subdir/git_cache.json");
233
234 let cache = GitCache::default();
235 cache.save(&cache_path).unwrap();
236 assert!(cache_path.exists());
237 }
238}