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", "username") => self.bitbucket.username = Some(value.to_string()),
170 ("bitbucket", "token") => self.bitbucket.token = Some(value.to_string()),
171 ("bitbucket", "accept_invalid_certs") => {
172 self.bitbucket.accept_invalid_certs = Some(value.parse().map_err(|_| {
173 CascadeError::config(format!("Invalid boolean value: {value}"))
174 })?);
175 }
176 ("bitbucket", "ca_bundle_path") => {
177 self.bitbucket.ca_bundle_path = Some(value.to_string());
178 }
179 ("git", "default_branch") => self.git.default_branch = value.to_string(),
180 ("git", "author_name") => self.git.author_name = Some(value.to_string()),
181 ("git", "author_email") => self.git.author_email = Some(value.to_string()),
182 ("git", "auto_cleanup_merged") => {
183 self.git.auto_cleanup_merged = value
184 .parse()
185 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
186 }
187 ("git", "prefer_rebase") => {
188 self.git.prefer_rebase = value
189 .parse()
190 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
191 }
192 ("cascade", "api_port") => {
193 self.cascade.api_port = value
194 .parse()
195 .map_err(|_| CascadeError::config(format!("Invalid port number: {value}")))?;
196 }
197 ("cascade", "auto_cleanup") => {
198 self.cascade.auto_cleanup = value
199 .parse()
200 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
201 }
202 ("cascade", "default_sync_strategy") => {
203 self.cascade.default_sync_strategy = value.to_string();
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", "version_suffix_pattern") => {
238 self.cascade.rebase.version_suffix_pattern = value.to_string();
239 }
240 ("rebase", "backup_before_rebase") => {
241 self.cascade.rebase.backup_before_rebase = value
242 .parse()
243 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
244 }
245 _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
246 }
247
248 Ok(())
249 }
250
251 pub fn get_value(&self, key: &str) -> Result<String> {
253 let parts: Vec<&str> = key.split('.').collect();
254 if parts.len() != 2 {
255 return Err(CascadeError::config(format!(
256 "Invalid config key format: {key}"
257 )));
258 }
259
260 let value = match (parts[0], parts[1]) {
261 ("bitbucket", "url") => &self.bitbucket.url,
262 ("bitbucket", "project") => &self.bitbucket.project,
263 ("bitbucket", "repo") => &self.bitbucket.repo,
264 ("bitbucket", "username") => self.bitbucket.username.as_deref().unwrap_or(""),
265 ("bitbucket", "token") => self.bitbucket.token.as_deref().unwrap_or(""),
266 ("bitbucket", "accept_invalid_certs") => {
267 return Ok(self
268 .bitbucket
269 .accept_invalid_certs
270 .unwrap_or(false)
271 .to_string())
272 }
273 ("bitbucket", "ca_bundle_path") => {
274 self.bitbucket.ca_bundle_path.as_deref().unwrap_or("")
275 }
276 ("git", "default_branch") => &self.git.default_branch,
277 ("git", "author_name") => self.git.author_name.as_deref().unwrap_or(""),
278 ("git", "author_email") => self.git.author_email.as_deref().unwrap_or(""),
279 ("git", "auto_cleanup_merged") => return Ok(self.git.auto_cleanup_merged.to_string()),
280 ("git", "prefer_rebase") => return Ok(self.git.prefer_rebase.to_string()),
281 ("cascade", "api_port") => return Ok(self.cascade.api_port.to_string()),
282 ("cascade", "auto_cleanup") => return Ok(self.cascade.auto_cleanup.to_string()),
283 ("cascade", "default_sync_strategy") => &self.cascade.default_sync_strategy,
284 ("cascade", "max_stack_size") => return Ok(self.cascade.max_stack_size.to_string()),
285 ("cascade", "enable_notifications") => {
286 return Ok(self.cascade.enable_notifications.to_string())
287 }
288 ("cascade", "pr_description_template") => self
289 .cascade
290 .pr_description_template
291 .as_deref()
292 .unwrap_or(""),
293 ("rebase", "auto_resolve_conflicts") => {
294 return Ok(self.cascade.rebase.auto_resolve_conflicts.to_string())
295 }
296 ("rebase", "max_retry_attempts") => {
297 return Ok(self.cascade.rebase.max_retry_attempts.to_string())
298 }
299 ("rebase", "preserve_merges") => {
300 return Ok(self.cascade.rebase.preserve_merges.to_string())
301 }
302 ("rebase", "version_suffix_pattern") => &self.cascade.rebase.version_suffix_pattern,
303 ("rebase", "backup_before_rebase") => {
304 return Ok(self.cascade.rebase.backup_before_rebase.to_string())
305 }
306 _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
307 };
308
309 Ok(value.to_string())
310 }
311
312 pub fn validate(&self) -> Result<()> {
314 if !self.bitbucket.url.is_empty()
316 && !self.bitbucket.url.starts_with("http://")
317 && !self.bitbucket.url.starts_with("https://")
318 {
319 return Err(CascadeError::config(
320 "Bitbucket URL must start with http:// or https://",
321 ));
322 }
323
324 if self.cascade.api_port == 0 {
326 return Err(CascadeError::config("API port must be between 1 and 65535"));
327 }
328
329 let valid_strategies = [
331 "rebase",
332 "cherry-pick",
333 "branch-versioning",
334 "three-way-merge",
335 ];
336 if !valid_strategies.contains(&self.cascade.default_sync_strategy.as_str()) {
337 return Err(CascadeError::config(format!(
338 "Invalid sync strategy: {}. Valid options: {}",
339 self.cascade.default_sync_strategy,
340 valid_strategies.join(", ")
341 )));
342 }
343
344 Ok(())
345 }
346}