cascade_cli/cli/
mod.rs

1pub mod commands;
2
3use crate::errors::Result;
4use clap::{Parser, Subcommand};
5use clap_complete::Shell;
6use commands::entry::EntryAction;
7use commands::stack::StackAction;
8use commands::{MergeStrategyArg, RebaseStrategyArg};
9
10#[derive(Parser)]
11#[command(name = "ca")]
12#[command(about = "Cascade CLI - Stacked Diffs for Bitbucket")]
13#[command(version)]
14pub struct Cli {
15    #[command(subcommand)]
16    pub command: Commands,
17
18    /// Enable verbose logging
19    #[arg(long, short, global = true)]
20    pub verbose: bool,
21
22    /// Disable colored output
23    #[arg(long, global = true)]
24    pub no_color: bool,
25}
26
27/// Commands available in the CLI
28#[derive(Debug, Subcommand)]
29pub enum Commands {
30    /// Initialize repository for Cascade
31    Init {
32        /// Bitbucket Server URL
33        #[arg(long)]
34        bitbucket_url: Option<String>,
35
36        /// Force initialization even if already initialized
37        #[arg(long)]
38        force: bool,
39    },
40
41    /// Configuration management
42    Config {
43        #[command(subcommand)]
44        action: ConfigAction,
45    },
46
47    /// Stack management
48    Stacks {
49        #[command(subcommand)]
50        action: StackAction,
51    },
52
53    /// Entry management and editing
54    Entry {
55        #[command(subcommand)]
56        action: EntryAction,
57    },
58
59    /// Show repository overview and all stacks
60    Repo,
61
62    /// Show version information  
63    Version,
64
65    /// Check repository health and configuration
66    Doctor,
67
68    /// Diagnose git2 TLS/SSH support issues
69    Diagnose,
70
71    /// Generate shell completions
72    Completions {
73        #[command(subcommand)]
74        action: CompletionsAction,
75    },
76
77    /// Interactive setup wizard
78    Setup {
79        /// Force reconfiguration if already initialized
80        #[arg(long)]
81        force: bool,
82    },
83
84    /// Launch interactive TUI for stack management
85    Tui,
86
87    /// Git hooks management
88    Hooks {
89        #[command(subcommand)]
90        action: HooksAction,
91    },
92
93    /// Visualize stacks and dependencies
94    Viz {
95        #[command(subcommand)]
96        action: VizAction,
97    },
98
99    // Stack command shortcuts for commonly used operations
100    /// Show current stack details
101    Stack {
102        /// Show detailed pull request information
103        #[arg(short, long)]
104        verbose: bool,
105        /// Show mergability status for all PRs
106        #[arg(short, long)]
107        mergeable: bool,
108    },
109
110    /// Push current commit to the top of the stack (shortcut for 'stack push')
111    Push {
112        /// Branch name for this commit
113        #[arg(long, short)]
114        branch: Option<String>,
115        /// Commit message (if creating a new commit)
116        #[arg(long, short)]
117        message: Option<String>,
118        /// Use specific commit hash instead of HEAD
119        #[arg(long)]
120        commit: Option<String>,
121        /// Push commits since this reference (e.g., HEAD~3)
122        #[arg(long)]
123        since: Option<String>,
124        /// Push multiple specific commits (comma-separated)
125        #[arg(long)]
126        commits: Option<String>,
127        /// Squash last N commits into one before pushing
128        #[arg(long)]
129        squash: Option<usize>,
130        /// Squash all commits since this reference (e.g., HEAD~5)
131        #[arg(long)]
132        squash_since: Option<String>,
133        /// Auto-create feature branch when pushing from base branch
134        #[arg(long)]
135        auto_branch: bool,
136        /// Allow pushing commits from base branch (not recommended)
137        #[arg(long)]
138        allow_base_branch: bool,
139    },
140
141    /// Pop the top commit from the stack (shortcut for 'stack pop')
142    Pop {
143        /// Keep the branch (don't delete it)
144        #[arg(long)]
145        keep_branch: bool,
146    },
147
148    /// Land (merge) approved stack entries (shortcut for 'stack land')
149    Land {
150        /// Stack entry number to land (1-based index, optional)
151        entry: Option<usize>,
152        /// Force land even with blocking issues (dangerous)
153        #[arg(short, long)]
154        force: bool,
155        /// Dry run - show what would be landed without doing it
156        #[arg(short, long)]
157        dry_run: bool,
158        /// Use server-side validation (safer, checks approvals/builds)
159        #[arg(long)]
160        auto: bool,
161        /// Wait for builds to complete before merging
162        #[arg(long)]
163        wait_for_builds: bool,
164        /// Merge strategy to use
165        #[arg(long, value_enum, default_value = "squash")]
166        strategy: Option<MergeStrategyArg>,
167        /// Maximum time to wait for builds (seconds)
168        #[arg(long, default_value = "1800")]
169        build_timeout: u64,
170    },
171
172    /// Auto-land all ready PRs (shortcut for 'stack autoland')
173    Autoland {
174        /// Force land even with blocking issues (dangerous)
175        #[arg(short, long)]
176        force: bool,
177        /// Dry run - show what would be landed without doing it
178        #[arg(short, long)]
179        dry_run: bool,
180        /// Wait for builds to complete before merging
181        #[arg(long)]
182        wait_for_builds: bool,
183        /// Merge strategy to use
184        #[arg(long, value_enum, default_value = "squash")]
185        strategy: Option<MergeStrategyArg>,
186        /// Maximum time to wait for builds (seconds)
187        #[arg(long, default_value = "1800")]
188        build_timeout: u64,
189    },
190
191    /// Sync stack with remote repository (shortcut for 'stack sync')
192    Sync {
193        /// Force sync even if there are conflicts
194        #[arg(long)]
195        force: bool,
196        /// Skip cleanup of merged branches
197        #[arg(long)]
198        skip_cleanup: bool,
199        /// Interactive mode for conflict resolution
200        #[arg(long, short)]
201        interactive: bool,
202    },
203
204    /// Rebase stack on updated base branch (shortcut for 'stack rebase')
205    Rebase {
206        /// Interactive rebase
207        #[arg(long, short)]
208        interactive: bool,
209        /// Target base branch (defaults to stack's base branch)
210        #[arg(long)]
211        onto: Option<String>,
212        /// Rebase strategy to use
213        #[arg(long, value_enum)]
214        strategy: Option<RebaseStrategyArg>,
215    },
216
217    /// Switch to a different stack (shortcut for 'stacks switch')
218    Switch {
219        /// Name of the stack to switch to
220        name: String,
221    },
222
223    /// Deactivate the current stack - turn off stack mode (shortcut for 'stacks deactivate')
224    Deactivate {
225        /// Force deactivation without confirmation
226        #[arg(long)]
227        force: bool,
228    },
229
230    /// Submit a stack entry for review (shortcut for 'stacks submit')
231    Submit {
232        /// Stack entry number (1-based, defaults to all unsubmitted)
233        entry: Option<usize>,
234        /// Pull request title
235        #[arg(long, short)]
236        title: Option<String>,
237        /// Pull request description
238        #[arg(long, short)]
239        description: Option<String>,
240        /// Submit range of entries (e.g., "1-3" or "2,4,6")
241        #[arg(long)]
242        range: Option<String>,
243        /// Create draft pull requests (can be edited later)
244        #[arg(long)]
245        draft: bool,
246    },
247
248    /// Validate stack integrity and handle branch modifications (shortcut for 'stacks validate')
249    Validate {
250        /// Name of the stack (defaults to active stack)
251        name: Option<String>,
252        /// Auto-fix mode: incorporate, split, or reset
253        #[arg(long)]
254        fix: Option<String>,
255    },
256}
257
258/// Git hooks actions
259#[derive(Debug, Subcommand)]
260pub enum HooksAction {
261    /// Install all Cascade Git hooks
262    Install {
263        /// Skip prerequisite checks (repository type, configuration validation)
264        #[arg(long)]
265        skip_checks: bool,
266
267        /// Allow installation on main/master branches (not recommended)
268        #[arg(long)]
269        allow_main_branch: bool,
270
271        /// Skip confirmation prompt
272        #[arg(long, short)]
273        yes: bool,
274
275        /// Force installation even if checks fail (not recommended)
276        #[arg(long)]
277        force: bool,
278    },
279
280    /// Uninstall all Cascade Git hooks
281    Uninstall,
282
283    /// Show Git hooks status
284    Status,
285
286    /// Install a specific hook
287    Add {
288        /// Hook name (post-commit, pre-push, commit-msg, prepare-commit-msg)
289        hook: String,
290
291        /// Skip prerequisite checks
292        #[arg(long)]
293        skip_checks: bool,
294
295        /// Force installation even if checks fail
296        #[arg(long)]
297        force: bool,
298    },
299
300    /// Remove a specific hook
301    Remove {
302        /// Hook name (post-commit, pre-push, commit-msg, prepare-commit-msg)
303        hook: String,
304    },
305}
306
307/// Visualization actions
308#[derive(Debug, Subcommand)]
309pub enum VizAction {
310    /// Show stack diagram
311    Stack {
312        /// Stack name (defaults to active stack)
313        name: Option<String>,
314        /// Output format (ascii, mermaid, dot, plantuml)
315        #[arg(long, short)]
316        format: Option<String>,
317        /// Output file path
318        #[arg(long, short)]
319        output: Option<String>,
320        /// Compact mode (less details)
321        #[arg(long)]
322        compact: bool,
323        /// Disable colors
324        #[arg(long)]
325        no_colors: bool,
326    },
327
328    /// Show dependency graph of all stacks
329    Deps {
330        /// Output format (ascii, mermaid, dot, plantuml)
331        #[arg(long, short)]
332        format: Option<String>,
333        /// Output file path
334        #[arg(long, short)]
335        output: Option<String>,
336        /// Compact mode (less details)
337        #[arg(long)]
338        compact: bool,
339        /// Disable colors
340        #[arg(long)]
341        no_colors: bool,
342    },
343}
344
345/// Shell completion actions
346#[derive(Debug, Subcommand)]
347pub enum CompletionsAction {
348    /// Generate completions for a shell
349    Generate {
350        /// Shell to generate completions for
351        #[arg(value_enum)]
352        shell: Shell,
353    },
354
355    /// Install completions for available shells
356    Install {
357        /// Specific shell to install for
358        #[arg(long, value_enum)]
359        shell: Option<Shell>,
360    },
361
362    /// Show completion installation status
363    Status,
364}
365
366#[derive(Debug, Subcommand)]
367pub enum ConfigAction {
368    /// Set a configuration value
369    Set {
370        /// Configuration key (e.g., bitbucket.url)
371        key: String,
372        /// Configuration value
373        value: String,
374    },
375
376    /// Get a configuration value
377    Get {
378        /// Configuration key
379        key: String,
380    },
381
382    /// List all configuration values
383    List,
384
385    /// Remove a configuration value
386    Unset {
387        /// Configuration key
388        key: String,
389    },
390}
391
392impl Cli {
393    pub async fn run(self) -> Result<()> {
394        // Set up logging based on verbosity
395        self.setup_logging();
396
397        // Initialize git2 to use system certificates by default
398        // This ensures we work out-of-the-box in corporate environments
399        // just like git CLI and other modern dev tools (Graphite, Sapling, Phabricator)
400        self.init_git2_ssl()?;
401
402        match self.command {
403            Commands::Init {
404                bitbucket_url,
405                force,
406            } => commands::init::run(bitbucket_url, force).await,
407            Commands::Config { action } => commands::config::run(action).await,
408            Commands::Stacks { action } => commands::stack::run(action).await,
409            Commands::Entry { action } => commands::entry::run(action).await,
410            Commands::Repo => commands::status::run().await,
411            Commands::Version => commands::version::run().await,
412            Commands::Doctor => commands::doctor::run().await,
413            Commands::Diagnose => commands::diagnose::run().await,
414
415            Commands::Completions { action } => match action {
416                CompletionsAction::Generate { shell } => {
417                    commands::completions::generate_completions(shell)
418                }
419                CompletionsAction::Install { shell } => {
420                    commands::completions::install_completions(shell)
421                }
422                CompletionsAction::Status => commands::completions::show_completions_status(),
423            },
424
425            Commands::Setup { force } => commands::setup::run(force).await,
426
427            Commands::Tui => commands::tui::run().await,
428
429            Commands::Hooks { action } => match action {
430                HooksAction::Install {
431                    skip_checks,
432                    allow_main_branch,
433                    yes,
434                    force,
435                } => {
436                    commands::hooks::install_with_options(
437                        skip_checks,
438                        allow_main_branch,
439                        yes,
440                        force,
441                    )
442                    .await
443                }
444                HooksAction::Uninstall => commands::hooks::uninstall().await,
445                HooksAction::Status => commands::hooks::status().await,
446                HooksAction::Add {
447                    hook,
448                    skip_checks,
449                    force,
450                } => commands::hooks::install_hook_with_options(&hook, skip_checks, force).await,
451                HooksAction::Remove { hook } => commands::hooks::uninstall_hook(&hook).await,
452            },
453
454            Commands::Viz { action } => match action {
455                VizAction::Stack {
456                    name,
457                    format,
458                    output,
459                    compact,
460                    no_colors,
461                } => {
462                    commands::viz::show_stack(
463                        name.clone(),
464                        format.clone(),
465                        output.clone(),
466                        compact,
467                        no_colors,
468                    )
469                    .await
470                }
471                VizAction::Deps {
472                    format,
473                    output,
474                    compact,
475                    no_colors,
476                } => {
477                    commands::viz::show_dependencies(
478                        format.clone(),
479                        output.clone(),
480                        compact,
481                        no_colors,
482                    )
483                    .await
484                }
485            },
486
487            Commands::Stack { verbose, mergeable } => {
488                commands::stack::show(verbose, mergeable).await
489            }
490
491            Commands::Push {
492                branch,
493                message,
494                commit,
495                since,
496                commits,
497                squash,
498                squash_since,
499                auto_branch,
500                allow_base_branch,
501            } => {
502                commands::stack::push(
503                    branch,
504                    message,
505                    commit,
506                    since,
507                    commits,
508                    squash,
509                    squash_since,
510                    auto_branch,
511                    allow_base_branch,
512                )
513                .await
514            }
515
516            Commands::Pop { keep_branch } => commands::stack::pop(keep_branch).await,
517
518            Commands::Land {
519                entry,
520                force,
521                dry_run,
522                auto,
523                wait_for_builds,
524                strategy,
525                build_timeout,
526            } => {
527                commands::stack::land(
528                    entry,
529                    force,
530                    dry_run,
531                    auto,
532                    wait_for_builds,
533                    strategy,
534                    build_timeout,
535                )
536                .await
537            }
538
539            Commands::Autoland {
540                force,
541                dry_run,
542                wait_for_builds,
543                strategy,
544                build_timeout,
545            } => {
546                commands::stack::autoland(force, dry_run, wait_for_builds, strategy, build_timeout)
547                    .await
548            }
549
550            Commands::Sync {
551                force,
552                skip_cleanup,
553                interactive,
554            } => commands::stack::sync(force, skip_cleanup, interactive).await,
555
556            Commands::Rebase {
557                interactive,
558                onto,
559                strategy,
560            } => commands::stack::rebase(interactive, onto, strategy).await,
561
562            Commands::Switch { name } => commands::stack::switch(name).await,
563
564            Commands::Deactivate { force } => commands::stack::deactivate(force).await,
565
566            Commands::Submit {
567                entry,
568                title,
569                description,
570                range,
571                draft,
572            } => {
573                // Delegate to the stacks submit functionality
574                let submit_action = StackAction::Submit {
575                    entry,
576                    title,
577                    description,
578                    range,
579                    draft,
580                };
581                commands::stack::run(submit_action).await
582            }
583
584            Commands::Validate { name, fix } => {
585                // Delegate to the stacks validate functionality
586                let validate_action = StackAction::Validate { name, fix };
587                commands::stack::run(validate_action).await
588            }
589        }
590    }
591
592    /// Initialize git2 to use system certificates by default
593    /// This makes Cascade work like git CLI in corporate environments
594    fn init_git2_ssl(&self) -> Result<()> {
595        // Only import SSL functions on platforms that use them
596        #[cfg(any(target_os = "macos", target_os = "linux"))]
597        use git2::opts::{set_ssl_cert_dir, set_ssl_cert_file};
598
599        // Configure git2 to use system certificate store
600        // This matches behavior of git CLI and tools like Graphite/Sapling
601        tracing::debug!("Initializing git2 SSL configuration with system certificates");
602
603        // Try to use system certificate locations
604        // On macOS: /etc/ssl/cert.pem, /usr/local/etc/ssl/cert.pem
605        // On Linux: /etc/ssl/certs/ca-certificates.crt, /etc/ssl/certs/ca-bundle.crt
606        // On Windows: Uses Windows certificate store automatically
607
608        #[cfg(target_os = "macos")]
609        {
610            // macOS certificate locations (certificate files)
611            let cert_files = [
612                "/etc/ssl/cert.pem",
613                "/usr/local/etc/ssl/cert.pem",
614                "/opt/homebrew/etc/ca-certificates/cert.pem",
615            ];
616
617            for cert_path in &cert_files {
618                if std::path::Path::new(cert_path).exists() {
619                    tracing::debug!("Using macOS system certificates from: {}", cert_path);
620                    if let Err(e) = unsafe { set_ssl_cert_file(cert_path) } {
621                        tracing::trace!(
622                            "SSL cert file {} not supported by TLS backend: {}",
623                            cert_path,
624                            e
625                        );
626                    } else {
627                        return Ok(());
628                    }
629                }
630            }
631
632            // Fallback to certificate directories
633            let cert_dirs = ["/etc/ssl/certs", "/usr/local/etc/ssl/certs"];
634
635            for cert_dir in &cert_dirs {
636                if std::path::Path::new(cert_dir).exists() {
637                    tracing::debug!("Using macOS system certificate directory: {}", cert_dir);
638                    if let Err(e) = unsafe { set_ssl_cert_dir(cert_dir) } {
639                        tracing::trace!(
640                            "SSL cert directory {} not supported by TLS backend: {}",
641                            cert_dir,
642                            e
643                        );
644                    } else {
645                        return Ok(());
646                    }
647                }
648            }
649        }
650
651        #[cfg(target_os = "linux")]
652        {
653            // Linux certificate files
654            let cert_files = [
655                "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu
656                "/etc/ssl/certs/ca-bundle.crt",       // RHEL/CentOS
657                "/etc/pki/tls/certs/ca-bundle.crt",   // Fedora/RHEL
658                "/etc/ssl/ca-bundle.pem",             // OpenSUSE
659            ];
660
661            for cert_path in &cert_files {
662                if std::path::Path::new(cert_path).exists() {
663                    tracing::debug!("Using Linux system certificates from: {}", cert_path);
664                    if let Err(e) = unsafe { set_ssl_cert_file(cert_path) } {
665                        tracing::trace!(
666                            "SSL cert file {} not supported by TLS backend: {}",
667                            cert_path,
668                            e
669                        );
670                    } else {
671                        return Ok(());
672                    }
673                }
674            }
675
676            // Fallback to certificate directories
677            let cert_dirs = ["/etc/ssl/certs", "/etc/pki/tls/certs"];
678
679            for cert_dir in &cert_dirs {
680                if std::path::Path::new(cert_dir).exists() {
681                    tracing::debug!("Using Linux system certificate directory: {}", cert_dir);
682                    if let Err(e) = unsafe { set_ssl_cert_dir(cert_dir) } {
683                        tracing::trace!(
684                            "SSL cert directory {} not supported by TLS backend: {}",
685                            cert_dir,
686                            e
687                        );
688                    } else {
689                        return Ok(());
690                    }
691                }
692            }
693        }
694
695        #[cfg(target_os = "windows")]
696        {
697            // Windows uses system certificate store automatically via git2's default configuration
698            tracing::debug!("Using Windows system certificate store (automatic)");
699        }
700
701        tracing::debug!("System SSL certificate configuration complete");
702        tracing::info!(
703            "Note: SSL warnings from libgit2 are normal - git CLI fallback will be used if needed"
704        );
705        Ok(())
706    }
707
708    fn setup_logging(&self) {
709        let level = if self.verbose {
710            tracing::Level::DEBUG
711        } else {
712            tracing::Level::INFO
713        };
714
715        let subscriber = tracing_subscriber::fmt()
716            .with_max_level(level)
717            .with_target(false)
718            .without_time();
719
720        if self.no_color {
721            subscriber.with_ansi(false).init();
722        } else {
723            subscriber.init();
724        }
725    }
726}