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