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.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 Output::sub_item(format!("{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 Output::success(format!("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}