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