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