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}