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