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