cascade_cli/cli/commands/
config.rs

1use 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
8/// Handle configuration commands
9pub async fn run(action: ConfigAction) -> Result<()> {
10    // Find the repository root
11    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(&current_dir)?;
15
16    // Check if repository is initialized
17    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    // Provide contextual hints
43    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    // Mask sensitive values
80    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    // Bitbucket configuration
103    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    // Git configuration
111    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    // Cascade configuration
120    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    // Mask sensitive values
135    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    // Set the value to empty string to "unset" it
156    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        // Initialize git repository
175        let repo = Repository::init(&repo_path).unwrap();
176
177        // Create initial commit
178        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 Cascade
196        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        // Test directly with config file instead of changing directories
206        let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
207        let config_file = config_dir.join("config.json");
208
209        // Set a configuration value
210        set_config_value(&config_file, "bitbucket.url", "https://test.bitbucket.com")
211            .await
212            .unwrap();
213
214        // Get the configuration value
215        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        // Test directly with config file instead of changing directories
225        let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
226        let config_file = config_dir.join("config.json");
227
228        // List all configuration values
229        list_config_values(&config_file).await.unwrap();
230    }
231}