git_semantic/index/
storage.rs1use 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}