cascade_cli/cli/commands/
config.rs

1use 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
7/// Handle configuration commands
8pub async fn run(action: ConfigAction) -> Result<()> {
9    // Find the repository root
10    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(&current_dir)?;
14
15    // Check if repository is initialized
16    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    // Provide contextual hints
42    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        "bitbucket.accept_invalid_certs" => {
53            println!("💡 SSL Configuration:");
54            if value == "true" {
55                println!("   ⚠️  SSL certificate verification is disabled (development only)");
56                println!("   This setting affects both API calls and git operations");
57            } else {
58                println!("   ✅ SSL certificate verification is enabled (recommended)");
59                println!("   For custom CA certificates, use: ca config set bitbucket.ca_bundle_path /path/to/ca-bundle.crt");
60            }
61        }
62        "bitbucket.ca_bundle_path" => {
63            println!("💡 SSL Configuration:");
64            println!("   📁 Custom CA bundle path set for SSL certificate verification");
65            println!("   This affects both API calls and git operations");
66            println!("   Make sure the file exists and contains valid PEM certificates");
67        }
68        _ => {}
69    }
70
71    Ok(())
72}
73
74async fn get_config_value(config_file: &std::path::Path, key: &str) -> Result<()> {
75    let settings = Settings::load_from_file(config_file)?;
76    let value = settings.get_value(key)?;
77
78    // Mask sensitive values
79    let display_value = if key.contains("token") || key.contains("password") {
80        if value.is_empty() {
81            "(not set)".to_string()
82        } else {
83            format!("{}***", &value[..std::cmp::min(4, value.len())])
84        }
85    } else if value.is_empty() {
86        "(not set)".to_string()
87    } else {
88        value
89    };
90
91    println!("{key} = {display_value}");
92    Ok(())
93}
94
95async fn list_config_values(config_file: &std::path::Path) -> Result<()> {
96    let settings = Settings::load_from_file(config_file)?;
97
98    println!("📋 Cascade Configuration:");
99    println!();
100
101    // Bitbucket configuration
102    println!("🔗 Bitbucket Server:");
103    print_config_value(&settings, "  bitbucket.url")?;
104    print_config_value(&settings, "  bitbucket.project")?;
105    print_config_value(&settings, "  bitbucket.repo")?;
106    print_config_value(&settings, "  bitbucket.token")?;
107    println!();
108
109    // Git configuration
110    println!("📦 Git:");
111    print_config_value(&settings, "  git.default_branch")?;
112    print_config_value(&settings, "  git.author_name")?;
113    print_config_value(&settings, "  git.author_email")?;
114    print_config_value(&settings, "  git.auto_cleanup_merged")?;
115    print_config_value(&settings, "  git.prefer_rebase")?;
116    println!();
117
118    // Cascade configuration
119    println!("⚙️  Cascade:");
120    print_config_value(&settings, "  cascade.api_port")?;
121    print_config_value(&settings, "  cascade.auto_cleanup")?;
122    print_config_value(&settings, "  cascade.default_sync_strategy")?;
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    println!("{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    println!("✅ 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}