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 _ => {}
53 }
54
55 Ok(())
56}
57
58async fn get_config_value(config_file: &std::path::Path, key: &str) -> Result<()> {
59 let settings = Settings::load_from_file(config_file)?;
60 let value = settings.get_value(key)?;
61
62 let display_value = if key.contains("token") || key.contains("password") {
64 if value.is_empty() {
65 "(not set)".to_string()
66 } else {
67 format!("{}***", &value[..std::cmp::min(4, value.len())])
68 }
69 } else if value.is_empty() {
70 "(not set)".to_string()
71 } else {
72 value
73 };
74
75 println!("{key} = {display_value}");
76 Ok(())
77}
78
79async fn list_config_values(config_file: &std::path::Path) -> Result<()> {
80 let settings = Settings::load_from_file(config_file)?;
81
82 println!("📋 Cascade Configuration:");
83 println!();
84
85 println!("🔗 Bitbucket Server:");
87 print_config_value(&settings, " bitbucket.url")?;
88 print_config_value(&settings, " bitbucket.project")?;
89 print_config_value(&settings, " bitbucket.repo")?;
90 print_config_value(&settings, " bitbucket.token")?;
91 println!();
92
93 println!("📦 Git:");
95 print_config_value(&settings, " git.default_branch")?;
96 print_config_value(&settings, " git.author_name")?;
97 print_config_value(&settings, " git.author_email")?;
98 print_config_value(&settings, " git.auto_cleanup_merged")?;
99 print_config_value(&settings, " git.prefer_rebase")?;
100 println!();
101
102 println!("⚙️ Cascade:");
104 print_config_value(&settings, " cascade.api_port")?;
105 print_config_value(&settings, " cascade.auto_cleanup")?;
106 print_config_value(&settings, " cascade.default_sync_strategy")?;
107 print_config_value(&settings, " cascade.max_stack_size")?;
108 print_config_value(&settings, " cascade.enable_notifications")?;
109
110 Ok(())
111}
112
113fn print_config_value(settings: &Settings, key: &str) -> Result<()> {
114 let key_without_spaces = key.trim();
115 let value = settings.get_value(key_without_spaces)?;
116
117 let display_value =
119 if key_without_spaces.contains("token") || key_without_spaces.contains("password") {
120 if value.is_empty() {
121 "(not set)".to_string()
122 } else {
123 format!("{}***", &value[..std::cmp::min(4, value.len())])
124 }
125 } else if value.is_empty() {
126 "(not set)".to_string()
127 } else {
128 value
129 };
130
131 println!("{key} = {display_value}");
132 Ok(())
133}
134
135async fn unset_config_value(config_file: &std::path::Path, key: &str) -> Result<()> {
136 let mut settings = Settings::load_from_file(config_file)?;
137
138 settings.set_value(key, "")?;
140 settings.save_to_file(config_file)?;
141
142 println!("✅ Configuration value unset: {key}");
143 Ok(())
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::config::initialize_repo;
150 use git2::{Repository, Signature};
151 use tempfile::TempDir;
152
153 async fn create_initialized_repo() -> (TempDir, std::path::PathBuf) {
154 let temp_dir = TempDir::new().unwrap();
155 let repo_path = temp_dir.path().to_path_buf();
156
157 let repo = Repository::init(&repo_path).unwrap();
159
160 let signature = Signature::now("Test User", "test@example.com").unwrap();
162 let tree_id = {
163 let mut index = repo.index().unwrap();
164 index.write_tree().unwrap()
165 };
166 let tree = repo.find_tree(tree_id).unwrap();
167
168 repo.commit(
169 Some("HEAD"),
170 &signature,
171 &signature,
172 "Initial commit",
173 &tree,
174 &[],
175 )
176 .unwrap();
177
178 initialize_repo(&repo_path, None).unwrap();
180
181 (temp_dir, repo_path)
182 }
183
184 #[tokio::test]
185 async fn test_config_set_get() {
186 let (_temp_dir, repo_path) = create_initialized_repo().await;
187
188 let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
190 let config_file = config_dir.join("config.json");
191
192 set_config_value(&config_file, "bitbucket.url", "https://test.bitbucket.com")
194 .await
195 .unwrap();
196
197 get_config_value(&config_file, "bitbucket.url")
199 .await
200 .unwrap();
201 }
202
203 #[tokio::test]
204 async fn test_config_list() {
205 let (_temp_dir, repo_path) = create_initialized_repo().await;
206
207 let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
209 let config_file = config_dir.join("config.json");
210
211 list_config_values(&config_file).await.unwrap();
213 }
214}