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}