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