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 atomically
142    pub fn save_to_file(&self, path: &Path) -> Result<()> {
143        crate::utils::atomic_file::write_json(path, self)
144    }
145
146    /// Update a configuration value by key
147    pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
148        let parts: Vec<&str> = key.split('.').collect();
149        if parts.len() != 2 {
150            return Err(CascadeError::config(format!(
151                "Invalid config key format: {key}"
152            )));
153        }
154
155        match (parts[0], parts[1]) {
156            ("bitbucket", "url") => self.bitbucket.url = value.to_string(),
157            ("bitbucket", "project") => self.bitbucket.project = value.to_string(),
158            ("bitbucket", "repo") => self.bitbucket.repo = value.to_string(),
159            ("bitbucket", "token") => self.bitbucket.token = Some(value.to_string()),
160            ("git", "default_branch") => self.git.default_branch = value.to_string(),
161            ("git", "author_name") => self.git.author_name = Some(value.to_string()),
162            ("git", "author_email") => self.git.author_email = Some(value.to_string()),
163            ("git", "auto_cleanup_merged") => {
164                self.git.auto_cleanup_merged = value
165                    .parse()
166                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
167            }
168            ("git", "prefer_rebase") => {
169                self.git.prefer_rebase = value
170                    .parse()
171                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
172            }
173            ("cascade", "api_port") => {
174                self.cascade.api_port = value
175                    .parse()
176                    .map_err(|_| CascadeError::config(format!("Invalid port number: {value}")))?;
177            }
178            ("cascade", "auto_cleanup") => {
179                self.cascade.auto_cleanup = value
180                    .parse()
181                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
182            }
183            ("cascade", "default_sync_strategy") => {
184                self.cascade.default_sync_strategy = value.to_string();
185            }
186            ("cascade", "max_stack_size") => {
187                self.cascade.max_stack_size = value
188                    .parse()
189                    .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
190            }
191            ("cascade", "enable_notifications") => {
192                self.cascade.enable_notifications = value
193                    .parse()
194                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
195            }
196            ("rebase", "auto_resolve_conflicts") => {
197                self.cascade.rebase.auto_resolve_conflicts = value
198                    .parse()
199                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
200            }
201            ("rebase", "max_retry_attempts") => {
202                self.cascade.rebase.max_retry_attempts = value
203                    .parse()
204                    .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
205            }
206            ("rebase", "preserve_merges") => {
207                self.cascade.rebase.preserve_merges = value
208                    .parse()
209                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
210            }
211            ("rebase", "version_suffix_pattern") => {
212                self.cascade.rebase.version_suffix_pattern = value.to_string();
213            }
214            ("rebase", "backup_before_rebase") => {
215                self.cascade.rebase.backup_before_rebase = value
216                    .parse()
217                    .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
218            }
219            _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
220        }
221
222        Ok(())
223    }
224
225    /// Get a configuration value by key
226    pub fn get_value(&self, key: &str) -> Result<String> {
227        let parts: Vec<&str> = key.split('.').collect();
228        if parts.len() != 2 {
229            return Err(CascadeError::config(format!(
230                "Invalid config key format: {key}"
231            )));
232        }
233
234        let value = match (parts[0], parts[1]) {
235            ("bitbucket", "url") => &self.bitbucket.url,
236            ("bitbucket", "project") => &self.bitbucket.project,
237            ("bitbucket", "repo") => &self.bitbucket.repo,
238            ("bitbucket", "token") => self.bitbucket.token.as_deref().unwrap_or(""),
239            ("git", "default_branch") => &self.git.default_branch,
240            ("git", "author_name") => self.git.author_name.as_deref().unwrap_or(""),
241            ("git", "author_email") => self.git.author_email.as_deref().unwrap_or(""),
242            ("git", "auto_cleanup_merged") => return Ok(self.git.auto_cleanup_merged.to_string()),
243            ("git", "prefer_rebase") => return Ok(self.git.prefer_rebase.to_string()),
244            ("cascade", "api_port") => return Ok(self.cascade.api_port.to_string()),
245            ("cascade", "auto_cleanup") => return Ok(self.cascade.auto_cleanup.to_string()),
246            ("cascade", "default_sync_strategy") => &self.cascade.default_sync_strategy,
247            ("cascade", "max_stack_size") => return Ok(self.cascade.max_stack_size.to_string()),
248            ("cascade", "enable_notifications") => {
249                return Ok(self.cascade.enable_notifications.to_string())
250            }
251            ("rebase", "auto_resolve_conflicts") => {
252                return Ok(self.cascade.rebase.auto_resolve_conflicts.to_string())
253            }
254            ("rebase", "max_retry_attempts") => {
255                return Ok(self.cascade.rebase.max_retry_attempts.to_string())
256            }
257            ("rebase", "preserve_merges") => {
258                return Ok(self.cascade.rebase.preserve_merges.to_string())
259            }
260            ("rebase", "version_suffix_pattern") => &self.cascade.rebase.version_suffix_pattern,
261            ("rebase", "backup_before_rebase") => {
262                return Ok(self.cascade.rebase.backup_before_rebase.to_string())
263            }
264            _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
265        };
266
267        Ok(value.to_string())
268    }
269
270    /// Validate the configuration
271    pub fn validate(&self) -> Result<()> {
272        // Validate Bitbucket configuration if provided
273        if !self.bitbucket.url.is_empty()
274            && !self.bitbucket.url.starts_with("http://")
275            && !self.bitbucket.url.starts_with("https://")
276        {
277            return Err(CascadeError::config(
278                "Bitbucket URL must start with http:// or https://",
279            ));
280        }
281
282        // Validate port
283        if self.cascade.api_port == 0 {
284            return Err(CascadeError::config("API port must be between 1 and 65535"));
285        }
286
287        // Validate sync strategy
288        let valid_strategies = [
289            "rebase",
290            "cherry-pick",
291            "branch-versioning",
292            "three-way-merge",
293        ];
294        if !valid_strategies.contains(&self.cascade.default_sync_strategy.as_str()) {
295            return Err(CascadeError::config(format!(
296                "Invalid sync strategy: {}. Valid options: {}",
297                self.cascade.default_sync_strategy,
298                valid_strategies.join(", ")
299            )));
300        }
301
302        Ok(())
303    }
304}