cascade_cli/config/
settings.rs

1use crate::config::auth::AuthConfig;
2use crate::errors::{CascadeError, Result};
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct CascadeConfig {
9    pub bitbucket: Option<BitbucketConfig>,
10    pub git: GitConfig,
11    pub auth: AuthConfig,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, Default)]
15pub struct Settings {
16    pub bitbucket: BitbucketConfig,
17    pub git: GitConfig,
18    pub cascade: CascadeSettings,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct BitbucketConfig {
23    pub url: String,
24    pub project: String,
25    pub repo: String,
26    pub username: Option<String>,
27    pub token: Option<String>,
28    pub default_reviewers: Vec<String>,
29    /// Accept invalid TLS certificates (development only)
30    pub accept_invalid_certs: Option<bool>,
31    /// Path to custom CA certificate bundle
32    pub ca_bundle_path: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct GitConfig {
37    pub default_branch: String,
38    pub author_name: Option<String>,
39    pub author_email: Option<String>,
40    pub auto_cleanup_merged: bool,
41    pub prefer_rebase: bool,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct CascadeSettings {
46    pub api_port: u16,
47    pub auto_cleanup: bool,
48    pub default_sync_strategy: String,
49    pub max_stack_size: usize,
50    pub enable_notifications: bool,
51    /// Rebase-specific settings
52    pub rebase: RebaseSettings,
53}
54
55/// Settings specific to rebase operations
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct RebaseSettings {
58    /// Whether to auto-resolve simple conflicts
59    pub auto_resolve_conflicts: bool,
60    /// Maximum number of retry attempts for rebase operations
61    pub max_retry_attempts: usize,
62    /// Whether to preserve merge commits during rebase
63    pub preserve_merges: bool,
64    /// Default branch versioning suffix pattern
65    pub version_suffix_pattern: String,
66    /// Whether to backup branches before rebasing
67    pub backup_before_rebase: bool,
68}
69
70impl Default for BitbucketConfig {
71    fn default() -> Self {
72        Self {
73            url: "https://bitbucket.example.com".to_string(),
74            project: "PROJECT".to_string(),
75            repo: "repo".to_string(),
76            username: None,
77            token: None,
78            default_reviewers: Vec::new(),
79            accept_invalid_certs: None,
80            ca_bundle_path: None,
81        }
82    }
83}
84
85impl Default for GitConfig {
86    fn default() -> Self {
87        Self {
88            default_branch: "main".to_string(),
89            author_name: None,
90            author_email: None,
91            auto_cleanup_merged: true,
92            prefer_rebase: true,
93        }
94    }
95}
96
97impl Default for CascadeSettings {
98    fn default() -> Self {
99        Self {
100            api_port: 8080,
101            auto_cleanup: true,
102            default_sync_strategy: "branch-versioning".to_string(),
103            max_stack_size: 20,
104            enable_notifications: true,
105            rebase: RebaseSettings::default(),
106        }
107    }
108}
109
110impl Default for RebaseSettings {
111    fn default() -> Self {
112        Self {
113            auto_resolve_conflicts: true,
114            max_retry_attempts: 3,
115            preserve_merges: true,
116            version_suffix_pattern: "v{}".to_string(),
117            backup_before_rebase: true,
118        }
119    }
120}
121
122impl Settings {
123    /// Create default settings for a repository
124    pub fn default_for_repo(bitbucket_url: Option<String>) -> Self {
125        let mut settings = Self::default();
126        if let Some(url) = bitbucket_url {
127            settings.bitbucket.url = url;
128        }
129        settings
130    }
131
132    /// Load settings from a file
133    pub fn load_from_file(path: &Path) -> Result<Self> {
134        if !path.exists() {
135            return Ok(Self::default());
136        }
137
138        let content = fs::read_to_string(path)
139            .map_err(|e| CascadeError::config(format!("Failed to read config file: {e}")))?;
140
141        let settings: Settings = serde_json::from_str(&content)
142            .map_err(|e| CascadeError::config(format!("Failed to parse config file: {e}")))?;
143
144        Ok(settings)
145    }
146
147    /// Save settings to a file atomically
148    pub fn save_to_file(&self, path: &Path) -> Result<()> {
149        crate::utils::atomic_file::write_json(path, self)
150    }
151
152    /// Update a configuration value by key
153    pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
154        let parts: Vec<&str> = key.split('.').collect();
155        if parts.len() != 2 {
156            return Err(CascadeError::config(format!(
157                "Invalid config key format: {key}"
158            )));
159        }
160
161        match (parts[0], parts[1]) {
162            ("bitbucket", "url") => self.bitbucket.url = value.to_string(),
163            ("bitbucket", "project") => self.bitbucket.project = value.to_string(),
164            ("bitbucket", "repo") => self.bitbucket.repo = value.to_string(),
165            ("bitbucket", "token") => self.bitbucket.token = Some(value.to_string()),
166            ("bitbucket", "accept_invalid_certs") => {
167                self.bitbucket.accept_invalid_certs = Some(value.parse().map_err(|_| {
168                    CascadeError::config(format!("Invalid boolean value: {value}"))
169                })?);
170            }
171            ("bitbucket", "ca_bundle_path") => {
172                self.bitbucket.ca_bundle_path = Some(value.to_string());
173            }
174            ("git", "default_branch") => self.git.default_branch = value.to_string(),
175            ("git", "author_name") => self.git.author_name = Some(value.to_string()),
176            ("git", "author_email") => self.git.author_email = Some(value.to_string()),
177            ("git", "auto_cleanup_merged") => {
178                self.git.auto_cleanup_merged = value
179                    .parse()
180                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
181            }
182            ("git", "prefer_rebase") => {
183                self.git.prefer_rebase = value
184                    .parse()
185                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
186            }
187            ("cascade", "api_port") => {
188                self.cascade.api_port = value
189                    .parse()
190                    .map_err(|_| CascadeError::config(format!("Invalid port number: {value}")))?;
191            }
192            ("cascade", "auto_cleanup") => {
193                self.cascade.auto_cleanup = value
194                    .parse()
195                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
196            }
197            ("cascade", "default_sync_strategy") => {
198                self.cascade.default_sync_strategy = value.to_string();
199            }
200            ("cascade", "max_stack_size") => {
201                self.cascade.max_stack_size = value
202                    .parse()
203                    .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
204            }
205            ("cascade", "enable_notifications") => {
206                self.cascade.enable_notifications = value
207                    .parse()
208                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
209            }
210            ("rebase", "auto_resolve_conflicts") => {
211                self.cascade.rebase.auto_resolve_conflicts = value
212                    .parse()
213                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
214            }
215            ("rebase", "max_retry_attempts") => {
216                self.cascade.rebase.max_retry_attempts = value
217                    .parse()
218                    .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
219            }
220            ("rebase", "preserve_merges") => {
221                self.cascade.rebase.preserve_merges = value
222                    .parse()
223                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
224            }
225            ("rebase", "version_suffix_pattern") => {
226                self.cascade.rebase.version_suffix_pattern = value.to_string();
227            }
228            ("rebase", "backup_before_rebase") => {
229                self.cascade.rebase.backup_before_rebase = value
230                    .parse()
231                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
232            }
233            _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
234        }
235
236        Ok(())
237    }
238
239    /// Get a configuration value by key
240    pub fn get_value(&self, key: &str) -> Result<String> {
241        let parts: Vec<&str> = key.split('.').collect();
242        if parts.len() != 2 {
243            return Err(CascadeError::config(format!(
244                "Invalid config key format: {key}"
245            )));
246        }
247
248        let value = match (parts[0], parts[1]) {
249            ("bitbucket", "url") => &self.bitbucket.url,
250            ("bitbucket", "project") => &self.bitbucket.project,
251            ("bitbucket", "repo") => &self.bitbucket.repo,
252            ("bitbucket", "token") => self.bitbucket.token.as_deref().unwrap_or(""),
253            ("bitbucket", "accept_invalid_certs") => {
254                return Ok(self
255                    .bitbucket
256                    .accept_invalid_certs
257                    .unwrap_or(false)
258                    .to_string())
259            }
260            ("bitbucket", "ca_bundle_path") => {
261                self.bitbucket.ca_bundle_path.as_deref().unwrap_or("")
262            }
263            ("git", "default_branch") => &self.git.default_branch,
264            ("git", "author_name") => self.git.author_name.as_deref().unwrap_or(""),
265            ("git", "author_email") => self.git.author_email.as_deref().unwrap_or(""),
266            ("git", "auto_cleanup_merged") => return Ok(self.git.auto_cleanup_merged.to_string()),
267            ("git", "prefer_rebase") => return Ok(self.git.prefer_rebase.to_string()),
268            ("cascade", "api_port") => return Ok(self.cascade.api_port.to_string()),
269            ("cascade", "auto_cleanup") => return Ok(self.cascade.auto_cleanup.to_string()),
270            ("cascade", "default_sync_strategy") => &self.cascade.default_sync_strategy,
271            ("cascade", "max_stack_size") => return Ok(self.cascade.max_stack_size.to_string()),
272            ("cascade", "enable_notifications") => {
273                return Ok(self.cascade.enable_notifications.to_string())
274            }
275            ("rebase", "auto_resolve_conflicts") => {
276                return Ok(self.cascade.rebase.auto_resolve_conflicts.to_string())
277            }
278            ("rebase", "max_retry_attempts") => {
279                return Ok(self.cascade.rebase.max_retry_attempts.to_string())
280            }
281            ("rebase", "preserve_merges") => {
282                return Ok(self.cascade.rebase.preserve_merges.to_string())
283            }
284            ("rebase", "version_suffix_pattern") => &self.cascade.rebase.version_suffix_pattern,
285            ("rebase", "backup_before_rebase") => {
286                return Ok(self.cascade.rebase.backup_before_rebase.to_string())
287            }
288            _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
289        };
290
291        Ok(value.to_string())
292    }
293
294    /// Validate the configuration
295    pub fn validate(&self) -> Result<()> {
296        // Validate Bitbucket configuration if provided
297        if !self.bitbucket.url.is_empty()
298            && !self.bitbucket.url.starts_with("http://")
299            && !self.bitbucket.url.starts_with("https://")
300        {
301            return Err(CascadeError::config(
302                "Bitbucket URL must start with http:// or https://",
303            ));
304        }
305
306        // Validate port
307        if self.cascade.api_port == 0 {
308            return Err(CascadeError::config("API port must be between 1 and 65535"));
309        }
310
311        // Validate sync strategy
312        let valid_strategies = [
313            "rebase",
314            "cherry-pick",
315            "branch-versioning",
316            "three-way-merge",
317        ];
318        if !valid_strategies.contains(&self.cascade.default_sync_strategy.as_str()) {
319            return Err(CascadeError::config(format!(
320                "Invalid sync strategy: {}. Valid options: {}",
321                self.cascade.default_sync_strategy,
322                valid_strategies.join(", ")
323            )));
324        }
325
326        Ok(())
327    }
328}