1use crate::crypto::encryption::EncryptionManager;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Reference {
10 pub name: String,
11 pub hash: String,
12}
13
14pub fn get_lit_dir(repo_path: &Path) -> PathBuf {
16 repo_path.join(".lit")
17}
18
19pub fn is_lit_repo(path: &Path) -> bool {
21 get_lit_dir(path).exists()
22}
23
24pub fn find_repo_root() -> Result<PathBuf, String> {
26 let mut current =
27 std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?;
28
29 loop {
30 if is_lit_repo(¤t) {
31 return Ok(current);
32 }
33
34 if !current.pop() {
35 return Err("Not in a Lit repository".to_string());
36 }
37 }
38}
39
40pub fn read_ref(repo_path: &Path, ref_name: &str) -> Result<String, String> {
42 let ref_path = get_lit_dir(repo_path).join("refs").join(ref_name);
43
44 if !ref_path.exists() {
45 return Err(format!("Reference '{}' not found", ref_name));
46 }
47
48 fs::read_to_string(&ref_path)
49 .map_err(|e| format!("Failed to read reference: {}", e))
50 .map(|s| s.trim().to_string())
51}
52
53pub fn read_ref_encrypted(
55 repo_path: &Path,
56 ref_name: &str,
57 encryption: &Arc<Mutex<EncryptionManager>>,
58) -> Result<String, String> {
59 let ref_path = get_lit_dir(repo_path).join("refs").join(ref_name);
60
61 if !ref_path.exists() {
62 return Err(format!("Reference '{}' not found", ref_name));
63 }
64
65 let encrypted_data =
66 fs::read(&ref_path).map_err(|e| format!("Failed to read reference: {}", e))?;
67
68 let enc_guard = encryption
69 .lock()
70 .map_err(|_| "Failed to lock encryption manager".to_string())?;
71
72 let decrypted = enc_guard.decrypt(&encrypted_data)?;
73
74 String::from_utf8(decrypted)
75 .map_err(|e| format!("Invalid UTF-8 in decrypted reference: {}", e))
76 .map(|s| s.trim().to_string())
77}
78
79pub fn write_ref(repo_path: &Path, ref_name: &str, hash: &str) -> Result<(), String> {
81 let ref_path = get_lit_dir(repo_path).join("refs").join(ref_name);
82
83 if let Some(parent) = ref_path.parent() {
84 fs::create_dir_all(parent).map_err(|e| format!("Failed to create ref directory: {}", e))?;
85 }
86
87 fs::write(&ref_path, format!("{}\n", hash))
88 .map_err(|e| format!("Failed to write reference: {}", e))
89}
90
91pub fn write_ref_encrypted(
93 repo_path: &Path,
94 ref_name: &str,
95 hash: &str,
96 encryption: &Arc<Mutex<EncryptionManager>>,
97) -> Result<(), String> {
98 let ref_path = get_lit_dir(repo_path).join("refs").join(ref_name);
99
100 if let Some(parent) = ref_path.parent() {
101 fs::create_dir_all(parent).map_err(|e| format!("Failed to create ref directory: {}", e))?;
102 }
103
104 let data = format!("{}\n", hash);
105
106 let enc_guard = encryption
107 .lock()
108 .map_err(|_| "Failed to lock encryption manager".to_string())?;
109
110 let encrypted = enc_guard.encrypt(data.as_bytes())?;
111 drop(enc_guard);
112
113 fs::write(&ref_path, encrypted).map_err(|e| format!("Failed to write reference: {}", e))
114}
115
116pub fn delete_ref(repo_path: &Path, ref_name: &str) -> Result<(), String> {
118 let ref_path = get_lit_dir(repo_path).join("refs").join(ref_name);
119
120 if !ref_path.exists() {
121 return Err(format!("Reference '{}' not found", ref_name));
122 }
123
124 fs::remove_file(&ref_path).map_err(|e| format!("Failed to delete reference: {}", e))
125}
126
127pub fn list_refs(repo_path: &Path, prefix: &str) -> Result<Vec<Reference>, String> {
129 let refs_dir = get_lit_dir(repo_path).join("refs").join(prefix);
130
131 if !refs_dir.exists() {
132 return Ok(Vec::new());
133 }
134
135 let mut refs = Vec::new();
136
137 for entry in walkdir::WalkDir::new(&refs_dir) {
138 let entry = entry.map_err(|e| format!("Failed to read refs: {}", e))?;
139
140 if entry.file_type().is_file() {
141 let path = entry.path();
142 let name = path
143 .strip_prefix(&refs_dir)
144 .map_err(|e| format!("Path error: {}", e))?
145 .to_string_lossy()
146 .to_string();
147
148 let hash = fs::read_to_string(path)
149 .map_err(|e| format!("Failed to read ref: {}", e))?
150 .trim()
151 .to_string();
152
153 refs.push(Reference { name, hash });
154 }
155 }
156
157 Ok(refs)
158}
159
160pub fn read_head(repo_path: &Path) -> Result<String, String> {
162 let head_path = get_lit_dir(repo_path).join("HEAD");
163
164 if !head_path.exists() {
165 return Err("HEAD not found".to_string());
166 }
167
168 let content =
169 fs::read_to_string(&head_path).map_err(|e| format!("Failed to read HEAD: {}", e))?;
170
171 let content = content.trim();
172
173 if let Some(ref_name) = content.strip_prefix("ref: ") {
175 read_ref(
176 repo_path,
177 ref_name.strip_prefix("refs/").unwrap_or(ref_name),
178 )
179 } else {
180 Ok(content.to_string())
182 }
183}
184
185pub fn get_current_branch(repo_path: &Path) -> Result<String, String> {
187 let head_path = get_lit_dir(repo_path).join("HEAD");
188
189 let content =
190 fs::read_to_string(&head_path).map_err(|e| format!("Failed to read HEAD: {}", e))?;
191
192 let content = content.trim();
193
194 if let Some(branch) = content.strip_prefix("ref: refs/heads/") {
195 Ok(branch.to_string())
196 } else {
197 Err("HEAD is detached".to_string())
198 }
199}
200
201pub fn update_head(repo_path: &Path, branch: &str) -> Result<(), String> {
203 let head_path = get_lit_dir(repo_path).join("HEAD");
204
205 fs::write(&head_path, format!("ref: refs/heads/{}\n", branch))
206 .map_err(|e| format!("Failed to update HEAD: {}", e))
207}
208
209pub fn update_head_encrypted(
211 repo_path: &Path,
212 branch: &str,
213 encryption: &Arc<Mutex<EncryptionManager>>,
214) -> Result<(), String> {
215 let head_path = get_lit_dir(repo_path).join("HEAD");
216
217 let data = format!("ref: refs/heads/{}\n", branch);
218
219 let enc_guard = encryption
220 .lock()
221 .map_err(|_| "Failed to lock encryption manager".to_string())?;
222
223 let encrypted = enc_guard.encrypt(data.as_bytes())?;
224 drop(enc_guard);
225
226 fs::write(&head_path, encrypted).map_err(|e| format!("Failed to update HEAD: {}", e))
227}
228
229pub fn set_head_detached(repo_path: &Path, hash: &str) -> Result<(), String> {
231 let head_path = get_lit_dir(repo_path).join("HEAD");
232
233 fs::write(&head_path, format!("{}\n", hash)).map_err(|e| format!("Failed to set HEAD: {}", e))
234}
235
236pub fn set_head_detached_encrypted(
238 repo_path: &Path,
239 hash: &str,
240 encryption: &Arc<Mutex<EncryptionManager>>,
241) -> Result<(), String> {
242 let head_path = get_lit_dir(repo_path).join("HEAD");
243
244 let data = format!("{}\n", hash);
245
246 let enc_guard = encryption
247 .lock()
248 .map_err(|_| "Failed to lock encryption manager".to_string())?;
249
250 let encrypted = enc_guard.encrypt(data.as_bytes())?;
251 drop(enc_guard);
252
253 fs::write(&head_path, encrypted).map_err(|e| format!("Failed to set HEAD: {}", e))
254}
255
256pub fn read_head_encrypted(
258 repo_path: &Path,
259 encryption: &Arc<Mutex<EncryptionManager>>,
260) -> Result<String, String> {
261 let head_path = get_lit_dir(repo_path).join("HEAD");
262
263 if !head_path.exists() {
264 return Err("HEAD not found".to_string());
265 }
266
267 let encrypted_data = fs::read(&head_path).map_err(|e| format!("Failed to read HEAD: {}", e))?;
268
269 let enc_guard = encryption
270 .lock()
271 .map_err(|_| "Failed to lock encryption manager".to_string())?;
272
273 let decrypted = enc_guard.decrypt(&encrypted_data)?;
274 drop(enc_guard);
275
276 let content = String::from_utf8(decrypted)
277 .map_err(|e| format!("Invalid UTF-8 in decrypted HEAD: {}", e))?;
278
279 let content = content.trim();
280
281 if let Some(ref_name) = content.strip_prefix("ref: ") {
283 read_ref_encrypted(
284 repo_path,
285 ref_name.strip_prefix("refs/").unwrap_or(ref_name),
286 encryption,
287 )
288 } else {
289 Ok(content.to_string())
291 }
292}
293
294pub fn get_current_branch_encrypted(
296 repo_path: &Path,
297 encryption: &Arc<Mutex<EncryptionManager>>,
298) -> Result<String, String> {
299 let head_path = get_lit_dir(repo_path).join("HEAD");
300
301 let encrypted_data = fs::read(&head_path).map_err(|e| format!("Failed to read HEAD: {}", e))?;
302
303 let enc_guard = encryption
304 .lock()
305 .map_err(|_| "Failed to lock encryption manager".to_string())?;
306
307 let decrypted = enc_guard.decrypt(&encrypted_data)?;
308 drop(enc_guard);
309
310 let content = String::from_utf8(decrypted)
311 .map_err(|e| format!("Invalid UTF-8 in decrypted HEAD: {}", e))?;
312
313 let content = content.trim();
314
315 if let Some(branch) = content.strip_prefix("ref: refs/heads/") {
316 Ok(branch.to_string())
317 } else {
318 Err("HEAD is detached".to_string())
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::crypto::encryption::EncryptionConfig;
326 use tempfile::TempDir;
327
328 #[test]
329 fn test_encrypted_ref_write_read() {
330 let temp = TempDir::new().unwrap();
331 let temp_dir = temp.path().to_path_buf();
332 fs::create_dir_all(get_lit_dir(&temp_dir).join("refs/heads")).unwrap();
333
334 let config = EncryptionConfig {
335 enabled: true,
336 key_file: temp_dir
337 .join("encryption.key")
338 .to_string_lossy()
339 .to_string(),
340 ..Default::default()
341 };
342
343 let mut enc_manager = EncryptionManager::new(config);
344 enc_manager.initialize("test-passphrase-refs").unwrap();
345 let encryption = Arc::new(Mutex::new(enc_manager));
346
347 let test_hash = "abc123def456";
348 let ref_name = "heads/test-branch";
349
350 write_ref_encrypted(&temp_dir, ref_name, test_hash, &encryption).unwrap();
352
353 let read_hash = read_ref_encrypted(&temp_dir, ref_name, &encryption).unwrap();
355
356 assert_eq!(read_hash, test_hash);
357 }
358
359 #[test]
360 fn test_encrypted_head_operations() {
361 let temp = TempDir::new().unwrap();
362 let temp_dir = temp.path().to_path_buf();
363 fs::create_dir_all(get_lit_dir(&temp_dir).join("refs/heads")).unwrap();
364
365 let config = EncryptionConfig {
366 enabled: true,
367 key_file: temp_dir
368 .join("encryption.key")
369 .to_string_lossy()
370 .to_string(),
371 ..Default::default()
372 };
373
374 let mut enc_manager = EncryptionManager::new(config);
375 enc_manager.initialize("test-passphrase-head").unwrap();
376 let encryption = Arc::new(Mutex::new(enc_manager));
377
378 let branch_name = "main";
379 let commit_hash = "deadbeef123456";
380
381 write_ref_encrypted(&temp_dir, "heads/main", commit_hash, &encryption).unwrap();
383
384 update_head_encrypted(&temp_dir, branch_name, &encryption).unwrap();
386
387 let current = get_current_branch_encrypted(&temp_dir, &encryption).unwrap();
389 assert_eq!(current, branch_name);
390
391 let head_commit = read_head_encrypted(&temp_dir, &encryption).unwrap();
393 assert_eq!(head_commit, commit_hash);
394 }
395
396 #[test]
397 fn test_encrypted_detached_head() {
398 let temp = TempDir::new().unwrap();
399 let temp_dir = temp.path().to_path_buf();
400 fs::create_dir_all(get_lit_dir(&temp_dir)).unwrap();
401
402 let config = EncryptionConfig {
403 enabled: true,
404 key_file: temp_dir
405 .join("encryption.key")
406 .to_string_lossy()
407 .to_string(),
408 ..Default::default()
409 };
410
411 let mut enc_manager = EncryptionManager::new(config);
412 enc_manager.initialize("test-passphrase-detached").unwrap();
413 let encryption = Arc::new(Mutex::new(enc_manager));
414
415 let commit_hash = "cafebabe987654";
416
417 set_head_detached_encrypted(&temp_dir, commit_hash, &encryption).unwrap();
419
420 let head = read_head_encrypted(&temp_dir, &encryption).unwrap();
422 assert_eq!(head, commit_hash);
423
424 assert!(get_current_branch_encrypted(&temp_dir, &encryption).is_err());
426 }
427
428 #[test]
429 fn test_encrypted_ref_tamper_detection() {
430 let temp = TempDir::new().unwrap();
431 let temp_dir = temp.path().to_path_buf();
432 fs::create_dir_all(get_lit_dir(&temp_dir).join("refs/heads")).unwrap();
433
434 let config = EncryptionConfig {
435 enabled: true,
436 key_file: temp_dir
437 .join("encryption.key")
438 .to_string_lossy()
439 .to_string(),
440 ..Default::default()
441 };
442
443 let mut enc_manager = EncryptionManager::new(config);
444 enc_manager.initialize("test-passphrase-tamper").unwrap();
445 let encryption = Arc::new(Mutex::new(enc_manager));
446
447 let test_hash = "original123";
448 let ref_name = "heads/tamper-test";
449
450 write_ref_encrypted(&temp_dir, ref_name, test_hash, &encryption).unwrap();
452
453 let ref_path = get_lit_dir(&temp_dir).join("refs").join(ref_name);
455 let mut data = fs::read(&ref_path).unwrap();
456 let len = data.len();
457 data[len - 1] ^= 0x01; fs::write(&ref_path, data).unwrap();
459
460 assert!(read_ref_encrypted(&temp_dir, ref_name, &encryption).is_err());
462 }
463}