Skip to main content

lit/storage/
index.rs

1use crate::crypto::encryption::{EncryptionConfig, EncryptionManager};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7/// Index entry - represents a staged file
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct IndexEntry {
10    pub path: String,
11    pub hash: String,
12    pub mode: String,
13}
14
15/// The staging area (index)
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Index {
18    pub entries: HashMap<String, IndexEntry>,
19}
20
21impl Default for Index {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl Index {
28    /// Create a new empty index
29    pub fn new() -> Self {
30        Index {
31            entries: HashMap::new(),
32        }
33    }
34
35    /// Load index from disk
36    pub fn load(repo_path: &Path) -> Result<Self, String> {
37        Self::load_with_encryption(repo_path, None)
38    }
39
40    /// Load index from disk with encryption support
41    pub fn load_with_encryption(
42        repo_path: &Path,
43        passphrase: Option<&str>,
44    ) -> Result<Self, String> {
45        let index_path = repo_path.join(".lit").join("index");
46
47        if !index_path.exists() {
48            return Ok(Index::new());
49        }
50
51        let encrypted_data =
52            fs::read(&index_path).map_err(|e| format!("Failed to read index: {}", e))?;
53
54        // Decrypt if encryption is enabled
55        let data = {
56            let encryption_config = EncryptionConfig::load(repo_path)?;
57            // An explicit passphrase wins; otherwise fall back to the
58            // non-interactive sources, the same way the object store does.
59            // Without this the index stayed locked even when the caller had
60            // LIT_PASSPHRASE set, so every command that loads it failed.
61            let encryption_manager = match passphrase {
62                Some(pass) => {
63                    let mut manager = EncryptionManager::new(encryption_config);
64                    manager.initialize(pass)?;
65                    manager
66                }
67                None => EncryptionManager::new_auto(encryption_config, repo_path),
68            };
69
70            encryption_manager.decrypt(&encrypted_data)?
71        };
72
73        serde_json::from_slice(&data).map_err(|e| format!("Failed to parse index: {}", e))
74    }
75
76    /// Save index to disk
77    pub fn save(&self, repo_path: &Path) -> Result<(), String> {
78        self.save_with_encryption(repo_path, None)
79    }
80
81    /// Save index to disk with encryption support
82    pub fn save_with_encryption(
83        &self,
84        repo_path: &Path,
85        passphrase: Option<&str>,
86    ) -> Result<(), String> {
87        let index_path = repo_path.join(".lit").join("index");
88
89        let data = serde_json::to_vec_pretty(self)
90            .map_err(|e| format!("Failed to serialize index: {}", e))?;
91
92        // Encrypt if encryption is enabled
93        let final_data = {
94            let encryption_config = EncryptionConfig::load(repo_path)?;
95            let encryption_manager = match passphrase {
96                Some(pass) => {
97                    let mut manager = EncryptionManager::new(encryption_config);
98                    manager.initialize(pass)?;
99                    manager
100                }
101                None => EncryptionManager::new_auto(encryption_config, repo_path),
102            };
103
104            encryption_manager.encrypt(&data)?
105        };
106
107        fs::write(&index_path, final_data).map_err(|e| format!("Failed to write index: {}", e))
108    }
109
110    /// Add or update an entry in the index
111    pub fn add(&mut self, path: String, hash: String, mode: String) {
112        self.entries
113            .insert(path.clone(), IndexEntry { path, hash, mode });
114    }
115
116    /// Remove an entry from the index
117    pub fn remove(&mut self, path: &str) -> Option<IndexEntry> {
118        self.entries.remove(path)
119    }
120
121    /// Get all entries sorted by path
122    pub fn sorted_entries(&self) -> Vec<&IndexEntry> {
123        let mut entries: Vec<&IndexEntry> = self.entries.values().collect();
124        entries.sort_by(|a, b| a.path.cmp(&b.path));
125        entries
126    }
127
128    /// Clear the index
129    pub fn clear(&mut self) {
130        self.entries.clear();
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use tempfile::TempDir;
138
139    #[test]
140    fn test_index() {
141        let temp_dir = TempDir::new().unwrap();
142        let repo_path = temp_dir.path();
143
144        fs::create_dir_all(repo_path.join(".lit")).unwrap();
145
146        let mut index = Index::new();
147        index.add(
148            "file.txt".to_string(),
149            "abc123".to_string(),
150            "100644".to_string(),
151        );
152
153        index.save(repo_path).unwrap();
154
155        let loaded = Index::load(repo_path).unwrap();
156        assert_eq!(loaded.entries.len(), 1);
157        assert!(loaded.entries.contains_key("file.txt"));
158    }
159}