guts_storage/
refs.rs

1//! Git reference management.
2
3use crate::{ObjectId, Result, StorageError};
4use parking_lot::RwLock;
5use std::collections::HashMap;
6
7/// A git reference (branch, tag, or symbolic ref).
8#[derive(Debug, Clone)]
9pub enum Reference {
10    /// Direct reference to an object.
11    Direct(ObjectId),
12    /// Symbolic reference (e.g., HEAD -> refs/heads/main).
13    Symbolic(String),
14}
15
16impl Reference {
17    /// Resolves a symbolic reference to a direct object ID.
18    pub fn resolve(&self, store: &RefStore) -> Result<ObjectId> {
19        match self {
20            Self::Direct(id) => Ok(*id),
21            Self::Symbolic(target) => {
22                let target_ref = store.get(target)?;
23                target_ref.resolve(store)
24            }
25        }
26    }
27
28    /// Returns the object ID if this is a direct reference.
29    pub fn as_direct(&self) -> Option<ObjectId> {
30        match self {
31            Self::Direct(id) => Some(*id),
32            Self::Symbolic(_) => None,
33        }
34    }
35}
36
37/// Thread-safe reference store.
38#[derive(Debug, Default)]
39pub struct RefStore {
40    refs: RwLock<HashMap<String, Reference>>,
41}
42
43impl RefStore {
44    /// Creates a new empty reference store.
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Gets a reference by name.
50    pub fn get(&self, name: &str) -> Result<Reference> {
51        self.refs
52            .read()
53            .get(name)
54            .cloned()
55            .ok_or_else(|| StorageError::RefNotFound(name.to_string()))
56    }
57
58    /// Sets a reference to point to an object.
59    pub fn set(&self, name: &str, target: ObjectId) {
60        self.refs
61            .write()
62            .insert(name.to_string(), Reference::Direct(target));
63    }
64
65    /// Sets a symbolic reference.
66    pub fn set_symbolic(&self, name: &str, target: &str) {
67        self.refs
68            .write()
69            .insert(name.to_string(), Reference::Symbolic(target.to_string()));
70    }
71
72    /// Deletes a reference.
73    pub fn delete(&self, name: &str) -> Result<()> {
74        self.refs
75            .write()
76            .remove(name)
77            .map(|_| ())
78            .ok_or_else(|| StorageError::RefNotFound(name.to_string()))
79    }
80
81    /// Lists all references with a given prefix.
82    pub fn list(&self, prefix: &str) -> Vec<(String, Reference)> {
83        self.refs
84            .read()
85            .iter()
86            .filter(|(name, _)| name.starts_with(prefix))
87            .map(|(name, refr)| (name.clone(), refr.clone()))
88            .collect()
89    }
90
91    /// Lists all references.
92    pub fn list_all(&self) -> Vec<(String, Reference)> {
93        self.refs
94            .read()
95            .iter()
96            .map(|(name, refr)| (name.clone(), refr.clone()))
97            .collect()
98    }
99
100    /// Resolves HEAD to find the current branch and commit.
101    pub fn resolve_head(&self) -> Result<ObjectId> {
102        let head = self.get("HEAD")?;
103        match head {
104            Reference::Direct(id) => Ok(id),
105            Reference::Symbolic(target) => {
106                let target_ref = self.get(&target)?;
107                match target_ref {
108                    Reference::Direct(id) => Ok(id),
109                    Reference::Symbolic(_) => Err(StorageError::InvalidRef(
110                        "deeply nested symbolic refs not supported".to_string(),
111                    )),
112                }
113            }
114        }
115    }
116
117    /// Gets the current branch name (if HEAD is symbolic).
118    pub fn current_branch(&self) -> Option<String> {
119        match self.get("HEAD").ok()? {
120            Reference::Symbolic(target) => {
121                target.strip_prefix("refs/heads/").map(|s| s.to_string())
122            }
123            Reference::Direct(_) => None,
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_ref_store_basic() {
134        let store = RefStore::new();
135        let id = ObjectId::from_hex("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3").unwrap();
136
137        store.set("refs/heads/main", id);
138        store.set_symbolic("HEAD", "refs/heads/main");
139
140        assert_eq!(store.current_branch(), Some("main".to_string()));
141
142        let resolved = store.resolve_head().unwrap();
143        assert_eq!(resolved.to_hex(), id.to_hex());
144    }
145
146    #[test]
147    fn test_ref_listing() {
148        let store = RefStore::new();
149        let id = ObjectId::from_hex("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3").unwrap();
150
151        store.set("refs/heads/main", id);
152        store.set("refs/heads/feature", id);
153        store.set("refs/tags/v1.0", id);
154
155        let heads = store.list("refs/heads/");
156        assert_eq!(heads.len(), 2);
157
158        let tags = store.list("refs/tags/");
159        assert_eq!(tags.len(), 1);
160    }
161
162    #[test]
163    fn test_ref_store_get_not_found() {
164        let store = RefStore::new();
165        let result = store.get("refs/heads/nonexistent");
166        assert!(matches!(result, Err(StorageError::RefNotFound(_))));
167    }
168
169    #[test]
170    fn test_ref_store_delete() {
171        let store = RefStore::new();
172        let id = ObjectId::from_bytes([1u8; 20]);
173
174        store.set("refs/heads/feature", id);
175        assert!(store.get("refs/heads/feature").is_ok());
176
177        store.delete("refs/heads/feature").unwrap();
178        assert!(store.get("refs/heads/feature").is_err());
179    }
180
181    #[test]
182    fn test_ref_store_delete_not_found() {
183        let store = RefStore::new();
184        let result = store.delete("refs/heads/nonexistent");
185        assert!(matches!(result, Err(StorageError::RefNotFound(_))));
186    }
187
188    #[test]
189    fn test_ref_store_list_all() {
190        let store = RefStore::new();
191        let id = ObjectId::from_bytes([1u8; 20]);
192
193        store.set("refs/heads/main", id);
194        store.set("refs/heads/feature", id);
195        store.set("refs/tags/v1.0", id);
196        store.set_symbolic("HEAD", "refs/heads/main");
197
198        let all = store.list_all();
199        assert_eq!(all.len(), 4);
200    }
201
202    #[test]
203    fn test_reference_direct() {
204        let id = ObjectId::from_bytes([1u8; 20]);
205        let reference = Reference::Direct(id);
206
207        assert_eq!(reference.as_direct(), Some(id));
208    }
209
210    #[test]
211    fn test_reference_symbolic() {
212        let reference = Reference::Symbolic("refs/heads/main".to_string());
213
214        assert!(reference.as_direct().is_none());
215    }
216
217    #[test]
218    fn test_reference_resolve_direct() {
219        let store = RefStore::new();
220        let id = ObjectId::from_bytes([1u8; 20]);
221
222        let reference = Reference::Direct(id);
223        let resolved = reference.resolve(&store).unwrap();
224
225        assert_eq!(resolved, id);
226    }
227
228    #[test]
229    fn test_reference_resolve_symbolic() {
230        let store = RefStore::new();
231        let id = ObjectId::from_bytes([1u8; 20]);
232
233        store.set("refs/heads/main", id);
234
235        let reference = Reference::Symbolic("refs/heads/main".to_string());
236        let resolved = reference.resolve(&store).unwrap();
237
238        assert_eq!(resolved, id);
239    }
240
241    #[test]
242    fn test_resolve_head_direct() {
243        let store = RefStore::new();
244        let id = ObjectId::from_bytes([1u8; 20]);
245
246        store.set("HEAD", id);
247
248        let resolved = store.resolve_head().unwrap();
249        assert_eq!(resolved, id);
250    }
251
252    #[test]
253    fn test_resolve_head_symbolic() {
254        let store = RefStore::new();
255        let id = ObjectId::from_bytes([1u8; 20]);
256
257        store.set("refs/heads/main", id);
258        store.set_symbolic("HEAD", "refs/heads/main");
259
260        let resolved = store.resolve_head().unwrap();
261        assert_eq!(resolved, id);
262    }
263
264    #[test]
265    fn test_resolve_head_not_found() {
266        let store = RefStore::new();
267        let result = store.resolve_head();
268        assert!(result.is_err());
269    }
270
271    #[test]
272    fn test_resolve_head_dangling_symbolic() {
273        let store = RefStore::new();
274        store.set_symbolic("HEAD", "refs/heads/nonexistent");
275
276        let result = store.resolve_head();
277        assert!(result.is_err());
278    }
279
280    #[test]
281    fn test_current_branch_with_direct_head() {
282        let store = RefStore::new();
283        let id = ObjectId::from_bytes([1u8; 20]);
284
285        store.set("HEAD", id);
286
287        assert!(store.current_branch().is_none());
288    }
289
290    #[test]
291    fn test_current_branch_feature() {
292        let store = RefStore::new();
293        store.set_symbolic("HEAD", "refs/heads/feature-branch");
294
295        assert_eq!(store.current_branch(), Some("feature-branch".to_string()));
296    }
297
298    #[test]
299    fn test_ref_update() {
300        let store = RefStore::new();
301        let id1 = ObjectId::from_bytes([1u8; 20]);
302        let id2 = ObjectId::from_bytes([2u8; 20]);
303
304        store.set("refs/heads/main", id1);
305        assert_eq!(store.get("refs/heads/main").unwrap().as_direct(), Some(id1));
306
307        store.set("refs/heads/main", id2);
308        assert_eq!(store.get("refs/heads/main").unwrap().as_direct(), Some(id2));
309    }
310
311    #[test]
312    fn test_ref_list_empty_prefix() {
313        let store = RefStore::new();
314        let id = ObjectId::from_bytes([1u8; 20]);
315
316        store.set("refs/heads/main", id);
317        store.set("refs/tags/v1", id);
318
319        let all = store.list("");
320        assert_eq!(all.len(), 2);
321    }
322
323    #[test]
324    fn test_ref_list_no_matches() {
325        let store = RefStore::new();
326        let id = ObjectId::from_bytes([1u8; 20]);
327
328        store.set("refs/heads/main", id);
329
330        let remotes = store.list("refs/remotes/");
331        assert!(remotes.is_empty());
332    }
333
334    #[test]
335    fn test_reference_clone() {
336        let id = ObjectId::from_bytes([1u8; 20]);
337        let original = Reference::Direct(id);
338        let cloned = original.clone();
339
340        assert_eq!(original.as_direct(), cloned.as_direct());
341    }
342
343    #[test]
344    fn test_symbolic_reference_update() {
345        let store = RefStore::new();
346        let id = ObjectId::from_bytes([1u8; 20]);
347
348        store.set("refs/heads/main", id);
349        store.set("refs/heads/feature", id);
350        store.set_symbolic("HEAD", "refs/heads/main");
351
352        assert_eq!(store.current_branch(), Some("main".to_string()));
353
354        store.set_symbolic("HEAD", "refs/heads/feature");
355
356        assert_eq!(store.current_branch(), Some("feature".to_string()));
357    }
358
359    #[test]
360    fn test_ref_store_default() {
361        let store: RefStore = Default::default();
362        assert!(store.list_all().is_empty());
363    }
364
365    #[test]
366    fn test_current_branch_non_heads() {
367        let store = RefStore::new();
368        // HEAD pointing to something that's not under refs/heads/
369        store.set_symbolic("HEAD", "refs/remotes/origin/main");
370
371        assert!(store.current_branch().is_none());
372    }
373}