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        _ => {}
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    // Mask sensitive values
63    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    // Bitbucket configuration
86    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    // Git configuration
94    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    // Cascade configuration
103    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    // Mask sensitive values
118    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    // Set the value to empty string to "unset" it
139    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        // Initialize git repository
158        let repo = Repository::init(&repo_path).unwrap();
159
160        // Create initial commit
161        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 Cascade
179        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        // Test directly with config file instead of changing directories
189        let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
190        let config_file = config_dir.join("config.json");
191
192        // Set a configuration value
193        set_config_value(&config_file, "bitbucket.url", "https://test.bitbucket.com")
194            .await
195            .unwrap();
196
197        // Get the configuration value
198        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        // Test directly with config file instead of changing directories
208        let config_dir = crate::config::get_repo_config_dir(&repo_path).unwrap();
209        let config_file = config_dir.join("config.json");
210
211        // List all configuration values
212        list_config_values(&config_file).await.unwrap();
213    }
214}