Skip to main content

git_warp/
config.rs

1use crate::error::{GitWarpError, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use figment::{Figment, providers::{Format, Toml, Env}};
5use dirs::config_dir;
6use std::fs;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Config {
10    /// Default terminal mode for worktree switching
11    #[serde(default = "default_terminal_mode")]
12    pub terminal_mode: String,
13    
14    /// Default worktree base directory
15    pub worktrees_path: Option<PathBuf>,
16    
17    /// Whether to use Copy-on-Write by default
18    #[serde(default = "default_true")]
19    pub use_cow: bool,
20    
21    /// Whether to auto-confirm destructive operations
22    #[serde(default)]
23    pub auto_confirm: bool,
24    
25    /// Git configuration
26    #[serde(default)]
27    pub git: GitConfig,
28    
29    /// Process management settings
30    #[serde(default)]
31    pub process: ProcessConfig,
32    
33    /// Terminal integration settings
34    #[serde(default)]
35    pub terminal: TerminalConfig,
36    
37    /// Agent monitoring settings
38    #[serde(default)]
39    pub agent: AgentConfig,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct GitConfig {
44    /// Default branch name (main, master, develop)
45    #[serde(default = "default_main_branch")]
46    pub default_branch: String,
47    
48    /// Whether to auto-fetch before operations
49    #[serde(default = "default_true")]
50    pub auto_fetch: bool,
51    
52    /// Whether to prune remote tracking branches
53    #[serde(default = "default_true")]
54    pub auto_prune: bool,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ProcessConfig {
59    /// Whether to check for processes before cleanup
60    #[serde(default = "default_true")]
61    pub check_processes: bool,
62    
63    /// Whether to kill processes automatically
64    #[serde(default)]
65    pub auto_kill: bool,
66    
67    /// Grace period before force killing (seconds)
68    #[serde(default = "default_kill_timeout")]
69    pub kill_timeout: u64,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TerminalConfig {
74    /// Preferred terminal application (iterm2, terminal, auto)
75    #[serde(default = "default_terminal_app")]
76    pub app: String,
77    
78    /// Whether to activate new tabs/windows
79    #[serde(default = "default_true")]
80    pub auto_activate: bool,
81    
82    /// Custom init commands for new worktrees
83    #[serde(default)]
84    pub init_commands: Vec<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct AgentConfig {
89    /// Enable agent monitoring
90    #[serde(default = "default_true")]
91    pub enabled: bool,
92    
93    /// Agent monitoring refresh rate (milliseconds)
94    #[serde(default = "default_refresh_rate")]
95    pub refresh_rate: u64,
96    
97    /// Maximum number of activities to track
98    #[serde(default = "default_max_activities")]
99    pub max_activities: usize,
100    
101    /// Enable Claude Code hooks integration
102    #[serde(default = "default_true")]
103    pub claude_hooks: bool,
104}
105
106// Default value functions
107fn default_terminal_mode() -> String {
108    "tab".to_string()
109}
110
111fn default_true() -> bool {
112    true
113}
114
115fn default_main_branch() -> String {
116    "main".to_string()
117}
118
119fn default_kill_timeout() -> u64 {
120    5
121}
122
123fn default_terminal_app() -> String {
124    "auto".to_string()
125}
126
127fn default_refresh_rate() -> u64 {
128    1000
129}
130
131fn default_max_activities() -> usize {
132    100
133}
134
135// Default implementations
136impl Default for Config {
137    fn default() -> Self {
138        Self {
139            terminal_mode: default_terminal_mode(),
140            worktrees_path: None,
141            use_cow: true,
142            auto_confirm: false,
143            git: GitConfig::default(),
144            process: ProcessConfig::default(),
145            terminal: TerminalConfig::default(),
146            agent: AgentConfig::default(),
147        }
148    }
149}
150
151impl Default for GitConfig {
152    fn default() -> Self {
153        Self {
154            default_branch: default_main_branch(),
155            auto_fetch: true,
156            auto_prune: true,
157        }
158    }
159}
160
161impl Default for ProcessConfig {
162    fn default() -> Self {
163        Self {
164            check_processes: true,
165            auto_kill: false,
166            kill_timeout: default_kill_timeout(),
167        }
168    }
169}
170
171impl Default for TerminalConfig {
172    fn default() -> Self {
173        Self {
174            app: default_terminal_app(),
175            auto_activate: true,
176            init_commands: Vec::new(),
177        }
178    }
179}
180
181impl Default for AgentConfig {
182    fn default() -> Self {
183        Self {
184            enabled: true,
185            refresh_rate: default_refresh_rate(),
186            max_activities: default_max_activities(),
187            claude_hooks: true,
188        }
189    }
190}
191
192impl Config {
193    /// Create a configuration with intelligent defaults
194    pub fn with_defaults() -> Self {
195        Self::default()
196    }
197    
198    /// Update configuration from environment variables
199    pub fn apply_env_overrides(&mut self) {
200        // Terminal mode
201        if let Ok(mode) = std::env::var("GIT_WARP_TERMINAL_MODE") {
202            self.terminal_mode = mode;
203        }
204        
205        // Auto-confirm
206        if let Ok(confirm) = std::env::var("GIT_WARP_AUTO_CONFIRM") {
207            self.auto_confirm = confirm.parse().unwrap_or(false);
208        }
209        
210        // CoW usage
211        if let Ok(cow) = std::env::var("GIT_WARP_USE_COW") {
212            self.use_cow = cow.parse().unwrap_or(true);
213        }
214        
215        // Worktrees path
216        if let Ok(path) = std::env::var("GIT_WARP_WORKTREES_PATH") {
217            self.worktrees_path = Some(PathBuf::from(path));
218        }
219    }
220    
221    /// Generate a sample configuration file content
222    pub fn sample_config() -> String {
223        let config = Config::default();
224        format!(
225            r#"# Git-Warp Configuration
226# This file configures git-warp behavior
227# You can also set these values via environment variables with GIT_WARP_ prefix
228
229# Terminal mode: tab, window, inplace, echo
230terminal_mode = "{}"
231
232# Use Copy-on-Write when available
233use_cow = {}
234
235# Auto-confirm destructive operations
236auto_confirm = {}
237
238# Custom worktrees directory (optional)
239# worktrees_path = "/custom/path/to/worktrees"
240
241[git]
242# Default main branch name
243default_branch = "{}"
244
245# Auto-fetch before operations
246auto_fetch = {}
247
248# Auto-prune remote tracking branches
249auto_prune = {}
250
251[process]
252# Check for processes before cleanup
253check_processes = {}
254
255# Auto-kill processes during cleanup
256auto_kill = {}
257
258# Grace period before force killing (seconds)
259kill_timeout = {}
260
261[terminal]
262# Terminal app: auto, iterm2, terminal
263app = "{}"
264
265# Auto-activate new tabs/windows
266auto_activate = {}
267
268# Custom commands to run in new worktrees
269# init_commands = ["npm install", "source .env"]
270
271[agent]
272# Enable agent monitoring
273enabled = {}
274
275# Refresh rate for agent dashboard (milliseconds)
276refresh_rate = {}
277
278# Maximum activities to track
279max_activities = {}
280
281# Enable Claude Code hooks integration
282claude_hooks = {}
283"#,
284            config.terminal_mode,
285            config.use_cow,
286            config.auto_confirm,
287            config.git.default_branch,
288            config.git.auto_fetch,
289            config.git.auto_prune,
290            config.process.check_processes,
291            config.process.auto_kill,
292            config.process.kill_timeout,
293            config.terminal.app,
294            config.terminal.auto_activate,
295            config.agent.enabled,
296            config.agent.refresh_rate,
297            config.agent.max_activities,
298            config.agent.claude_hooks,
299        )
300    }
301}
302
303pub struct ConfigManager {
304    pub config: Config,
305    pub config_path: PathBuf,
306}
307
308
309impl ConfigManager {
310    /// Create a new config manager with default or loaded configuration
311    pub fn new() -> Result<Self> {
312        let config_path = get_config_path()?;
313        let config = Self::load_config(&config_path)?;
314        Ok(Self { config, config_path })
315    }
316    
317    /// Load configuration from file, environment, and defaults
318    fn load_config(config_path: &PathBuf) -> Result<Config> {
319        let figment = Figment::new()
320            // Override with config file if it exists
321            .merge(Toml::file(config_path))
322            // Override with environment variables
323            .merge(Env::prefixed("GIT_WARP_"));
324            
325        figment.extract().map_err(|e| {
326            GitWarpError::ConfigError { 
327                message: format!("Failed to load configuration: {}", e) 
328            }.into()
329        })
330    }
331    
332    /// Get the current configuration
333    pub fn get(&self) -> &Config {
334        &self.config
335    }
336    
337    /// Get a mutable reference to the configuration
338    pub fn get_mut(&mut self) -> &mut Config {
339        &mut self.config
340    }
341    
342    /// Save the configuration to file
343    pub fn save(&self) -> Result<()> {
344        self.save_config(&self.config_path, &self.config)
345    }
346    
347    /// Save configuration to a specific path
348    fn save_config(&self, path: &PathBuf, config: &Config) -> Result<()> {
349        // Create config directory if it doesn't exist
350        if let Some(parent) = path.parent() {
351            fs::create_dir_all(parent)?;
352        }
353        
354        let toml_content = toml::to_string_pretty(config)
355            .map_err(|e| GitWarpError::ConfigError {
356                message: format!("Failed to serialize configuration: {}", e)
357            })?;
358            
359        fs::write(path, toml_content)
360            .map_err(|e| GitWarpError::ConfigError {
361                message: format!("Failed to write config file: {}", e)
362            })?;
363            
364        Ok(())
365    }
366    
367    /// Get the path to the configuration file
368    pub fn config_path(&self) -> &PathBuf {
369        &self.config_path
370    }
371    
372    /// Create a default configuration file
373    pub fn create_default_config(&self) -> Result<()> {
374        let default_config = Config::default();
375        self.save_config(&self.config_path, &default_config)
376    }
377    
378    /// Check if configuration file exists
379    pub fn config_exists(&self) -> bool {
380        self.config_path.exists()
381    }
382    
383    /// Generate and display sample configuration
384    pub fn show_sample_config(&self) {
385        println!("{}", Config::sample_config());
386    }
387}
388
389/// Get the path to the configuration file
390fn get_config_path() -> Result<PathBuf> {
391    let config_dir = config_dir()
392        .ok_or_else(|| GitWarpError::ConfigError {
393            message: "Could not determine config directory".to_string()
394        })?;
395    
396    Ok(config_dir.join("git-warp").join("config.toml"))
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use tempfile::tempdir;
403    
404    #[test]
405    fn test_config_defaults() {
406        let config = Config::default();
407        assert_eq!(config.terminal_mode, "tab");
408        assert_eq!(config.use_cow, true);
409        assert_eq!(config.auto_confirm, false);
410        assert_eq!(config.git.default_branch, "main");
411        assert_eq!(config.process.kill_timeout, 5);
412    }
413    
414    #[test]
415    fn test_config_serialization() {
416        let config = Config::default();
417        let toml_str = toml::to_string(&config).unwrap();
418        let parsed: Config = toml::from_str(&toml_str).unwrap();
419        
420        assert_eq!(config.terminal_mode, parsed.terminal_mode);
421        assert_eq!(config.use_cow, parsed.use_cow);
422        assert_eq!(config.git.default_branch, parsed.git.default_branch);
423    }
424    
425    #[test]
426    fn test_config_manager_creation() {
427        let temp_dir = tempdir().unwrap();
428        
429        // Create config manager (will create default config)
430        let manager = ConfigManager {
431            config: Config::default(),
432            config_path: temp_dir.path().join("config.toml"),
433        };
434        
435        assert_eq!(manager.get().terminal_mode, "tab");
436    }
437    
438    #[test]
439    fn test_config_environment_overrides() {
440        let mut config = Config::default();
441        
442        // Set environment variable
443        unsafe {
444            std::env::set_var("GIT_WARP_TERMINAL_MODE", "window");
445            std::env::set_var("GIT_WARP_AUTO_CONFIRM", "true");
446        }
447        
448        config.apply_env_overrides();
449        
450        assert_eq!(config.terminal_mode, "window");
451        assert_eq!(config.auto_confirm, true);
452        
453        // Clean up
454        unsafe {
455            std::env::remove_var("GIT_WARP_TERMINAL_MODE");
456            std::env::remove_var("GIT_WARP_AUTO_CONFIRM");
457        }
458    }
459    
460    #[test]
461    fn test_sample_config_generation() {
462        let sample = Config::sample_config();
463        assert!(sample.contains("terminal_mode"));
464        assert!(sample.contains("[git]"));
465        assert!(sample.contains("[process]"));
466        assert!(sample.contains("[agent]"));
467    }
468}