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 crate::utils::atomic_file::write_json(path, self)
144 }
145
146 pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
148 let parts: Vec<&str> = key.split('.').collect();
149 if parts.len() != 2 {
150 return Err(CascadeError::config(format!(
151 "Invalid config key format: {key}"
152 )));
153 }
154
155 match (parts[0], parts[1]) {
156 ("bitbucket", "url") => self.bitbucket.url = value.to_string(),
157 ("bitbucket", "project") => self.bitbucket.project = value.to_string(),
158 ("bitbucket", "repo") => self.bitbucket.repo = value.to_string(),
159 ("bitbucket", "token") => self.bitbucket.token = Some(value.to_string()),
160 ("git", "default_branch") => self.git.default_branch = value.to_string(),
161 ("git", "author_name") => self.git.author_name = Some(value.to_string()),
162 ("git", "author_email") => self.git.author_email = Some(value.to_string()),
163 ("git", "auto_cleanup_merged") => {
164 self.git.auto_cleanup_merged = value
165 .parse()
166 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
167 }
168 ("git", "prefer_rebase") => {
169 self.git.prefer_rebase = value
170 .parse()
171 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
172 }
173 ("cascade", "api_port") => {
174 self.cascade.api_port = value
175 .parse()
176 .map_err(|_| CascadeError::config(format!("Invalid port number: {value}")))?;
177 }
178 ("cascade", "auto_cleanup") => {
179 self.cascade.auto_cleanup = value
180 .parse()
181 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
182 }
183 ("cascade", "default_sync_strategy") => {
184 self.cascade.default_sync_strategy = value.to_string();
185 }
186 ("cascade", "max_stack_size") => {
187 self.cascade.max_stack_size = value
188 .parse()
189 .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
190 }
191 ("cascade", "enable_notifications") => {
192 self.cascade.enable_notifications = value
193 .parse()
194 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
195 }
196 ("rebase", "auto_resolve_conflicts") => {
197 self.cascade.rebase.auto_resolve_conflicts = value
198 .parse()
199 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
200 }
201 ("rebase", "max_retry_attempts") => {
202 self.cascade.rebase.max_retry_attempts = value
203 .parse()
204 .map_err(|_| CascadeError::config(format!("Invalid number: {value}")))?;
205 }
206 ("rebase", "preserve_merges") => {
207 self.cascade.rebase.preserve_merges = value
208 .parse()
209 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
210 }
211 ("rebase", "version_suffix_pattern") => {
212 self.cascade.rebase.version_suffix_pattern = value.to_string();
213 }
214 ("rebase", "backup_before_rebase") => {
215 self.cascade.rebase.backup_before_rebase = value
216 .parse()
217 .map_err(|_| CascadeError::config(format!("Invalid boolean value: {value}")))?;
218 }
219 _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
220 }
221
222 Ok(())
223 }
224
225 pub fn get_value(&self, key: &str) -> Result<String> {
227 let parts: Vec<&str> = key.split('.').collect();
228 if parts.len() != 2 {
229 return Err(CascadeError::config(format!(
230 "Invalid config key format: {key}"
231 )));
232 }
233
234 let value = match (parts[0], parts[1]) {
235 ("bitbucket", "url") => &self.bitbucket.url,
236 ("bitbucket", "project") => &self.bitbucket.project,
237 ("bitbucket", "repo") => &self.bitbucket.repo,
238 ("bitbucket", "token") => self.bitbucket.token.as_deref().unwrap_or(""),
239 ("git", "default_branch") => &self.git.default_branch,
240 ("git", "author_name") => self.git.author_name.as_deref().unwrap_or(""),
241 ("git", "author_email") => self.git.author_email.as_deref().unwrap_or(""),
242 ("git", "auto_cleanup_merged") => return Ok(self.git.auto_cleanup_merged.to_string()),
243 ("git", "prefer_rebase") => return Ok(self.git.prefer_rebase.to_string()),
244 ("cascade", "api_port") => return Ok(self.cascade.api_port.to_string()),
245 ("cascade", "auto_cleanup") => return Ok(self.cascade.auto_cleanup.to_string()),
246 ("cascade", "default_sync_strategy") => &self.cascade.default_sync_strategy,
247 ("cascade", "max_stack_size") => return Ok(self.cascade.max_stack_size.to_string()),
248 ("cascade", "enable_notifications") => {
249 return Ok(self.cascade.enable_notifications.to_string())
250 }
251 ("rebase", "auto_resolve_conflicts") => {
252 return Ok(self.cascade.rebase.auto_resolve_conflicts.to_string())
253 }
254 ("rebase", "max_retry_attempts") => {
255 return Ok(self.cascade.rebase.max_retry_attempts.to_string())
256 }
257 ("rebase", "preserve_merges") => {
258 return Ok(self.cascade.rebase.preserve_merges.to_string())
259 }
260 ("rebase", "version_suffix_pattern") => &self.cascade.rebase.version_suffix_pattern,
261 ("rebase", "backup_before_rebase") => {
262 return Ok(self.cascade.rebase.backup_before_rebase.to_string())
263 }
264 _ => return Err(CascadeError::config(format!("Unknown config key: {key}"))),
265 };
266
267 Ok(value.to_string())
268 }
269
270 pub fn validate(&self) -> Result<()> {
272 if !self.bitbucket.url.is_empty()
274 && !self.bitbucket.url.starts_with("http://")
275 && !self.bitbucket.url.starts_with("https://")
276 {
277 return Err(CascadeError::config(
278 "Bitbucket URL must start with http:// or https://",
279 ));
280 }
281
282 if self.cascade.api_port == 0 {
284 return Err(CascadeError::config("API port must be between 1 and 65535"));
285 }
286
287 let valid_strategies = [
289 "rebase",
290 "cherry-pick",
291 "branch-versioning",
292 "three-way-merge",
293 ];
294 if !valid_strategies.contains(&self.cascade.default_sync_strategy.as_str()) {
295 return Err(CascadeError::config(format!(
296 "Invalid sync strategy: {}. Valid options: {}",
297 self.cascade.default_sync_strategy,
298 valid_strategies.join(", ")
299 )));
300 }
301
302 Ok(())
303 }
304}