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", "token") => self.bitbucket.token = Some(value.to_string()),
170            ("bitbucket", "accept_invalid_certs") => {
171                self.bitbucket.accept_invalid_certs = Some(value.parse().map_err(|_| {
172                    CascadeError::config(format!("Invalid boolean value: {value}"))
173                })?);
174            }
175            ("bitbucket", "ca_bundle_path") => {
176                self.bitbucket.ca_bundle_path = Some(value.to_string());
177            }
178            ("git", "default_branch") => self.git.default_branch = value.to_string(),
179            ("git", "author_name") => self.git.author_name = Some(value.to_string()),
180            ("git", "author_email") => self.git.author_email = Some(value.to_string()),
181            ("git", "auto_cleanup_merged") => {
182                self.git.auto_cleanup_merged = value
183                    .parse()
184                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
185            }
186            ("git", "prefer_rebase") => {
187                self.git.prefer_rebase = value
188                    .parse()
189                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
190            }
191            ("cascade", "api_port") => {
192                self.cascade.api_port = value
193                    .parse()
194                    .map_err(|_| CascadeError::config(format!("Invalid port number: {value}")))?;
195            }
196            ("cascade", "auto_cleanup") => {
197                self.cascade.auto_cleanup = value
198                    .parse()
199                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
200            }
201            ("cascade", "default_sync_strategy") => {
202                self.cascade.default_sync_strategy = value.to_string();
203            }
204            ("cascade", "max_stack_size") => {
205                self.cascade.max_stack_size = value
206                    .parse()
207                    .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
208            }
209            ("cascade", "enable_notifications") => {
210                self.cascade.enable_notifications = value
211                    .parse()
212                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
213            }
214            ("cascade", "pr_description_template") => {
215                self.cascade.pr_description_template = if value.is_empty() {
216                    None
217                } else {
218                    Some(value.to_string())
219                };
220            }
221            ("rebase", "auto_resolve_conflicts") => {
222                self.cascade.rebase.auto_resolve_conflicts = value
223                    .parse()
224                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
225            }
226            ("rebase", "max_retry_attempts") => {
227                self.cascade.rebase.max_retry_attempts = value
228                    .parse()
229                    .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
230            }
231            ("rebase", "preserve_merges") => {
232                self.cascade.rebase.preserve_merges = value
233                    .parse()
234                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
235            }
236            ("rebase", "version_suffix_pattern") => {
237                self.cascade.rebase.version_suffix_pattern = value.to_string();
238            }
239            ("rebase", "backup_before_rebase") => {
240                self.cascade.rebase.backup_before_rebase = value
241                    .parse()
242                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
243            }
244            _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
245        }
246
247        Ok(())
248    }
249
250    /// Get a configuration value by key
251    pub fn get_value(&self, key: &str) -> Result<String> {
252        let parts: Vec<&str> = key.split('.').collect();
253        if parts.len() != 2 {
254            return Err(CascadeError::config(format!(
255                "Invalid config key format: {key}"
256            )));
257        }
258
259        let value = match (parts[0], parts[1]) {
260            ("bitbucket", "url") => &self.bitbucket.url,
261            ("bitbucket", "project") => &self.bitbucket.project,
262            ("bitbucket", "repo") => &self.bitbucket.repo,
263            ("bitbucket", "token") => self.bitbucket.token.as_deref().unwrap_or(""),
264            ("bitbucket", "accept_invalid_certs") => {
265                return Ok(self
266                    .bitbucket
267                    .accept_invalid_certs
268                    .unwrap_or(false)
269                    .to_string())
270            }
271            ("bitbucket", "ca_bundle_path") => {
272                self.bitbucket.ca_bundle_path.as_deref().unwrap_or("")
273            }
274            ("git", "default_branch") => &self.git.default_branch,
275            ("git", "author_name") => self.git.author_name.as_deref().unwrap_or(""),
276            ("git", "author_email") => self.git.author_email.as_deref().unwrap_or(""),
277            ("git", "auto_cleanup_merged") => return Ok(self.git.auto_cleanup_merged.to_string()),
278            ("git", "prefer_rebase") => return Ok(self.git.prefer_rebase.to_string()),
279            ("cascade", "api_port") => return Ok(self.cascade.api_port.to_string()),
280            ("cascade", "auto_cleanup") => return Ok(self.cascade.auto_cleanup.to_string()),
281            ("cascade", "default_sync_strategy") => &self.cascade.default_sync_strategy,
282            ("cascade", "max_stack_size") => return Ok(self.cascade.max_stack_size.to_string()),
283            ("cascade", "enable_notifications") => {
284                return Ok(self.cascade.enable_notifications.to_string())
285            }
286            ("cascade", "pr_description_template") => self
287                .cascade
288                .pr_description_template
289                .as_deref()
290                .unwrap_or(""),
291            ("rebase", "auto_resolve_conflicts") => {
292                return Ok(self.cascade.rebase.auto_resolve_conflicts.to_string())
293            }
294            ("rebase", "max_retry_attempts") => {
295                return Ok(self.cascade.rebase.max_retry_attempts.to_string())
296            }
297            ("rebase", "preserve_merges") => {
298                return Ok(self.cascade.rebase.preserve_merges.to_string())
299            }
300            ("rebase", "version_suffix_pattern") => &self.cascade.rebase.version_suffix_pattern,
301            ("rebase", "backup_before_rebase") => {
302                return Ok(self.cascade.rebase.backup_before_rebase.to_string())
303            }
304            _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
305        };
306
307        Ok(value.to_string())
308    }
309
310    /// Validate the configuration
311    pub fn validate(&self) -> Result<()> {
312        // Validate Bitbucket configuration if provided
313        if !self.bitbucket.url.is_empty()
314            && !self.bitbucket.url.starts_with("http://")
315            && !self.bitbucket.url.starts_with("https://")
316        {
317            return Err(CascadeError::config(
318                "Bitbucket URL must start with http:// or https://",
319            ));
320        }
321
322        // Validate port
323        if self.cascade.api_port == 0 {
324            return Err(CascadeError::config("API port must be between 1 and 65535"));
325        }
326
327        // Validate sync strategy
328        let valid_strategies = [
329            "rebase",
330            "cherry-pick",
331            "branch-versioning",
332            "three-way-merge",
333        ];
334        if !valid_strategies.contains(&self.cascade.default_sync_strategy.as_str()) {
335            return Err(CascadeError::config(format!(
336                "Invalid sync strategy: {}. Valid options: {}",
337                self.cascade.default_sync_strategy,
338                valid_strategies.join(", ")
339            )));
340        }
341
342        Ok(())
343    }
344}