cascade_cli/cli/commands/
stack.rs

1use crate::bitbucket::BitbucketIntegration;
2use crate::errors::{CascadeError, Result};
3use crate::git::{find_repository_root, GitRepository};
4use crate::stack::{StackManager, StackStatus};
5use clap::{Subcommand, ValueEnum};
6use indicatif::{ProgressBar, ProgressStyle};
7use std::env;
8use tracing::{info, warn};
9
10/// CLI argument version of RebaseStrategy
11#[derive(ValueEnum, Clone, Debug)]
12pub enum RebaseStrategyArg {
13    /// Create new branches with version suffixes
14    BranchVersioning,
15    /// Use cherry-pick to apply commits
16    CherryPick,
17    /// Create merge commits
18    ThreeWayMerge,
19    /// Interactive rebase
20    Interactive,
21}
22
23#[derive(ValueEnum, Clone, Debug)]
24pub enum MergeStrategyArg {
25    /// Create a merge commit
26    Merge,
27    /// Squash all commits into one
28    Squash,
29    /// Fast-forward merge when possible
30    FastForward,
31}
32
33impl From<MergeStrategyArg> for crate::bitbucket::pull_request::MergeStrategy {
34    fn from(arg: MergeStrategyArg) -> Self {
35        match arg {
36            MergeStrategyArg::Merge => Self::Merge,
37            MergeStrategyArg::Squash => Self::Squash,
38            MergeStrategyArg::FastForward => Self::FastForward,
39        }
40    }
41}
42
43#[derive(Subcommand)]
44pub enum StackAction {
45    /// Create a new stack
46    Create {
47        /// Name of the stack
48        name: String,
49        /// Base branch for the stack
50        #[arg(long, short)]
51        base: Option<String>,
52        /// Description of the stack
53        #[arg(long, short)]
54        description: Option<String>,
55    },
56
57    /// List all stacks
58    List {
59        /// Show detailed information
60        #[arg(long, short)]
61        verbose: bool,
62        /// Show only active stack
63        #[arg(long)]
64        active: bool,
65        /// Output format (name, id, status)
66        #[arg(long)]
67        format: Option<String>,
68    },
69
70    /// Switch to a different stack
71    Switch {
72        /// Name of the stack to switch to
73        name: String,
74    },
75
76    /// Deactivate the current stack (turn off stack mode)
77    Deactivate {
78        /// Force deactivation without confirmation
79        #[arg(long)]
80        force: bool,
81    },
82
83    /// Show the current stack status  
84    Show {
85        /// Show detailed pull request information
86        #[arg(short, long)]
87        verbose: bool,
88        /// Show mergability status for all PRs
89        #[arg(short, long)]
90        mergeable: bool,
91    },
92
93    /// Push current commit to the top of the stack
94    Push {
95        /// Branch name for this commit
96        #[arg(long, short)]
97        branch: Option<String>,
98        /// Commit message (if creating a new commit)
99        #[arg(long, short)]
100        message: Option<String>,
101        /// Use specific commit hash instead of HEAD
102        #[arg(long)]
103        commit: Option<String>,
104        /// Push commits since this reference (e.g., HEAD~3)
105        #[arg(long)]
106        since: Option<String>,
107        /// Push multiple specific commits (comma-separated)
108        #[arg(long)]
109        commits: Option<String>,
110        /// Squash unpushed commits before pushing (optional: specify count)
111        #[arg(long, num_args = 0..=1, default_missing_value = "0")]
112        squash: Option<usize>,
113        /// Squash all commits since this reference (e.g., HEAD~5)
114        #[arg(long)]
115        squash_since: Option<String>,
116        /// Auto-create feature branch when pushing from base branch
117        #[arg(long)]
118        auto_branch: bool,
119        /// Allow pushing commits from base branch (not recommended)
120        #[arg(long)]
121        allow_base_branch: bool,
122    },
123
124    /// Pop the top commit from the stack
125    Pop {
126        /// Keep the branch (don't delete it)
127        #[arg(long)]
128        keep_branch: bool,
129    },
130
131    /// Submit a stack entry for review
132    Submit {
133        /// Stack entry number (1-based, defaults to all unsubmitted)
134        entry: Option<usize>,
135        /// Pull request title
136        #[arg(long, short)]
137        title: Option<String>,
138        /// Pull request description
139        #[arg(long, short)]
140        description: Option<String>,
141        /// Submit range of entries (e.g., "1-3" or "2,4,6")
142        #[arg(long)]
143        range: Option<String>,
144        /// Create draft pull requests (can be edited later)
145        #[arg(long)]
146        draft: bool,
147    },
148
149    /// Check status of all pull requests in a stack
150    Status {
151        /// Name of the stack (defaults to active stack)
152        name: Option<String>,
153    },
154
155    /// List all pull requests for the repository
156    Prs {
157        /// Filter by state (open, merged, declined)
158        #[arg(long)]
159        state: Option<String>,
160        /// Show detailed information
161        #[arg(long, short)]
162        verbose: bool,
163    },
164
165    /// Check stack status with remote repository (read-only)
166    Check {
167        /// Force check even if there are issues
168        #[arg(long)]
169        force: bool,
170    },
171
172    /// Sync stack with remote repository (pull + rebase + cleanup)
173    Sync {
174        /// Force sync even if there are conflicts
175        #[arg(long)]
176        force: bool,
177        /// Skip cleanup of merged branches
178        #[arg(long)]
179        skip_cleanup: bool,
180        /// Interactive mode for conflict resolution
181        #[arg(long, short)]
182        interactive: bool,
183    },
184
185    /// Rebase stack on updated base branch
186    Rebase {
187        /// Interactive rebase
188        #[arg(long, short)]
189        interactive: bool,
190        /// Target base branch (defaults to stack's base branch)
191        #[arg(long)]
192        onto: Option<String>,
193        /// Rebase strategy to use
194        #[arg(long, value_enum)]
195        strategy: Option<RebaseStrategyArg>,
196    },
197
198    /// Continue an in-progress rebase after resolving conflicts
199    ContinueRebase,
200
201    /// Abort an in-progress rebase
202    AbortRebase,
203
204    /// Show rebase status and conflict resolution guidance
205    RebaseStatus,
206
207    /// Delete a stack
208    Delete {
209        /// Name of the stack to delete
210        name: String,
211        /// Force deletion without confirmation
212        #[arg(long)]
213        force: bool,
214    },
215
216    /// Validate stack integrity
217    Validate {
218        /// Name of the stack (defaults to active stack)
219        name: Option<String>,
220    },
221
222    /// Land (merge) approved stack entries
223    Land {
224        /// Stack entry number to land (1-based index, optional)
225        entry: Option<usize>,
226        /// Force land even with blocking issues (dangerous)
227        #[arg(short, long)]
228        force: bool,
229        /// Dry run - show what would be landed without doing it
230        #[arg(short, long)]
231        dry_run: bool,
232        /// Use server-side validation (safer, checks approvals/builds)
233        #[arg(long)]
234        auto: bool,
235        /// Wait for builds to complete before merging
236        #[arg(long)]
237        wait_for_builds: bool,
238        /// Merge strategy to use
239        #[arg(long, value_enum, default_value = "squash")]
240        strategy: Option<MergeStrategyArg>,
241        /// Maximum time to wait for builds (seconds)
242        #[arg(long, default_value = "1800")]
243        build_timeout: u64,
244    },
245
246    /// Auto-land all ready PRs (shorthand for land --auto)
247    AutoLand {
248        /// Force land even with blocking issues (dangerous)
249        #[arg(short, long)]
250        force: bool,
251        /// Dry run - show what would be landed without doing it
252        #[arg(short, long)]
253        dry_run: bool,
254        /// Wait for builds to complete before merging
255        #[arg(long)]
256        wait_for_builds: bool,
257        /// Merge strategy to use
258        #[arg(long, value_enum, default_value = "squash")]
259        strategy: Option<MergeStrategyArg>,
260        /// Maximum time to wait for builds (seconds)
261        #[arg(long, default_value = "1800")]
262        build_timeout: u64,
263    },
264
265    /// List pull requests from Bitbucket
266    ListPrs {
267        /// Filter by state (open, merged, declined)
268        #[arg(short, long)]
269        state: Option<String>,
270        /// Show detailed information
271        #[arg(short, long)]
272        verbose: bool,
273    },
274
275    /// Continue an in-progress land operation after resolving conflicts
276    ContinueLand,
277
278    /// Abort an in-progress land operation  
279    AbortLand,
280
281    /// Show status of in-progress land operation
282    LandStatus,
283}
284
285pub async fn run(action: StackAction) -> Result<()> {
286    match action {
287        StackAction::Create {
288            name,
289            base,
290            description,
291        } => create_stack(name, base, description).await,
292        StackAction::List {
293            verbose,
294            active,
295            format,
296        } => list_stacks(verbose, active, format).await,
297        StackAction::Switch { name } => switch_stack(name).await,
298        StackAction::Deactivate { force } => deactivate_stack(force).await,
299        StackAction::Show { verbose, mergeable } => show_stack(verbose, mergeable).await,
300        StackAction::Push {
301            branch,
302            message,
303            commit,
304            since,
305            commits,
306            squash,
307            squash_since,
308            auto_branch,
309            allow_base_branch,
310        } => {
311            push_to_stack(
312                branch,
313                message,
314                commit,
315                since,
316                commits,
317                squash,
318                squash_since,
319                auto_branch,
320                allow_base_branch,
321            )
322            .await
323        }
324        StackAction::Pop { keep_branch } => pop_from_stack(keep_branch).await,
325        StackAction::Submit {
326            entry,
327            title,
328            description,
329            range,
330            draft,
331        } => submit_entry(entry, title, description, range, draft).await,
332        StackAction::Status { name } => check_stack_status(name).await,
333        StackAction::Prs { state, verbose } => list_pull_requests(state, verbose).await,
334        StackAction::Check { force } => check_stack(force).await,
335        StackAction::Sync {
336            force,
337            skip_cleanup,
338            interactive,
339        } => sync_stack(force, skip_cleanup, interactive).await,
340        StackAction::Rebase {
341            interactive,
342            onto,
343            strategy,
344        } => rebase_stack(interactive, onto, strategy).await,
345        StackAction::ContinueRebase => continue_rebase().await,
346        StackAction::AbortRebase => abort_rebase().await,
347        StackAction::RebaseStatus => rebase_status().await,
348        StackAction::Delete { name, force } => delete_stack(name, force).await,
349        StackAction::Validate { name } => validate_stack(name).await,
350        StackAction::Land {
351            entry,
352            force,
353            dry_run,
354            auto,
355            wait_for_builds,
356            strategy,
357            build_timeout,
358        } => {
359            land_stack(
360                entry,
361                force,
362                dry_run,
363                auto,
364                wait_for_builds,
365                strategy,
366                build_timeout,
367            )
368            .await
369        }
370        StackAction::AutoLand {
371            force,
372            dry_run,
373            wait_for_builds,
374            strategy,
375            build_timeout,
376        } => auto_land_stack(force, dry_run, wait_for_builds, strategy, build_timeout).await,
377        StackAction::ListPrs { state, verbose } => list_pull_requests(state, verbose).await,
378        StackAction::ContinueLand => continue_land().await,
379        StackAction::AbortLand => abort_land().await,
380        StackAction::LandStatus => land_status().await,
381    }
382}
383
384// Public functions for shortcut commands
385pub async fn show(verbose: bool, mergeable: bool) -> Result<()> {
386    show_stack(verbose, mergeable).await
387}
388
389#[allow(clippy::too_many_arguments)]
390pub async fn push(
391    branch: Option<String>,
392    message: Option<String>,
393    commit: Option<String>,
394    since: Option<String>,
395    commits: Option<String>,
396    squash: Option<usize>,
397    squash_since: Option<String>,
398    auto_branch: bool,
399    allow_base_branch: bool,
400) -> Result<()> {
401    push_to_stack(
402        branch,
403        message,
404        commit,
405        since,
406        commits,
407        squash,
408        squash_since,
409        auto_branch,
410        allow_base_branch,
411    )
412    .await
413}
414
415pub async fn pop(keep_branch: bool) -> Result<()> {
416    pop_from_stack(keep_branch).await
417}
418
419pub async fn land(
420    entry: Option<usize>,
421    force: bool,
422    dry_run: bool,
423    auto: bool,
424    wait_for_builds: bool,
425    strategy: Option<MergeStrategyArg>,
426    build_timeout: u64,
427) -> Result<()> {
428    land_stack(
429        entry,
430        force,
431        dry_run,
432        auto,
433        wait_for_builds,
434        strategy,
435        build_timeout,
436    )
437    .await
438}
439
440pub async fn autoland(
441    force: bool,
442    dry_run: bool,
443    wait_for_builds: bool,
444    strategy: Option<MergeStrategyArg>,
445    build_timeout: u64,
446) -> Result<()> {
447    auto_land_stack(force, dry_run, wait_for_builds, strategy, build_timeout).await
448}
449
450pub async fn sync(force: bool, skip_cleanup: bool, interactive: bool) -> Result<()> {
451    sync_stack(force, skip_cleanup, interactive).await
452}
453
454pub async fn rebase(
455    interactive: bool,
456    onto: Option<String>,
457    strategy: Option<RebaseStrategyArg>,
458) -> Result<()> {
459    rebase_stack(interactive, onto, strategy).await
460}
461
462pub async fn deactivate(force: bool) -> Result<()> {
463    deactivate_stack(force).await
464}
465
466pub async fn switch(name: String) -> Result<()> {
467    switch_stack(name).await
468}
469
470async fn create_stack(
471    name: String,
472    base: Option<String>,
473    description: Option<String>,
474) -> Result<()> {
475    let current_dir = env::current_dir()
476        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
477
478    let repo_root = find_repository_root(&current_dir)
479        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
480
481    let mut manager = StackManager::new(&repo_root)?;
482    let stack_id = manager.create_stack(name.clone(), base.clone(), description.clone())?;
483
484    info!("✅ Created stack '{}'", name);
485    if let Some(base_branch) = base {
486        info!("   Base branch: {}", base_branch);
487    }
488    if let Some(desc) = description {
489        info!("   Description: {}", desc);
490    }
491    info!("   Stack ID: {}", stack_id);
492    info!("   Stack is now active");
493
494    Ok(())
495}
496
497async fn list_stacks(verbose: bool, _active: bool, _format: Option<String>) -> Result<()> {
498    let current_dir = env::current_dir()
499        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
500
501    let repo_root = find_repository_root(&current_dir)
502        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
503
504    let manager = StackManager::new(&repo_root)?;
505    let stacks = manager.list_stacks();
506
507    if stacks.is_empty() {
508        info!("No stacks found. Create one with: ca stack create <name>");
509        return Ok(());
510    }
511
512    println!("📚 Stacks:");
513    for (stack_id, name, status, entry_count, active_marker) in stacks {
514        let status_icon = match status {
515            StackStatus::Clean => "✅",
516            StackStatus::Dirty => "🔄",
517            StackStatus::OutOfSync => "⚠️",
518            StackStatus::Conflicted => "❌",
519            StackStatus::Rebasing => "🔀",
520            StackStatus::NeedsSync => "🔄",
521            StackStatus::Corrupted => "💥",
522        };
523
524        let active_indicator = if active_marker.is_some() {
525            " (active)"
526        } else {
527            ""
528        };
529
530        if verbose {
531            println!("  {status_icon} {name} [{entry_count}]{active_indicator}");
532            println!("    ID: {stack_id}");
533            if let Some(stack_meta) = manager.get_stack_metadata(&stack_id) {
534                println!("    Base: {}", stack_meta.base_branch);
535                if let Some(desc) = &stack_meta.description {
536                    println!("    Description: {desc}");
537                }
538                println!(
539                    "    Commits: {} total, {} submitted",
540                    stack_meta.total_commits, stack_meta.submitted_commits
541                );
542                if stack_meta.has_conflicts {
543                    println!("    ⚠️  Has conflicts");
544                }
545            }
546            println!();
547        } else {
548            println!("  {status_icon} {name} [{entry_count}]{active_indicator}");
549        }
550    }
551
552    if !verbose {
553        println!("\nUse --verbose for more details");
554    }
555
556    Ok(())
557}
558
559async fn switch_stack(name: String) -> Result<()> {
560    let current_dir = env::current_dir()
561        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
562
563    let repo_root = find_repository_root(&current_dir)
564        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
565
566    let mut manager = StackManager::new(&repo_root)?;
567
568    // Verify stack exists
569    if manager.get_stack_by_name(&name).is_none() {
570        return Err(CascadeError::config(format!("Stack '{name}' not found")));
571    }
572
573    manager.set_active_stack_by_name(&name)?;
574    info!("✅ Switched to stack '{}'", name);
575
576    Ok(())
577}
578
579async fn deactivate_stack(force: bool) -> Result<()> {
580    let current_dir = env::current_dir()
581        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
582
583    let repo_root = find_repository_root(&current_dir)
584        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
585
586    let mut manager = StackManager::new(&repo_root)?;
587
588    let active_stack = manager.get_active_stack();
589
590    if active_stack.is_none() {
591        println!("ℹ️  No active stack to deactivate");
592        return Ok(());
593    }
594
595    let stack_name = active_stack.unwrap().name.clone();
596
597    if !force {
598        println!("⚠️  This will deactivate stack '{stack_name}' and return to normal Git workflow");
599        println!("   You can reactivate it later with 'ca stacks switch {stack_name}'");
600        print!("   Continue? (y/N): ");
601
602        use std::io::{self, Write};
603        io::stdout().flush().unwrap();
604
605        let mut input = String::new();
606        io::stdin().read_line(&mut input).unwrap();
607
608        if !input.trim().to_lowercase().starts_with('y') {
609            println!("Cancelled deactivation");
610            return Ok(());
611        }
612    }
613
614    // Deactivate the stack
615    manager.set_active_stack(None)?;
616
617    println!("✅ Deactivated stack '{stack_name}'");
618    println!("   Stack management is now OFF - you can use normal Git workflow");
619    println!("   To reactivate: ca stacks switch {stack_name}");
620
621    Ok(())
622}
623
624async fn show_stack(verbose: bool, show_mergeable: bool) -> Result<()> {
625    let current_dir = env::current_dir()
626        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
627
628    let repo_root = find_repository_root(&current_dir)
629        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
630
631    let stack_manager = StackManager::new(&repo_root)?;
632
633    // Get stack information first to avoid borrow conflicts
634    let (stack_id, stack_name, stack_base, stack_entries) = {
635        let active_stack = stack_manager.get_active_stack().ok_or_else(|| {
636            CascadeError::config(
637                "No active stack. Use 'ca stacks create' or 'ca stacks switch' to select a stack"
638                    .to_string(),
639            )
640        })?;
641
642        (
643            active_stack.id,
644            active_stack.name.clone(),
645            active_stack.base_branch.clone(),
646            active_stack.entries.clone(),
647        )
648    };
649
650    println!("📊 Stack: {stack_name}");
651    println!("   Base branch: {stack_base}");
652    println!("   Total entries: {}", stack_entries.len());
653
654    if stack_entries.is_empty() {
655        println!("   No entries in this stack yet");
656        println!("   Use 'ca push' to add commits to this stack");
657        return Ok(());
658    }
659
660    // Show entries
661    println!("\n📚 Stack Entries:");
662    for (i, entry) in stack_entries.iter().enumerate() {
663        let entry_num = i + 1;
664        let short_hash = entry.short_hash();
665        let short_msg = entry.short_message(50);
666
667        // Get source branch information if available
668        let metadata = stack_manager.get_repository_metadata();
669        let source_branch_info = if let Some(commit_meta) = metadata.get_commit(&entry.commit_hash)
670        {
671            if commit_meta.source_branch != commit_meta.branch
672                && !commit_meta.source_branch.is_empty()
673            {
674                format!(" (from {})", commit_meta.source_branch)
675            } else {
676                String::new()
677            }
678        } else {
679            String::new()
680        };
681
682        println!(
683            "   {entry_num}. {} {} {}{}",
684            short_hash,
685            if entry.is_submitted { "📤" } else { "📝" },
686            short_msg,
687            source_branch_info
688        );
689
690        if verbose {
691            println!("      Branch: {}", entry.branch);
692            println!(
693                "      Created: {}",
694                entry.created_at.format("%Y-%m-%d %H:%M")
695            );
696            if let Some(pr_id) = &entry.pull_request_id {
697                println!("      PR: #{pr_id}");
698            }
699        }
700    }
701
702    // Enhanced PR status if requested and available
703    if show_mergeable {
704        println!("\n🔍 Mergability Status:");
705
706        // Load configuration and create Bitbucket integration
707        let config_dir = crate::config::get_repo_config_dir(&current_dir)?;
708        let config_path = config_dir.join("config.json");
709        let settings = crate::config::Settings::load_from_file(&config_path)?;
710
711        let cascade_config = crate::config::CascadeConfig {
712            bitbucket: Some(settings.bitbucket.clone()),
713            git: settings.git.clone(),
714            auth: crate::config::AuthConfig::default(),
715        };
716
717        let integration =
718            crate::bitbucket::BitbucketIntegration::new(stack_manager, cascade_config)?;
719
720        match integration.check_enhanced_stack_status(&stack_id).await {
721            Ok(status) => {
722                println!("   Total entries: {}", status.total_entries);
723                println!("   Submitted: {}", status.submitted_entries);
724                println!("   Open PRs: {}", status.open_prs);
725                println!("   Merged PRs: {}", status.merged_prs);
726                println!("   Declined PRs: {}", status.declined_prs);
727                println!("   Completion: {:.1}%", status.completion_percentage());
728
729                if !status.enhanced_statuses.is_empty() {
730                    println!("\n📋 Pull Request Status:");
731                    let mut ready_to_land = 0;
732
733                    for enhanced in &status.enhanced_statuses {
734                        let status_display = enhanced.get_display_status();
735                        let ready_icon = if enhanced.is_ready_to_land() {
736                            ready_to_land += 1;
737                            "🚀"
738                        } else {
739                            "⏳"
740                        };
741
742                        println!(
743                            "   {} PR #{}: {} ({})",
744                            ready_icon, enhanced.pr.id, enhanced.pr.title, status_display
745                        );
746
747                        if verbose {
748                            println!(
749                                "      {} -> {}",
750                                enhanced.pr.from_ref.display_id, enhanced.pr.to_ref.display_id
751                            );
752
753                            // Show blocking reasons if not ready
754                            if !enhanced.is_ready_to_land() {
755                                let blocking = enhanced.get_blocking_reasons();
756                                if !blocking.is_empty() {
757                                    println!("      Blocking: {}", blocking.join(", "));
758                                }
759                            }
760
761                            // Show review details
762                            println!(
763                                "      Reviews: {}/{} approvals",
764                                enhanced.review_status.current_approvals,
765                                enhanced.review_status.required_approvals
766                            );
767
768                            if enhanced.review_status.needs_work_count > 0 {
769                                println!(
770                                    "      {} reviewers requested changes",
771                                    enhanced.review_status.needs_work_count
772                                );
773                            }
774
775                            // Show build status
776                            if let Some(build) = &enhanced.build_status {
777                                let build_icon = match build.state {
778                                    crate::bitbucket::pull_request::BuildState::Successful => "✅",
779                                    crate::bitbucket::pull_request::BuildState::Failed => "❌",
780                                    crate::bitbucket::pull_request::BuildState::InProgress => "🔄",
781                                    _ => "⚪",
782                                };
783                                println!("      Build: {} {:?}", build_icon, build.state);
784                            }
785
786                            if let Some(url) = enhanced.pr.web_url() {
787                                println!("      URL: {url}");
788                            }
789                            println!();
790                        }
791                    }
792
793                    if ready_to_land > 0 {
794                        println!(
795                            "\n🎯 {} PR{} ready to land! Use 'ca land' to land them all.",
796                            ready_to_land,
797                            if ready_to_land == 1 { " is" } else { "s are" }
798                        );
799                    }
800                }
801            }
802            Err(e) => {
803                warn!("Failed to get enhanced stack status: {}", e);
804                println!("   ⚠️  Could not fetch mergability status");
805                println!("   Use 'ca stack show --verbose' for basic PR information");
806            }
807        }
808    } else {
809        // Original PR status display for compatibility
810        let config_dir = crate::config::get_repo_config_dir(&current_dir)?;
811        let config_path = config_dir.join("config.json");
812        let settings = crate::config::Settings::load_from_file(&config_path)?;
813
814        let cascade_config = crate::config::CascadeConfig {
815            bitbucket: Some(settings.bitbucket.clone()),
816            git: settings.git.clone(),
817            auth: crate::config::AuthConfig::default(),
818        };
819
820        let integration =
821            crate::bitbucket::BitbucketIntegration::new(stack_manager, cascade_config)?;
822
823        match integration.check_stack_status(&stack_id).await {
824            Ok(status) => {
825                println!("\n📊 Pull Request Status:");
826                println!("   Total entries: {}", status.total_entries);
827                println!("   Submitted: {}", status.submitted_entries);
828                println!("   Open PRs: {}", status.open_prs);
829                println!("   Merged PRs: {}", status.merged_prs);
830                println!("   Declined PRs: {}", status.declined_prs);
831                println!("   Completion: {:.1}%", status.completion_percentage());
832
833                if !status.pull_requests.is_empty() {
834                    println!("\n📋 Pull Requests:");
835                    for pr in &status.pull_requests {
836                        let state_icon = match pr.state {
837                            crate::bitbucket::PullRequestState::Open => "🔄",
838                            crate::bitbucket::PullRequestState::Merged => "✅",
839                            crate::bitbucket::PullRequestState::Declined => "❌",
840                        };
841                        println!(
842                            "   {} PR #{}: {} ({} -> {})",
843                            state_icon,
844                            pr.id,
845                            pr.title,
846                            pr.from_ref.display_id,
847                            pr.to_ref.display_id
848                        );
849                        if let Some(url) = pr.web_url() {
850                            println!("      URL: {url}");
851                        }
852                    }
853                }
854
855                println!("\n💡 Use 'ca stack show --mergeable' to see detailed status including build and review information");
856            }
857            Err(e) => {
858                warn!("Failed to check stack status: {}", e);
859            }
860        }
861    }
862
863    Ok(())
864}
865
866#[allow(clippy::too_many_arguments)]
867async fn push_to_stack(
868    branch: Option<String>,
869    message: Option<String>,
870    commit: Option<String>,
871    since: Option<String>,
872    commits: Option<String>,
873    squash: Option<usize>,
874    squash_since: Option<String>,
875    auto_branch: bool,
876    allow_base_branch: bool,
877) -> Result<()> {
878    let current_dir = env::current_dir()
879        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
880
881    let repo_root = find_repository_root(&current_dir)
882        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
883
884    let mut manager = StackManager::new(&repo_root)?;
885    let repo = GitRepository::open(&repo_root)?;
886
887    // Check for branch changes and prompt user if needed
888    if !manager.check_for_branch_change()? {
889        return Ok(()); // User chose to cancel or deactivate stack
890    }
891
892    // Get the active stack to check base branch
893    let active_stack = manager.get_active_stack().ok_or_else(|| {
894        CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
895    })?;
896
897    // 🛡️ BASE BRANCH PROTECTION
898    let current_branch = repo.get_current_branch()?;
899    let base_branch = &active_stack.base_branch;
900
901    if current_branch == *base_branch {
902        println!("🚨 WARNING: You're currently on the base branch '{base_branch}'");
903        println!("   Making commits directly on the base branch is not recommended.");
904        println!("   This can pollute the base branch with work-in-progress commits.");
905
906        // Check if user explicitly allowed base branch work
907        if allow_base_branch {
908            println!("   ⚠️  Proceeding anyway due to --allow-base-branch flag");
909        } else {
910            // Check if we have uncommitted changes
911            let has_changes = repo.is_dirty()?;
912
913            if has_changes {
914                if auto_branch {
915                    // Auto-create branch and commit changes
916                    let feature_branch = format!("feature/{}-work", active_stack.name);
917                    println!("🚀 Auto-creating feature branch '{feature_branch}'...");
918
919                    repo.create_branch(&feature_branch, None)?;
920                    repo.checkout_branch(&feature_branch)?;
921
922                    println!("✅ Created and switched to '{feature_branch}'");
923                    println!("   You can now commit and push your changes safely");
924
925                    // Continue with normal flow
926                } else {
927                    println!("\n💡 You have uncommitted changes. Here are your options:");
928                    println!("   1. Create a feature branch first:");
929                    println!("      git checkout -b feature/my-work");
930                    println!("      git commit -am \"your work\"");
931                    println!("      ca push");
932                    println!("\n   2. Auto-create a branch (recommended):");
933                    println!("      ca push --auto-branch");
934                    println!("\n   3. Force push to base branch (dangerous):");
935                    println!("      ca push --allow-base-branch");
936
937                    return Err(CascadeError::config(
938                        "Refusing to push uncommitted changes from base branch. Use one of the options above."
939                    ));
940                }
941            } else {
942                // Check if there are existing commits to push
943                let commits_to_check = if let Some(commits_str) = &commits {
944                    commits_str
945                        .split(',')
946                        .map(|s| s.trim().to_string())
947                        .collect::<Vec<String>>()
948                } else if let Some(since_ref) = &since {
949                    let since_commit = repo.resolve_reference(since_ref)?;
950                    let head_commit = repo.get_head_commit()?;
951                    let commits = repo.get_commits_between(
952                        &since_commit.id().to_string(),
953                        &head_commit.id().to_string(),
954                    )?;
955                    commits.into_iter().map(|c| c.id().to_string()).collect()
956                } else if commit.is_none() {
957                    let mut unpushed = Vec::new();
958                    let head_commit = repo.get_head_commit()?;
959                    let mut current_commit = head_commit;
960
961                    loop {
962                        let commit_hash = current_commit.id().to_string();
963                        let already_in_stack = active_stack
964                            .entries
965                            .iter()
966                            .any(|entry| entry.commit_hash == commit_hash);
967
968                        if already_in_stack {
969                            break;
970                        }
971
972                        unpushed.push(commit_hash);
973
974                        if let Some(parent) = current_commit.parents().next() {
975                            current_commit = parent;
976                        } else {
977                            break;
978                        }
979                    }
980
981                    unpushed.reverse();
982                    unpushed
983                } else {
984                    vec![repo.get_head_commit()?.id().to_string()]
985                };
986
987                if !commits_to_check.is_empty() {
988                    if auto_branch {
989                        // Auto-create feature branch and cherry-pick commits
990                        let feature_branch = format!("feature/{}-work", active_stack.name);
991                        println!("🚀 Auto-creating feature branch '{feature_branch}'...");
992
993                        repo.create_branch(&feature_branch, Some(base_branch))?;
994                        repo.checkout_branch(&feature_branch)?;
995
996                        // Cherry-pick the commits to the new branch
997                        println!(
998                            "🍒 Cherry-picking {} commit(s) to new branch...",
999                            commits_to_check.len()
1000                        );
1001                        for commit_hash in &commits_to_check {
1002                            match repo.cherry_pick(commit_hash) {
1003                                Ok(_) => println!("   ✅ Cherry-picked {}", &commit_hash[..8]),
1004                                Err(e) => {
1005                                    println!(
1006                                        "   ❌ Failed to cherry-pick {}: {}",
1007                                        &commit_hash[..8],
1008                                        e
1009                                    );
1010                                    println!("   💡 You may need to resolve conflicts manually");
1011                                    return Err(CascadeError::branch(format!(
1012                                        "Failed to cherry-pick commit {commit_hash}: {e}"
1013                                    )));
1014                                }
1015                            }
1016                        }
1017
1018                        println!(
1019                            "✅ Successfully moved {} commit(s) to '{feature_branch}'",
1020                            commits_to_check.len()
1021                        );
1022                        println!(
1023                            "   You're now on the feature branch and can continue with 'ca push'"
1024                        );
1025
1026                        // Continue with normal flow
1027                    } else {
1028                        println!(
1029                            "\n💡 Found {} commit(s) to push from base branch '{base_branch}'",
1030                            commits_to_check.len()
1031                        );
1032                        println!("   These commits are currently ON the base branch, which may not be intended.");
1033                        println!("\n   Options:");
1034                        println!("   1. Auto-create feature branch and cherry-pick commits:");
1035                        println!("      ca push --auto-branch");
1036                        println!("\n   2. Manually create branch and move commits:");
1037                        println!("      git checkout -b feature/my-work");
1038                        println!("      ca push");
1039                        println!("\n   3. Force push from base branch (not recommended):");
1040                        println!("      ca push --allow-base-branch");
1041
1042                        return Err(CascadeError::config(
1043                            "Refusing to push commits from base branch. Use --auto-branch or create a feature branch manually."
1044                        ));
1045                    }
1046                }
1047            }
1048        }
1049    }
1050
1051    // Handle squash operations first
1052    if let Some(squash_count) = squash {
1053        if squash_count == 0 {
1054            // User used --squash without specifying count, auto-detect unpushed commits
1055            let active_stack = manager.get_active_stack().ok_or_else(|| {
1056                CascadeError::config(
1057                    "No active stack. Create a stack first with 'ca stacks create'",
1058                )
1059            })?;
1060
1061            let unpushed_count = get_unpushed_commits(&repo, active_stack)?.len();
1062
1063            if unpushed_count == 0 {
1064                println!("ℹ️  No unpushed commits to squash");
1065            } else if unpushed_count == 1 {
1066                println!("ℹ️  Only 1 unpushed commit, no squashing needed");
1067            } else {
1068                println!("🔄 Auto-detected {unpushed_count} unpushed commits, squashing...");
1069                squash_commits(&repo, unpushed_count, None).await?;
1070                println!("✅ Squashed {unpushed_count} unpushed commits into one");
1071            }
1072        } else {
1073            println!("🔄 Squashing last {squash_count} commits...");
1074            squash_commits(&repo, squash_count, None).await?;
1075            println!("✅ Squashed {squash_count} commits into one");
1076        }
1077    } else if let Some(since_ref) = squash_since {
1078        println!("🔄 Squashing commits since {since_ref}...");
1079        let since_commit = repo.resolve_reference(&since_ref)?;
1080        let commits_count = count_commits_since(&repo, &since_commit.id().to_string())?;
1081        squash_commits(&repo, commits_count, Some(since_ref.clone())).await?;
1082        println!("✅ Squashed {commits_count} commits since {since_ref} into one");
1083    }
1084
1085    // Determine which commits to push
1086    let commits_to_push = if let Some(commits_str) = commits {
1087        // Parse comma-separated commit hashes
1088        commits_str
1089            .split(',')
1090            .map(|s| s.trim().to_string())
1091            .collect::<Vec<String>>()
1092    } else if let Some(since_ref) = since {
1093        // Get commits since the specified reference
1094        let since_commit = repo.resolve_reference(&since_ref)?;
1095        let head_commit = repo.get_head_commit()?;
1096
1097        // Get commits between since_ref and HEAD
1098        let commits = repo.get_commits_between(
1099            &since_commit.id().to_string(),
1100            &head_commit.id().to_string(),
1101        )?;
1102        commits.into_iter().map(|c| c.id().to_string()).collect()
1103    } else if let Some(hash) = commit {
1104        // Single specific commit
1105        vec![hash]
1106    } else {
1107        // Default: Get all unpushed commits (commits not in any stack entry)
1108        let active_stack = manager.get_active_stack().ok_or_else(|| {
1109            CascadeError::config("No active stack. Create a stack first with 'ca stacks create'")
1110        })?;
1111
1112        let mut unpushed = Vec::new();
1113        let head_commit = repo.get_head_commit()?;
1114        let mut current_commit = head_commit;
1115
1116        // Walk back from HEAD until we find a commit that's already in the stack
1117        loop {
1118            let commit_hash = current_commit.id().to_string();
1119            let already_in_stack = active_stack
1120                .entries
1121                .iter()
1122                .any(|entry| entry.commit_hash == commit_hash);
1123
1124            if already_in_stack {
1125                break;
1126            }
1127
1128            unpushed.push(commit_hash);
1129
1130            // Move to parent commit
1131            if let Some(parent) = current_commit.parents().next() {
1132                current_commit = parent;
1133            } else {
1134                break;
1135            }
1136        }
1137
1138        unpushed.reverse(); // Reverse to get chronological order
1139        unpushed
1140    };
1141
1142    if commits_to_push.is_empty() {
1143        println!("ℹ️  No commits to push to stack");
1144        return Ok(());
1145    }
1146
1147    // Push each commit to the stack
1148    let mut pushed_count = 0;
1149    let mut source_branches = std::collections::HashSet::new();
1150
1151    for (i, commit_hash) in commits_to_push.iter().enumerate() {
1152        let commit_obj = repo.get_commit(commit_hash)?;
1153        let commit_msg = commit_obj.message().unwrap_or("").to_string();
1154
1155        // Check which branch this commit belongs to
1156        let commit_source_branch = repo
1157            .find_branch_containing_commit(commit_hash)
1158            .unwrap_or_else(|_| current_branch.clone());
1159        source_branches.insert(commit_source_branch.clone());
1160
1161        // Generate branch name (use provided branch for first commit, generate for others)
1162        let branch_name = if i == 0 && branch.is_some() {
1163            branch.clone().unwrap()
1164        } else {
1165            // Create a temporary GitRepository for branch name generation
1166            let temp_repo = GitRepository::open(&repo_root)?;
1167            let branch_mgr = crate::git::BranchManager::new(temp_repo);
1168            branch_mgr.generate_branch_name(&commit_msg)
1169        };
1170
1171        // Use provided message for first commit, original message for others
1172        let final_message = if i == 0 && message.is_some() {
1173            message.clone().unwrap()
1174        } else {
1175            commit_msg.clone()
1176        };
1177
1178        let entry_id = manager.push_to_stack(
1179            branch_name.clone(),
1180            commit_hash.clone(),
1181            final_message.clone(),
1182            commit_source_branch.clone(),
1183        )?;
1184        pushed_count += 1;
1185
1186        println!(
1187            "✅ Pushed commit {}/{} to stack",
1188            i + 1,
1189            commits_to_push.len()
1190        );
1191        println!(
1192            "   Commit: {} ({})",
1193            &commit_hash[..8],
1194            commit_msg.split('\n').next().unwrap_or("")
1195        );
1196        println!("   Branch: {branch_name}");
1197        println!("   Source: {commit_source_branch}");
1198        println!("   Entry ID: {entry_id}");
1199        println!();
1200    }
1201
1202    // 🚨 SCATTERED COMMIT WARNING
1203    if source_branches.len() > 1 {
1204        println!("⚠️  WARNING: Scattered Commit Detection");
1205        println!(
1206            "   You've pushed commits from {} different Git branches:",
1207            source_branches.len()
1208        );
1209        for branch in &source_branches {
1210            println!("   • {branch}");
1211        }
1212        println!();
1213        println!("   This can lead to confusion because:");
1214        println!("   • Stack appears sequential but commits are scattered across branches");
1215        println!("   • Team members won't know which branch contains which work");
1216        println!("   • Branch cleanup becomes unclear after merge");
1217        println!("   • Rebase operations become more complex");
1218        println!();
1219        println!("   💡 Consider consolidating work to a single feature branch:");
1220        println!("   1. Create a new feature branch: git checkout -b feature/consolidated-work");
1221        println!("   2. Cherry-pick commits in order: git cherry-pick <commit1> <commit2> ...");
1222        println!("   3. Delete old scattered branches");
1223        println!("   4. Push the consolidated branch to your stack");
1224        println!();
1225    }
1226
1227    println!(
1228        "🎉 Successfully pushed {} commit{} to stack",
1229        pushed_count,
1230        if pushed_count == 1 { "" } else { "s" }
1231    );
1232
1233    Ok(())
1234}
1235
1236async fn pop_from_stack(keep_branch: bool) -> Result<()> {
1237    let current_dir = env::current_dir()
1238        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1239
1240    let repo_root = find_repository_root(&current_dir)
1241        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1242
1243    let mut manager = StackManager::new(&repo_root)?;
1244    let repo = GitRepository::open(&repo_root)?;
1245
1246    let entry = manager.pop_from_stack()?;
1247
1248    info!("✅ Popped commit from stack");
1249    info!(
1250        "   Commit: {} ({})",
1251        entry.short_hash(),
1252        entry.short_message(50)
1253    );
1254    info!("   Branch: {}", entry.branch);
1255
1256    // Delete branch if requested and it's not the current branch
1257    if !keep_branch && entry.branch != repo.get_current_branch()? {
1258        match repo.delete_branch(&entry.branch) {
1259            Ok(_) => info!("   Deleted branch: {}", entry.branch),
1260            Err(e) => warn!("   Could not delete branch {}: {}", entry.branch, e),
1261        }
1262    }
1263
1264    Ok(())
1265}
1266
1267async fn submit_entry(
1268    entry: Option<usize>,
1269    title: Option<String>,
1270    description: Option<String>,
1271    range: Option<String>,
1272    draft: bool,
1273) -> Result<()> {
1274    let current_dir = env::current_dir()
1275        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1276
1277    let repo_root = find_repository_root(&current_dir)
1278        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1279
1280    let mut stack_manager = StackManager::new(&repo_root)?;
1281
1282    // Check for branch changes and prompt user if needed
1283    if !stack_manager.check_for_branch_change()? {
1284        return Ok(()); // User chose to cancel or deactivate stack
1285    }
1286
1287    // Load configuration first
1288    let config_dir = crate::config::get_repo_config_dir(&current_dir)?;
1289    let config_path = config_dir.join("config.json");
1290    let settings = crate::config::Settings::load_from_file(&config_path)?;
1291
1292    // Create the main config structure
1293    let cascade_config = crate::config::CascadeConfig {
1294        bitbucket: Some(settings.bitbucket.clone()),
1295        git: settings.git.clone(),
1296        auth: crate::config::AuthConfig::default(),
1297    };
1298
1299    // Get the active stack
1300    let active_stack = stack_manager.get_active_stack().ok_or_else(|| {
1301        CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
1302    })?;
1303    let stack_id = active_stack.id;
1304
1305    // Determine which entries to submit
1306    let entries_to_submit = if let Some(range_str) = range {
1307        // Parse range (e.g., "1-3" or "2,4,6")
1308        let mut entries = Vec::new();
1309
1310        if range_str.contains('-') {
1311            // Handle range like "1-3"
1312            let parts: Vec<&str> = range_str.split('-').collect();
1313            if parts.len() != 2 {
1314                return Err(CascadeError::config(
1315                    "Invalid range format. Use 'start-end' (e.g., '1-3')",
1316                ));
1317            }
1318
1319            let start: usize = parts[0]
1320                .parse()
1321                .map_err(|_| CascadeError::config("Invalid start number in range"))?;
1322            let end: usize = parts[1]
1323                .parse()
1324                .map_err(|_| CascadeError::config("Invalid end number in range"))?;
1325
1326            if start == 0
1327                || end == 0
1328                || start > active_stack.entries.len()
1329                || end > active_stack.entries.len()
1330            {
1331                return Err(CascadeError::config(format!(
1332                    "Range out of bounds. Stack has {} entries",
1333                    active_stack.entries.len()
1334                )));
1335            }
1336
1337            for i in start..=end {
1338                entries.push((i, active_stack.entries[i - 1].clone()));
1339            }
1340        } else {
1341            // Handle comma-separated list like "2,4,6"
1342            for entry_str in range_str.split(',') {
1343                let entry_num: usize = entry_str.trim().parse().map_err(|_| {
1344                    CascadeError::config(format!("Invalid entry number: {entry_str}"))
1345                })?;
1346
1347                if entry_num == 0 || entry_num > active_stack.entries.len() {
1348                    return Err(CascadeError::config(format!(
1349                        "Entry {} out of bounds. Stack has {} entries",
1350                        entry_num,
1351                        active_stack.entries.len()
1352                    )));
1353                }
1354
1355                entries.push((entry_num, active_stack.entries[entry_num - 1].clone()));
1356            }
1357        }
1358
1359        entries
1360    } else if let Some(entry_num) = entry {
1361        // Single entry specified
1362        if entry_num == 0 || entry_num > active_stack.entries.len() {
1363            return Err(CascadeError::config(format!(
1364                "Invalid entry number: {}. Stack has {} entries",
1365                entry_num,
1366                active_stack.entries.len()
1367            )));
1368        }
1369        vec![(entry_num, active_stack.entries[entry_num - 1].clone())]
1370    } else {
1371        // Default: Submit all unsubmitted entries
1372        active_stack
1373            .entries
1374            .iter()
1375            .enumerate()
1376            .filter(|(_, entry)| !entry.is_submitted)
1377            .map(|(i, entry)| (i + 1, entry.clone())) // Convert to 1-based indexing
1378            .collect::<Vec<(usize, _)>>()
1379    };
1380
1381    if entries_to_submit.is_empty() {
1382        println!("ℹ️  No entries to submit");
1383        return Ok(());
1384    }
1385
1386    // Create progress bar for the submission process
1387    let total_operations = entries_to_submit.len() + 2; // +2 for setup steps
1388    let pb = ProgressBar::new(total_operations as u64);
1389    pb.set_style(
1390        ProgressStyle::default_bar()
1391            .template("📤 {msg} [{bar:40.cyan/blue}] {pos}/{len}")
1392            .map_err(|e| CascadeError::config(format!("Progress bar template error: {e}")))?,
1393    );
1394
1395    pb.set_message("Connecting to Bitbucket");
1396    pb.inc(1);
1397
1398    // Create a new StackManager for the integration (since the original was moved)
1399    let integration_stack_manager = StackManager::new(&repo_root)?;
1400    let mut integration =
1401        BitbucketIntegration::new(integration_stack_manager, cascade_config.clone())?;
1402
1403    pb.set_message("Starting batch submission");
1404    pb.inc(1);
1405
1406    // Submit each entry
1407    let mut submitted_count = 0;
1408    let mut failed_entries = Vec::new();
1409    let total_entries = entries_to_submit.len();
1410
1411    for (entry_num, entry_to_submit) in &entries_to_submit {
1412        pb.set_message("Submitting entries...");
1413
1414        // Use provided title/description only for first entry or single entry submissions
1415        let entry_title = if total_entries == 1 {
1416            title.clone()
1417        } else {
1418            None
1419        };
1420        let entry_description = if total_entries == 1 {
1421            description.clone()
1422        } else {
1423            None
1424        };
1425
1426        match integration
1427            .submit_entry(
1428                &stack_id,
1429                &entry_to_submit.id,
1430                entry_title,
1431                entry_description,
1432                draft,
1433            )
1434            .await
1435        {
1436            Ok(pr) => {
1437                submitted_count += 1;
1438                println!("✅ Entry {} - PR #{}: {}", entry_num, pr.id, pr.title);
1439                if let Some(url) = pr.web_url() {
1440                    println!("   URL: {url}");
1441                }
1442                println!(
1443                    "   From: {} -> {}",
1444                    pr.from_ref.display_id, pr.to_ref.display_id
1445                );
1446                println!();
1447            }
1448            Err(e) => {
1449                failed_entries.push((*entry_num, e.to_string()));
1450                println!("❌ Entry {entry_num} failed: {e}");
1451            }
1452        }
1453
1454        pb.inc(1);
1455    }
1456
1457    if failed_entries.is_empty() {
1458        pb.finish_with_message("✅ All pull requests created successfully");
1459        println!(
1460            "🎉 Successfully submitted {} entr{}",
1461            submitted_count,
1462            if submitted_count == 1 { "y" } else { "ies" }
1463        );
1464    } else {
1465        pb.abandon_with_message("⚠️  Some submissions failed");
1466        println!("📊 Submission Summary:");
1467        println!("   ✅ Successful: {submitted_count}");
1468        println!("   ❌ Failed: {}", failed_entries.len());
1469        println!();
1470        println!("💡 Failed entries:");
1471        for (entry_num, error) in failed_entries {
1472            println!("   - Entry {entry_num}: {error}");
1473        }
1474        println!();
1475        println!("💡 You can retry failed entries individually:");
1476        println!("   ca stack submit <ENTRY_NUMBER>");
1477    }
1478
1479    Ok(())
1480}
1481
1482async fn check_stack_status(name: Option<String>) -> Result<()> {
1483    let current_dir = env::current_dir()
1484        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1485
1486    let repo_root = find_repository_root(&current_dir)
1487        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1488
1489    let stack_manager = StackManager::new(&repo_root)?;
1490
1491    // Load configuration
1492    let config_dir = crate::config::get_repo_config_dir(&current_dir)?;
1493    let config_path = config_dir.join("config.json");
1494    let settings = crate::config::Settings::load_from_file(&config_path)?;
1495
1496    // Create the main config structure
1497    let cascade_config = crate::config::CascadeConfig {
1498        bitbucket: Some(settings.bitbucket.clone()),
1499        git: settings.git.clone(),
1500        auth: crate::config::AuthConfig::default(),
1501    };
1502
1503    // Get stack information BEFORE moving stack_manager
1504    let stack = if let Some(name) = name {
1505        stack_manager
1506            .get_stack_by_name(&name)
1507            .ok_or_else(|| CascadeError::config(format!("Stack '{name}' not found")))?
1508    } else {
1509        stack_manager.get_active_stack().ok_or_else(|| {
1510            CascadeError::config("No active stack. Use 'ca stack list' to see available stacks")
1511        })?
1512    };
1513    let stack_id = stack.id;
1514
1515    println!("📋 Stack: {}", stack.name);
1516    println!("   ID: {}", stack.id);
1517    println!("   Base: {}", stack.base_branch);
1518
1519    if let Some(description) = &stack.description {
1520        println!("   Description: {description}");
1521    }
1522
1523    // Create Bitbucket integration (this takes ownership of stack_manager)
1524    let integration = crate::bitbucket::BitbucketIntegration::new(stack_manager, cascade_config)?;
1525
1526    // Check stack status
1527    match integration.check_stack_status(&stack_id).await {
1528        Ok(status) => {
1529            println!("\n📊 Pull Request Status:");
1530            println!("   Total entries: {}", status.total_entries);
1531            println!("   Submitted: {}", status.submitted_entries);
1532            println!("   Open PRs: {}", status.open_prs);
1533            println!("   Merged PRs: {}", status.merged_prs);
1534            println!("   Declined PRs: {}", status.declined_prs);
1535            println!("   Completion: {:.1}%", status.completion_percentage());
1536
1537            if !status.pull_requests.is_empty() {
1538                println!("\n📋 Pull Requests:");
1539                for pr in &status.pull_requests {
1540                    let state_icon = match pr.state {
1541                        crate::bitbucket::PullRequestState::Open => "🔄",
1542                        crate::bitbucket::PullRequestState::Merged => "✅",
1543                        crate::bitbucket::PullRequestState::Declined => "❌",
1544                    };
1545                    println!(
1546                        "   {} PR #{}: {} ({} -> {})",
1547                        state_icon, pr.id, pr.title, pr.from_ref.display_id, pr.to_ref.display_id
1548                    );
1549                    if let Some(url) = pr.web_url() {
1550                        println!("      URL: {url}");
1551                    }
1552                }
1553            }
1554        }
1555        Err(e) => {
1556            warn!("Failed to check stack status: {}", e);
1557            return Err(e);
1558        }
1559    }
1560
1561    Ok(())
1562}
1563
1564async fn list_pull_requests(state: Option<String>, verbose: bool) -> Result<()> {
1565    let current_dir = env::current_dir()
1566        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1567
1568    let repo_root = find_repository_root(&current_dir)
1569        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1570
1571    let stack_manager = StackManager::new(&repo_root)?;
1572
1573    // Load configuration
1574    let config_dir = crate::config::get_repo_config_dir(&current_dir)?;
1575    let config_path = config_dir.join("config.json");
1576    let settings = crate::config::Settings::load_from_file(&config_path)?;
1577
1578    // Create the main config structure
1579    let cascade_config = crate::config::CascadeConfig {
1580        bitbucket: Some(settings.bitbucket.clone()),
1581        git: settings.git.clone(),
1582        auth: crate::config::AuthConfig::default(),
1583    };
1584
1585    // Create Bitbucket integration
1586    let integration = crate::bitbucket::BitbucketIntegration::new(stack_manager, cascade_config)?;
1587
1588    // Parse state filter
1589    let pr_state = if let Some(state_str) = state {
1590        match state_str.to_lowercase().as_str() {
1591            "open" => Some(crate::bitbucket::PullRequestState::Open),
1592            "merged" => Some(crate::bitbucket::PullRequestState::Merged),
1593            "declined" => Some(crate::bitbucket::PullRequestState::Declined),
1594            _ => {
1595                return Err(CascadeError::config(format!(
1596                    "Invalid state '{state_str}'. Use: open, merged, declined"
1597                )))
1598            }
1599        }
1600    } else {
1601        None
1602    };
1603
1604    // Get pull requests
1605    match integration.list_pull_requests(pr_state).await {
1606        Ok(pr_page) => {
1607            if pr_page.values.is_empty() {
1608                info!("No pull requests found.");
1609                return Ok(());
1610            }
1611
1612            println!("📋 Pull Requests ({} total):", pr_page.values.len());
1613            for pr in &pr_page.values {
1614                let state_icon = match pr.state {
1615                    crate::bitbucket::PullRequestState::Open => "🔄",
1616                    crate::bitbucket::PullRequestState::Merged => "✅",
1617                    crate::bitbucket::PullRequestState::Declined => "❌",
1618                };
1619                println!("   {} PR #{}: {}", state_icon, pr.id, pr.title);
1620                if verbose {
1621                    println!(
1622                        "      From: {} -> {}",
1623                        pr.from_ref.display_id, pr.to_ref.display_id
1624                    );
1625                    println!("      Author: {}", pr.author.user.display_name);
1626                    if let Some(url) = pr.web_url() {
1627                        println!("      URL: {url}");
1628                    }
1629                    if let Some(desc) = &pr.description {
1630                        if !desc.is_empty() {
1631                            println!("      Description: {desc}");
1632                        }
1633                    }
1634                    println!();
1635                }
1636            }
1637
1638            if !verbose {
1639                println!("\nUse --verbose for more details");
1640            }
1641        }
1642        Err(e) => {
1643            warn!("Failed to list pull requests: {}", e);
1644            return Err(e);
1645        }
1646    }
1647
1648    Ok(())
1649}
1650
1651async fn check_stack(_force: bool) -> Result<()> {
1652    let current_dir = env::current_dir()
1653        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1654
1655    let repo_root = find_repository_root(&current_dir)
1656        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1657
1658    let mut manager = StackManager::new(&repo_root)?;
1659
1660    let active_stack = manager
1661        .get_active_stack()
1662        .ok_or_else(|| CascadeError::config("No active stack"))?;
1663    let stack_id = active_stack.id;
1664
1665    manager.sync_stack(&stack_id)?;
1666
1667    info!("✅ Stack check completed successfully");
1668
1669    Ok(())
1670}
1671
1672async fn sync_stack(force: bool, skip_cleanup: bool, interactive: bool) -> Result<()> {
1673    let current_dir = env::current_dir()
1674        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1675
1676    let repo_root = find_repository_root(&current_dir)
1677        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1678
1679    let stack_manager = StackManager::new(&repo_root)?;
1680    let git_repo = GitRepository::open(&repo_root)?;
1681
1682    // Get active stack
1683    let active_stack = stack_manager.get_active_stack().ok_or_else(|| {
1684        CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
1685    })?;
1686
1687    let base_branch = active_stack.base_branch.clone();
1688    let stack_name = active_stack.name.clone();
1689
1690    println!("🔄 Syncing stack '{stack_name}' with remote...");
1691
1692    // Step 1: Pull latest changes from base branch
1693    println!("📥 Pulling latest changes from '{base_branch}'...");
1694
1695    // Checkout base branch first
1696    match git_repo.checkout_branch(&base_branch) {
1697        Ok(_) => {
1698            println!("   ✅ Switched to '{base_branch}'");
1699
1700            // Pull latest changes
1701            match git_repo.pull(&base_branch) {
1702                Ok(_) => {
1703                    println!("   ✅ Successfully pulled latest changes");
1704                }
1705                Err(e) => {
1706                    if force {
1707                        println!("   ⚠️  Failed to pull: {e} (continuing due to --force)");
1708                    } else {
1709                        return Err(CascadeError::branch(format!(
1710                            "Failed to pull latest changes from '{base_branch}': {e}. Use --force to continue anyway."
1711                        )));
1712                    }
1713                }
1714            }
1715        }
1716        Err(e) => {
1717            if force {
1718                println!(
1719                    "   ⚠️  Failed to checkout '{base_branch}': {e} (continuing due to --force)"
1720                );
1721            } else {
1722                return Err(CascadeError::branch(format!(
1723                    "Failed to checkout base branch '{base_branch}': {e}. Use --force to continue anyway."
1724                )));
1725            }
1726        }
1727    }
1728
1729    // Step 2: Check if stack needs rebase
1730    println!("🔍 Checking if stack needs rebase...");
1731
1732    let mut updated_stack_manager = StackManager::new(&repo_root)?;
1733    let stack_id = active_stack.id;
1734
1735    match updated_stack_manager.sync_stack(&stack_id) {
1736        Ok(_) => {
1737            // Check the updated status
1738            if let Some(updated_stack) = updated_stack_manager.get_stack(&stack_id) {
1739                match &updated_stack.status {
1740                    crate::stack::StackStatus::NeedsSync => {
1741                        println!("   🔄 Stack needs rebase due to new commits on '{base_branch}'");
1742
1743                        // Step 3: Rebase the stack
1744                        println!("🔀 Rebasing stack onto updated '{base_branch}'...");
1745
1746                        // Load configuration for Bitbucket integration
1747                        let config_dir = crate::config::get_repo_config_dir(&repo_root)?;
1748                        let config_path = config_dir.join("config.json");
1749                        let settings = crate::config::Settings::load_from_file(&config_path)?;
1750
1751                        let cascade_config = crate::config::CascadeConfig {
1752                            bitbucket: Some(settings.bitbucket.clone()),
1753                            git: settings.git.clone(),
1754                            auth: crate::config::AuthConfig::default(),
1755                        };
1756
1757                        // Use the existing rebase system
1758                        let options = crate::stack::RebaseOptions {
1759                            strategy: crate::stack::RebaseStrategy::BranchVersioning,
1760                            interactive,
1761                            target_base: Some(base_branch.clone()),
1762                            preserve_merges: true,
1763                            auto_resolve: !interactive,
1764                            max_retries: 3,
1765                        };
1766
1767                        let mut rebase_manager = crate::stack::RebaseManager::new(
1768                            updated_stack_manager,
1769                            git_repo,
1770                            options,
1771                        );
1772
1773                        match rebase_manager.rebase_stack(&stack_id) {
1774                            Ok(result) => {
1775                                println!("   ✅ Rebase completed successfully!");
1776
1777                                if !result.branch_mapping.is_empty() {
1778                                    println!("   📋 Updated branches:");
1779                                    for (old, new) in &result.branch_mapping {
1780                                        println!("      {old} → {new}");
1781                                    }
1782
1783                                    // Update PRs if enabled
1784                                    if let Some(ref _bitbucket_config) = cascade_config.bitbucket {
1785                                        println!("   🔄 Updating pull requests...");
1786
1787                                        let integration_stack_manager =
1788                                            StackManager::new(&repo_root)?;
1789                                        let mut integration =
1790                                            crate::bitbucket::BitbucketIntegration::new(
1791                                                integration_stack_manager,
1792                                                cascade_config,
1793                                            )?;
1794
1795                                        match integration
1796                                            .update_prs_after_rebase(
1797                                                &stack_id,
1798                                                &result.branch_mapping,
1799                                            )
1800                                            .await
1801                                        {
1802                                            Ok(updated_prs) => {
1803                                                if !updated_prs.is_empty() {
1804                                                    println!(
1805                                                        "      ✅ Updated {} pull requests",
1806                                                        updated_prs.len()
1807                                                    );
1808                                                }
1809                                            }
1810                                            Err(e) => {
1811                                                println!(
1812                                                    "      ⚠️  Failed to update pull requests: {e}"
1813                                                );
1814                                            }
1815                                        }
1816                                    }
1817                                }
1818                            }
1819                            Err(e) => {
1820                                println!("   ❌ Rebase failed: {e}");
1821                                println!("   💡 To resolve conflicts:");
1822                                println!("      1. Fix conflicts in the affected files");
1823                                println!("      2. Stage resolved files: git add <files>");
1824                                println!("      3. Continue: ca stack continue-rebase");
1825                                return Err(e);
1826                            }
1827                        }
1828                    }
1829                    crate::stack::StackStatus::Clean => {
1830                        println!("   ✅ Stack is already up to date");
1831                    }
1832                    other => {
1833                        println!("   ℹ️  Stack status: {other:?}");
1834                    }
1835                }
1836            }
1837        }
1838        Err(e) => {
1839            if force {
1840                println!("   ⚠️  Failed to check stack status: {e} (continuing due to --force)");
1841            } else {
1842                return Err(e);
1843            }
1844        }
1845    }
1846
1847    // Step 4: Cleanup merged branches (optional)
1848    if !skip_cleanup {
1849        println!("🧹 Checking for merged branches to clean up...");
1850        // TODO: Implement merged branch cleanup
1851        // This would:
1852        // 1. Find branches that have been merged into base
1853        // 2. Ask user if they want to delete them
1854        // 3. Remove them from the stack metadata
1855        println!("   ℹ️  Branch cleanup not yet implemented");
1856    } else {
1857        println!("⏭️  Skipping branch cleanup");
1858    }
1859
1860    println!("🎉 Sync completed successfully!");
1861    println!("   Base branch: {base_branch}");
1862    println!("   💡 Next steps:");
1863    println!("      • Review your updated stack: ca stack show");
1864    println!("      • Check PR status: ca stack status");
1865
1866    Ok(())
1867}
1868
1869async fn rebase_stack(
1870    interactive: bool,
1871    onto: Option<String>,
1872    strategy: Option<RebaseStrategyArg>,
1873) -> Result<()> {
1874    let current_dir = env::current_dir()
1875        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
1876
1877    let repo_root = find_repository_root(&current_dir)
1878        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
1879
1880    let stack_manager = StackManager::new(&repo_root)?;
1881    let git_repo = GitRepository::open(&repo_root)?;
1882
1883    // Load configuration for potential Bitbucket integration
1884    let config_dir = crate::config::get_repo_config_dir(&repo_root)?;
1885    let config_path = config_dir.join("config.json");
1886    let settings = crate::config::Settings::load_from_file(&config_path)?;
1887
1888    // Create the main config structure
1889    let cascade_config = crate::config::CascadeConfig {
1890        bitbucket: Some(settings.bitbucket.clone()),
1891        git: settings.git.clone(),
1892        auth: crate::config::AuthConfig::default(),
1893    };
1894
1895    // Get active stack
1896    let active_stack = stack_manager.get_active_stack().ok_or_else(|| {
1897        CascadeError::config("No active stack. Create a stack first with 'ca stack create'")
1898    })?;
1899    let stack_id = active_stack.id;
1900
1901    let active_stack = stack_manager
1902        .get_stack(&stack_id)
1903        .ok_or_else(|| CascadeError::config("Active stack not found"))?
1904        .clone();
1905
1906    if active_stack.entries.is_empty() {
1907        println!("ℹ️  Stack is empty. Nothing to rebase.");
1908        return Ok(());
1909    }
1910
1911    println!("🔄 Rebasing stack: {}", active_stack.name);
1912    println!("   Base: {}", active_stack.base_branch);
1913
1914    // Determine rebase strategy
1915    let rebase_strategy = if let Some(cli_strategy) = strategy {
1916        match cli_strategy {
1917            RebaseStrategyArg::BranchVersioning => crate::stack::RebaseStrategy::BranchVersioning,
1918            RebaseStrategyArg::CherryPick => crate::stack::RebaseStrategy::CherryPick,
1919            RebaseStrategyArg::ThreeWayMerge => crate::stack::RebaseStrategy::ThreeWayMerge,
1920            RebaseStrategyArg::Interactive => crate::stack::RebaseStrategy::Interactive,
1921        }
1922    } else {
1923        // Use strategy from config
1924        match settings.cascade.default_sync_strategy.as_str() {
1925            "branch-versioning" => crate::stack::RebaseStrategy::BranchVersioning,
1926            "cherry-pick" => crate::stack::RebaseStrategy::CherryPick,
1927            "three-way-merge" => crate::stack::RebaseStrategy::ThreeWayMerge,
1928            "rebase" => crate::stack::RebaseStrategy::Interactive,
1929            _ => crate::stack::RebaseStrategy::BranchVersioning, // default fallback
1930        }
1931    };
1932
1933    // Create rebase options
1934    let options = crate::stack::RebaseOptions {
1935        strategy: rebase_strategy.clone(),
1936        interactive,
1937        target_base: onto,
1938        preserve_merges: true,
1939        auto_resolve: !interactive, // Auto-resolve unless interactive
1940        max_retries: 3,
1941    };
1942
1943    info!("   Strategy: {:?}", rebase_strategy);
1944    info!("   Interactive: {}", interactive);
1945    info!("   Target base: {:?}", options.target_base);
1946    info!("   Entries: {}", active_stack.entries.len());
1947
1948    // Check if there's already a rebase in progress
1949    let mut rebase_manager = crate::stack::RebaseManager::new(stack_manager, git_repo, options);
1950
1951    if rebase_manager.is_rebase_in_progress() {
1952        println!("⚠️  Rebase already in progress!");
1953        println!("   Use 'git status' to check the current state");
1954        println!("   Use 'ca stack continue-rebase' to continue");
1955        println!("   Use 'ca stack abort-rebase' to abort");
1956        return Ok(());
1957    }
1958
1959    // Perform the rebase
1960    match rebase_manager.rebase_stack(&stack_id) {
1961        Ok(result) => {
1962            println!("🎉 Rebase completed!");
1963            println!("   {}", result.get_summary());
1964
1965            if result.has_conflicts() {
1966                println!("   ⚠️  {} conflicts were resolved", result.conflicts.len());
1967                for conflict in &result.conflicts {
1968                    println!("      - {}", &conflict[..8.min(conflict.len())]);
1969                }
1970            }
1971
1972            if !result.branch_mapping.is_empty() {
1973                println!("   📋 Branch mapping:");
1974                for (old, new) in &result.branch_mapping {
1975                    println!("      {old} -> {new}");
1976                }
1977
1978                // Handle PR updates if enabled
1979                if let Some(ref _bitbucket_config) = cascade_config.bitbucket {
1980                    // Create a new StackManager for the integration (since the original was moved)
1981                    let integration_stack_manager = StackManager::new(&repo_root)?;
1982                    let mut integration = BitbucketIntegration::new(
1983                        integration_stack_manager,
1984                        cascade_config.clone(),
1985                    )?;
1986
1987                    match integration
1988                        .update_prs_after_rebase(&stack_id, &result.branch_mapping)
1989                        .await
1990                    {
1991                        Ok(updated_prs) => {
1992                            if !updated_prs.is_empty() {
1993                                println!("   🔄 Preserved pull request history:");
1994                                for pr_update in updated_prs {
1995                                    println!("      ✅ {pr_update}");
1996                                }
1997                            }
1998                        }
1999                        Err(e) => {
2000                            eprintln!("   ⚠️  Failed to update pull requests: {e}");
2001                            eprintln!("      You may need to manually update PRs in Bitbucket");
2002                        }
2003                    }
2004                }
2005            }
2006
2007            println!(
2008                "   ✅ {} commits successfully rebased",
2009                result.success_count()
2010            );
2011
2012            // Show next steps
2013            if matches!(
2014                rebase_strategy,
2015                crate::stack::RebaseStrategy::BranchVersioning
2016            ) {
2017                println!("\n📝 Next steps:");
2018                if !result.branch_mapping.is_empty() {
2019                    println!("   1. ✅ New versioned branches have been created");
2020                    println!("   2. ✅ Pull requests have been updated automatically");
2021                    println!("   3. 🔍 Review the updated PRs in Bitbucket");
2022                    println!("   4. 🧪 Test your changes on the new branches");
2023                    println!(
2024                        "   5. 🗑️  Old branches are preserved for safety (can be deleted later)"
2025                    );
2026                } else {
2027                    println!("   1. Review the rebased stack");
2028                    println!("   2. Test your changes");
2029                    println!("   3. Submit new pull requests with 'ca stack submit'");
2030                }
2031            }
2032        }
2033        Err(e) => {
2034            warn!("❌ Rebase failed: {}", e);
2035            println!("💡 Tips for resolving rebase issues:");
2036            println!("   - Check for uncommitted changes with 'git status'");
2037            println!("   - Ensure base branch is up to date");
2038            println!("   - Try interactive mode: 'ca stack rebase --interactive'");
2039            return Err(e);
2040        }
2041    }
2042
2043    Ok(())
2044}
2045
2046async fn continue_rebase() -> Result<()> {
2047    let current_dir = env::current_dir()
2048        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
2049
2050    let repo_root = find_repository_root(&current_dir)
2051        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
2052
2053    let stack_manager = StackManager::new(&repo_root)?;
2054    let git_repo = crate::git::GitRepository::open(&repo_root)?;
2055    let options = crate::stack::RebaseOptions::default();
2056    let rebase_manager = crate::stack::RebaseManager::new(stack_manager, git_repo, options);
2057
2058    if !rebase_manager.is_rebase_in_progress() {
2059        println!("ℹ️  No rebase in progress");
2060        return Ok(());
2061    }
2062
2063    println!("🔄 Continuing rebase...");
2064    match rebase_manager.continue_rebase() {
2065        Ok(_) => {
2066            println!("✅ Rebase continued successfully");
2067            println!("   Check 'ca stack rebase-status' for current state");
2068        }
2069        Err(e) => {
2070            warn!("❌ Failed to continue rebase: {}", e);
2071            println!("💡 You may need to resolve conflicts first:");
2072            println!("   1. Edit conflicted files");
2073            println!("   2. Stage resolved files with 'git add'");
2074            println!("   3. Run 'ca stack continue-rebase' again");
2075        }
2076    }
2077
2078    Ok(())
2079}
2080
2081async fn abort_rebase() -> Result<()> {
2082    let current_dir = env::current_dir()
2083        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
2084
2085    let repo_root = find_repository_root(&current_dir)
2086        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
2087
2088    let stack_manager = StackManager::new(&repo_root)?;
2089    let git_repo = crate::git::GitRepository::open(&repo_root)?;
2090    let options = crate::stack::RebaseOptions::default();
2091    let rebase_manager = crate::stack::RebaseManager::new(stack_manager, git_repo, options);
2092
2093    if !rebase_manager.is_rebase_in_progress() {
2094        println!("ℹ️  No rebase in progress");
2095        return Ok(());
2096    }
2097
2098    println!("⚠️  Aborting rebase...");
2099    match rebase_manager.abort_rebase() {
2100        Ok(_) => {
2101            println!("✅ Rebase aborted successfully");
2102            println!("   Repository restored to pre-rebase state");
2103        }
2104        Err(e) => {
2105            warn!("❌ Failed to abort rebase: {}", e);
2106            println!("⚠️  You may need to manually clean up the repository state");
2107        }
2108    }
2109
2110    Ok(())
2111}
2112
2113async fn rebase_status() -> Result<()> {
2114    let current_dir = env::current_dir()
2115        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
2116
2117    let repo_root = find_repository_root(&current_dir)
2118        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
2119
2120    let stack_manager = StackManager::new(&repo_root)?;
2121    let git_repo = crate::git::GitRepository::open(&repo_root)?;
2122
2123    println!("📊 Rebase Status");
2124
2125    // Check if rebase is in progress by checking git state directly
2126    let git_dir = current_dir.join(".git");
2127    let rebase_in_progress = git_dir.join("REBASE_HEAD").exists()
2128        || git_dir.join("rebase-merge").exists()
2129        || git_dir.join("rebase-apply").exists();
2130
2131    if rebase_in_progress {
2132        println!("   Status: 🔄 Rebase in progress");
2133        println!(
2134            "   
2135📝 Actions available:"
2136        );
2137        println!("     - 'ca stack continue-rebase' to continue");
2138        println!("     - 'ca stack abort-rebase' to abort");
2139        println!("     - 'git status' to see conflicted files");
2140
2141        // Check for conflicts
2142        match git_repo.get_status() {
2143            Ok(statuses) => {
2144                let mut conflicts = Vec::new();
2145                for status in statuses.iter() {
2146                    if status.status().contains(git2::Status::CONFLICTED) {
2147                        if let Some(path) = status.path() {
2148                            conflicts.push(path.to_string());
2149                        }
2150                    }
2151                }
2152
2153                if !conflicts.is_empty() {
2154                    println!("   ⚠️  Conflicts in {} files:", conflicts.len());
2155                    for conflict in conflicts {
2156                        println!("      - {conflict}");
2157                    }
2158                    println!(
2159                        "   
2160💡 To resolve conflicts:"
2161                    );
2162                    println!("     1. Edit the conflicted files");
2163                    println!("     2. Stage resolved files: git add <file>");
2164                    println!("     3. Continue: ca stack continue-rebase");
2165                }
2166            }
2167            Err(e) => {
2168                warn!("Failed to get git status: {}", e);
2169            }
2170        }
2171    } else {
2172        println!("   Status: ✅ No rebase in progress");
2173
2174        // Show stack status instead
2175        if let Some(active_stack) = stack_manager.get_active_stack() {
2176            println!("   Active stack: {}", active_stack.name);
2177            println!("   Entries: {}", active_stack.entries.len());
2178            println!("   Base branch: {}", active_stack.base_branch);
2179        }
2180    }
2181
2182    Ok(())
2183}
2184
2185async fn delete_stack(name: String, force: bool) -> Result<()> {
2186    let current_dir = env::current_dir()
2187        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
2188
2189    let repo_root = find_repository_root(&current_dir)
2190        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
2191
2192    let mut manager = StackManager::new(&repo_root)?;
2193
2194    let stack = manager
2195        .get_stack_by_name(&name)
2196        .ok_or_else(|| CascadeError::config(format!("Stack '{name}' not found")))?;
2197    let stack_id = stack.id;
2198
2199    if !force && !stack.entries.is_empty() {
2200        return Err(CascadeError::config(format!(
2201            "Stack '{}' has {} entries. Use --force to delete anyway",
2202            name,
2203            stack.entries.len()
2204        )));
2205    }
2206
2207    let deleted = manager.delete_stack(&stack_id)?;
2208
2209    info!("✅ Deleted stack '{}'", deleted.name);
2210    if !deleted.entries.is_empty() {
2211        warn!("   {} entries were removed", deleted.entries.len());
2212    }
2213
2214    Ok(())
2215}
2216
2217async fn validate_stack(name: Option<String>) -> Result<()> {
2218    let current_dir = env::current_dir()
2219        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
2220
2221    let repo_root = find_repository_root(&current_dir)
2222        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
2223
2224    let manager = StackManager::new(&repo_root)?;
2225
2226    if let Some(name) = name {
2227        // Validate specific stack
2228        let stack = manager
2229            .get_stack_by_name(&name)
2230            .ok_or_else(|| CascadeError::config(format!("Stack '{name}' not found")))?;
2231
2232        match stack.validate() {
2233            Ok(_) => {
2234                println!("✅ Stack '{name}' validation passed");
2235                Ok(())
2236            }
2237            Err(e) => {
2238                println!("❌ Stack '{name}' validation failed: {e}");
2239                Err(CascadeError::config(e))
2240            }
2241        }
2242    } else {
2243        // Validate all stacks
2244        match manager.validate_all() {
2245            Ok(_) => {
2246                println!("✅ All stacks validation passed");
2247                Ok(())
2248            }
2249            Err(e) => {
2250                println!("❌ Stack validation failed: {e}");
2251                Err(e)
2252            }
2253        }
2254    }
2255}
2256
2257/// Get commits that are not yet in any stack entry
2258#[allow(dead_code)]
2259fn get_unpushed_commits(repo: &GitRepository, stack: &crate::stack::Stack) -> Result<Vec<String>> {
2260    let mut unpushed = Vec::new();
2261    let head_commit = repo.get_head_commit()?;
2262    let mut current_commit = head_commit;
2263
2264    // Walk back from HEAD until we find a commit that's already in the stack
2265    loop {
2266        let commit_hash = current_commit.id().to_string();
2267        let already_in_stack = stack
2268            .entries
2269            .iter()
2270            .any(|entry| entry.commit_hash == commit_hash);
2271
2272        if already_in_stack {
2273            break;
2274        }
2275
2276        unpushed.push(commit_hash);
2277
2278        // Move to parent commit
2279        if let Some(parent) = current_commit.parents().next() {
2280            current_commit = parent;
2281        } else {
2282            break;
2283        }
2284    }
2285
2286    unpushed.reverse(); // Reverse to get chronological order
2287    Ok(unpushed)
2288}
2289
2290/// Squash the last N commits into a single commit
2291pub async fn squash_commits(
2292    repo: &GitRepository,
2293    count: usize,
2294    since_ref: Option<String>,
2295) -> Result<()> {
2296    if count <= 1 {
2297        return Ok(()); // Nothing to squash
2298    }
2299
2300    // Get the current branch
2301    let _current_branch = repo.get_current_branch()?;
2302
2303    // Determine the range for interactive rebase
2304    let rebase_range = if let Some(ref since) = since_ref {
2305        since.clone()
2306    } else {
2307        format!("HEAD~{count}")
2308    };
2309
2310    println!("   Analyzing {count} commits to create smart squash message...");
2311
2312    // Get the commits that will be squashed to create a smart message
2313    let head_commit = repo.get_head_commit()?;
2314    let mut commits_to_squash = Vec::new();
2315    let mut current = head_commit;
2316
2317    // Collect the last N commits
2318    for _ in 0..count {
2319        commits_to_squash.push(current.clone());
2320        if current.parent_count() > 0 {
2321            current = current.parent(0).map_err(CascadeError::Git)?;
2322        } else {
2323            break;
2324        }
2325    }
2326
2327    // Generate smart commit message from the squashed commits
2328    let smart_message = generate_squash_message(&commits_to_squash)?;
2329    println!(
2330        "   Smart message: {}",
2331        smart_message.lines().next().unwrap_or("")
2332    );
2333
2334    // Get the commit we want to reset to (the commit before our range)
2335    let reset_target = if since_ref.is_some() {
2336        // If squashing since a reference, reset to that reference
2337        format!("{rebase_range}~1")
2338    } else {
2339        // If squashing last N commits, reset to N commits before
2340        format!("HEAD~{count}")
2341    };
2342
2343    // Soft reset to preserve changes in staging area
2344    repo.reset_soft(&reset_target)?;
2345
2346    // Stage all changes (they should already be staged from the reset --soft)
2347    repo.stage_all()?;
2348
2349    // Create the new commit with the smart message
2350    let new_commit_hash = repo.commit(&smart_message)?;
2351
2352    println!(
2353        "   Created squashed commit: {} ({})",
2354        &new_commit_hash[..8],
2355        smart_message.lines().next().unwrap_or("")
2356    );
2357    println!("   💡 Tip: Use 'git commit --amend' to edit the commit message if needed");
2358
2359    Ok(())
2360}
2361
2362/// Generate a smart commit message from multiple commits being squashed
2363pub fn generate_squash_message(commits: &[git2::Commit]) -> Result<String> {
2364    if commits.is_empty() {
2365        return Ok("Squashed commits".to_string());
2366    }
2367
2368    // Get all commit messages
2369    let messages: Vec<String> = commits
2370        .iter()
2371        .map(|c| c.message().unwrap_or("").trim().to_string())
2372        .filter(|m| !m.is_empty())
2373        .collect();
2374
2375    if messages.is_empty() {
2376        return Ok("Squashed commits".to_string());
2377    }
2378
2379    // Strategy 1: If the last commit looks like a "Final:" commit, use it
2380    if let Some(last_msg) = messages.first() {
2381        // first() because we're in reverse chronological order
2382        if last_msg.starts_with("Final:") || last_msg.starts_with("final:") {
2383            return Ok(last_msg
2384                .trim_start_matches("Final:")
2385                .trim_start_matches("final:")
2386                .trim()
2387                .to_string());
2388        }
2389    }
2390
2391    // Strategy 2: If most commits are WIP, find the most descriptive non-WIP message
2392    let wip_count = messages
2393        .iter()
2394        .filter(|m| {
2395            m.to_lowercase().starts_with("wip") || m.to_lowercase().contains("work in progress")
2396        })
2397        .count();
2398
2399    if wip_count > messages.len() / 2 {
2400        // Mostly WIP commits, find the best non-WIP one or create a summary
2401        let non_wip: Vec<&String> = messages
2402            .iter()
2403            .filter(|m| {
2404                !m.to_lowercase().starts_with("wip")
2405                    && !m.to_lowercase().contains("work in progress")
2406            })
2407            .collect();
2408
2409        if let Some(best_msg) = non_wip.first() {
2410            return Ok(best_msg.to_string());
2411        }
2412
2413        // All are WIP, try to extract the feature being worked on
2414        let feature = extract_feature_from_wip(&messages);
2415        return Ok(feature);
2416    }
2417
2418    // Strategy 3: Use the last (most recent) commit message
2419    Ok(messages.first().unwrap().clone())
2420}
2421
2422/// Extract feature name from WIP commit messages
2423pub fn extract_feature_from_wip(messages: &[String]) -> String {
2424    // Look for patterns like "WIP: add authentication" -> "Add authentication"
2425    for msg in messages {
2426        // Check both case variations, but preserve original case
2427        if msg.to_lowercase().starts_with("wip:") {
2428            if let Some(rest) = msg
2429                .strip_prefix("WIP:")
2430                .or_else(|| msg.strip_prefix("wip:"))
2431            {
2432                let feature = rest.trim();
2433                if !feature.is_empty() && feature.len() > 3 {
2434                    // Capitalize first letter only, preserve rest
2435                    let mut chars: Vec<char> = feature.chars().collect();
2436                    if let Some(first) = chars.first_mut() {
2437                        *first = first.to_uppercase().next().unwrap_or(*first);
2438                    }
2439                    return chars.into_iter().collect();
2440                }
2441            }
2442        }
2443    }
2444
2445    // Fallback: Use the latest commit without WIP prefix
2446    if let Some(first) = messages.first() {
2447        let cleaned = first
2448            .trim_start_matches("WIP:")
2449            .trim_start_matches("wip:")
2450            .trim_start_matches("WIP")
2451            .trim_start_matches("wip")
2452            .trim();
2453
2454        if !cleaned.is_empty() {
2455            return format!("Implement {cleaned}");
2456        }
2457    }
2458
2459    format!("Squashed {} commits", messages.len())
2460}
2461
2462/// Count commits since a given reference
2463pub fn count_commits_since(repo: &GitRepository, since_commit_hash: &str) -> Result<usize> {
2464    let head_commit = repo.get_head_commit()?;
2465    let since_commit = repo.get_commit(since_commit_hash)?;
2466
2467    let mut count = 0;
2468    let mut current = head_commit;
2469
2470    // Walk backwards from HEAD until we reach the since commit
2471    loop {
2472        if current.id() == since_commit.id() {
2473            break;
2474        }
2475
2476        count += 1;
2477
2478        // Get parent commit
2479        if current.parent_count() == 0 {
2480            break; // Reached root commit
2481        }
2482
2483        current = current.parent(0).map_err(CascadeError::Git)?;
2484    }
2485
2486    Ok(count)
2487}
2488
2489/// Land (merge) approved stack entries
2490async fn land_stack(
2491    entry: Option<usize>,
2492    force: bool,
2493    dry_run: bool,
2494    auto: bool,
2495    wait_for_builds: bool,
2496    strategy: Option<MergeStrategyArg>,
2497    build_timeout: u64,
2498) -> Result<()> {
2499    let current_dir = env::current_dir()
2500        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
2501
2502    let repo_root = find_repository_root(&current_dir)
2503        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
2504
2505    let stack_manager = StackManager::new(&repo_root)?;
2506
2507    // Get stack ID and active stack before moving stack_manager
2508    let stack_id = stack_manager
2509        .get_active_stack()
2510        .map(|s| s.id)
2511        .ok_or_else(|| {
2512            CascadeError::config(
2513                "No active stack. Use 'ca stack create' or 'ca stack switch' to select a stack"
2514                    .to_string(),
2515            )
2516        })?;
2517
2518    let active_stack = stack_manager
2519        .get_active_stack()
2520        .cloned()
2521        .ok_or_else(|| CascadeError::config("No active stack found".to_string()))?;
2522
2523    // Load configuration and create Bitbucket integration
2524    let config_dir = crate::config::get_repo_config_dir(&repo_root)?;
2525    let config_path = config_dir.join("config.json");
2526    let settings = crate::config::Settings::load_from_file(&config_path)?;
2527
2528    let cascade_config = crate::config::CascadeConfig {
2529        bitbucket: Some(settings.bitbucket.clone()),
2530        git: settings.git.clone(),
2531        auth: crate::config::AuthConfig::default(),
2532    };
2533
2534    let integration = crate::bitbucket::BitbucketIntegration::new(stack_manager, cascade_config)?;
2535
2536    // Get enhanced status
2537    let status = integration.check_enhanced_stack_status(&stack_id).await?;
2538
2539    if status.enhanced_statuses.is_empty() {
2540        println!("❌ No pull requests found to land");
2541        return Ok(());
2542    }
2543
2544    // Filter PRs that are ready to land
2545    let ready_prs: Vec<_> = status
2546        .enhanced_statuses
2547        .iter()
2548        .filter(|pr_status| {
2549            // If specific entry requested, only include that one
2550            if let Some(entry_num) = entry {
2551                // Find the corresponding stack entry for this PR
2552                if let Some(stack_entry) = active_stack.entries.get(entry_num.saturating_sub(1)) {
2553                    // Check if this PR corresponds to the requested entry
2554                    if pr_status.pr.from_ref.display_id != stack_entry.branch {
2555                        return false;
2556                    }
2557                } else {
2558                    return false; // Invalid entry number
2559                }
2560            }
2561
2562            if force {
2563                // If force is enabled, include any open PR
2564                pr_status.pr.state == crate::bitbucket::pull_request::PullRequestState::Open
2565            } else {
2566                pr_status.is_ready_to_land()
2567            }
2568        })
2569        .collect();
2570
2571    if ready_prs.is_empty() {
2572        if let Some(entry_num) = entry {
2573            println!("❌ Entry {entry_num} is not ready to land or doesn't exist");
2574        } else {
2575            println!("❌ No pull requests are ready to land");
2576        }
2577
2578        // Show what's blocking them
2579        println!("\n🚫 Blocking Issues:");
2580        for pr_status in &status.enhanced_statuses {
2581            if pr_status.pr.state == crate::bitbucket::pull_request::PullRequestState::Open {
2582                let blocking = pr_status.get_blocking_reasons();
2583                if !blocking.is_empty() {
2584                    println!("   PR #{}: {}", pr_status.pr.id, blocking.join(", "));
2585                }
2586            }
2587        }
2588
2589        if !force {
2590            println!("\n💡 Use --force to land PRs with blocking issues (dangerous!)");
2591        }
2592        return Ok(());
2593    }
2594
2595    if dry_run {
2596        if let Some(entry_num) = entry {
2597            println!("🏃 Dry Run - Entry {entry_num} that would be landed:");
2598        } else {
2599            println!("🏃 Dry Run - PRs that would be landed:");
2600        }
2601        for pr_status in &ready_prs {
2602            println!("   ✅ PR #{}: {}", pr_status.pr.id, pr_status.pr.title);
2603            if !pr_status.is_ready_to_land() && force {
2604                let blocking = pr_status.get_blocking_reasons();
2605                println!(
2606                    "      ⚠️  Would force land despite: {}",
2607                    blocking.join(", ")
2608                );
2609            }
2610        }
2611        return Ok(());
2612    }
2613
2614    // Default behavior: land all ready PRs (safest approach)
2615    // Only land specific entry if explicitly requested
2616    if entry.is_some() && ready_prs.len() > 1 {
2617        println!(
2618            "🎯 {} PRs are ready to land, but landing only entry #{}",
2619            ready_prs.len(),
2620            entry.unwrap()
2621        );
2622    }
2623
2624    // Setup auto-merge conditions
2625    let merge_strategy: crate::bitbucket::pull_request::MergeStrategy =
2626        strategy.unwrap_or(MergeStrategyArg::Squash).into();
2627    let auto_merge_conditions = crate::bitbucket::pull_request::AutoMergeConditions {
2628        merge_strategy: merge_strategy.clone(),
2629        wait_for_builds,
2630        build_timeout: std::time::Duration::from_secs(build_timeout),
2631        allowed_authors: None, // Allow all authors for now
2632    };
2633
2634    // Land the PRs
2635    println!(
2636        "🚀 Landing {} PR{}...",
2637        ready_prs.len(),
2638        if ready_prs.len() == 1 { "" } else { "s" }
2639    );
2640
2641    let pr_manager = crate::bitbucket::pull_request::PullRequestManager::new(
2642        crate::bitbucket::BitbucketClient::new(&settings.bitbucket)?,
2643    );
2644
2645    // Land PRs in dependency order
2646    let mut landed_count = 0;
2647    let mut failed_count = 0;
2648    let total_ready_prs = ready_prs.len();
2649
2650    for pr_status in ready_prs {
2651        let pr_id = pr_status.pr.id;
2652
2653        print!("🚀 Landing PR #{}: {}", pr_id, pr_status.pr.title);
2654
2655        let land_result = if auto {
2656            // Use auto-merge with conditions checking
2657            pr_manager
2658                .auto_merge_if_ready(pr_id, &auto_merge_conditions)
2659                .await
2660        } else {
2661            // Manual merge without auto-conditions
2662            pr_manager
2663                .merge_pull_request(pr_id, merge_strategy.clone())
2664                .await
2665                .map(
2666                    |pr| crate::bitbucket::pull_request::AutoMergeResult::Merged {
2667                        pr: Box::new(pr),
2668                        merge_strategy: merge_strategy.clone(),
2669                    },
2670                )
2671        };
2672
2673        match land_result {
2674            Ok(crate::bitbucket::pull_request::AutoMergeResult::Merged { .. }) => {
2675                println!(" ✅");
2676                landed_count += 1;
2677
2678                // 🔄 AUTO-RETARGETING: After each merge, retarget remaining PRs
2679                if landed_count < total_ready_prs {
2680                    println!("🔄 Retargeting remaining PRs to latest base...");
2681
2682                    // 1️⃣ CRITICAL: Update base branch to get latest merged state
2683                    let base_branch = active_stack.base_branch.clone();
2684                    let git_repo = crate::git::GitRepository::open(&repo_root)?;
2685
2686                    println!("   📥 Updating base branch: {base_branch}");
2687                    match git_repo.pull(&base_branch) {
2688                        Ok(_) => println!("   ✅ Base branch updated successfully"),
2689                        Err(e) => {
2690                            println!("   ⚠️  Warning: Failed to update base branch: {e}");
2691                            println!(
2692                                "   💡 You may want to manually run: git pull origin {base_branch}"
2693                            );
2694                        }
2695                    }
2696
2697                    // 2️⃣ Use rebase system to retarget remaining PRs
2698                    let mut rebase_manager = crate::stack::RebaseManager::new(
2699                        StackManager::new(&repo_root)?,
2700                        git_repo,
2701                        crate::stack::RebaseOptions {
2702                            strategy: crate::stack::RebaseStrategy::BranchVersioning,
2703                            target_base: Some(base_branch.clone()),
2704                            ..Default::default()
2705                        },
2706                    );
2707
2708                    match rebase_manager.rebase_stack(&stack_id) {
2709                        Ok(rebase_result) => {
2710                            if !rebase_result.branch_mapping.is_empty() {
2711                                // Update PRs using the rebase result
2712                                let retarget_config = crate::config::CascadeConfig {
2713                                    bitbucket: Some(settings.bitbucket.clone()),
2714                                    git: settings.git.clone(),
2715                                    auth: crate::config::AuthConfig::default(),
2716                                };
2717                                let mut retarget_integration = BitbucketIntegration::new(
2718                                    StackManager::new(&repo_root)?,
2719                                    retarget_config,
2720                                )?;
2721
2722                                match retarget_integration
2723                                    .update_prs_after_rebase(
2724                                        &stack_id,
2725                                        &rebase_result.branch_mapping,
2726                                    )
2727                                    .await
2728                                {
2729                                    Ok(updated_prs) => {
2730                                        if !updated_prs.is_empty() {
2731                                            println!(
2732                                                "   ✅ Updated {} PRs with new targets",
2733                                                updated_prs.len()
2734                                            );
2735                                        }
2736                                    }
2737                                    Err(e) => {
2738                                        println!("   ⚠️  Failed to update remaining PRs: {e}");
2739                                        println!(
2740                                            "   💡 You may need to run: ca stack rebase --onto {base_branch}"
2741                                        );
2742                                    }
2743                                }
2744                            }
2745                        }
2746                        Err(e) => {
2747                            // 🚨 CONFLICTS DETECTED - Give clear next steps
2748                            println!("   ❌ Auto-retargeting conflicts detected!");
2749                            println!("   📝 To resolve conflicts and continue landing:");
2750                            println!("      1. Resolve conflicts in the affected files");
2751                            println!("      2. Stage resolved files: git add <files>");
2752                            println!("      3. Continue the process: ca stack continue-land");
2753                            println!("      4. Or abort the operation: ca stack abort-land");
2754                            println!();
2755                            println!("   💡 Check current status: ca stack land-status");
2756                            println!("   ⚠️  Error details: {e}");
2757
2758                            // Stop the land operation here - user needs to resolve conflicts
2759                            break;
2760                        }
2761                    }
2762                }
2763            }
2764            Ok(crate::bitbucket::pull_request::AutoMergeResult::NotReady { blocking_reasons }) => {
2765                println!(" ❌ Not ready: {}", blocking_reasons.join(", "));
2766                failed_count += 1;
2767                if !force {
2768                    break;
2769                }
2770            }
2771            Ok(crate::bitbucket::pull_request::AutoMergeResult::Failed { error }) => {
2772                println!(" ❌ Failed: {error}");
2773                failed_count += 1;
2774                if !force {
2775                    break;
2776                }
2777            }
2778            Err(e) => {
2779                println!(" ❌");
2780                eprintln!("Failed to land PR #{pr_id}: {e}");
2781                failed_count += 1;
2782
2783                if !force {
2784                    break;
2785                }
2786            }
2787        }
2788    }
2789
2790    // Show summary
2791    println!("\n🎯 Landing Summary:");
2792    println!("   ✅ Successfully landed: {landed_count}");
2793    if failed_count > 0 {
2794        println!("   ❌ Failed to land: {failed_count}");
2795    }
2796
2797    if landed_count > 0 {
2798        println!("✅ Landing operation completed!");
2799    } else {
2800        println!("❌ No PRs were successfully landed");
2801    }
2802
2803    Ok(())
2804}
2805
2806/// Auto-land all ready PRs (shorthand for land --auto)
2807async fn auto_land_stack(
2808    force: bool,
2809    dry_run: bool,
2810    wait_for_builds: bool,
2811    strategy: Option<MergeStrategyArg>,
2812    build_timeout: u64,
2813) -> Result<()> {
2814    // This is a shorthand for land with --auto
2815    land_stack(
2816        None,
2817        force,
2818        dry_run,
2819        true, // auto = true
2820        wait_for_builds,
2821        strategy,
2822        build_timeout,
2823    )
2824    .await
2825}
2826
2827async fn continue_land() -> Result<()> {
2828    let current_dir = env::current_dir()
2829        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
2830
2831    let repo_root = find_repository_root(&current_dir)
2832        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
2833
2834    let stack_manager = StackManager::new(&repo_root)?;
2835    let git_repo = crate::git::GitRepository::open(&repo_root)?;
2836    let options = crate::stack::RebaseOptions::default();
2837    let rebase_manager = crate::stack::RebaseManager::new(stack_manager, git_repo, options);
2838
2839    if !rebase_manager.is_rebase_in_progress() {
2840        println!("ℹ️  No rebase in progress");
2841        return Ok(());
2842    }
2843
2844    println!("🔄 Continuing land operation...");
2845    match rebase_manager.continue_rebase() {
2846        Ok(_) => {
2847            println!("✅ Land operation continued successfully");
2848            println!("   Check 'ca stack land-status' for current state");
2849        }
2850        Err(e) => {
2851            warn!("❌ Failed to continue land operation: {}", e);
2852            println!("💡 You may need to resolve conflicts first:");
2853            println!("   1. Edit conflicted files");
2854            println!("   2. Stage resolved files with 'git add'");
2855            println!("   3. Run 'ca stack continue-land' again");
2856        }
2857    }
2858
2859    Ok(())
2860}
2861
2862async fn abort_land() -> Result<()> {
2863    let current_dir = env::current_dir()
2864        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
2865
2866    let repo_root = find_repository_root(&current_dir)
2867        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
2868
2869    let stack_manager = StackManager::new(&repo_root)?;
2870    let git_repo = crate::git::GitRepository::open(&repo_root)?;
2871    let options = crate::stack::RebaseOptions::default();
2872    let rebase_manager = crate::stack::RebaseManager::new(stack_manager, git_repo, options);
2873
2874    if !rebase_manager.is_rebase_in_progress() {
2875        println!("ℹ️  No rebase in progress");
2876        return Ok(());
2877    }
2878
2879    println!("⚠️  Aborting land operation...");
2880    match rebase_manager.abort_rebase() {
2881        Ok(_) => {
2882            println!("✅ Land operation aborted successfully");
2883            println!("   Repository restored to pre-land state");
2884        }
2885        Err(e) => {
2886            warn!("❌ Failed to abort land operation: {}", e);
2887            println!("⚠️  You may need to manually clean up the repository state");
2888        }
2889    }
2890
2891    Ok(())
2892}
2893
2894async fn land_status() -> Result<()> {
2895    let current_dir = env::current_dir()
2896        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
2897
2898    let repo_root = find_repository_root(&current_dir)
2899        .map_err(|e| CascadeError::config(format!("Could not find git repository: {e}")))?;
2900
2901    let stack_manager = StackManager::new(&repo_root)?;
2902    let git_repo = crate::git::GitRepository::open(&repo_root)?;
2903
2904    println!("📊 Land Status");
2905
2906    // Check if land operation is in progress by checking git state directly
2907    let git_dir = repo_root.join(".git");
2908    let land_in_progress = git_dir.join("REBASE_HEAD").exists()
2909        || git_dir.join("rebase-merge").exists()
2910        || git_dir.join("rebase-apply").exists();
2911
2912    if land_in_progress {
2913        println!("   Status: 🔄 Land operation in progress");
2914        println!(
2915            "   
2916📝 Actions available:"
2917        );
2918        println!("     - 'ca stack continue-land' to continue");
2919        println!("     - 'ca stack abort-land' to abort");
2920        println!("     - 'git status' to see conflicted files");
2921
2922        // Check for conflicts
2923        match git_repo.get_status() {
2924            Ok(statuses) => {
2925                let mut conflicts = Vec::new();
2926                for status in statuses.iter() {
2927                    if status.status().contains(git2::Status::CONFLICTED) {
2928                        if let Some(path) = status.path() {
2929                            conflicts.push(path.to_string());
2930                        }
2931                    }
2932                }
2933
2934                if !conflicts.is_empty() {
2935                    println!("   ⚠️  Conflicts in {} files:", conflicts.len());
2936                    for conflict in conflicts {
2937                        println!("      - {conflict}");
2938                    }
2939                    println!(
2940                        "   
2941💡 To resolve conflicts:"
2942                    );
2943                    println!("     1. Edit the conflicted files");
2944                    println!("     2. Stage resolved files: git add <file>");
2945                    println!("     3. Continue: ca stack continue-land");
2946                }
2947            }
2948            Err(e) => {
2949                warn!("Failed to get git status: {}", e);
2950            }
2951        }
2952    } else {
2953        println!("   Status: ✅ No land operation in progress");
2954
2955        // Show stack status instead
2956        if let Some(active_stack) = stack_manager.get_active_stack() {
2957            println!("   Active stack: {}", active_stack.name);
2958            println!("   Entries: {}", active_stack.entries.len());
2959            println!("   Base branch: {}", active_stack.base_branch);
2960        }
2961    }
2962
2963    Ok(())
2964}
2965
2966#[cfg(test)]
2967mod tests {
2968    use super::*;
2969    use std::process::Command;
2970    use tempfile::TempDir;
2971
2972    async fn create_test_repo() -> (TempDir, std::path::PathBuf) {
2973        let temp_dir = TempDir::new().unwrap();
2974        let repo_path = temp_dir.path().to_path_buf();
2975
2976        // Initialize git repository
2977        Command::new("git")
2978            .args(["init"])
2979            .current_dir(&repo_path)
2980            .output()
2981            .unwrap();
2982
2983        Command::new("git")
2984            .args(["config", "user.name", "Test User"])
2985            .current_dir(&repo_path)
2986            .output()
2987            .unwrap();
2988
2989        Command::new("git")
2990            .args(["config", "user.email", "test@example.com"])
2991            .current_dir(&repo_path)
2992            .output()
2993            .unwrap();
2994
2995        // Create initial commit
2996        std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
2997        Command::new("git")
2998            .args(["add", "."])
2999            .current_dir(&repo_path)
3000            .output()
3001            .unwrap();
3002
3003        Command::new("git")
3004            .args(["commit", "-m", "Initial commit"])
3005            .current_dir(&repo_path)
3006            .output()
3007            .unwrap();
3008
3009        // Initialize cascade
3010        crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
3011            .unwrap();
3012
3013        (temp_dir, repo_path)
3014    }
3015
3016    #[tokio::test]
3017    async fn test_create_stack() {
3018        let (_temp_dir, repo_path) = create_test_repo().await;
3019
3020        // Initialize cascade in the test repo
3021        crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
3022            .expect("Failed to initialize Cascade in test repo");
3023
3024        // Change to the repo directory (with proper error handling)
3025        let original_dir = env::current_dir().map_err(|_| "Failed to get current dir");
3026        match env::set_current_dir(&repo_path) {
3027            Ok(_) => {
3028                let result = create_stack(
3029                    "test-stack".to_string(),
3030                    None, // Use default branch
3031                    Some("Test description".to_string()),
3032                )
3033                .await;
3034
3035                // Restore original directory (best effort)
3036                if let Ok(orig) = original_dir {
3037                    let _ = env::set_current_dir(orig);
3038                }
3039
3040                assert!(
3041                    result.is_ok(),
3042                    "Stack creation should succeed in initialized repository"
3043                );
3044            }
3045            Err(_) => {
3046                // Skip test if we can't change directories (CI environment issue)
3047                println!("Skipping test due to directory access restrictions");
3048            }
3049        }
3050    }
3051
3052    #[tokio::test]
3053    async fn test_list_empty_stacks() {
3054        let (_temp_dir, repo_path) = create_test_repo().await;
3055
3056        // Initialize cascade in the test repo
3057        crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
3058            .expect("Failed to initialize Cascade in test repo");
3059
3060        // Change to the repo directory (with proper error handling)
3061        let original_dir = env::current_dir().map_err(|_| "Failed to get current dir");
3062        match env::set_current_dir(&repo_path) {
3063            Ok(_) => {
3064                let result = list_stacks(false, false, None).await;
3065
3066                // Restore original directory (best effort)
3067                if let Ok(orig) = original_dir {
3068                    let _ = env::set_current_dir(orig);
3069                }
3070
3071                assert!(
3072                    result.is_ok(),
3073                    "Listing stacks should succeed in initialized repository"
3074                );
3075            }
3076            Err(_) => {
3077                // Skip test if we can't change directories (CI environment issue)
3078                println!("Skipping test due to directory access restrictions");
3079            }
3080        }
3081    }
3082
3083    // Tests for squashing functionality
3084
3085    #[test]
3086    fn test_extract_feature_from_wip_basic() {
3087        let messages = vec![
3088            "WIP: add authentication".to_string(),
3089            "WIP: implement login flow".to_string(),
3090        ];
3091
3092        let result = extract_feature_from_wip(&messages);
3093        assert_eq!(result, "Add authentication");
3094    }
3095
3096    #[test]
3097    fn test_extract_feature_from_wip_capitalize() {
3098        let messages = vec!["WIP: fix user validation bug".to_string()];
3099
3100        let result = extract_feature_from_wip(&messages);
3101        assert_eq!(result, "Fix user validation bug");
3102    }
3103
3104    #[test]
3105    fn test_extract_feature_from_wip_fallback() {
3106        let messages = vec![
3107            "WIP user interface changes".to_string(),
3108            "wip: css styling".to_string(),
3109        ];
3110
3111        let result = extract_feature_from_wip(&messages);
3112        // Should create a fallback message since no "WIP:" prefix found
3113        assert!(result.contains("Implement") || result.contains("Squashed") || result.len() > 5);
3114    }
3115
3116    #[test]
3117    fn test_extract_feature_from_wip_empty() {
3118        let messages = vec![];
3119
3120        let result = extract_feature_from_wip(&messages);
3121        assert_eq!(result, "Squashed 0 commits");
3122    }
3123
3124    #[test]
3125    fn test_extract_feature_from_wip_short_message() {
3126        let messages = vec!["WIP: x".to_string()]; // Too short
3127
3128        let result = extract_feature_from_wip(&messages);
3129        assert!(result.starts_with("Implement") || result.contains("Squashed"));
3130    }
3131
3132    // Integration tests for squashing that don't require real git commits
3133
3134    #[test]
3135    fn test_squash_message_final_strategy() {
3136        // This test would need real git2::Commit objects, so we'll test the logic indirectly
3137        // through the extract_feature_from_wip function which handles the core logic
3138
3139        let messages = [
3140            "Final: implement user authentication system".to_string(),
3141            "WIP: add tests".to_string(),
3142            "WIP: fix validation".to_string(),
3143        ];
3144
3145        // Test that we can identify final commits
3146        assert!(messages[0].starts_with("Final:"));
3147
3148        // Test message extraction
3149        let extracted = messages[0].trim_start_matches("Final:").trim();
3150        assert_eq!(extracted, "implement user authentication system");
3151    }
3152
3153    #[test]
3154    fn test_squash_message_wip_detection() {
3155        let messages = [
3156            "WIP: start feature".to_string(),
3157            "WIP: continue work".to_string(),
3158            "WIP: almost done".to_string(),
3159            "Regular commit message".to_string(),
3160        ];
3161
3162        let wip_count = messages
3163            .iter()
3164            .filter(|m| {
3165                m.to_lowercase().starts_with("wip") || m.to_lowercase().contains("work in progress")
3166            })
3167            .count();
3168
3169        assert_eq!(wip_count, 3); // Should detect 3 WIP commits
3170        assert!(wip_count > messages.len() / 2); // Majority are WIP
3171
3172        // Should find the non-WIP message
3173        let non_wip: Vec<&String> = messages
3174            .iter()
3175            .filter(|m| {
3176                !m.to_lowercase().starts_with("wip")
3177                    && !m.to_lowercase().contains("work in progress")
3178            })
3179            .collect();
3180
3181        assert_eq!(non_wip.len(), 1);
3182        assert_eq!(non_wip[0], "Regular commit message");
3183    }
3184
3185    #[test]
3186    fn test_squash_message_all_wip() {
3187        let messages = vec![
3188            "WIP: add feature A".to_string(),
3189            "WIP: add feature B".to_string(),
3190            "WIP: finish implementation".to_string(),
3191        ];
3192
3193        let result = extract_feature_from_wip(&messages);
3194        // Should use the first message as the main feature
3195        assert_eq!(result, "Add feature A");
3196    }
3197
3198    #[test]
3199    fn test_squash_message_edge_cases() {
3200        // Test empty messages
3201        let empty_messages: Vec<String> = vec![];
3202        let result = extract_feature_from_wip(&empty_messages);
3203        assert_eq!(result, "Squashed 0 commits");
3204
3205        // Test messages with only whitespace
3206        let whitespace_messages = vec!["   ".to_string(), "\t\n".to_string()];
3207        let result = extract_feature_from_wip(&whitespace_messages);
3208        assert!(result.contains("Squashed") || result.contains("Implement"));
3209
3210        // Test case sensitivity
3211        let mixed_case = vec!["wip: Add Feature".to_string()];
3212        let result = extract_feature_from_wip(&mixed_case);
3213        assert_eq!(result, "Add Feature");
3214    }
3215
3216    // Tests for auto-land functionality
3217
3218    #[tokio::test]
3219    async fn test_auto_land_wrapper() {
3220        // Test that auto_land_stack correctly calls land_stack with auto=true
3221        let (_temp_dir, repo_path) = create_test_repo().await;
3222
3223        // Initialize cascade in the test repo
3224        crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
3225            .expect("Failed to initialize Cascade in test repo");
3226
3227        let original_dir = env::current_dir().map_err(|_| "Failed to get current dir");
3228        match env::set_current_dir(&repo_path) {
3229            Ok(_) => {
3230                // Create a stack first
3231                let result = create_stack(
3232                    "test-stack".to_string(),
3233                    None,
3234                    Some("Test stack for auto-land".to_string()),
3235                )
3236                .await;
3237
3238                if let Ok(orig) = original_dir {
3239                    let _ = env::set_current_dir(orig);
3240                }
3241
3242                // For now, just test that the function can be called without panic
3243                // (It will fail due to missing Bitbucket config, but that's expected)
3244                assert!(
3245                    result.is_ok(),
3246                    "Stack creation should succeed in initialized repository"
3247                );
3248            }
3249            Err(_) => {
3250                println!("Skipping test due to directory access restrictions");
3251            }
3252        }
3253    }
3254
3255    #[test]
3256    fn test_auto_land_action_enum() {
3257        // Test that AutoLand action is properly defined
3258        use crate::cli::commands::stack::StackAction;
3259
3260        // This ensures the AutoLand variant exists and has the expected fields
3261        let _action = StackAction::AutoLand {
3262            force: false,
3263            dry_run: true,
3264            wait_for_builds: true,
3265            strategy: Some(MergeStrategyArg::Squash),
3266            build_timeout: 1800,
3267        };
3268
3269        // Test passes if we reach this point without errors
3270    }
3271
3272    #[test]
3273    fn test_merge_strategy_conversion() {
3274        // Test that MergeStrategyArg converts properly
3275        let squash_strategy = MergeStrategyArg::Squash;
3276        let merge_strategy: crate::bitbucket::pull_request::MergeStrategy = squash_strategy.into();
3277
3278        match merge_strategy {
3279            crate::bitbucket::pull_request::MergeStrategy::Squash => {
3280                // Correct conversion
3281            }
3282            _ => panic!("Expected Squash strategy"),
3283        }
3284
3285        let merge_strategy = MergeStrategyArg::Merge;
3286        let converted: crate::bitbucket::pull_request::MergeStrategy = merge_strategy.into();
3287
3288        match converted {
3289            crate::bitbucket::pull_request::MergeStrategy::Merge => {
3290                // Correct conversion
3291            }
3292            _ => panic!("Expected Merge strategy"),
3293        }
3294    }
3295
3296    #[test]
3297    fn test_auto_merge_conditions_structure() {
3298        // Test that AutoMergeConditions can be created with expected values
3299        use std::time::Duration;
3300
3301        let conditions = crate::bitbucket::pull_request::AutoMergeConditions {
3302            merge_strategy: crate::bitbucket::pull_request::MergeStrategy::Squash,
3303            wait_for_builds: true,
3304            build_timeout: Duration::from_secs(1800),
3305            allowed_authors: None,
3306        };
3307
3308        // Verify the conditions are set as expected for auto-land
3309        assert!(conditions.wait_for_builds);
3310        assert_eq!(conditions.build_timeout.as_secs(), 1800);
3311        assert!(conditions.allowed_authors.is_none());
3312        assert!(matches!(
3313            conditions.merge_strategy,
3314            crate::bitbucket::pull_request::MergeStrategy::Squash
3315        ));
3316    }
3317
3318    #[test]
3319    fn test_polling_constants() {
3320        // Test that polling frequency is documented and reasonable
3321        use std::time::Duration;
3322
3323        // The polling frequency should be 30 seconds as mentioned in documentation
3324        let expected_polling_interval = Duration::from_secs(30);
3325
3326        // Verify it's a reasonable value (not too frequent, not too slow)
3327        assert!(expected_polling_interval.as_secs() >= 10); // At least 10 seconds
3328        assert!(expected_polling_interval.as_secs() <= 60); // At most 1 minute
3329        assert_eq!(expected_polling_interval.as_secs(), 30); // Exactly 30 seconds
3330    }
3331
3332    #[test]
3333    fn test_build_timeout_defaults() {
3334        // Verify build timeout default is reasonable
3335        const DEFAULT_TIMEOUT: u64 = 1800; // 30 minutes
3336        assert_eq!(DEFAULT_TIMEOUT, 1800);
3337        // Test that our default timeout value is within reasonable bounds
3338        let timeout_value = 1800u64;
3339        assert!(timeout_value >= 300); // At least 5 minutes
3340        assert!(timeout_value <= 3600); // At most 1 hour
3341    }
3342
3343    #[test]
3344    fn test_scattered_commit_detection() {
3345        use std::collections::HashSet;
3346
3347        // Test scattered commit detection logic
3348        let mut source_branches = HashSet::new();
3349        source_branches.insert("feature-branch-1".to_string());
3350        source_branches.insert("feature-branch-2".to_string());
3351        source_branches.insert("feature-branch-3".to_string());
3352
3353        // Single branch should not trigger warning
3354        let single_branch = HashSet::from(["main".to_string()]);
3355        assert_eq!(single_branch.len(), 1);
3356
3357        // Multiple branches should trigger warning
3358        assert!(source_branches.len() > 1);
3359        assert_eq!(source_branches.len(), 3);
3360
3361        // Verify branch names are preserved correctly
3362        assert!(source_branches.contains("feature-branch-1"));
3363        assert!(source_branches.contains("feature-branch-2"));
3364        assert!(source_branches.contains("feature-branch-3"));
3365    }
3366
3367    #[test]
3368    fn test_source_branch_tracking() {
3369        // Test that source branch tracking correctly handles different scenarios
3370
3371        // Same branch should be consistent
3372        let branch_a = "feature-work";
3373        let branch_b = "feature-work";
3374        assert_eq!(branch_a, branch_b);
3375
3376        // Different branches should be detected
3377        let branch_1 = "feature-ui";
3378        let branch_2 = "feature-api";
3379        assert_ne!(branch_1, branch_2);
3380
3381        // Branch naming patterns
3382        assert!(branch_1.starts_with("feature-"));
3383        assert!(branch_2.starts_with("feature-"));
3384    }
3385
3386    // Tests for new default behavior (removing --all flag)
3387
3388    #[tokio::test]
3389    async fn test_push_default_behavior() {
3390        // Test the push_to_stack function structure and error handling
3391        // This test focuses on ensuring the function can be called and handles errors gracefully
3392        // rather than testing full integration (which requires complex setup)
3393
3394        let (_temp_dir, _repo_path) = create_test_repo().await;
3395
3396        // Test that push_to_stack properly handles the case when no stack is active
3397        let result = push_to_stack(
3398            None,  // branch
3399            None,  // message
3400            None,  // commit
3401            None,  // since
3402            None,  // commits
3403            None,  // squash
3404            None,  // squash_since
3405            false, // auto_branch
3406            false, // allow_base_branch
3407        )
3408        .await;
3409
3410        // Should fail gracefully with appropriate error message when no stack is active
3411        match &result {
3412            Err(e) => {
3413                let error_msg = e.to_string();
3414                // This is the expected behavior - no active stack should produce this error
3415                assert!(
3416                    error_msg.contains("No active stack")
3417                        || error_msg.contains("config")
3418                        || error_msg.contains("current directory")
3419                        || error_msg.contains("Not a git repository")
3420                        || error_msg.contains("could not find repository"),
3421                    "Expected 'No active stack' or repository error, got: {error_msg}"
3422                );
3423            }
3424            Ok(_) => {
3425                // If it somehow succeeds, that's also fine (e.g., if environment is set up differently)
3426                println!("Push succeeded unexpectedly - test environment may have active stack");
3427            }
3428        }
3429
3430        // Verify we can construct the command structure correctly
3431        let push_action = StackAction::Push {
3432            branch: None,
3433            message: None,
3434            commit: None,
3435            since: None,
3436            commits: None,
3437            squash: None,
3438            squash_since: None,
3439            auto_branch: false,
3440            allow_base_branch: false,
3441        };
3442
3443        assert!(matches!(
3444            push_action,
3445            StackAction::Push {
3446                branch: None,
3447                message: None,
3448                commit: None,
3449                since: None,
3450                commits: None,
3451                squash: None,
3452                squash_since: None,
3453                auto_branch: false,
3454                allow_base_branch: false
3455            }
3456        ));
3457    }
3458
3459    #[tokio::test]
3460    async fn test_submit_default_behavior() {
3461        // Test the submit_entry function structure and error handling
3462        // This test focuses on ensuring the function can be called and handles errors gracefully
3463        // rather than testing full integration (which requires complex setup)
3464
3465        let (_temp_dir, _repo_path) = create_test_repo().await;
3466
3467        // Test that submit_entry properly handles the case when no stack is active
3468        let result = submit_entry(
3469            None,  // entry (should default to all unsubmitted)
3470            None,  // title
3471            None,  // description
3472            None,  // range
3473            false, // draft
3474        )
3475        .await;
3476
3477        // Should fail gracefully with appropriate error message when no stack is active
3478        match &result {
3479            Err(e) => {
3480                let error_msg = e.to_string();
3481                // This is the expected behavior - no active stack should produce this error
3482                assert!(
3483                    error_msg.contains("No active stack")
3484                        || error_msg.contains("config")
3485                        || error_msg.contains("current directory")
3486                        || error_msg.contains("Not a git repository")
3487                        || error_msg.contains("could not find repository"),
3488                    "Expected 'No active stack' or repository error, got: {error_msg}"
3489                );
3490            }
3491            Ok(_) => {
3492                // If it somehow succeeds, that's also fine (e.g., if environment is set up differently)
3493                println!("Submit succeeded unexpectedly - test environment may have active stack");
3494            }
3495        }
3496
3497        // Verify we can construct the command structure correctly
3498        let submit_action = StackAction::Submit {
3499            entry: None,
3500            title: None,
3501            description: None,
3502            range: None,
3503            draft: false,
3504        };
3505
3506        assert!(matches!(
3507            submit_action,
3508            StackAction::Submit {
3509                entry: None,
3510                title: None,
3511                description: None,
3512                range: None,
3513                draft: false
3514            }
3515        ));
3516    }
3517
3518    #[test]
3519    fn test_targeting_options_still_work() {
3520        // Test that specific targeting options still work correctly
3521
3522        // Test commit list parsing
3523        let commits = "abc123,def456,ghi789";
3524        let parsed: Vec<&str> = commits.split(',').map(|s| s.trim()).collect();
3525        assert_eq!(parsed.len(), 3);
3526        assert_eq!(parsed[0], "abc123");
3527        assert_eq!(parsed[1], "def456");
3528        assert_eq!(parsed[2], "ghi789");
3529
3530        // Test range parsing would work
3531        let range = "1-3";
3532        assert!(range.contains('-'));
3533        let parts: Vec<&str> = range.split('-').collect();
3534        assert_eq!(parts.len(), 2);
3535
3536        // Test since reference pattern
3537        let since_ref = "HEAD~3";
3538        assert!(since_ref.starts_with("HEAD"));
3539        assert!(since_ref.contains('~'));
3540    }
3541
3542    #[test]
3543    fn test_command_flow_logic() {
3544        // These just test the command structure exists
3545        assert!(matches!(
3546            StackAction::Push {
3547                branch: None,
3548                message: None,
3549                commit: None,
3550                since: None,
3551                commits: None,
3552                squash: None,
3553                squash_since: None,
3554                auto_branch: false,
3555                allow_base_branch: false
3556            },
3557            StackAction::Push { .. }
3558        ));
3559
3560        assert!(matches!(
3561            StackAction::Submit {
3562                entry: None,
3563                title: None,
3564                description: None,
3565                range: None,
3566                draft: false
3567            },
3568            StackAction::Submit { .. }
3569        ));
3570    }
3571
3572    #[tokio::test]
3573    async fn test_deactivate_command_structure() {
3574        // Test that deactivate command structure exists and can be constructed
3575        let deactivate_action = StackAction::Deactivate { force: false };
3576
3577        // Verify it matches the expected pattern
3578        assert!(matches!(
3579            deactivate_action,
3580            StackAction::Deactivate { force: false }
3581        ));
3582
3583        // Test with force flag
3584        let force_deactivate = StackAction::Deactivate { force: true };
3585        assert!(matches!(
3586            force_deactivate,
3587            StackAction::Deactivate { force: true }
3588        ));
3589    }
3590}