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