cascade_cli/cli/commands/
stack.rs

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