Skip to main content

git_semantic/index/
storage.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use super::{IndexError, SemanticIndex};
5
6pub struct IndexStorage {
7    index_path: PathBuf,
8}
9
10impl IndexStorage {
11    pub fn new(repo_path: &Path) -> Result<Self, IndexError> {
12        let git_dir = repo_path.join(".git");
13
14        let index_path = if git_dir.is_dir() {
15            git_dir.join("semantic-index")
16        } else if git_dir.is_file() {
17            let content = fs::read_to_string(&git_dir)?;
18            let git_dir_path = content
19                .strip_prefix("gitdir: ")
20                .and_then(|s| s.trim().split('\n').next())
21                .ok_or(IndexError::InvalidGitFile)?;
22            PathBuf::from(git_dir_path).join("semantic-index")
23        } else {
24            return Err(IndexError::NotAGitRepository);
25        };
26
27        Ok(Self { index_path })
28    }
29
30    pub fn save(&self, index: &SemanticIndex) -> Result<(), IndexError> {
31        let encoded = bincode::serialize(index)?;
32        fs::write(&self.index_path, encoded)?;
33        Ok(())
34    }
35
36    pub fn load(&self) -> Result<SemanticIndex, IndexError> {
37        let data = fs::read(&self.index_path).map_err(|e| {
38            if e.kind() == std::io::ErrorKind::NotFound {
39                IndexError::IndexNotFound
40            } else {
41                IndexError::Io(e)
42            }
43        })?;
44
45        let index = bincode::deserialize(&data)?;
46        Ok(index)
47    }
48
49    pub fn index_size_mb(&self) -> Result<f64, IndexError> {
50        let metadata = fs::metadata(&self.index_path)?;
51        Ok(metadata.len() as f64 / 1_024_000.0)
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::git::CommitInfo;
59    use crate::index::{IndexEntry, SemanticIndex};
60    use tempfile::TempDir;
61
62    fn create_git_repo() -> TempDir {
63        let dir = TempDir::new().unwrap();
64        fs::create_dir_all(dir.path().join(".git")).unwrap();
65        dir
66    }
67
68    fn sample_index() -> SemanticIndex {
69        let mut index =
70            SemanticIndex::new("bge-small-en-v1.5".to_string(), "abc1234".to_string(), true);
71        index.entries.push(IndexEntry {
72            commit: CommitInfo {
73                hash: "abc1234".to_string(),
74                author: "Alice".to_string(),
75                date: chrono::DateTime::parse_from_rfc3339("2024-06-15T12:00:00Z")
76                    .unwrap()
77                    .with_timezone(&chrono::Utc),
78                message: "test commit".to_string(),
79                diff_summary: String::new(),
80            },
81            embedding: vec![0.1; 384],
82        });
83        index.metadata.total_commits = 1;
84        index
85    }
86
87    #[test]
88    fn test_storage_new_with_git_dir() {
89        let dir = create_git_repo();
90        let storage = IndexStorage::new(dir.path());
91        assert!(storage.is_ok());
92    }
93
94    #[test]
95    fn test_storage_new_without_git_dir() {
96        let dir = TempDir::new().unwrap();
97        let storage = IndexStorage::new(dir.path());
98        assert!(storage.is_err());
99    }
100
101    #[test]
102    fn test_save_and_load_roundtrip() {
103        let dir = create_git_repo();
104        let storage = IndexStorage::new(dir.path()).unwrap();
105
106        let original = sample_index();
107        storage.save(&original).unwrap();
108
109        let loaded = storage.load().unwrap();
110        assert_eq!(loaded.entries.len(), 1);
111        assert_eq!(loaded.entries[0].commit.hash, "abc1234");
112        assert_eq!(loaded.entries[0].embedding.len(), 384);
113        assert_eq!(loaded.model_version, "bge-small-en-v1.5");
114        assert_eq!(loaded.last_commit, "abc1234");
115        assert!(loaded.metadata.include_diffs);
116    }
117
118    #[test]
119    fn test_load_nonexistent_index() {
120        let dir = create_git_repo();
121        let storage = IndexStorage::new(dir.path()).unwrap();
122        let result = storage.load();
123        assert!(matches!(result, Err(IndexError::IndexNotFound)));
124    }
125
126    #[test]
127    fn test_index_size_mb() {
128        let dir = create_git_repo();
129        let storage = IndexStorage::new(dir.path()).unwrap();
130        let index = sample_index();
131        storage.save(&index).unwrap();
132
133        let size = storage.index_size_mb().unwrap();
134        assert!(size > 0.0, "index size should be > 0, got {size}");
135    }
136
137    #[test]
138    fn test_save_overwrites_existing() {
139        let dir = create_git_repo();
140        let storage = IndexStorage::new(dir.path()).unwrap();
141
142        let mut index1 = sample_index();
143        index1.last_commit = "first".to_string();
144        storage.save(&index1).unwrap();
145
146        let mut index2 = sample_index();
147        index2.last_commit = "second".to_string();
148        storage.save(&index2).unwrap();
149
150        let loaded = storage.load().unwrap();
151        assert_eq!(loaded.last_commit, "second");
152    }
153
154    #[test]
155    fn test_index_stored_in_git_dir() {
156        let dir = create_git_repo();
157        let storage = IndexStorage::new(dir.path()).unwrap();
158        let index = sample_index();
159        storage.save(&index).unwrap();
160
161        let index_file = dir.path().join(".git").join("semantic-index");
162        assert!(
163            index_file.exists(),
164            "index should be stored in .git/semantic-index"
165        );
166    }
167}