gitai/remote/cache/
manager.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::{Arc, Mutex};
3
4use super::key_generator::CacheKeyGenerator;
5use crate::remote::models::{
6    cached_repo::CachedRepository, repo_config::RepositoryConfiguration,
7    wire_operation::WireOperation,
8};
9
10type CacheKey = String;
11
12#[derive(Default)]
13pub struct CacheManager {
14    // Maps cache key (hash of URL + branch + optional commit) to cached repository info
15    cache: Arc<Mutex<HashMap<CacheKey, CachedRepository>>>,
16}
17
18impl CacheManager {
19    pub fn new() -> Self {
20        Self {
21            cache: Arc::new(Mutex::new(HashMap::new())),
22        }
23    }
24
25    /// Determine if we need to fetch a repository, or if it's already cached.
26    /// Returns the cache path for the repository.
27    pub fn get_or_schedule_fetch(
28        &self,
29        config: &RepositoryConfiguration,
30    ) -> Result<String, String> {
31        let key = CacheKeyGenerator::generate_key(config);
32        let mut cache = self
33            .cache
34            .lock()
35            .expect("Failed to lock cache mutex, likely due to a poisoned mutex (another thread panicked while holding the lock)");
36
37        if let Some(cached_repo) = cache.get(&key) {
38            // Repository is already cached, return its path
39            Ok(cached_repo.local_cache_path.clone())
40        } else {
41            // Repository is not cached yet, create a simulated cache entry
42            // In a real implementation, this would involve fetching the repo
43            let cache_path = Self::get_cache_path(&key)?;
44
45            let new_cached_repo = CachedRepository::new(
46                config.url.clone(),
47                config.branch.clone(),
48                cache_path.clone(),
49                "placeholder_commit_hash".to_string(), // This would be obtained from the actual fetch
50            );
51
52            cache.insert(key, new_cached_repo);
53
54            // Return the cache path for this repository
55            Ok(cache_path)
56        }
57    }
58
59    /// Get a unique cache path for a given cache key.
60    fn get_cache_path(key: &str) -> Result<String, String> {
61        let cache_dir = std::env::temp_dir().join("git-wire-cache").join(key);
62
63        std::fs::create_dir_all(&cache_dir)
64            .map_err(|e| format!("Failed to create cache directory: {e}"))?;
65
66        Ok(cache_dir.to_string_lossy().to_string())
67    }
68
69    /// Process a list of repository configurations to determine the optimal fetching strategy.
70    /// Identifies unique repositories based on URL, branch, and optional commit hash.
71    /// Returns a list of unique configurations to fetch and corresponding wire operations.
72    pub fn plan_fetch_operations(
73        &self,
74        configs: &[RepositoryConfiguration],
75    ) -> Result<(Vec<RepositoryConfiguration>, Vec<WireOperation>), String> {
76        // Identify unique repositories using cache keys
77        let mut unique_configs: Vec<RepositoryConfiguration> = Vec::new();
78        let mut seen_keys: HashSet<String> = HashSet::new();
79        let mut operations: Vec<WireOperation> = Vec::new();
80
81        for config in configs {
82            let key = CacheKeyGenerator::generate_key(config);
83
84            if !seen_keys.contains(&key) {
85                // This is a new unique repository (based on URL + branch + commit), add to fetch list
86                unique_configs.push(config.clone());
87                seen_keys.insert(key);
88            }
89
90            // Get or create cached path for this repository
91            let cached_path = self.get_or_schedule_fetch(config)?;
92
93            // Create a wire operation to extract content from the cached repo
94            operations.push(WireOperation::new(config.clone(), cached_path));
95        }
96
97        Ok((unique_configs, operations))
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_cache_manager_creation() {
107        let cache_manager = CacheManager::new();
108        assert_eq!(
109            cache_manager
110                .cache
111                .lock()
112                .expect("Failed to lock cache in test")
113                .len(),
114            0
115        );
116    }
117
118    #[test]
119    fn test_get_or_schedule_fetch_new_repo() {
120        let cache_manager = CacheManager::new();
121        let config = RepositoryConfiguration::new(
122            "https://github.com/example/repo.git".to_string(),
123            "main".to_string(),
124            "./src/module1".to_string(),
125            vec!["src/".to_string()],
126            None,
127            None,
128        );
129
130        let result = cache_manager.get_or_schedule_fetch(&config);
131        assert!(result.is_ok());
132
133        // Check that the repo was added to cache using the generated key
134        let key = CacheKeyGenerator::generate_key(&config);
135        let cache = cache_manager
136            .cache
137            .lock()
138            .expect("Failed to lock cache in test");
139        assert!(cache.contains_key(&key));
140    }
141
142    #[test]
143    fn test_plan_fetch_operations_with_duplicates() {
144        let cache_manager = CacheManager::new();
145
146        // Create configs with duplicate repositories (same URL and branch)
147        let configs = vec![
148            RepositoryConfiguration::new(
149                "https://github.com/example/repo.git".to_string(),
150                "main".to_string(),
151                "./src/module1".to_string(),
152                vec!["src/".to_string(), "lib/".to_string()],
153                None,
154                None,
155            ),
156            RepositoryConfiguration::new(
157                "https://github.com/example/repo.git".to_string(), // Same repo and branch
158                "main".to_string(),
159                "./src/module2".to_string(),
160                vec!["utils/".to_string()],
161                None,
162                None,
163            ),
164            RepositoryConfiguration::new(
165                "https://github.com/other/repo.git".to_string(), // Different repo
166                "main".to_string(),
167                "./src/module3".to_string(),
168                vec!["docs/".to_string()],
169                None,
170                None,
171            ),
172        ];
173
174        let (unique_configs, operations) = cache_manager
175            .plan_fetch_operations(&configs)
176            .expect("Failed to plan fetch operations");
177
178        // Should only have 2 unique configs (not 3, since first two share URL + branch)
179        assert_eq!(unique_configs.len(), 2);
180
181        // Should have 3 operations (one for each original config)
182        assert_eq!(operations.len(), 3);
183    }
184}