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
245/// Git hooks actions
246#[derive(Debug, Subcommand)]
247pub enum HooksAction {
248    /// Install all Cascade Git hooks
249    Install {
250        /// Skip prerequisite checks (repository type, configuration validation)
251        #[arg(long)]
252        skip_checks: bool,
253
254        /// Allow installation on main/master branches (not recommended)
255        #[arg(long)]
256        allow_main_branch: bool,
257
258        /// Skip confirmation prompt
259        #[arg(long, short)]
260        yes: bool,
261
262        /// Force installation even if checks fail (not recommended)
263        #[arg(long)]
264        force: bool,
265    },
266
267    /// Uninstall all Cascade Git hooks
268    Uninstall,
269
270    /// Show Git hooks status
271    Status,
272
273    /// Install a specific hook
274    Add {
275        /// Hook name (post-commit, pre-push, commit-msg, prepare-commit-msg)
276        hook: String,
277
278        /// Skip prerequisite checks
279        #[arg(long)]
280        skip_checks: bool,
281
282        /// Force installation even if checks fail
283        #[arg(long)]
284        force: bool,
285    },
286
287    /// Remove a specific hook
288    Remove {
289        /// Hook name (post-commit, pre-push, commit-msg, prepare-commit-msg)
290        hook: String,
291    },
292}
293
294/// Visualization actions
295#[derive(Debug, Subcommand)]
296pub enum VizAction {
297    /// Show stack diagram
298    Stack {
299        /// Stack name (defaults to active stack)
300        name: Option<String>,
301        /// Output format (ascii, mermaid, dot, plantuml)
302        #[arg(long, short)]
303        format: Option<String>,
304        /// Output file path
305        #[arg(long, short)]
306        output: Option<String>,
307        /// Compact mode (less details)
308        #[arg(long)]
309        compact: bool,
310        /// Disable colors
311        #[arg(long)]
312        no_colors: bool,
313    },
314
315    /// Show dependency graph of all stacks
316    Deps {
317        /// Output format (ascii, mermaid, dot, plantuml)
318        #[arg(long, short)]
319        format: Option<String>,
320        /// Output file path
321        #[arg(long, short)]
322        output: Option<String>,
323        /// Compact mode (less details)
324        #[arg(long)]
325        compact: bool,
326        /// Disable colors
327        #[arg(long)]
328        no_colors: bool,
329    },
330}
331
332/// Shell completion actions
333#[derive(Debug, Subcommand)]
334pub enum CompletionsAction {
335    /// Generate completions for a shell
336    Generate {
337        /// Shell to generate completions for
338        #[arg(value_enum)]
339        shell: Shell,
340    },
341
342    /// Install completions for available shells
343    Install {
344        /// Specific shell to install for
345        #[arg(long, value_enum)]
346        shell: Option<Shell>,
347    },
348
349    /// Show completion installation status
350    Status,
351}
352
353#[derive(Subcommand)]
354pub enum ConfigAction {
355    /// Set a configuration value
356    Set {
357        /// Configuration key (e.g., bitbucket.url)
358        key: String,
359        /// Configuration value
360        value: String,
361    },
362
363    /// Get a configuration value
364    Get {
365        /// Configuration key
366        key: String,
367    },
368
369    /// List all configuration values
370    List,
371
372    /// Remove a configuration value
373    Unset {
374        /// Configuration key
375        key: String,
376    },
377}
378
379impl Cli {
380    pub async fn run(self) -> Result<()> {
381        // Set up logging based on verbosity
382        self.setup_logging();
383
384        match self.command {
385            Commands::Init {
386                bitbucket_url,
387                force,
388            } => commands::init::run(bitbucket_url, force).await,
389            Commands::Config { action } => commands::config::run(action).await,
390            Commands::Stacks { action } => commands::stack::run(action).await,
391            Commands::Entry { action } => commands::entry::run(action).await,
392            Commands::Repo => commands::status::run().await,
393            Commands::Version => commands::version::run().await,
394            Commands::Doctor => commands::doctor::run().await,
395
396            Commands::Completions { action } => match action {
397                CompletionsAction::Generate { shell } => {
398                    commands::completions::generate_completions(shell)
399                }
400                CompletionsAction::Install { shell } => {
401                    commands::completions::install_completions(shell)
402                }
403                CompletionsAction::Status => commands::completions::show_completions_status(),
404            },
405
406            Commands::Setup { force } => commands::setup::run(force).await,
407
408            Commands::Tui => commands::tui::run().await,
409
410            Commands::Hooks { action } => match action {
411                HooksAction::Install {
412                    skip_checks,
413                    allow_main_branch,
414                    yes,
415                    force,
416                } => {
417                    commands::hooks::install_with_options(
418                        skip_checks,
419                        allow_main_branch,
420                        yes,
421                        force,
422                    )
423                    .await
424                }
425                HooksAction::Uninstall => commands::hooks::uninstall().await,
426                HooksAction::Status => commands::hooks::status().await,
427                HooksAction::Add {
428                    hook,
429                    skip_checks,
430                    force,
431                } => commands::hooks::install_hook_with_options(&hook, skip_checks, force).await,
432                HooksAction::Remove { hook } => commands::hooks::uninstall_hook(&hook).await,
433            },
434
435            Commands::Viz { action } => match action {
436                VizAction::Stack {
437                    name,
438                    format,
439                    output,
440                    compact,
441                    no_colors,
442                } => {
443                    commands::viz::show_stack(
444                        name.clone(),
445                        format.clone(),
446                        output.clone(),
447                        compact,
448                        no_colors,
449                    )
450                    .await
451                }
452                VizAction::Deps {
453                    format,
454                    output,
455                    compact,
456                    no_colors,
457                } => {
458                    commands::viz::show_dependencies(
459                        format.clone(),
460                        output.clone(),
461                        compact,
462                        no_colors,
463                    )
464                    .await
465                }
466            },
467
468            Commands::Stack { verbose, mergeable } => {
469                commands::stack::show(verbose, mergeable).await
470            }
471
472            Commands::Push {
473                branch,
474                message,
475                commit,
476                since,
477                commits,
478                squash,
479                squash_since,
480                auto_branch,
481                allow_base_branch,
482            } => {
483                commands::stack::push(
484                    branch,
485                    message,
486                    commit,
487                    since,
488                    commits,
489                    squash,
490                    squash_since,
491                    auto_branch,
492                    allow_base_branch,
493                )
494                .await
495            }
496
497            Commands::Pop { keep_branch } => commands::stack::pop(keep_branch).await,
498
499            Commands::Land {
500                entry,
501                force,
502                dry_run,
503                auto,
504                wait_for_builds,
505                strategy,
506                build_timeout,
507            } => {
508                commands::stack::land(
509                    entry,
510                    force,
511                    dry_run,
512                    auto,
513                    wait_for_builds,
514                    strategy,
515                    build_timeout,
516                )
517                .await
518            }
519
520            Commands::Autoland {
521                force,
522                dry_run,
523                wait_for_builds,
524                strategy,
525                build_timeout,
526            } => {
527                commands::stack::autoland(force, dry_run, wait_for_builds, strategy, build_timeout)
528                    .await
529            }
530
531            Commands::Sync {
532                force,
533                skip_cleanup,
534                interactive,
535            } => commands::stack::sync(force, skip_cleanup, interactive).await,
536
537            Commands::Rebase {
538                interactive,
539                onto,
540                strategy,
541            } => commands::stack::rebase(interactive, onto, strategy).await,
542
543            Commands::Switch { name } => commands::stack::switch(name).await,
544
545            Commands::Deactivate { force } => commands::stack::deactivate(force).await,
546
547            Commands::Submit {
548                entry,
549                title,
550                description,
551                range,
552                draft,
553            } => {
554                // Delegate to the stacks submit functionality
555                let submit_action = StackAction::Submit {
556                    entry,
557                    title,
558                    description,
559                    range,
560                    draft,
561                };
562                commands::stack::run(submit_action).await
563            }
564        }
565    }
566
567    fn setup_logging(&self) {
568        let level = if self.verbose {
569            tracing::Level::DEBUG
570        } else {
571            tracing::Level::INFO
572        };
573
574        let subscriber = tracing_subscriber::fmt()
575            .with_max_level(level)
576            .with_target(false)
577            .without_time();
578
579        if self.no_color {
580            subscriber.with_ansi(false).init();
581        } else {
582            subscriber.init();
583        }
584    }
585}