Skip to main content

lit/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::Path;
3
4/// Unified configuration with repo-local, user-global, and system hierarchy.
5/// Priority: CLI args > env vars > repo-local > user-global > defaults
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7pub struct LitConfig {
8    #[serde(default)]
9    pub core: CoreConfig,
10    #[serde(default)]
11    pub agent: AgentConfig,
12    #[serde(default)]
13    pub merge: MergeConfig,
14    #[serde(default)]
15    pub security: SecurityConfig,
16    #[serde(default)]
17    pub performance: PerformanceConfig,
18    #[serde(default)]
19    pub lfs: LfsConfig,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CoreConfig {
24    pub default_branch: String,
25    pub default_output: String,
26}
27
28impl Default for CoreConfig {
29    fn default() -> Self {
30        CoreConfig {
31            default_branch: "main".to_string(),
32            default_output: "json".to_string(),
33        }
34    }
35}
36
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38pub struct AgentConfig {
39    pub auto_sign: bool,
40    pub default_metadata: Option<serde_json::Value>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct MergeConfig {
45    pub default_strategy: String,
46    pub auto_resolve: bool,
47}
48
49impl Default for MergeConfig {
50    fn default() -> Self {
51        MergeConfig {
52            default_strategy: "recursive".to_string(),
53            auto_resolve: false,
54        }
55    }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct SecurityConfig {
60    pub encryption: String,
61    pub fips_mode: bool,
62    pub audit_log: bool,
63}
64
65impl Default for SecurityConfig {
66    fn default() -> Self {
67        SecurityConfig {
68            encryption: "aes-256-gcm".to_string(),
69            fips_mode: false,
70            audit_log: true,
71        }
72    }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct PerformanceConfig {
77    /// Use parallel I/O for status, hashing, pack operations
78    pub parallel_io: bool,
79    /// Number of threads (0 = auto-detect)
80    pub threads: usize,
81    /// Pack objects when loose object count exceeds this threshold
82    pub auto_pack_threshold: usize,
83    /// Large file threshold in bytes for LFS (default: 10 MB)
84    pub lfs_threshold: u64,
85}
86
87impl Default for PerformanceConfig {
88    fn default() -> Self {
89        PerformanceConfig {
90            parallel_io: true,
91            threads: 0,
92            auto_pack_threshold: 1000,
93            lfs_threshold: 10 * 1024 * 1024,
94        }
95    }
96}
97
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct LfsConfig {
100    /// Enable large file storage
101    pub enabled: bool,
102    /// Glob patterns for files to track as LFS
103    pub track_patterns: Vec<String>,
104}
105
106impl LitConfig {
107    /// Load configuration with full hierarchy:
108    /// repo-local .lit/config.toml > user ~/.litconfig.toml > defaults
109    pub fn load(repo_path: Option<&Path>) -> Self {
110        let mut config = LitConfig::default();
111
112        // Layer 1: User global config
113        if let Some(home) = dirs::home_dir() {
114            let global_path = home.join(".litconfig.toml");
115            if let Ok(content) = std::fs::read_to_string(&global_path) {
116                if let Ok(global) = toml::from_str::<LitConfig>(&content) {
117                    config = config.merge_with(global);
118                }
119            }
120        }
121
122        // Layer 2: Repo-local config (overrides global)
123        if let Some(repo) = repo_path {
124            let local_path = repo.join(".lit").join("config.toml");
125            if let Ok(content) = std::fs::read_to_string(&local_path) {
126                if let Ok(local) = toml::from_str::<LitConfig>(&content) {
127                    config = config.merge_with(local);
128                }
129            }
130        }
131
132        // Layer 3: Environment variables (override everything)
133        if let Ok(v) = std::env::var("LIT_DEFAULT_BRANCH") {
134            config.core.default_branch = v;
135        }
136        if let Ok(v) = std::env::var("LIT_OUTPUT") {
137            config.core.default_output = v;
138        }
139        if let Ok(v) = std::env::var("LIT_FIPS_MODE") {
140            config.security.fips_mode = v == "true" || v == "1";
141        }
142        if let Ok(v) = std::env::var("LIT_PARALLEL") {
143            config.performance.parallel_io = v != "false" && v != "0";
144        }
145
146        config
147    }
148
149    /// Save repo-local configuration
150    pub fn save_local(&self, repo_path: &Path) -> Result<(), String> {
151        let config_path = repo_path.join(".lit").join("config.toml");
152        let content = toml::to_string_pretty(self)
153            .map_err(|e| format!("Failed to serialize config: {}", e))?;
154        std::fs::write(&config_path, content).map_err(|e| format!("Failed to write config: {}", e))
155    }
156
157    /// Save user-global configuration
158    pub fn save_global(&self) -> Result<(), String> {
159        let home = dirs::home_dir().ok_or("Could not determine home directory")?;
160        let config_path = home.join(".litconfig.toml");
161        let content = toml::to_string_pretty(self)
162            .map_err(|e| format!("Failed to serialize config: {}", e))?;
163        std::fs::write(&config_path, content).map_err(|e| format!("Failed to write config: {}", e))
164    }
165
166    /// Get a config value by dotted key path
167    pub fn get(&self, key: &str) -> Option<String> {
168        match key {
169            "core.default_branch" => Some(self.core.default_branch.clone()),
170            "core.default_output" => Some(self.core.default_output.clone()),
171            "agent.auto_sign" => Some(self.agent.auto_sign.to_string()),
172            "merge.default_strategy" => Some(self.merge.default_strategy.clone()),
173            "merge.auto_resolve" => Some(self.merge.auto_resolve.to_string()),
174            "security.encryption" => Some(self.security.encryption.clone()),
175            "security.fips_mode" => Some(self.security.fips_mode.to_string()),
176            "security.audit_log" => Some(self.security.audit_log.to_string()),
177            "performance.parallel_io" => Some(self.performance.parallel_io.to_string()),
178            "performance.threads" => Some(self.performance.threads.to_string()),
179            "performance.auto_pack_threshold" => {
180                Some(self.performance.auto_pack_threshold.to_string())
181            }
182            "performance.lfs_threshold" => Some(self.performance.lfs_threshold.to_string()),
183            "lfs.enabled" => Some(self.lfs.enabled.to_string()),
184            _ => None,
185        }
186    }
187
188    /// Set a config value by dotted key path
189    pub fn set(&mut self, key: &str, value: &str) -> Result<(), String> {
190        match key {
191            "core.default_branch" => self.core.default_branch = value.to_string(),
192            "core.default_output" => self.core.default_output = value.to_string(),
193            "agent.auto_sign" => {
194                self.agent.auto_sign = value == "true" || value == "1";
195            }
196            "merge.default_strategy" => self.merge.default_strategy = value.to_string(),
197            "merge.auto_resolve" => {
198                self.merge.auto_resolve = value == "true" || value == "1";
199            }
200            "security.encryption" => self.security.encryption = value.to_string(),
201            "security.fips_mode" => {
202                self.security.fips_mode = value == "true" || value == "1";
203            }
204            "security.audit_log" => {
205                self.security.audit_log = value == "true" || value == "1";
206            }
207            "performance.parallel_io" => {
208                self.performance.parallel_io = value == "true" || value == "1";
209            }
210            "performance.threads" => {
211                self.performance.threads = value
212                    .parse()
213                    .map_err(|_| format!("Invalid thread count: {}", value))?;
214            }
215            "performance.auto_pack_threshold" => {
216                self.performance.auto_pack_threshold = value
217                    .parse()
218                    .map_err(|_| format!("Invalid threshold: {}", value))?;
219            }
220            "performance.lfs_threshold" => {
221                self.performance.lfs_threshold = value
222                    .parse()
223                    .map_err(|_| format!("Invalid threshold: {}", value))?;
224            }
225            "lfs.enabled" => {
226                self.lfs.enabled = value == "true" || value == "1";
227            }
228            _ => return Err(format!("Unknown config key: {}", key)),
229        }
230        Ok(())
231    }
232
233    /// Get all config entries as key-value pairs
234    pub fn entries(&self) -> Vec<(String, String)> {
235        vec![
236            (
237                "core.default_branch".into(),
238                self.core.default_branch.clone(),
239            ),
240            (
241                "core.default_output".into(),
242                self.core.default_output.clone(),
243            ),
244            ("agent.auto_sign".into(), self.agent.auto_sign.to_string()),
245            (
246                "merge.default_strategy".into(),
247                self.merge.default_strategy.clone(),
248            ),
249            (
250                "merge.auto_resolve".into(),
251                self.merge.auto_resolve.to_string(),
252            ),
253            (
254                "security.encryption".into(),
255                self.security.encryption.clone(),
256            ),
257            (
258                "security.fips_mode".into(),
259                self.security.fips_mode.to_string(),
260            ),
261            (
262                "security.audit_log".into(),
263                self.security.audit_log.to_string(),
264            ),
265            (
266                "performance.parallel_io".into(),
267                self.performance.parallel_io.to_string(),
268            ),
269            (
270                "performance.threads".into(),
271                self.performance.threads.to_string(),
272            ),
273            (
274                "performance.auto_pack_threshold".into(),
275                self.performance.auto_pack_threshold.to_string(),
276            ),
277            (
278                "performance.lfs_threshold".into(),
279                self.performance.lfs_threshold.to_string(),
280            ),
281            ("lfs.enabled".into(), self.lfs.enabled.to_string()),
282        ]
283    }
284
285    /// Merge another config on top of this one (other wins on conflicts)
286    fn merge_with(mut self, other: LitConfig) -> Self {
287        // Only override non-default values
288        if other.core.default_branch != CoreConfig::default().default_branch {
289            self.core.default_branch = other.core.default_branch;
290        }
291        if other.core.default_output != CoreConfig::default().default_output {
292            self.core.default_output = other.core.default_output;
293        }
294        if other.agent.auto_sign {
295            self.agent.auto_sign = true;
296        }
297        if other.agent.default_metadata.is_some() {
298            self.agent.default_metadata = other.agent.default_metadata;
299        }
300        if other.merge.default_strategy != MergeConfig::default().default_strategy {
301            self.merge.default_strategy = other.merge.default_strategy;
302        }
303        if other.merge.auto_resolve {
304            self.merge.auto_resolve = true;
305        }
306        if other.security.fips_mode {
307            self.security.fips_mode = true;
308        }
309        if !other.performance.parallel_io {
310            self.performance.parallel_io = false;
311        }
312        if other.performance.threads != 0 {
313            self.performance.threads = other.performance.threads;
314        }
315        if other.lfs.enabled {
316            self.lfs.enabled = true;
317        }
318        if !other.lfs.track_patterns.is_empty() {
319            self.lfs.track_patterns = other.lfs.track_patterns;
320        }
321        self
322    }
323}