Skip to main content

suture_driver/
cache.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2use std::collections::HashMap;
3use std::hash::{Hash, Hasher};
4
5/// LRU-style cache for merge results, keyed by content hashes.
6///
7/// Avoids re-running expensive semantic merges when the same triple
8/// (base, ours, theirs) is seen repeatedly (e.g., in CI rebases or
9/// cherry-pick workflows).
10///
11/// # Example
12///
13/// ```
14/// use suture_driver::MergeCache;
15///
16/// let mut cache = MergeCache::new(64);
17/// cache.insert("base", "ours", "theirs", "merged result".to_string());
18/// assert_eq!(cache.get("base", "ours", "theirs"), Some("merged result"));
19/// ```
20pub struct MergeCache {
21    entries: HashMap<(u64, u64, u64), String>,
22    max_entries: usize,
23}
24
25impl MergeCache {
26    pub fn new(max_entries: usize) -> Self {
27        Self {
28            entries: HashMap::new(),
29            max_entries,
30        }
31    }
32
33    pub fn get(&self, base: &str, ours: &str, theirs: &str) -> Option<&str> {
34        let key = (hash(base), hash(ours), hash(theirs));
35        self.entries.get(&key).map(|s| s.as_str())
36    }
37
38    pub fn insert(&mut self, base: &str, ours: &str, theirs: &str, result: String) {
39        if self.entries.len() >= self.max_entries {
40            if let Some(first_key) = self.entries.keys().next().copied() {
41                self.entries.remove(&first_key);
42            }
43        }
44        let key = (hash(base), hash(ours), hash(theirs));
45        self.entries.insert(key, result);
46    }
47
48    pub fn len(&self) -> usize {
49        self.entries.len()
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.entries.is_empty()
54    }
55
56    pub fn clear(&mut self) {
57        self.entries.clear();
58    }
59}
60
61fn hash(s: &str) -> u64 {
62    let mut hasher = std::collections::hash_map::DefaultHasher::new();
63    s.hash(&mut hasher);
64    hasher.finish()
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_cache_hit() {
73        let mut cache = MergeCache::new(16);
74        cache.insert("b", "o", "t", "result".to_string());
75        assert_eq!(cache.get("b", "o", "t"), Some("result"));
76    }
77
78    #[test]
79    fn test_cache_miss() {
80        let cache = MergeCache::new(16);
81        assert_eq!(cache.get("b", "o", "t"), None);
82    }
83
84    #[test]
85    fn test_cache_eviction() {
86        let mut cache = MergeCache::new(2);
87        cache.insert("a", "b", "c", "1".to_string());
88        cache.insert("d", "e", "f", "2".to_string());
89        cache.insert("g", "h", "i", "3".to_string());
90        assert_eq!(cache.len(), 2);
91    }
92
93    #[test]
94    fn test_cache_clear() {
95        let mut cache = MergeCache::new(16);
96        cache.insert("a", "b", "c", "1".to_string());
97        cache.clear();
98        assert!(cache.is_empty());
99    }
100
101    #[test]
102    fn test_cache_different_order_is_different() {
103        let mut cache = MergeCache::new(16);
104        cache.insert("base", "ours", "theirs", "result1".to_string());
105        cache.insert("base", "theirs", "ours", "result2".to_string());
106        assert_eq!(cache.get("base", "ours", "theirs"), Some("result1"));
107        assert_eq!(cache.get("base", "theirs", "ours"), Some("result2"));
108    }
109}