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 pub accept_invalid_certs: Option<bool>,
32 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 max_stack_size: usize,
50 pub enable_notifications: bool,
51 pub pr_description_template: Option<String>,
53 #[serde(default)]
57 pub advisory_merge_checks: Vec<String>,
58 pub rebase: RebaseSettings,
60 #[serde(default, skip_serializing)]
62 pub default_sync_strategy: Option<String>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct RebaseSettings {
68 pub auto_resolve_conflicts: bool,
70 pub max_retry_attempts: usize,
72 pub preserve_merges: bool,
74 pub backup_before_rebase: bool,
76 #[serde(default, skip_serializing)]
78 pub version_suffix_pattern: Option<String>,
79}
80
81impl Default for BitbucketConfig {
82 fn default() -> Self {
83 Self {
84 url: "https://bitbucket.example.com".to_string(),
85 project: "PROJECT".to_string(),
86 repo: "repo".to_string(),
87 username: None,
88 token: None,
89 default_reviewers: Vec::new(),
90 accept_invalid_certs: None,
91 ca_bundle_path: None,
92 }
93 }
94}
95
96impl Default for GitConfig {
97 fn default() -> Self {
98 Self {
99 default_branch: "main".to_string(),
100 author_name: None,
101 author_email: None,
102 auto_cleanup_merged: true,
103 prefer_rebase: true,
104 }
105 }
106}
107
108impl Default for CascadeSettings {
109 fn default() -> Self {
110 Self {
111 api_port: 8080,
112 auto_cleanup: true,
113 max_stack_size: 20,
114 enable_notifications: true,
115 pr_description_template: None,
116 advisory_merge_checks: Vec::new(),
117 rebase: RebaseSettings::default(),
118 default_sync_strategy: None, }
120 }
121}
122
123impl Default for RebaseSettings {
124 fn default() -> Self {
125 Self {
126 auto_resolve_conflicts: true,
127 max_retry_attempts: 3,
128 preserve_merges: true,
129 backup_before_rebase: true,
130 version_suffix_pattern: None, }
132 }
133}
134
135impl Settings {
136 pub fn default_for_repo(bitbucket_url: Option<String>) -> Self {
138 let mut settings = Self::default();
139 if let Some(url) = bitbucket_url {
140 settings.bitbucket.url = url;
141 }
142 settings
143 }
144
145 pub fn load_from_file(path: &Path) -> Result<Self> {
147 if !path.exists() {
148 return Ok(Self::default());
149 }
150
151 let content = fs::read_to_string(path)
152 .map_err(|e| CascadeError::config(format!("Failed to read config file: {e}")))?;
153
154 let settings: Settings = serde_json::from_str(&content)
155 .map_err(|e| CascadeError::config(format!("Failed to parse config file: {e}")))?;
156
157 Ok(settings)
158 }
159
160 pub fn save_to_file(&self, path: &Path) -> Result<()> {
162 crate::utils::atomic_file::write_json(path, self)
163 }
164
165 pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
167 let parts: Vec<&str> = key.split('.').collect();
168 if parts.len() != 2 {
169 return Err(CascadeError::config(format!(
170 "Invalid config key format: {key}"
171 )));
172 }
173
174 match (parts[0], parts[1]) {
175 ("bitbucket", "url") => self.bitbucket.url = value.to_string(),
176 ("bitbucket", "project") => self.bitbucket.project = value.to_string(),
177 ("bitbucket", "repo") => self.bitbucket.repo = value.to_string(),
178 ("bitbucket", "username") => self.bitbucket.username = Some(value.to_string()),
179 ("bitbucket", "token") => self.bitbucket.token = Some(value.to_string()),
180 ("bitbucket", "accept_invalid_certs") => {
181 self.bitbucket.accept_invalid_certs = Some(value.parse().map_err(|_| {
182 CascadeError::config(format!("Invalid boolean value: {value}"))
183 })?);
184 }
185 ("bitbucket", "ca_bundle_path") => {
186 self.bitbucket.ca_bundle_path = Some(value.to_string());
187 }
188 ("git", "default_branch") => self.git.default_branch = value.to_string(),
189 ("git", "author_name") => self.git.author_name = Some(value.to_string()),
190 ("git", "author_email") => self.git.author_email = Some(value.to_string()),
191 ("git", "auto_cleanup_merged") => {
192 self.git.auto_cleanup_merged = value
193 .parse()
194 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
195 }
196 ("git", "prefer_rebase") => {
197 self.git.prefer_rebase = value
198 .parse()
199 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
200 }
201 ("cascade", "api_port") => {
202 self.cascade.api_port = value
203 .parse()
204 .map_err(|_| CascadeError::config(format!("Invalid port number: {value}")))?;
205 }
206 ("cascade", "auto_cleanup") => {
207 self.cascade.auto_cleanup = value
208 .parse()
209 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
210 }
211 ("cascade", "max_stack_size") => {
212 self.cascade.max_stack_size = value
213 .parse()
214 .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
215 }
216 ("cascade", "enable_notifications") => {
217 self.cascade.enable_notifications = value
218 .parse()
219 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
220 }
221 ("cascade", "pr_description_template") => {
222 self.cascade.pr_description_template = if value.is_empty() {
223 None
224 } else {
225 Some(value.to_string())
226 };
227 }
228 ("cascade", "advisory_merge_checks") => {
229 if value.is_empty() {
230 self.cascade.advisory_merge_checks = Vec::new();
231 } else {
232 if let Ok(parsed) = serde_json::from_str::<Vec<String>>(value) {
234 self.cascade.advisory_merge_checks = parsed;
235 } else {
236 self.cascade.advisory_merge_checks =
237 value.split(',').map(|s| s.trim().to_string()).collect();
238 }
239 }
240 }
241 ("rebase", "auto_resolve_conflicts") => {
242 self.cascade.rebase.auto_resolve_conflicts = value
243 .parse()
244 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
245 }
246 ("rebase", "max_retry_attempts") => {
247 self.cascade.rebase.max_retry_attempts = value
248 .parse()
249 .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
250 }
251 ("rebase", "preserve_merges") => {
252 self.cascade.rebase.preserve_merges = value
253 .parse()
254 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
255 }
256 ("rebase", "backup_before_rebase") => {
257 self.cascade.rebase.backup_before_rebase = value
258 .parse()
259 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
260 }
261 _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
262 }
263
264 Ok(())
265 }
266
267 pub fn get_value(&self, key: &str) -> Result<String> {
269 let parts: Vec<&str> = key.split('.').collect();
270 if parts.len() != 2 {
271 return Err(CascadeError::config(format!(
272 "Invalid config key format: {key}"
273 )));
274 }
275
276 let value = match (parts[0], parts[1]) {
277 ("bitbucket", "url") => &self.bitbucket.url,
278 ("bitbucket", "project") => &self.bitbucket.project,
279 ("bitbucket", "repo") => &self.bitbucket.repo,
280 ("bitbucket", "username") => self.bitbucket.username.as_deref().unwrap_or(""),
281 ("bitbucket", "token") => self.bitbucket.token.as_deref().unwrap_or(""),
282 ("bitbucket", "accept_invalid_certs") => {
283 return Ok(self
284 .bitbucket
285 .accept_invalid_certs
286 .unwrap_or(false)
287 .to_string())
288 }
289 ("bitbucket", "ca_bundle_path") => {
290 self.bitbucket.ca_bundle_path.as_deref().unwrap_or("")
291 }
292 ("git", "default_branch") => &self.git.default_branch,
293 ("git", "author_name") => self.git.author_name.as_deref().unwrap_or(""),
294 ("git", "author_email") => self.git.author_email.as_deref().unwrap_or(""),
295 ("git", "auto_cleanup_merged") => return Ok(self.git.auto_cleanup_merged.to_string()),
296 ("git", "prefer_rebase") => return Ok(self.git.prefer_rebase.to_string()),
297 ("cascade", "api_port") => return Ok(self.cascade.api_port.to_string()),
298 ("cascade", "auto_cleanup") => return Ok(self.cascade.auto_cleanup.to_string()),
299 ("cascade", "max_stack_size") => return Ok(self.cascade.max_stack_size.to_string()),
300 ("cascade", "enable_notifications") => {
301 return Ok(self.cascade.enable_notifications.to_string())
302 }
303 ("cascade", "pr_description_template") => self
304 .cascade
305 .pr_description_template
306 .as_deref()
307 .unwrap_or(""),
308 ("cascade", "advisory_merge_checks") => {
309 return Ok(serde_json::to_string(&self.cascade.advisory_merge_checks)
310 .unwrap_or_else(|_| "[]".to_string()))
311 }
312 ("rebase", "auto_resolve_conflicts") => {
313 return Ok(self.cascade.rebase.auto_resolve_conflicts.to_string())
314 }
315 ("rebase", "max_retry_attempts") => {
316 return Ok(self.cascade.rebase.max_retry_attempts.to_string())
317 }
318 ("rebase", "preserve_merges") => {
319 return Ok(self.cascade.rebase.preserve_merges.to_string())
320 }
321 ("rebase", "backup_before_rebase") => {
322 return Ok(self.cascade.rebase.backup_before_rebase.to_string())
323 }
324 _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
325 };
326
327 Ok(value.to_string())
328 }
329
330 pub fn validate(&self) -> Result<()> {
332 if !self.bitbucket.url.is_empty()
334 && !self.bitbucket.url.starts_with("http://")
335 && !self.bitbucket.url.starts_with("https://")
336 {
337 return Err(CascadeError::config(
338 "Bitbucket URL must start with http:// or https://",
339 ));
340 }
341
342 if self.cascade.api_port == 0 {
344 return Err(CascadeError::config("API port must be between 1 and 65535"));
345 }
346
347 Ok(())
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_backward_compatibility_with_old_config_format() {
360 let old_config_json = r#"{
362 "bitbucket": {
363 "url": "https://bitbucket.example.com",
364 "project": "TEST",
365 "repo": "test-repo",
366 "username": null,
367 "token": null,
368 "default_reviewers": [],
369 "accept_invalid_certs": null,
370 "ca_bundle_path": null
371 },
372 "git": {
373 "default_branch": "main",
374 "author_name": null,
375 "author_email": null,
376 "auto_cleanup_merged": true,
377 "prefer_rebase": true
378 },
379 "cascade": {
380 "api_port": 8080,
381 "auto_cleanup": true,
382 "default_sync_strategy": "branch-versioning",
383 "max_stack_size": 20,
384 "enable_notifications": true,
385 "pr_description_template": null,
386 "rebase": {
387 "auto_resolve_conflicts": true,
388 "max_retry_attempts": 3,
389 "preserve_merges": true,
390 "version_suffix_pattern": "v{}",
391 "backup_before_rebase": true
392 }
393 }
394 }"#;
395
396 let settings: Settings = serde_json::from_str(old_config_json)
398 .expect("Failed to parse old config format - backward compatibility broken!");
399
400 assert_eq!(settings.cascade.api_port, 8080);
402 assert!(settings.cascade.auto_cleanup);
403 assert_eq!(settings.cascade.max_stack_size, 20);
404
405 assert_eq!(
407 settings.cascade.default_sync_strategy,
408 Some("branch-versioning".to_string())
409 );
410 assert_eq!(
411 settings.cascade.rebase.version_suffix_pattern,
412 Some("v{}".to_string())
413 );
414
415 let new_json =
417 serde_json::to_string_pretty(&settings).expect("Failed to serialize settings");
418
419 assert!(
420 !new_json.contains("default_sync_strategy"),
421 "Deprecated field should not appear in new config files"
422 );
423 assert!(
424 !new_json.contains("version_suffix_pattern"),
425 "Deprecated field should not appear in new config files"
426 );
427 }
428
429 #[test]
430 fn test_new_config_format_without_deprecated_fields() {
431 let new_config_json = r#"{
433 "bitbucket": {
434 "url": "https://bitbucket.example.com",
435 "project": "TEST",
436 "repo": "test-repo",
437 "username": null,
438 "token": null,
439 "default_reviewers": [],
440 "accept_invalid_certs": null,
441 "ca_bundle_path": null
442 },
443 "git": {
444 "default_branch": "main",
445 "author_name": null,
446 "author_email": null,
447 "auto_cleanup_merged": true,
448 "prefer_rebase": true
449 },
450 "cascade": {
451 "api_port": 8080,
452 "auto_cleanup": true,
453 "max_stack_size": 20,
454 "enable_notifications": true,
455 "pr_description_template": null,
456 "rebase": {
457 "auto_resolve_conflicts": true,
458 "max_retry_attempts": 3,
459 "preserve_merges": true,
460 "backup_before_rebase": true
461 }
462 }
463 }"#;
464
465 let settings: Settings =
467 serde_json::from_str(new_config_json).expect("Failed to parse new config format!");
468
469 assert_eq!(settings.cascade.default_sync_strategy, None);
471 assert_eq!(settings.cascade.rebase.version_suffix_pattern, None);
472 }
473}