cascade_cli/cli/commands/
config.rs1use crate::cli::ConfigAction;
2use crate::config::{get_repo_config_dir, is_repo_initialized, Settings};
3use crate::errors::{CascadeError, Result};
4use crate::git::find_repository_root;
5use std::env;
6
7pub async fn run(action: ConfigAction) -> Result<()> {
9 let current_dir = env::current_dir()
11 .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
12
13 let repo_root = find_repository_root(¤t_dir)?;
14
15 if !is_repo_initialized(&repo_root) {
17 return Err(CascadeError::not_initialized(
18 "Repository is not initialized for Cascade. Run 'ca init' first.",
19 ));
20 }
21
22 let config_dir = get_repo_config_dir(&repo_root)?;
23 let config_file = config_dir.join("config.json");
24
25 match action {
26 ConfigAction::Set { key, value } => set_config_value(&config_file, &key, &value).await,
27 ConfigAction::Get { key } => get_config_value(&config_file, &key).await,
28 ConfigAction::List => list_config_values(&config_file).await,
29 ConfigAction::Unset { key } => unset_config_value(&config_file, &key).await,
30 }
31}
32
33async fn set_config_value(config_file: &std::path::Path, key: &str, value: &str) -> Result<()> {
34 let mut settings = Settings::load_from_file(config_file)?;
35 settings.set_value(key, value)?;
36 settings.validate()?;
37 settings.save_to_file(config_file)?;
38
39 println!("✅ Configuration updated: {key} = {value}");
40
41 match key {
43 "bitbucket.token" => {
44 println!("💡 Tip: You can create a personal access token in Bitbucket Server under:");
45 println!(" Settings → Personal access tokens → Create token");
46 }
47 "bitbucket.url" => {
48 println!("💡 Next: Set your project and repository:");
49 println!(" ca config set bitbucket.project YOUR_PROJECT_KEY");
50 println!(" ca config set bitbucket.repo your-repo-name");
51 }
52 "bitbucket.accept_invalid_certs" => {
53 println!("💡 SSL Configuration:");
54 if value == "true" {
55 println!(" ⚠️ SSL certificate verification is disabled (development only)");
56 println!(" This setting affects both API calls and git operations");
57 } else {
58 println!(" ✅ SSL certificate verification is enabled (recommended)");
59 println!(" For custom CA certificates, use: ca config set bitbucket.ca_bundle_path /path/to/ca-bundle.crt");
60 }
61 }
62 "bitbucket.ca_bundle_path" => {
63 println!("💡 SSL Configuration:");
64 println!(" 📁 Custom CA bundle path set for SSL certificate verification");
65 println!(" This affects both API calls and git operations");
66 println!(" Make sure the file exists and contains valid PEM certificates");
67 }
68 _ => {}
69 }
70
71 Ok(())
72}
73
74async fn get_config_value(config_file: &std::path::Path, key: &str) -> Result<()> {
75 let settings = Settings::load_from_file(config_file)?;
76 let value = settings.get_value(key)?;
77
78 let display_value = if key.contains("token") || key.contains("password") {
80 if value.is_empty() {
81 "(not set)".to_string()
82 } else {
83 format!("{}***", &value[..std::cmp::min(4, value.len())])
84 }
85 } else if value.is_empty() {
86 "(not set)".to_string()
87 } else {
88 value
89 };
90
91 println!("{key} = {display_value}");
92 Ok(())
93}
94
95async fn list_config_values(config_file: &std::path::Path) -> Result<()> {
96 let settings = Settings::load_from_file(config_file)?;
97
98 println!("📋 Cascade Configuration:");
99 println!();
100
101 println!("🔗 Bitbucket Server:");
103 print_config_value(&settings, " bitbucket.url")?;
104 print_config_value(&settings, " bitbucket.project")?;
105 print_config_value(&settings, " bitbucket.repo")?;
106 print_config_value(&settings, " bitbucket.token")?;
107 println!();
108
109 println!("📦 Git:");
111 print_config_value(&settings, " git.default_branch")?;
112 print_config_value(&settings, " git.author_name")?;
113 print_config_value(&settings, " git.author_email")?;
114 print_config_value(&settings, " git.auto_cleanup_merged")?;
115 print_config_value(&settings, " git.prefer_rebase")?;
116 println!();
117
118 println!("⚙️ Cascade:");
120 print_config_value(&settings, " cascade.api_port")?;
121 print_config_value(&settings, " cascade.auto_cleanup")?;
122 print_config_value(&settings, " cascade.default_sync_strategy")?;
123 print_config_value(&settings, " cascade.max_stack_size")?;
124 print_config_value(&settings, " cascade.enable_notifications")?;
125
126 Ok(())
127}
128
129fn print_config_value(settings: &Settings, key: &str) -> Result<()> {
130 let key_without_spaces = key.trim();
131 let value = settings.get_value(key_without_spaces)?;
132
133 let display_value =
135 if key_without_spaces.contains("token") || key_without_spaces.contains("password") {
136 if value.is_empty() {
137 "(not set)".to_string()
138 } else {
139 format!("{}***", &value[..std::cmp::min(4, value.len())])
140 }
141 } else if value.is_empty() {
142 "(not set)".to_string()
143 } else {
144 value
145 };
146
147 println!("{key} = {display_value}");
148 Ok(())
149}
150
151async fn unset_config_value(config_file: &std::path::Path, key: &str) -> Result<()> {
152 let mut settings = Settings::load_from_file(config_file)?;
153
154 settings.set_value(key, "")?;
156 settings.save_to_file(config_file)?;
157
158 println!("✅ Configuration value unset: {key}");
159 Ok(())
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::config::initialize_repo;
166 use git2::{Repository, Signature};
167 use tempfile::TempDir;
168
169 async fn create_initialized_repo() -> (TempDir, std::path::PathBuf) {
170 let temp_dir = TempDir::new().unwrap();
171 let repo_path = temp_dir.path().to_path_buf();
172
173 let repo = Repository::init(&repo_path).unwrap();
175
176 let signature = Signature::now("Test User", "test@example.com").unwrap();
178 let tree_id = {
179 let mut index = repo.index().unwrap();
180 index.write_tree().unwrap()
181 };
182 let tree = repo.find_tree(tree_id).unwrap();
183
184 repo.commit(
185 Some("HEAD"),
186 &signature,
187 &signature,
188 "Initial commit",
189 &tree,
190 &[],
191 )
192 .unwrap();
193
194 initialize_repo(&repo_path, None).unwrap();
196
197 (temp_dir, repo_path)
198 }
199
200 #[tokio::test]
201 async fn test_config_set_get() {
202 let (_temp_dir, repo_path) = create_initialized_repo().await;
203
204 let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
206 let config_file = config_dir.join("config.json");
207
208 set_config_value(&config_file, "bitbucket.url", "https://test.bitbucket.com")
210 .await
211 .unwrap();
212
213 get_config_value(&config_file, "bitbucket.url")
215 .await
216 .unwrap();
217 }
218
219 #[tokio::test]
220 async fn test_config_list() {
221 let (_temp_dir, repo_path) = create_initialized_repo().await;
222
223 let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
225 let config_file = config_dir.join("config.json");
226
227 list_config_values(&config_file).await.unwrap();
229 }
230}