Skip to main content

vibe_workspace/git/
clone.rs

1use anyhow::Result;
2use colored::*;
3use console::style;
4use inquire::{Confirm, Select};
5use std::path::PathBuf;
6
7use crate::git::bulk_clone::{BulkCloneCommand, BulkCloneOptions};
8use crate::git::provider::github_cli::GitHubCliProvider;
9use crate::git::{GitConfig, Repository};
10use crate::workspace::install::RepositoryInstaller;
11use crate::workspace::manager::WorkspaceManager;
12
13pub struct CloneCommand;
14
15impl CloneCommand {
16    pub async fn execute(
17        url: String,
18        path: Option<PathBuf>,
19        open: bool,
20        install: bool,
21        workspace_manager: &mut WorkspaceManager,
22        git_config: &GitConfig,
23    ) -> Result<PathBuf> {
24        // Get workspace root from manager
25        let workspace_root = workspace_manager.config().workspace.root.clone();
26
27        // Create installer
28        let installer = RepositoryInstaller::new(workspace_root, git_config.clone());
29
30        // Clone repository
31        let installed = installer
32            .install_from_url_with_options(&url, path, open, install)
33            .await?;
34
35        // Add to workspace configuration
36        workspace_manager
37            .add_repository(installed.repository.clone())
38            .await?;
39
40        // Execute post-install actions
41        if !installed.post_install_actions.is_empty() {
42            installer
43                .execute_post_install_actions(&installed.post_install_actions, &installed.path)
44                .await?;
45        }
46
47        println!(
48            "\n{} Repository successfully added to workspace!",
49            "🎉".green()
50        );
51
52        println!("Path: {}", installed.path.display().to_string().cyan());
53
54        Ok(installed.path)
55    }
56
57    /// Execute clone with interactive post-clone workflow
58    pub async fn execute_interactive(
59        url: String,
60        path: Option<PathBuf>,
61        workspace_manager: &mut WorkspaceManager,
62        git_config: &GitConfig,
63    ) -> Result<PathBuf> {
64        // Clone the repository first
65        let cloned_path = Self::execute(
66            url.clone(),
67            path,
68            false,
69            false,
70            workspace_manager,
71            git_config,
72        )
73        .await?;
74
75        // Extract repository name from path
76        let repo_name = cloned_path
77            .file_name()
78            .and_then(|n| n.to_str())
79            .ok_or_else(|| anyhow::anyhow!("Could not determine repository name"))?;
80
81        // Run interactive post-clone workflow
82        Self::interactive_post_clone_workflow(repo_name, workspace_manager).await?;
83
84        Ok(cloned_path)
85    }
86
87    pub async fn clone_from_search_result(
88        repo: Repository,
89        workspace_manager: &mut WorkspaceManager,
90        git_config: &GitConfig,
91    ) -> Result<()> {
92        println!(
93            "\n{} Selected: {}",
94            "✅".green(),
95            repo.full_name.cyan().bold()
96        );
97
98        // Use HTTPS URL by default (more universal than SSH)
99        let _cloned_path =
100            Self::execute(repo.url, None, false, false, workspace_manager, git_config).await?;
101
102        // Run interactive post-clone workflow
103        Self::interactive_post_clone_workflow(&repo.name, workspace_manager).await?;
104
105        Ok(())
106    }
107
108    /// Interactive workflow after cloning a repository
109    pub async fn interactive_post_clone_workflow(
110        repo_name: &str,
111        workspace_manager: &mut WorkspaceManager,
112    ) -> Result<()> {
113        println!("\n{} Repository cloned successfully!", style("🎉").green());
114
115        // Step 1: Ask if they want to configure apps
116        let configure_apps = Confirm::new(&format!(
117            "Would you like to configure apps for '{}'?",
118            style(repo_name).cyan().bold()
119        ))
120        .with_default(true)
121        .with_help_message(
122            "Configure which applications can open this repository (VS Code, Warp, etc.)",
123        )
124        .prompt()?;
125
126        if configure_apps {
127            Self::configure_repository_apps(repo_name, workspace_manager).await?;
128        }
129
130        // Step 2: Ask if they want to open it now
131        let open_now = Confirm::new(&format!(
132            "Would you like to open '{}' now?",
133            style(repo_name).cyan().bold()
134        ))
135        .with_default(true)
136        .with_help_message("Open the repository with your configured app")
137        .prompt()?;
138
139        if open_now {
140            Self::open_repository_interactive(repo_name, workspace_manager).await?;
141        }
142
143        Ok(())
144    }
145
146    /// Configure apps for a repository interactively
147    async fn configure_repository_apps(
148        repo_name: &str,
149        workspace_manager: &mut WorkspaceManager,
150    ) -> Result<()> {
151        // Get available app choices (hardcoded for now, could be made configurable)
152        let available_apps = [
153            ("vscode", "Visual Studio Code - Code editor"),
154            ("warp", "Warp - Modern terminal"),
155            ("iterm2", "iTerm2 - Terminal emulator"),
156        ];
157
158        println!(
159            "\n{} Select an application to configure for this repository:",
160            style("📱").green()
161        );
162
163        let app_choices: Vec<String> = available_apps
164            .iter()
165            .map(|(name, desc)| format!("{name} - {desc}"))
166            .collect();
167
168        let selected_display = Select::new("Choose an application:", app_choices)
169            .with_help_message("Select an application to configure for this repository")
170            .prompt()?;
171
172        // Extract app name from the display string
173        let app_name = selected_display
174            .split(" - ")
175            .next()
176            .unwrap_or(&selected_display);
177
178        // Configure the app for this repository using the existing method
179        workspace_manager
180            .configure_app_for_repo(repo_name, app_name, "default")
181            .await?;
182
183        println!(
184            "{} Configured {} for {}",
185            style("✅").green(),
186            style(app_name).blue(),
187            style(repo_name).cyan()
188        );
189
190        Ok(())
191    }
192
193    /// Open repository interactively with app selection
194    async fn open_repository_interactive(
195        repo_name: &str,
196        workspace_manager: &mut WorkspaceManager,
197    ) -> Result<()> {
198        // Get the repository configuration
199        if let Some(repo_info) = workspace_manager.get_repository(repo_name) {
200            if repo_info.apps.is_empty() {
201                println!(
202                    "{} No apps configured for this repository",
203                    style("âš ī¸").yellow()
204                );
205                println!("   Configure apps first using the configuration workflow");
206                return Ok(());
207            }
208
209            // If only one app is configured, use it directly
210            let app_to_use = if repo_info.apps.len() == 1 {
211                repo_info.apps.keys().next().unwrap().clone()
212            } else {
213                // Multiple apps, let user choose
214                let app_choices: Vec<String> = repo_info.apps.keys().cloned().collect();
215                Select::new("Choose an app to open with:", app_choices)
216                    .with_help_message("Select which application to use")
217                    .prompt()?
218            };
219
220            // Open the repository
221            workspace_manager
222                .open_repo_with_app(repo_name, &app_to_use)
223                .await?;
224
225            println!(
226                "{} Opened {} with {}",
227                style("🚀").green(),
228                style(repo_name).cyan().bold(),
229                style(&app_to_use).blue()
230            );
231        } else {
232            println!(
233                "{} Repository '{}' not found in workspace",
234                style("❌").red(),
235                repo_name
236            );
237        }
238
239        Ok(())
240    }
241}
242
243/// Enhanced clone command with bulk detection capabilities
244pub struct EnhancedCloneCommand;
245
246impl EnhancedCloneCommand {
247    /// Execute clone with automatic detection of user/org patterns
248    pub async fn execute_with_detection(
249        url_or_target: String,
250        app: Option<String>,
251        no_configure: bool,
252        no_open: bool,
253        workspace_manager: &mut WorkspaceManager,
254        git_config: &GitConfig,
255    ) -> Result<()> {
256        let contains_slash = url_or_target.contains('/');
257        let is_url = url_or_target.starts_with("http") || url_or_target.starts_with("git@");
258
259        // Route based on input pattern
260        match (contains_slash, is_url) {
261            // Traditional repository URL or owner/repo format
262            (true, _) | (false, true) => {
263                Self::single_repository_workflow(
264                    url_or_target,
265                    app,
266                    no_configure,
267                    no_open,
268                    workspace_manager,
269                    git_config,
270                )
271                .await
272            }
273
274            // Potential user/org name - check if it exists
275            (false, false) => {
276                Self::detect_and_route(
277                    url_or_target,
278                    app,
279                    no_configure,
280                    no_open,
281                    workspace_manager,
282                    git_config,
283                )
284                .await
285            }
286        }
287    }
288
289    /// Detect if target is user/org and route accordingly
290    async fn detect_and_route(
291        target: String,
292        app: Option<String>,
293        no_configure: bool,
294        no_open: bool,
295        workspace_manager: &mut WorkspaceManager,
296        git_config: &GitConfig,
297    ) -> Result<()> {
298        println!("🔍 Analyzing '{}'...", style(&target).cyan());
299
300        // Initialize GitHub CLI provider
301        let github_cli = match GitHubCliProvider::new() {
302            Ok(cli) => cli,
303            Err(_) => {
304                println!(
305                    "{} GitHub CLI not available, searching repositories...",
306                    style("âš ī¸").yellow()
307                );
308                return Self::fallback_to_search(target, workspace_manager, git_config).await;
309            }
310        };
311
312        // Check if target exists as user or organization
313        match github_cli.user_or_org_exists(&target).await {
314            Ok(true) => {
315                // Get repository count
316                match github_cli.count_repositories(&target).await {
317                    Ok(0) => {
318                        println!(
319                            "{} '{}' has no public repositories.",
320                            style("â„šī¸").blue(),
321                            style(&target).cyan()
322                        );
323                        Self::fallback_to_search(target, workspace_manager, git_config).await
324                    }
325                    Ok(count) => {
326                        Self::interactive_clone_selection(
327                            target,
328                            count,
329                            app,
330                            no_configure,
331                            no_open,
332                            workspace_manager,
333                            git_config,
334                        )
335                        .await
336                    }
337                    Err(_) => {
338                        println!(
339                            "{} Failed to count repositories for '{}', searching instead...",
340                            style("âš ī¸").yellow(),
341                            style(&target).cyan()
342                        );
343                        Self::fallback_to_search(target, workspace_manager, git_config).await
344                    }
345                }
346            }
347            Ok(false) => {
348                println!(
349                    "🔍 '{}' not found as a GitHub user or organization.",
350                    &target
351                );
352                println!("🔍 Searching repositories for '{}'...", &target);
353                Self::fallback_to_search(target, workspace_manager, git_config).await
354            }
355            Err(_) => {
356                println!(
357                    "{} Failed to check GitHub, searching repositories instead...",
358                    style("âš ī¸").yellow()
359                );
360                Self::fallback_to_search(target, workspace_manager, git_config).await
361            }
362        }
363    }
364
365    /// Show interactive options for user/org with repositories
366    async fn interactive_clone_selection(
367        target: String,
368        repo_count: usize,
369        _app: Option<String>,
370        _no_configure: bool,
371        _no_open: bool,
372        workspace_manager: &mut WorkspaceManager,
373        git_config: &GitConfig,
374    ) -> Result<()> {
375        println!(
376            "✅ Found GitHub target '{}' with {} repositories",
377            style(&target).cyan().bold(),
378            style(repo_count).green().bold()
379        );
380
381        let options = vec![
382            format!("Clone all {} repositories", repo_count),
383            "Search for specific repository".to_string(),
384            "Cancel".to_string(),
385        ];
386
387        let selection = Select::new("What would you like to do?", options)
388            .with_help_message("Choose how to proceed with this GitHub target")
389            .prompt()?;
390
391        match selection.as_str() {
392            s if s.starts_with("Clone all") => {
393                Self::bulk_clone_workflow(target, workspace_manager, git_config).await
394            }
395            "Search for specific repository" => {
396                Self::fallback_to_search(target, workspace_manager, git_config).await
397            }
398            _ => {
399                println!("{} Operation cancelled", style("â„šī¸").blue());
400                Ok(())
401            }
402        }
403    }
404
405    /// Execute bulk cloning workflow
406    async fn bulk_clone_workflow(
407        target: String,
408        workspace_manager: &mut WorkspaceManager,
409        git_config: &GitConfig,
410    ) -> Result<()> {
411        let options = BulkCloneOptions {
412            exclude_patterns: Vec::new(),
413            include_patterns: Vec::new(),
414            skip_existing: true,
415            custom_path: None,
416            force: false, // Always show confirmation in interactive mode
417        };
418
419        match BulkCloneCommand::execute(target, options, workspace_manager, git_config).await {
420            Ok(result) => {
421                println!(
422                    "{} Bulk clone completed: {} successful, {} failed",
423                    style("✅").green().bold(),
424                    result.total_cloned,
425                    result.failed.len()
426                );
427                Ok(())
428            }
429            Err(e) => {
430                println!("{} Bulk clone failed: {}", style("❌").red(), e);
431                Err(e)
432            }
433        }
434    }
435
436    /// Fallback to repository search when user/org detection fails
437    async fn fallback_to_search(
438        target: String,
439        workspace_manager: &mut WorkspaceManager,
440        git_config: &GitConfig,
441    ) -> Result<()> {
442        use crate::git::SearchCommand;
443
444        // Use the existing search functionality
445        SearchCommand::execute_with_query(&target, workspace_manager, git_config).await
446    }
447
448    /// Execute single repository clone workflow
449    async fn single_repository_workflow(
450        url: String,
451        app: Option<String>,
452        no_configure: bool,
453        no_open: bool,
454        workspace_manager: &mut WorkspaceManager,
455        git_config: &GitConfig,
456    ) -> Result<()> {
457        use crate::ui::workflows::{execute_workflow, CloneWorkflow};
458
459        // Use existing workflow system if not skipping steps
460        if !no_configure || !no_open {
461            let workflow = Box::new(CloneWorkflow {
462                url: url.clone(),
463                app: app.clone(),
464            });
465
466            execute_workflow(workflow, workspace_manager).await?;
467        } else {
468            // Just clone without workflow
469            let _cloned_path =
470                CloneCommand::execute(url, None, false, false, workspace_manager, git_config)
471                    .await?;
472        }
473
474        Ok(())
475    }
476}