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.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    // Mask sensitive values
134    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    // Set the value to empty string to "unset" it
155    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        // Initialize git repository
174        let repo = Repository::init(&repo_path).unwrap();
175
176        // Create initial commit
177        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 Cascade
195        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        // Test directly with config file instead of changing directories
205        let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
206        let config_file = config_dir.join("config.json");
207
208        // Set a configuration value
209        set_config_value(&config_file, "bitbucket.url", "https://test.bitbucket.com")
210            .await
211            .unwrap();
212
213        // Get the configuration value
214        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        // Test directly with config file instead of changing directories
224        let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
225        let config_file = config_dir.join("config.json");
226
227        // List all configuration values
228        list_config_values(&config_file).await.unwrap();
229    }
230}