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 default_sync_strategy: String,
50 pub max_stack_size: usize,
51 pub enable_notifications: bool,
52 pub pr_description_template: Option<String>,
54 pub rebase: RebaseSettings,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct RebaseSettings {
61 pub auto_resolve_conflicts: bool,
63 pub max_retry_attempts: usize,
65 pub preserve_merges: bool,
67 pub version_suffix_pattern: String,
69 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 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 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 pub fn save_to_file(&self, path: &Path) -> Result<()> {
153 crate::utils::atomic_file::write_json(path, self)
154 }
155
156 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 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 pub fn validate(&self) -> Result<()> {
312 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 if self.cascade.api_port == 0 {
324 return Err(CascadeError::config("API port must be between 1 and 65535"));
325 }
326
327 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}