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