Skip to main content

lit/core/
refs.rs

1use crate::crypto::encryption::EncryptionManager;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6
7/// Reference - points to a commit (branch, tag, HEAD)
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Reference {
10    pub name: String,
11    pub hash: String,
12}
13
14/// Get the Lit directory path
15pub fn get_lit_dir(repo_path: &Path) -> PathBuf {
16    repo_path.join(".lit")
17}
18
19/// Check if a directory is a Lit repository
20pub fn is_lit_repo(path: &Path) -> bool {
21    get_lit_dir(path).exists()
22}
23
24/// Find the repository root from current directory
25pub 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(&current) {
31            return Ok(current);
32        }
33
34        if !current.pop() {
35            return Err("Not in a Lit repository".to_string());
36        }
37    }
38}
39
40/// Read a reference file
41pub 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
53/// Read an encrypted reference file
54pub 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
79/// Write a reference file
80pub 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
91/// Write an encrypted reference file
92pub 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
116/// Delete a reference
117pub 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
127/// List all references
128pub 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
160/// Read HEAD reference
161pub 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    // Check if HEAD is symbolic (ref: refs/heads/main)
174    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        // Direct hash
181        Ok(content.to_string())
182    }
183}
184
185/// Get current branch name
186pub 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
201/// Update HEAD to point to a branch
202pub 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
209/// Update HEAD to point to a branch (encrypted)
210pub 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
229/// Set HEAD to a specific commit (detached)
230pub 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
236/// Set HEAD to a specific commit (detached, encrypted)
237pub 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
256/// Read HEAD reference (encrypted)
257pub 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    // Check if HEAD is symbolic (ref: refs/heads/main)
282    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        // Direct hash
290        Ok(content.to_string())
291    }
292}
293
294/// Get current branch name (encrypted)
295pub 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 encrypted ref
351        write_ref_encrypted(&temp_dir, ref_name, test_hash, &encryption).unwrap();
352
353        // Read encrypted ref
354        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 branch ref
382        write_ref_encrypted(&temp_dir, "heads/main", commit_hash, &encryption).unwrap();
383
384        // Update HEAD to point to branch
385        update_head_encrypted(&temp_dir, branch_name, &encryption).unwrap();
386
387        // Get current branch
388        let current = get_current_branch_encrypted(&temp_dir, &encryption).unwrap();
389        assert_eq!(current, branch_name);
390
391        // Read HEAD (should resolve to commit)
392        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 to detached state
418        set_head_detached_encrypted(&temp_dir, commit_hash, &encryption).unwrap();
419
420        // Read HEAD
421        let head = read_head_encrypted(&temp_dir, &encryption).unwrap();
422        assert_eq!(head, commit_hash);
423
424        // Getting branch should fail (detached)
425        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 encrypted ref
451        write_ref_encrypted(&temp_dir, ref_name, test_hash, &encryption).unwrap();
452
453        // Tamper with the encrypted file
454        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; // Flip a bit
458        fs::write(&ref_path, data).unwrap();
459
460        // Reading should fail due to authentication tag mismatch
461        assert!(read_ref_encrypted(&temp_dir, ref_name, &encryption).is_err());
462    }
463}