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 pub accept_invalid_certs: Option<bool>,
31 pub ca_bundle_path: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct GitConfig {
37 pub default_branch: String,
38 pub author_name: Option<String>,
39 pub author_email: Option<String>,
40 pub auto_cleanup_merged: bool,
41 pub prefer_rebase: bool,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct CascadeSettings {
46 pub api_port: u16,
47 pub auto_cleanup: bool,
48 pub default_sync_strategy: String,
49 pub max_stack_size: usize,
50 pub enable_notifications: bool,
51 pub rebase: RebaseSettings,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct RebaseSettings {
58 pub auto_resolve_conflicts: bool,
60 pub max_retry_attempts: usize,
62 pub preserve_merges: bool,
64 pub version_suffix_pattern: String,
66 pub backup_before_rebase: bool,
68}
69
70impl Default for BitbucketConfig {
71 fn default() -> Self {
72 Self {
73 url: "https://bitbucket.example.com".to_string(),
74 project: "PROJECT".to_string(),
75 repo: "repo".to_string(),
76 username: None,
77 token: None,
78 default_reviewers: Vec::new(),
79 accept_invalid_certs: None,
80 ca_bundle_path: None,
81 }
82 }
83}
84
85impl Default for GitConfig {
86 fn default() -> Self {
87 Self {
88 default_branch: "main".to_string(),
89 author_name: None,
90 author_email: None,
91 auto_cleanup_merged: true,
92 prefer_rebase: true,
93 }
94 }
95}
96
97impl Default for CascadeSettings {
98 fn default() -> Self {
99 Self {
100 api_port: 8080,
101 auto_cleanup: true,
102 default_sync_strategy: "branch-versioning".to_string(),
103 max_stack_size: 20,
104 enable_notifications: true,
105 rebase: RebaseSettings::default(),
106 }
107 }
108}
109
110impl Default for RebaseSettings {
111 fn default() -> Self {
112 Self {
113 auto_resolve_conflicts: true,
114 max_retry_attempts: 3,
115 preserve_merges: true,
116 version_suffix_pattern: "v{}".to_string(),
117 backup_before_rebase: true,
118 }
119 }
120}
121
122impl Settings {
123 pub fn default_for_repo(bitbucket_url: Option<String>) -> Self {
125 let mut settings = Self::default();
126 if let Some(url) = bitbucket_url {
127 settings.bitbucket.url = url;
128 }
129 settings
130 }
131
132 pub fn load_from_file(path: &Path) -> Result<Self> {
134 if !path.exists() {
135 return Ok(Self::default());
136 }
137
138 let content = fs::read_to_string(path)
139 .map_err(|e| CascadeError::config(format!("Failed to read config file: {e}")))?;
140
141 let settings: Settings = serde_json::from_str(&content)
142 .map_err(|e| CascadeError::config(format!("Failed to parse config file: {e}")))?;
143
144 Ok(settings)
145 }
146
147 pub fn save_to_file(&self, path: &Path) -> Result<()> {
149 crate::utils::atomic_file::write_json(path, self)
150 }
151
152 pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
154 let parts: Vec<&str> = key.split('.').collect();
155 if parts.len() != 2 {
156 return Err(CascadeError::config(format!(
157 "Invalid config key format: {key}"
158 )));
159 }
160
161 match (parts[0], parts[1]) {
162 ("bitbucket", "url") => self.bitbucket.url = value.to_string(),
163 ("bitbucket", "project") => self.bitbucket.project = value.to_string(),
164 ("bitbucket", "repo") => self.bitbucket.repo = value.to_string(),
165 ("bitbucket", "token") => self.bitbucket.token = Some(value.to_string()),
166 ("bitbucket", "accept_invalid_certs") => {
167 self.bitbucket.accept_invalid_certs = Some(value.parse().map_err(|_| {
168 CascadeError::config(format!("Invalid boolean value: {value}"))
169 })?);
170 }
171 ("bitbucket", "ca_bundle_path") => {
172 self.bitbucket.ca_bundle_path = Some(value.to_string());
173 }
174 ("git", "default_branch") => self.git.default_branch = value.to_string(),
175 ("git", "author_name") => self.git.author_name = Some(value.to_string()),
176 ("git", "author_email") => self.git.author_email = Some(value.to_string()),
177 ("git", "auto_cleanup_merged") => {
178 self.git.auto_cleanup_merged = value
179 .parse()
180 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
181 }
182 ("git", "prefer_rebase") => {
183 self.git.prefer_rebase = value
184 .parse()
185 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
186 }
187 ("cascade", "api_port") => {
188 self.cascade.api_port = value
189 .parse()
190 .map_err(|_| CascadeError::config(format!("Invalid port number: {value}")))?;
191 }
192 ("cascade", "auto_cleanup") => {
193 self.cascade.auto_cleanup = value
194 .parse()
195 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
196 }
197 ("cascade", "default_sync_strategy") => {
198 self.cascade.default_sync_strategy = value.to_string();
199 }
200 ("cascade", "max_stack_size") => {
201 self.cascade.max_stack_size = value
202 .parse()
203 .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
204 }
205 ("cascade", "enable_notifications") => {
206 self.cascade.enable_notifications = value
207 .parse()
208 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
209 }
210 ("rebase", "auto_resolve_conflicts") => {
211 self.cascade.rebase.auto_resolve_conflicts = value
212 .parse()
213 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
214 }
215 ("rebase", "max_retry_attempts") => {
216 self.cascade.rebase.max_retry_attempts = value
217 .parse()
218 .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
219 }
220 ("rebase", "preserve_merges") => {
221 self.cascade.rebase.preserve_merges = value
222 .parse()
223 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
224 }
225 ("rebase", "version_suffix_pattern") => {
226 self.cascade.rebase.version_suffix_pattern = value.to_string();
227 }
228 ("rebase", "backup_before_rebase") => {
229 self.cascade.rebase.backup_before_rebase = value
230 .parse()
231 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
232 }
233 _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
234 }
235
236 Ok(())
237 }
238
239 pub fn get_value(&self, key: &str) -> Result<String> {
241 let parts: Vec<&str> = key.split('.').collect();
242 if parts.len() != 2 {
243 return Err(CascadeError::config(format!(
244 "Invalid config key format: {key}"
245 )));
246 }
247
248 let value = match (parts[0], parts[1]) {
249 ("bitbucket", "url") => &self.bitbucket.url,
250 ("bitbucket", "project") => &self.bitbucket.project,
251 ("bitbucket", "repo") => &self.bitbucket.repo,
252 ("bitbucket", "token") => self.bitbucket.token.as_deref().unwrap_or(""),
253 ("bitbucket", "accept_invalid_certs") => {
254 return Ok(self
255 .bitbucket
256 .accept_invalid_certs
257 .unwrap_or(false)
258 .to_string())
259 }
260 ("bitbucket", "ca_bundle_path") => {
261 self.bitbucket.ca_bundle_path.as_deref().unwrap_or("")
262 }
263 ("git", "default_branch") => &self.git.default_branch,
264 ("git", "author_name") => self.git.author_name.as_deref().unwrap_or(""),
265 ("git", "author_email") => self.git.author_email.as_deref().unwrap_or(""),
266 ("git", "auto_cleanup_merged") => return Ok(self.git.auto_cleanup_merged.to_string()),
267 ("git", "prefer_rebase") => return Ok(self.git.prefer_rebase.to_string()),
268 ("cascade", "api_port") => return Ok(self.cascade.api_port.to_string()),
269 ("cascade", "auto_cleanup") => return Ok(self.cascade.auto_cleanup.to_string()),
270 ("cascade", "default_sync_strategy") => &self.cascade.default_sync_strategy,
271 ("cascade", "max_stack_size") => return Ok(self.cascade.max_stack_size.to_string()),
272 ("cascade", "enable_notifications") => {
273 return Ok(self.cascade.enable_notifications.to_string())
274 }
275 ("rebase", "auto_resolve_conflicts") => {
276 return Ok(self.cascade.rebase.auto_resolve_conflicts.to_string())
277 }
278 ("rebase", "max_retry_attempts") => {
279 return Ok(self.cascade.rebase.max_retry_attempts.to_string())
280 }
281 ("rebase", "preserve_merges") => {
282 return Ok(self.cascade.rebase.preserve_merges.to_string())
283 }
284 ("rebase", "version_suffix_pattern") => &self.cascade.rebase.version_suffix_pattern,
285 ("rebase", "backup_before_rebase") => {
286 return Ok(self.cascade.rebase.backup_before_rebase.to_string())
287 }
288 _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
289 };
290
291 Ok(value.to_string())
292 }
293
294 pub fn validate(&self) -> Result<()> {
296 if !self.bitbucket.url.is_empty()
298 && !self.bitbucket.url.starts_with("http://")
299 && !self.bitbucket.url.starts_with("https://")
300 {
301 return Err(CascadeError::config(
302 "Bitbucket URL must start with http:// or https://",
303 ));
304 }
305
306 if self.cascade.api_port == 0 {
308 return Err(CascadeError::config("API port must be between 1 and 65535"));
309 }
310
311 let valid_strategies = [
313 "rebase",
314 "cherry-pick",
315 "branch-versioning",
316 "three-way-merge",
317 ];
318 if !valid_strategies.contains(&self.cascade.default_sync_strategy.as_str()) {
319 return Err(CascadeError::config(format!(
320 "Invalid sync strategy: {}. Valid options: {}",
321 self.cascade.default_sync_strategy,
322 valid_strategies.join(", ")
323 )));
324 }
325
326 Ok(())
327 }
328}