cascade_cli/stack/
manager.rs

1use super::metadata::RepositoryMetadata;
2use super::{CommitMetadata, Stack, StackEntry, StackMetadata, StackStatus};
3use crate::cli::output::Output;
4use crate::config::{get_repo_config_dir, Settings};
5use crate::errors::{CascadeError, Result};
6use crate::git::GitRepository;
7use chrono::Utc;
8use dialoguer::{theme::ColorfulTheme, Input, Select};
9use std::collections::HashMap;
10use std::fs;
11use std::path::{Path, PathBuf};
12use tracing::{debug, warn};
13use uuid::Uuid;
14
15/// Types of branch modifications detected during Git integrity checks
16#[derive(Debug)]
17pub enum BranchModification {
18    /// Branch is missing (needs to be created)
19    Missing {
20        branch: String,
21        entry_id: Uuid,
22        expected_commit: String,
23    },
24    /// Branch has extra commits beyond what's expected
25    ExtraCommits {
26        branch: String,
27        entry_id: Uuid,
28        expected_commit: String,
29        actual_commit: String,
30        extra_commit_count: usize,
31        extra_commit_messages: Vec<String>,
32    },
33}
34
35/// Manages all stack operations and persistence
36pub struct StackManager {
37    /// Git repository interface
38    repo: GitRepository,
39    /// Path to the repository root
40    repo_path: PathBuf,
41    /// Path to cascade config directory
42    config_dir: PathBuf,
43    /// Path to stacks data file
44    stacks_file: PathBuf,
45    /// Path to metadata file
46    metadata_file: PathBuf,
47    /// In-memory stack data
48    stacks: HashMap<Uuid, Stack>,
49    /// Repository metadata
50    metadata: RepositoryMetadata,
51}
52
53impl StackManager {
54    /// Create a new StackManager for the given repository
55    pub fn new(repo_path: &Path) -> Result<Self> {
56        let repo = GitRepository::open(repo_path)?;
57        let config_dir = get_repo_config_dir(repo_path)?;
58        let stacks_file = config_dir.join("stacks.json");
59        let metadata_file = config_dir.join("metadata.json");
60
61        // Load configuration to get the configured default branch
62        let config_file = config_dir.join("config.json");
63        let settings = Settings::load_from_file(&config_file).unwrap_or_default();
64        let configured_default = &settings.git.default_branch;
65
66        // Using configured default branch
67
68        // Determine default base branch - use configured default if it exists
69        let default_base = if repo.branch_exists(configured_default) {
70            // Found configured default branch locally
71            configured_default.clone()
72        } else {
73            // Fall back to detecting a suitable branch
74            match repo.detect_main_branch() {
75                Ok(detected) => {
76                    // Configured default branch not found, using detected branch
77                    detected
78                }
79                Err(_) => {
80                    // Use configured default even if it doesn't exist yet (might be created later)
81                    // Using configured default branch even though it doesn't exist locally
82                    configured_default.clone()
83                }
84            }
85        };
86
87        let mut manager = Self {
88            repo,
89            repo_path: repo_path.to_path_buf(),
90            config_dir,
91            stacks_file,
92            metadata_file,
93            stacks: HashMap::new(),
94            metadata: RepositoryMetadata::new(default_base),
95        };
96
97        // Load existing data if available
98        manager.load_from_disk()?;
99
100        Ok(manager)
101    }
102
103    /// Create a new stack
104    pub fn create_stack(
105        &mut self,
106        name: String,
107        base_branch: Option<String>,
108        description: Option<String>,
109    ) -> Result<Uuid> {
110        // Check if stack with this name already exists
111        if self.metadata.find_stack_by_name(&name).is_some() {
112            return Err(CascadeError::config(format!(
113                "Stack '{name}' already exists"
114            )));
115        }
116
117        // Use provided base branch, or try to detect parent branch, or fall back to default
118        let base_branch = base_branch.unwrap_or_else(|| {
119            // Try to detect the parent branch of the current branch
120            if let Ok(Some(detected_parent)) = self.repo.detect_parent_branch() {
121                detected_parent
122            } else {
123                // Fall back to default base branch
124                self.metadata.default_base_branch.clone()
125            }
126        });
127
128        // Verify base branch exists (try to fetch from remote if not local)
129        if !self.repo.branch_exists_or_fetch(&base_branch)? {
130            return Err(CascadeError::branch(format!(
131                "Base branch '{base_branch}' does not exist locally or remotely"
132            )));
133        }
134
135        // Get current branch as the working branch
136        let current_branch = self.repo.get_current_branch().ok();
137
138        // Create the stack
139        let mut stack = Stack::new(name.clone(), base_branch.clone(), description.clone());
140
141        // Set working branch if we're on a feature branch (not on base branch)
142        if let Some(ref branch) = current_branch {
143            if branch != &base_branch {
144                stack.working_branch = Some(branch.clone());
145            }
146        }
147
148        let stack_id = stack.id;
149
150        // Create metadata
151        let stack_metadata = StackMetadata::new(stack_id, name, base_branch, description);
152
153        // Store in memory
154        self.stacks.insert(stack_id, stack);
155        self.metadata.add_stack(stack_metadata);
156
157        // Always set newly created stack as active
158        self.set_active_stack(Some(stack_id))?;
159
160        Ok(stack_id)
161    }
162
163    /// Get a stack by ID
164    pub fn get_stack(&self, stack_id: &Uuid) -> Option<&Stack> {
165        self.stacks.get(stack_id)
166    }
167
168    /// Get a mutable stack by ID
169    pub fn get_stack_mut(&mut self, stack_id: &Uuid) -> Option<&mut Stack> {
170        self.stacks.get_mut(stack_id)
171    }
172
173    /// Get stack by name
174    pub fn get_stack_by_name(&self, name: &str) -> Option<&Stack> {
175        if let Some(metadata) = self.metadata.find_stack_by_name(name) {
176            self.stacks.get(&metadata.stack_id)
177        } else {
178            None
179        }
180    }
181
182    /// Get mutable stack by name
183    pub fn get_stack_by_name_mut(&mut self, name: &str) -> Option<&mut Stack> {
184        if let Some(metadata) = self.metadata.find_stack_by_name(name) {
185            self.stacks.get_mut(&metadata.stack_id)
186        } else {
187            None
188        }
189    }
190
191    /// Update working branch for a stack
192    pub fn update_stack_working_branch(&mut self, name: &str, branch: String) -> Result<()> {
193        if let Some(stack) = self.get_stack_by_name_mut(name) {
194            stack.working_branch = Some(branch);
195            self.save_to_disk()?;
196            Ok(())
197        } else {
198            Err(CascadeError::config(format!("Stack '{name}' not found")))
199        }
200    }
201
202    /// Get the currently active stack
203    pub fn get_active_stack(&self) -> Option<&Stack> {
204        self.metadata
205            .active_stack_id
206            .and_then(|id| self.stacks.get(&id))
207    }
208
209    /// Get the currently active stack mutably
210    pub fn get_active_stack_mut(&mut self) -> Option<&mut Stack> {
211        if let Some(id) = self.metadata.active_stack_id {
212            self.stacks.get_mut(&id)
213        } else {
214            None
215        }
216    }
217
218    /// Set the active stack
219    pub fn set_active_stack(&mut self, stack_id: Option<Uuid>) -> Result<()> {
220        // Verify stack exists if provided
221        if let Some(id) = stack_id {
222            if !self.stacks.contains_key(&id) {
223                return Err(CascadeError::config(format!(
224                    "Stack with ID {id} not found"
225                )));
226            }
227        }
228
229        // Clear edit mode when switching stacks or deactivating
230        // Edit mode is tied to a specific stack context
231        if self.is_in_edit_mode() {
232            if let Some(edit_info) = self.get_edit_mode_info() {
233                // If switching to a different stack, clear edit mode
234                if edit_info.target_stack_id != stack_id {
235                    tracing::debug!(
236                        "Clearing edit mode when switching from stack {:?} to {:?}",
237                        edit_info.target_stack_id,
238                        stack_id
239                    );
240                    self.exit_edit_mode()?;
241                }
242            }
243        }
244
245        // Update active flag on stacks
246        for stack in self.stacks.values_mut() {
247            stack.set_active(Some(stack.id) == stack_id);
248        }
249
250        // Track the current branch when activating a stack
251        if let Some(id) = stack_id {
252            let current_branch = self.repo.get_current_branch().ok();
253            if let Some(stack_meta) = self.metadata.get_stack_mut(&id) {
254                stack_meta.set_current_branch(current_branch);
255            }
256        }
257
258        self.metadata.set_active_stack(stack_id);
259        self.save_to_disk()?;
260
261        Ok(())
262    }
263
264    /// Set active stack by name
265    pub fn set_active_stack_by_name(&mut self, name: &str) -> Result<()> {
266        if let Some(metadata) = self.metadata.find_stack_by_name(name) {
267            self.set_active_stack(Some(metadata.stack_id))
268        } else {
269            Err(CascadeError::config(format!("Stack '{name}' not found")))
270        }
271    }
272
273    /// Delete a stack
274    pub fn delete_stack(&mut self, stack_id: &Uuid) -> Result<Stack> {
275        let stack = self
276            .stacks
277            .remove(stack_id)
278            .ok_or_else(|| CascadeError::config(format!("Stack with ID {stack_id} not found")))?;
279
280        // Remove metadata
281        self.metadata.remove_stack(stack_id);
282
283        // Remove all associated commit metadata
284        let stack_commits: Vec<String> = self
285            .metadata
286            .commits
287            .values()
288            .filter(|commit| &commit.stack_id == stack_id)
289            .map(|commit| commit.hash.clone())
290            .collect();
291
292        for commit_hash in stack_commits {
293            self.metadata.remove_commit(&commit_hash);
294        }
295
296        // If this was the active stack, find a new one
297        if self.metadata.active_stack_id == Some(*stack_id) {
298            let new_active = self.metadata.stacks.keys().next().copied();
299            self.set_active_stack(new_active)?;
300        }
301
302        self.save_to_disk()?;
303
304        Ok(stack)
305    }
306
307    /// Push a commit to a stack
308    pub fn push_to_stack(
309        &mut self,
310        branch: String,
311        commit_hash: String,
312        message: String,
313        source_branch: String,
314    ) -> Result<Uuid> {
315        let stack_id = self
316            .metadata
317            .active_stack_id
318            .ok_or_else(|| CascadeError::config("No active stack"))?;
319
320        // šŸ†• RECONCILE METADATA: Sync entry commit hashes with current branch HEADs before validation
321        // This prevents false "branch modification" errors from stale metadata (e.g., after ca sync)
322        let mut reconciled = false;
323        {
324            let stack = self
325                .stacks
326                .get_mut(&stack_id)
327                .ok_or_else(|| CascadeError::config("Active stack not found"))?;
328
329            if !stack.entries.is_empty() {
330                // Collect updates first to avoid borrow checker issues
331                let mut updates = Vec::new();
332                for entry in &stack.entries {
333                    if let Ok(current_commit) = self.repo.get_branch_head(&entry.branch) {
334                        if entry.commit_hash != current_commit {
335                            debug!(
336                                "Reconciling stale metadata for '{}': updating hash from {} to {} (current branch HEAD)",
337                                entry.branch,
338                                &entry.commit_hash[..8],
339                                &current_commit[..8]
340                            );
341                            updates.push((entry.id, current_commit));
342                        }
343                    }
344                }
345
346                // Apply updates using the safe wrapper function
347                for (entry_id, new_hash) in updates {
348                    stack
349                        .update_entry_commit_hash(&entry_id, new_hash)
350                        .map_err(CascadeError::config)?;
351                    reconciled = true;
352                }
353            }
354        } // End of reconciliation scope - stack borrow dropped here
355
356        // Save reconciled metadata before validation
357        if reconciled {
358            debug!("Saving reconciled metadata before validation");
359            self.save_to_disk()?;
360        }
361
362        // šŸ†• VALIDATE GIT INTEGRITY BEFORE PUSHING (after reconciliation)
363        let stack = self
364            .stacks
365            .get_mut(&stack_id)
366            .ok_or_else(|| CascadeError::config("Active stack not found"))?;
367
368        if !stack.entries.is_empty() {
369            if let Err(integrity_error) = stack.validate_git_integrity(&self.repo) {
370                return Err(CascadeError::validation(format!(
371                    "Git integrity validation failed:\n{}\n\n\
372                     Fix the stack integrity issues first using 'ca stack validate {}' for details.",
373                    integrity_error, stack.name
374                )));
375            }
376        }
377
378        // Verify the commit exists
379        if !self.repo.commit_exists(&commit_hash)? {
380            return Err(CascadeError::branch(format!(
381                "Commit {commit_hash} does not exist"
382            )));
383        }
384
385        // Check for duplicate commit messages within the same stack
386        // Trim both messages for comparison to handle trailing whitespace/newlines
387        let message_trimmed = message.trim();
388        if let Some(duplicate_entry) = stack
389            .entries
390            .iter()
391            .find(|entry| entry.message.trim() == message_trimmed)
392        {
393            return Err(CascadeError::validation(format!(
394                "Duplicate commit message in stack: \"{}\"\n\n\
395                 This message already exists in entry {} (commit: {})\n\n\
396                 šŸ’” Consider using a more specific message:\n\
397                    • Add context: \"{} - add validation\"\n\
398                    • Be more specific: \"Fix user authentication timeout bug\"\n\
399                    • Or amend the previous commit: git commit --amend",
400                message_trimmed,
401                duplicate_entry.id,
402                &duplicate_entry.commit_hash[..8],
403                message_trimmed
404            )));
405        }
406
407        // šŸŽÆ SMART BASE BRANCH UPDATE FOR FEATURE WORKFLOW
408        // If this is the first commit in an empty stack, and the user is on a feature branch
409        // that's different from the stack's base branch, update the base branch to match
410        // the current workflow.
411        if stack.entries.is_empty() {
412            let current_branch = self.repo.get_current_branch()?;
413
414            // Update working branch if not already set
415            if stack.working_branch.is_none() && current_branch != stack.base_branch {
416                stack.working_branch = Some(current_branch.clone());
417                tracing::debug!(
418                    "Set working branch for stack '{}' to '{}'",
419                    stack.name,
420                    current_branch
421                );
422            }
423
424            if current_branch != stack.base_branch && current_branch != "HEAD" {
425                // Check if current branch was created from the stack's base branch
426                let base_exists = self.repo.branch_exists(&stack.base_branch);
427                let current_is_feature = current_branch.starts_with("feature/")
428                    || current_branch.starts_with("fix/")
429                    || current_branch.starts_with("chore/")
430                    || current_branch.contains("feature")
431                    || current_branch.contains("fix");
432
433                if base_exists && current_is_feature {
434                    tracing::debug!(
435                        "First commit detected: updating stack '{}' base branch from '{}' to '{}'",
436                        stack.name,
437                        stack.base_branch,
438                        current_branch
439                    );
440
441                    Output::info("Smart Base Branch Update:");
442                    Output::sub_item(format!(
443                        "Stack '{}' was created with base '{}'",
444                        stack.name, stack.base_branch
445                    ));
446                    Output::sub_item(format!(
447                        "You're now working on feature branch '{current_branch}'"
448                    ));
449                    Output::sub_item("Updating stack base branch to match your workflow");
450
451                    // Update the stack's base branch
452                    stack.base_branch = current_branch.clone();
453
454                    // Update metadata as well
455                    if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
456                        stack_meta.base_branch = current_branch.clone();
457                        stack_meta.set_current_branch(Some(current_branch.clone()));
458                    }
459
460                    println!(
461                        "   āœ… Stack '{}' base branch updated to '{current_branch}'",
462                        stack.name
463                    );
464                }
465            }
466        }
467
468        // šŸ†• CREATE ACTUAL GIT BRANCH from the specific commit
469        // Check if branch already exists
470        if self.repo.branch_exists(&branch) {
471            // Branch already exists, skip creation
472        } else {
473            // Create the branch from the specific commit hash
474            self.repo
475                .create_branch(&branch, Some(&commit_hash))
476                .map_err(|e| {
477                    CascadeError::branch(format!(
478                        "Failed to create branch '{}' from commit {}: {}",
479                        branch,
480                        &commit_hash[..8],
481                        e
482                    ))
483                })?;
484
485            // Branch creation succeeded - logging handled by caller
486        }
487
488        // Add to stack
489        let entry_id = stack.push_entry(branch.clone(), commit_hash.clone(), message.clone());
490
491        // Create commit metadata
492        let commit_metadata = CommitMetadata::new(
493            commit_hash.clone(),
494            message,
495            entry_id,
496            stack_id,
497            branch.clone(),
498            source_branch,
499        );
500
501        // Update repository metadata
502        self.metadata.add_commit(commit_metadata);
503        if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
504            stack_meta.add_branch(branch);
505            stack_meta.add_commit(commit_hash);
506        }
507
508        self.save_to_disk()?;
509
510        Ok(entry_id)
511    }
512
513    /// Pop the top commit from the active stack
514    pub fn pop_from_stack(&mut self) -> Result<StackEntry> {
515        let stack_id = self
516            .metadata
517            .active_stack_id
518            .ok_or_else(|| CascadeError::config("No active stack"))?;
519
520        let stack = self
521            .stacks
522            .get_mut(&stack_id)
523            .ok_or_else(|| CascadeError::config("Active stack not found"))?;
524
525        let entry = stack
526            .pop_entry()
527            .ok_or_else(|| CascadeError::config("Stack is empty"))?;
528
529        // Remove commit metadata
530        self.metadata.remove_commit(&entry.commit_hash);
531
532        // Update stack metadata
533        if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
534            stack_meta.remove_commit(&entry.commit_hash);
535            // Note: We don't remove the branch as there might be other commits on it
536        }
537
538        self.save_to_disk()?;
539
540        Ok(entry)
541    }
542
543    /// Submit a stack entry for review (mark as submitted)
544    pub fn submit_entry(
545        &mut self,
546        stack_id: &Uuid,
547        entry_id: &Uuid,
548        pull_request_id: String,
549    ) -> Result<()> {
550        let stack = self
551            .stacks
552            .get_mut(stack_id)
553            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
554
555        let entry_commit_hash = {
556            let entry = stack
557                .get_entry(entry_id)
558                .ok_or_else(|| CascadeError::config(format!("Entry {entry_id} not found")))?;
559            entry.commit_hash.clone()
560        };
561
562        // Update stack entry
563        if !stack.mark_entry_submitted(entry_id, pull_request_id.clone()) {
564            return Err(CascadeError::config(format!(
565                "Failed to mark entry {entry_id} as submitted"
566            )));
567        }
568
569        // Update commit metadata
570        if let Some(commit_meta) = self.metadata.commits.get_mut(&entry_commit_hash) {
571            commit_meta.mark_submitted(pull_request_id);
572        }
573
574        // Update stack metadata statistics
575        if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
576            let submitted_count = stack.entries.iter().filter(|e| e.is_submitted).count();
577            let merged_count = stack.entries.iter().filter(|e| e.is_merged).count();
578            stack_meta.update_stats(stack.entries.len(), submitted_count, merged_count);
579        }
580
581        self.save_to_disk()?;
582
583        Ok(())
584    }
585
586    /// Remove a stack entry and update metadata safely (used by cleanup flows)
587    pub fn remove_stack_entry(
588        &mut self,
589        stack_id: &Uuid,
590        entry_id: &Uuid,
591    ) -> Result<Option<StackEntry>> {
592        let stack = match self.stacks.get_mut(stack_id) {
593            Some(stack) => stack,
594            None => return Err(CascadeError::config(format!("Stack {stack_id} not found"))),
595        };
596
597        let entry = match stack.entry_map.get(entry_id) {
598            Some(entry) => entry.clone(),
599            None => return Ok(None),
600        };
601
602        if !entry.children.is_empty() {
603            warn!(
604                "Skipping removal of stack entry {} (branch '{}') because it still has {} child entr{}",
605                entry.id,
606                entry.branch,
607                entry.children.len(),
608                if entry.children.len() == 1 { "y" } else { "ies" }
609            );
610            return Ok(None);
611        }
612
613        // Remove entry from the ordered list
614        stack.entries.retain(|e| e.id != entry.id);
615
616        // Remove entry from lookup map
617        stack.entry_map.remove(&entry.id);
618
619        // Detach from parent so metadata stays accurate
620        if let Some(parent_id) = entry.parent_id {
621            if let Some(parent) = stack.entry_map.get_mut(&parent_id) {
622                parent.children.retain(|child| child != &entry.id);
623            }
624        }
625
626        // Sync entries vector from map to ensure consistency
627        stack.repair_data_consistency();
628        stack.updated_at = Utc::now();
629
630        // Update repository metadata (commit + branch bookkeeping)
631        self.metadata.remove_commit(&entry.commit_hash);
632        if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
633            stack_meta.remove_commit(&entry.commit_hash);
634            stack_meta.remove_branch(&entry.branch);
635
636            let submitted = stack.entries.iter().filter(|e| e.is_submitted).count();
637            let merged = stack.entries.iter().filter(|e| e.is_merged).count();
638            stack_meta.update_stats(stack.entries.len(), submitted, merged);
639        }
640
641        self.save_to_disk()?;
642
643        Ok(Some(entry))
644    }
645
646    /// Update merged state for a stack entry
647    pub fn set_entry_merged(
648        &mut self,
649        stack_id: &Uuid,
650        entry_id: &Uuid,
651        merged: bool,
652    ) -> Result<()> {
653        let stack = self
654            .stacks
655            .get_mut(stack_id)
656            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
657
658        let current_entry = stack
659            .entry_map
660            .get(entry_id)
661            .cloned()
662            .ok_or_else(|| CascadeError::config(format!("Entry {entry_id} not found")))?;
663
664        if current_entry.is_merged == merged {
665            return Ok(());
666        }
667
668        if !stack.mark_entry_merged(entry_id, merged) {
669            return Err(CascadeError::config(format!(
670                "Entry {entry_id} not found in stack {stack_id}"
671            )));
672        }
673
674        // Update commit metadata using the stored commit hash
675        if let Some(commit_meta) = self.metadata.commits.get_mut(&current_entry.commit_hash) {
676            commit_meta.mark_merged(merged);
677        }
678
679        // Update stack metadata statistics
680        if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
681            let submitted_count = stack.entries.iter().filter(|e| e.is_submitted).count();
682            let merged_count = stack.entries.iter().filter(|e| e.is_merged).count();
683            stack_meta.update_stats(stack.entries.len(), submitted_count, merged_count);
684        }
685
686        self.save_to_disk()?;
687
688        Ok(())
689    }
690
691    /// Repair data consistency issues in all stacks
692    pub fn repair_all_stacks(&mut self) -> Result<()> {
693        for stack in self.stacks.values_mut() {
694            stack.repair_data_consistency();
695        }
696        self.save_to_disk()?;
697        Ok(())
698    }
699
700    /// Get all stacks
701    pub fn get_all_stacks(&self) -> Vec<&Stack> {
702        self.stacks.values().collect()
703    }
704
705    /// Get stack metadata
706    pub fn get_stack_metadata(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
707        self.metadata.get_stack(stack_id)
708    }
709
710    /// Get repository metadata
711    pub fn get_repository_metadata(&self) -> &RepositoryMetadata {
712        &self.metadata
713    }
714
715    /// Get the Git repository
716    pub fn git_repo(&self) -> &GitRepository {
717        &self.repo
718    }
719
720    /// Get the repository path
721    pub fn repo_path(&self) -> &Path {
722        &self.repo_path
723    }
724
725    // Edit mode management methods
726
727    /// Check if currently in edit mode
728    pub fn is_in_edit_mode(&self) -> bool {
729        self.metadata
730            .edit_mode
731            .as_ref()
732            .map(|edit_state| edit_state.is_active)
733            .unwrap_or(false)
734    }
735
736    /// Get current edit mode information
737    pub fn get_edit_mode_info(&self) -> Option<&super::metadata::EditModeState> {
738        self.metadata.edit_mode.as_ref()
739    }
740
741    /// Enter edit mode for a specific stack entry
742    pub fn enter_edit_mode(&mut self, stack_id: Uuid, entry_id: Uuid) -> Result<()> {
743        // Get the commit hash first to avoid borrow checker issues
744        let commit_hash = {
745            let stack = self
746                .get_stack(&stack_id)
747                .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
748
749            let entry = stack.get_entry(&entry_id).ok_or_else(|| {
750                CascadeError::config(format!("Entry {entry_id} not found in stack"))
751            })?;
752
753            entry.commit_hash.clone()
754        };
755
756        // If already in edit mode, exit the current one first
757        if self.is_in_edit_mode() {
758            self.exit_edit_mode()?;
759        }
760
761        // Create new edit mode state
762        let edit_state = super::metadata::EditModeState::new(stack_id, entry_id, commit_hash);
763
764        self.metadata.edit_mode = Some(edit_state);
765        self.save_to_disk()?;
766
767        debug!(
768            "Entered edit mode for entry {} in stack {}",
769            entry_id, stack_id
770        );
771        Ok(())
772    }
773
774    /// Exit edit mode
775    pub fn exit_edit_mode(&mut self) -> Result<()> {
776        if !self.is_in_edit_mode() {
777            return Err(CascadeError::config("Not currently in edit mode"));
778        }
779
780        // Clear edit mode state
781        self.metadata.edit_mode = None;
782        self.save_to_disk()?;
783
784        debug!("Exited edit mode");
785        Ok(())
786    }
787
788    /// Sync stack with Git repository state
789    pub fn sync_stack(&mut self, stack_id: &Uuid) -> Result<()> {
790        let stack = self
791            .stacks
792            .get_mut(stack_id)
793            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
794
795        // šŸ†• ENHANCED: Check Git integrity first (branch HEAD matches stored commits)
796        if let Err(integrity_error) = stack.validate_git_integrity(&self.repo) {
797            stack.update_status(StackStatus::Corrupted);
798            return Err(CascadeError::branch(format!(
799                "Stack '{}' Git integrity check failed:\n{}",
800                stack.name, integrity_error
801            )));
802        }
803
804        // Check if all commits still exist
805        let mut missing_commits = Vec::new();
806        for entry in &stack.entries {
807            if !self.repo.commit_exists(&entry.commit_hash)? {
808                missing_commits.push(entry.commit_hash.clone());
809            }
810        }
811
812        if !missing_commits.is_empty() {
813            stack.update_status(StackStatus::Corrupted);
814            return Err(CascadeError::branch(format!(
815                "Stack {} has missing commits: {}",
816                stack.name,
817                missing_commits.join(", ")
818            )));
819        }
820
821        // Check if base branch exists and has new commits (try to fetch from remote if not local)
822        if !self.repo.branch_exists_or_fetch(&stack.base_branch)? {
823            return Err(CascadeError::branch(format!(
824                "Base branch '{}' does not exist locally or remotely. Check the branch name or switch to a different base.",
825                stack.base_branch
826            )));
827        }
828
829        let _base_hash = self.repo.get_branch_head(&stack.base_branch)?;
830
831        // Check if any stack entries are missing their commits
832        let mut corrupted_entry = None;
833        for entry in &stack.entries {
834            if !self.repo.commit_exists(&entry.commit_hash)? {
835                corrupted_entry = Some((entry.commit_hash.clone(), entry.branch.clone()));
836                break;
837            }
838        }
839
840        if let Some((commit_hash, branch)) = corrupted_entry {
841            stack.update_status(StackStatus::Corrupted);
842            return Err(CascadeError::branch(format!(
843                "Commit {commit_hash} from stack entry '{branch}' no longer exists"
844            )));
845        }
846
847        // Compare base branch with the earliest commit in the stack
848        let needs_sync = if let Some(first_entry) = stack.entries.first() {
849            // Get commits between base and first entry
850            match self
851                .repo
852                .get_commits_between(&stack.base_branch, &first_entry.commit_hash)
853            {
854                Ok(commits) => !commits.is_empty(), // If there are commits, we need to sync
855                Err(_) => true,                     // If we can't compare, assume we need to sync
856            }
857        } else {
858            false // Empty stack is always clean
859        };
860
861        // Update stack status based on sync needs
862        if needs_sync {
863            stack.update_status(StackStatus::NeedsSync);
864            debug!(
865                "Stack '{}' needs sync - new commits on base branch",
866                stack.name
867            );
868        } else {
869            stack.update_status(StackStatus::Clean);
870            debug!("Stack '{}' is clean", stack.name);
871        }
872
873        // Update metadata
874        if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
875            stack_meta.set_up_to_date(true);
876        }
877
878        self.save_to_disk()?;
879
880        Ok(())
881    }
882
883    /// List all stacks with their status
884    pub fn list_stacks(&self) -> Vec<(Uuid, &str, &StackStatus, usize, Option<&str>)> {
885        self.stacks
886            .values()
887            .map(|stack| {
888                (
889                    stack.id,
890                    stack.name.as_str(),
891                    &stack.status,
892                    stack.entries.len(),
893                    if stack.is_active {
894                        Some("active")
895                    } else {
896                        None
897                    },
898                )
899            })
900            .collect()
901    }
902
903    /// Get all stacks as Stack objects for TUI
904    pub fn get_all_stacks_objects(&self) -> Result<Vec<Stack>> {
905        let mut stacks: Vec<Stack> = self.stacks.values().cloned().collect();
906        stacks.sort_by(|a, b| a.name.cmp(&b.name));
907        Ok(stacks)
908    }
909
910    /// Validate all stacks including Git integrity
911    pub fn validate_all(&self) -> Result<()> {
912        for stack in self.stacks.values() {
913            // Basic structure validation
914            stack.validate().map_err(|e| {
915                CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
916            })?;
917
918            // Git integrity validation
919            stack.validate_git_integrity(&self.repo).map_err(|e| {
920                CascadeError::config(format!(
921                    "Stack '{}' Git integrity validation failed: {}",
922                    stack.name, e
923                ))
924            })?;
925        }
926        Ok(())
927    }
928
929    /// Validate a specific stack including Git integrity
930    pub fn validate_stack(&self, stack_id: &Uuid) -> Result<()> {
931        let stack = self
932            .stacks
933            .get(stack_id)
934            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
935
936        // Basic structure validation
937        stack.validate().map_err(|e| {
938            CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
939        })?;
940
941        // Git integrity validation
942        stack.validate_git_integrity(&self.repo).map_err(|e| {
943            CascadeError::config(format!(
944                "Stack '{}' Git integrity validation failed: {}",
945                stack.name, e
946            ))
947        })?;
948
949        Ok(())
950    }
951
952    /// Save all data to disk
953    pub fn save_to_disk(&self) -> Result<()> {
954        // Ensure config directory exists
955        if !self.config_dir.exists() {
956            fs::create_dir_all(&self.config_dir).map_err(|e| {
957                CascadeError::config(format!("Failed to create config directory: {e}"))
958            })?;
959        }
960
961        // Save stacks atomically
962        crate::utils::atomic_file::write_json(&self.stacks_file, &self.stacks)?;
963
964        // Save metadata atomically
965        crate::utils::atomic_file::write_json(&self.metadata_file, &self.metadata)?;
966
967        Ok(())
968    }
969
970    /// Load data from disk
971    fn load_from_disk(&mut self) -> Result<()> {
972        // Load stacks if file exists
973        if self.stacks_file.exists() {
974            let stacks_content = fs::read_to_string(&self.stacks_file)
975                .map_err(|e| CascadeError::config(format!("Failed to read stacks file: {e}")))?;
976
977            self.stacks = serde_json::from_str(&stacks_content)
978                .map_err(|e| CascadeError::config(format!("Failed to parse stacks file: {e}")))?;
979        }
980
981        // Load metadata if file exists
982        if self.metadata_file.exists() {
983            let metadata_content = fs::read_to_string(&self.metadata_file)
984                .map_err(|e| CascadeError::config(format!("Failed to read metadata file: {e}")))?;
985
986            self.metadata = serde_json::from_str(&metadata_content)
987                .map_err(|e| CascadeError::config(format!("Failed to parse metadata file: {e}")))?;
988        }
989
990        Ok(())
991    }
992
993    /// Check if the user has changed branches since the stack was activated
994    /// Returns true if branch change detected and user wants to proceed
995    pub fn check_for_branch_change(&mut self) -> Result<bool> {
996        // Extract stack information first to avoid borrow conflicts
997        let (stack_id, stack_name, stored_branch) = {
998            if let Some(active_stack) = self.get_active_stack() {
999                let stack_id = active_stack.id;
1000                let stack_name = active_stack.name.clone();
1001                let stored_branch = if let Some(stack_meta) = self.metadata.get_stack(&stack_id) {
1002                    stack_meta.current_branch.clone()
1003                } else {
1004                    None
1005                };
1006                (Some(stack_id), stack_name, stored_branch)
1007            } else {
1008                (None, String::new(), None)
1009            }
1010        };
1011
1012        // If no active stack, nothing to check
1013        let Some(stack_id) = stack_id else {
1014            return Ok(true);
1015        };
1016
1017        let current_branch = self.repo.get_current_branch().ok();
1018
1019        // Check if branch has changed
1020        if stored_branch.as_ref() != current_branch.as_ref() {
1021            Output::warning("Branch change detected!");
1022            Output::sub_item(format!(
1023                "Stack '{}' was active on: {}",
1024                stack_name,
1025                stored_branch.as_deref().unwrap_or("unknown")
1026            ));
1027            Output::sub_item(format!(
1028                "Current branch: {}",
1029                current_branch.as_deref().unwrap_or("unknown")
1030            ));
1031            Output::spacing();
1032
1033            let options = vec![
1034                format!("Keep stack '{stack_name}' active (continue with stack workflow)"),
1035                "Deactivate stack (use normal Git workflow)".to_string(),
1036                "Switch to a different stack".to_string(),
1037                "Cancel and stay on current workflow".to_string(),
1038            ];
1039
1040            let choice = Select::with_theme(&ColorfulTheme::default())
1041                .with_prompt("What would you like to do?")
1042                .default(0)
1043                .items(&options)
1044                .interact()
1045                .map_err(|e| CascadeError::config(format!("Failed to get user choice: {e}")))?;
1046
1047            match choice {
1048                0 => {
1049                    // Update the tracked branch and continue
1050                    if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
1051                        stack_meta.set_current_branch(current_branch);
1052                    }
1053                    self.save_to_disk()?;
1054                    Output::success(format!(
1055                        "Continuing with stack '{stack_name}' on current branch"
1056                    ));
1057                    return Ok(true);
1058                }
1059                1 => {
1060                    // Deactivate the stack
1061                    self.set_active_stack(None)?;
1062                    Output::success(format!(
1063                        "Deactivated stack '{stack_name}' - using normal Git workflow"
1064                    ));
1065                    return Ok(false);
1066                }
1067                2 => {
1068                    // Show available stacks
1069                    let stacks = self.get_all_stacks();
1070                    if stacks.len() <= 1 {
1071                        Output::warning("No other stacks available. Deactivating current stack.");
1072                        self.set_active_stack(None)?;
1073                        return Ok(false);
1074                    }
1075
1076                    Output::spacing();
1077                    Output::info("Available stacks:");
1078                    for (i, stack) in stacks.iter().enumerate() {
1079                        if stack.id != stack_id {
1080                            Output::numbered_item(i + 1, &stack.name);
1081                        }
1082                    }
1083                    let stack_name_input: String = Input::with_theme(&ColorfulTheme::default())
1084                        .with_prompt("Enter stack name")
1085                        .validate_with(|input: &String| -> std::result::Result<(), &str> {
1086                            if input.trim().is_empty() {
1087                                Err("Stack name cannot be empty")
1088                            } else {
1089                                Ok(())
1090                            }
1091                        })
1092                        .interact_text()
1093                        .map_err(|e| {
1094                            CascadeError::config(format!("Failed to get user input: {e}"))
1095                        })?;
1096                    let stack_name_input = stack_name_input.trim();
1097
1098                    if let Err(e) = self.set_active_stack_by_name(stack_name_input) {
1099                        Output::warning(format!("{e}"));
1100                        Output::sub_item("Deactivating stack instead.");
1101                        self.set_active_stack(None)?;
1102                        return Ok(false);
1103                    } else {
1104                        Output::success(format!("Switched to stack '{stack_name_input}'"));
1105                        return Ok(true);
1106                    }
1107                }
1108                3 => {
1109                    Output::info("Cancelled - no changes made");
1110                    return Ok(false);
1111                }
1112                _ => {
1113                    Output::info("Invalid choice - no changes made");
1114                    return Ok(false);
1115                }
1116            }
1117        }
1118
1119        // No branch change detected
1120        Ok(true)
1121    }
1122
1123    /// Handle Git integrity issues with multiple user-friendly options
1124    /// Provides non-destructive choices for dealing with branch modifications
1125    pub fn handle_branch_modifications(
1126        &mut self,
1127        stack_id: &Uuid,
1128        auto_mode: Option<String>,
1129    ) -> Result<()> {
1130        let stack = self
1131            .stacks
1132            .get_mut(stack_id)
1133            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
1134
1135        debug!("Checking Git integrity for stack '{}'", stack.name);
1136
1137        // Detect all modifications
1138        let mut modifications = Vec::new();
1139        for entry in &stack.entries {
1140            if !self.repo.branch_exists(&entry.branch) {
1141                modifications.push(BranchModification::Missing {
1142                    branch: entry.branch.clone(),
1143                    entry_id: entry.id,
1144                    expected_commit: entry.commit_hash.clone(),
1145                });
1146            } else if let Ok(branch_head) = self.repo.get_branch_head(&entry.branch) {
1147                if branch_head != entry.commit_hash {
1148                    // Get extra commits and their messages
1149                    let extra_commits = self
1150                        .repo
1151                        .get_commits_between(&entry.commit_hash, &branch_head)?;
1152                    let mut extra_messages = Vec::new();
1153                    for commit in &extra_commits {
1154                        if let Some(message) = commit.message() {
1155                            let first_line =
1156                                message.lines().next().unwrap_or("(no message)").to_string();
1157                            extra_messages.push(format!(
1158                                "{}: {}",
1159                                &commit.id().to_string()[..8],
1160                                first_line
1161                            ));
1162                        }
1163                    }
1164
1165                    modifications.push(BranchModification::ExtraCommits {
1166                        branch: entry.branch.clone(),
1167                        entry_id: entry.id,
1168                        expected_commit: entry.commit_hash.clone(),
1169                        actual_commit: branch_head,
1170                        extra_commit_count: extra_commits.len(),
1171                        extra_commit_messages: extra_messages,
1172                    });
1173                }
1174            }
1175        }
1176
1177        if modifications.is_empty() {
1178            // Silent success - no issues to report
1179            return Ok(());
1180        }
1181
1182        // Show detected modifications
1183        println!();
1184        Output::section(format!("Branch modifications detected in '{}'", stack.name));
1185        for (i, modification) in modifications.iter().enumerate() {
1186            match modification {
1187                BranchModification::Missing { branch, .. } => {
1188                    Output::numbered_item(i + 1, format!("Branch '{branch}' is missing"));
1189                }
1190                BranchModification::ExtraCommits {
1191                    branch,
1192                    expected_commit,
1193                    actual_commit,
1194                    extra_commit_count,
1195                    extra_commit_messages,
1196                    ..
1197                } => {
1198                    println!(
1199                        "   {}. Branch '{}' has {} extra commit(s)",
1200                        i + 1,
1201                        branch,
1202                        extra_commit_count
1203                    );
1204                    println!(
1205                        "      Expected: {} | Actual: {}",
1206                        &expected_commit[..8],
1207                        &actual_commit[..8]
1208                    );
1209
1210                    // Show extra commit messages (first few only)
1211                    for (j, message) in extra_commit_messages.iter().enumerate() {
1212                        match j.cmp(&3) {
1213                            std::cmp::Ordering::Less => {
1214                                Output::sub_item(format!("     + {message}"));
1215                            }
1216                            std::cmp::Ordering::Equal => {
1217                                Output::sub_item(format!(
1218                                    "     + ... and {} more",
1219                                    extra_commit_count - 3
1220                                ));
1221                                break;
1222                            }
1223                            std::cmp::Ordering::Greater => {
1224                                break;
1225                            }
1226                        }
1227                    }
1228                }
1229            }
1230        }
1231        Output::spacing();
1232
1233        // Auto mode handling
1234        if let Some(mode) = auto_mode {
1235            return self.apply_auto_fix(stack_id, &modifications, &mode);
1236        }
1237
1238        // Interactive mode - ask user for each modification
1239        let mut handled_count = 0;
1240        let mut skipped_count = 0;
1241        for modification in modifications.iter() {
1242            let was_skipped = self.handle_single_modification(stack_id, modification)?;
1243            if was_skipped {
1244                skipped_count += 1;
1245            } else {
1246                handled_count += 1;
1247            }
1248        }
1249
1250        self.save_to_disk()?;
1251
1252        // Show appropriate summary based on what was done
1253        if skipped_count == 0 {
1254            Output::success("All branch modifications resolved");
1255        } else if handled_count > 0 {
1256            Output::warning(format!(
1257                "Resolved {} modification(s), {} skipped",
1258                handled_count, skipped_count
1259            ));
1260        } else {
1261            Output::warning("All modifications skipped - integrity issues remain");
1262        }
1263
1264        Ok(())
1265    }
1266
1267    /// Handle a single branch modification interactively
1268    /// Returns true if the modification was skipped, false if handled
1269    fn handle_single_modification(
1270        &mut self,
1271        stack_id: &Uuid,
1272        modification: &BranchModification,
1273    ) -> Result<bool> {
1274        match modification {
1275            BranchModification::Missing {
1276                branch,
1277                expected_commit,
1278                ..
1279            } => {
1280                Output::info(format!("Missing branch '{branch}'"));
1281                Output::sub_item(format!(
1282                    "Will create the branch at commit {}",
1283                    &expected_commit[..8]
1284                ));
1285
1286                self.repo.create_branch(branch, Some(expected_commit))?;
1287                Output::success(format!("Created branch '{branch}'"));
1288                Ok(false) // Not skipped
1289            }
1290
1291            BranchModification::ExtraCommits {
1292                branch,
1293                entry_id,
1294                expected_commit,
1295                extra_commit_count,
1296                ..
1297            } => {
1298                println!();
1299                Output::info(format!(
1300                    "Branch '{}' has {} extra commit(s)",
1301                    branch, extra_commit_count
1302                ));
1303                let options = vec![
1304                    "Incorporate - Update stack entry to include extra commits",
1305                    "Split - Create new stack entry for extra commits",
1306                    "Reset - Remove extra commits (DESTRUCTIVE)",
1307                    "Skip - Leave as-is for now",
1308                ];
1309
1310                let choice = Select::with_theme(&ColorfulTheme::default())
1311                    .with_prompt("Choose how to handle extra commits")
1312                    .default(0)
1313                    .items(&options)
1314                    .interact()
1315                    .map_err(|e| CascadeError::config(format!("Failed to get user choice: {e}")))?;
1316
1317                match choice {
1318                    0 => {
1319                        self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1320                        Ok(false) // Not skipped
1321                    }
1322                    1 => {
1323                        self.split_extra_commits(stack_id, *entry_id, branch)?;
1324                        Ok(false) // Not skipped
1325                    }
1326                    2 => {
1327                        self.reset_branch_destructive(branch, expected_commit)?;
1328                        Ok(false) // Not skipped
1329                    }
1330                    3 => {
1331                        Output::warning(format!("Skipped '{branch}' - integrity issue remains"));
1332                        Ok(true) // Skipped
1333                    }
1334                    _ => {
1335                        Output::warning(format!("Invalid choice - skipped '{branch}'"));
1336                        Ok(true) // Skipped
1337                    }
1338                }
1339            }
1340        }
1341    }
1342
1343    /// Apply automatic fix based on mode
1344    fn apply_auto_fix(
1345        &mut self,
1346        stack_id: &Uuid,
1347        modifications: &[BranchModification],
1348        mode: &str,
1349    ) -> Result<()> {
1350        Output::info(format!("šŸ¤– Applying automatic fix mode: {mode}"));
1351
1352        for modification in modifications {
1353            match (modification, mode) {
1354                (
1355                    BranchModification::Missing {
1356                        branch,
1357                        expected_commit,
1358                        ..
1359                    },
1360                    _,
1361                ) => {
1362                    self.repo.create_branch(branch, Some(expected_commit))?;
1363                    Output::success(format!("Created missing branch '{branch}'"));
1364                }
1365
1366                (
1367                    BranchModification::ExtraCommits {
1368                        branch, entry_id, ..
1369                    },
1370                    "incorporate",
1371                ) => {
1372                    self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1373                }
1374
1375                (
1376                    BranchModification::ExtraCommits {
1377                        branch, entry_id, ..
1378                    },
1379                    "split",
1380                ) => {
1381                    self.split_extra_commits(stack_id, *entry_id, branch)?;
1382                }
1383
1384                (
1385                    BranchModification::ExtraCommits {
1386                        branch,
1387                        expected_commit,
1388                        ..
1389                    },
1390                    "reset",
1391                ) => {
1392                    self.reset_branch_destructive(branch, expected_commit)?;
1393                }
1394
1395                _ => {
1396                    return Err(CascadeError::config(format!(
1397                        "Unknown auto-fix mode '{mode}'. Use: incorporate, split, reset"
1398                    )));
1399                }
1400            }
1401        }
1402
1403        self.save_to_disk()?;
1404        Output::success(format!("Auto-fix completed for mode: {mode}"));
1405        Ok(())
1406    }
1407
1408    /// Incorporate extra commits into the existing stack entry (update commit hash)
1409    fn incorporate_extra_commits(
1410        &mut self,
1411        stack_id: &Uuid,
1412        entry_id: Uuid,
1413        branch: &str,
1414    ) -> Result<()> {
1415        let stack = self
1416            .stacks
1417            .get_mut(stack_id)
1418            .ok_or_else(|| CascadeError::config(format!("Stack not found: {}", stack_id)))?;
1419
1420        // Find entry and get info we need before mutation
1421        let entry_info = stack
1422            .entries
1423            .iter()
1424            .find(|e| e.id == entry_id)
1425            .map(|e| (e.commit_hash.clone(), e.id));
1426
1427        if let Some((old_commit_hash, entry_id)) = entry_info {
1428            let new_head = self.repo.get_branch_head(branch)?;
1429            let old_commit = old_commit_hash[..8].to_string();
1430
1431            // Get the extra commits for message update
1432            let extra_commits = self.repo.get_commits_between(&old_commit_hash, &new_head)?;
1433
1434            // Update the entry to point to the new HEAD using safe wrapper
1435            // Note: We intentionally do NOT append commit messages here
1436            // The entry's message should describe the feature/change, not list all commits
1437            stack
1438                .update_entry_commit_hash(&entry_id, new_head.clone())
1439                .map_err(CascadeError::config)?;
1440
1441            Output::success(format!(
1442                "Incorporated {} commit(s) into entry '{}'",
1443                extra_commits.len(),
1444                &new_head[..8]
1445            ));
1446            Output::sub_item(format!("Updated: {} -> {}", old_commit, &new_head[..8]));
1447        }
1448
1449        Ok(())
1450    }
1451
1452    /// Split extra commits into a new stack entry
1453    fn split_extra_commits(&mut self, stack_id: &Uuid, entry_id: Uuid, branch: &str) -> Result<()> {
1454        let stack = self
1455            .stacks
1456            .get_mut(stack_id)
1457            .ok_or_else(|| CascadeError::config(format!("Stack not found: {}", stack_id)))?;
1458        let new_head = self.repo.get_branch_head(branch)?;
1459
1460        // Find the position of the current entry
1461        let entry_position = stack
1462            .entries
1463            .iter()
1464            .position(|e| e.id == entry_id)
1465            .ok_or_else(|| CascadeError::config("Entry not found in stack"))?;
1466
1467        // Create a new branch name for the split
1468        let base_name = branch.trim_end_matches(|c: char| c.is_ascii_digit() || c == '-');
1469        let new_branch = format!("{base_name}-continued");
1470
1471        // Create new branch at the current HEAD
1472        self.repo.create_branch(&new_branch, Some(&new_head))?;
1473
1474        // Get extra commits for message creation
1475        let original_entry = &stack.entries[entry_position];
1476        let original_commit_hash = original_entry.commit_hash.clone(); // Clone to avoid borrowing issue
1477        let extra_commits = self
1478            .repo
1479            .get_commits_between(&original_commit_hash, &new_head)?;
1480
1481        // Create commit message from extra commits
1482        let mut extra_messages = Vec::new();
1483        for commit in &extra_commits {
1484            if let Some(message) = commit.message() {
1485                let first_line = message.lines().next().unwrap_or("").to_string();
1486                extra_messages.push(first_line);
1487            }
1488        }
1489
1490        let new_message = if extra_messages.len() == 1 {
1491            extra_messages[0].clone()
1492        } else {
1493            format!("Combined changes:\n• {}", extra_messages.join("\n• "))
1494        };
1495
1496        // Create new stack entry manually (no constructor method exists)
1497        let now = Utc::now();
1498        let new_entry = crate::stack::StackEntry {
1499            id: uuid::Uuid::new_v4(),
1500            branch: new_branch.clone(),
1501            commit_hash: new_head,
1502            message: new_message,
1503            parent_id: Some(entry_id), // Parent is the current entry
1504            children: Vec::new(),
1505            created_at: now,
1506            updated_at: now,
1507            is_submitted: false,
1508            pull_request_id: None,
1509            is_synced: false,
1510            is_merged: false,
1511        };
1512
1513        // Insert the new entry after the current one
1514        stack.entries.insert(entry_position + 1, new_entry);
1515
1516        // Reset the original branch to its expected commit
1517        self.repo
1518            .reset_branch_to_commit(branch, &original_commit_hash)?;
1519
1520        println!(
1521            "   āœ… Split {} commit(s) into new entry '{}'",
1522            extra_commits.len(),
1523            new_branch
1524        );
1525        println!("      Original branch '{branch}' reset to expected commit");
1526
1527        Ok(())
1528    }
1529
1530    /// Reset branch to expected commit (destructive - loses extra work)
1531    fn reset_branch_destructive(&self, branch: &str, expected_commit: &str) -> Result<()> {
1532        self.repo.reset_branch_to_commit(branch, expected_commit)?;
1533        Output::warning(format!(
1534            "Reset branch '{}' to {} (extra commits lost)",
1535            branch,
1536            &expected_commit[..8]
1537        ));
1538        Ok(())
1539    }
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544    use super::*;
1545    use std::process::Command;
1546    use tempfile::TempDir;
1547
1548    fn create_test_repo() -> (TempDir, PathBuf) {
1549        let temp_dir = TempDir::new().unwrap();
1550        let repo_path = temp_dir.path().to_path_buf();
1551
1552        // Initialize git repository
1553        Command::new("git")
1554            .args(["init"])
1555            .current_dir(&repo_path)
1556            .output()
1557            .unwrap();
1558
1559        // Configure git
1560        Command::new("git")
1561            .args(["config", "user.name", "Test User"])
1562            .current_dir(&repo_path)
1563            .output()
1564            .unwrap();
1565
1566        Command::new("git")
1567            .args(["config", "user.email", "test@example.com"])
1568            .current_dir(&repo_path)
1569            .output()
1570            .unwrap();
1571
1572        // Create an initial commit
1573        std::fs::write(repo_path.join("README.md"), "# Test Repo").unwrap();
1574        Command::new("git")
1575            .args(["add", "."])
1576            .current_dir(&repo_path)
1577            .output()
1578            .unwrap();
1579
1580        Command::new("git")
1581            .args(["commit", "-m", "Initial commit"])
1582            .current_dir(&repo_path)
1583            .output()
1584            .unwrap();
1585
1586        // Initialize cascade
1587        crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
1588            .unwrap();
1589
1590        (temp_dir, repo_path)
1591    }
1592
1593    #[test]
1594    fn test_create_stack_manager() {
1595        let (_temp_dir, repo_path) = create_test_repo();
1596        let manager = StackManager::new(&repo_path).unwrap();
1597
1598        assert_eq!(manager.stacks.len(), 0);
1599        assert!(manager.get_active_stack().is_none());
1600    }
1601
1602    #[test]
1603    fn test_create_and_manage_stack() {
1604        let (_temp_dir, repo_path) = create_test_repo();
1605        let mut manager = StackManager::new(&repo_path).unwrap();
1606
1607        // Create a stack using the default branch
1608        let stack_id = manager
1609            .create_stack(
1610                "test-stack".to_string(),
1611                None, // Use default branch
1612                Some("Test stack description".to_string()),
1613            )
1614            .unwrap();
1615
1616        // Verify stack was created
1617        assert_eq!(manager.stacks.len(), 1);
1618        let stack = manager.get_stack(&stack_id).unwrap();
1619        assert_eq!(stack.name, "test-stack");
1620        // Should use the default branch (which gets set from the Git repo)
1621        assert!(!stack.base_branch.is_empty());
1622        assert!(stack.is_active);
1623
1624        // Verify it's the active stack
1625        let active = manager.get_active_stack().unwrap();
1626        assert_eq!(active.id, stack_id);
1627
1628        // Test get by name
1629        let found = manager.get_stack_by_name("test-stack").unwrap();
1630        assert_eq!(found.id, stack_id);
1631    }
1632
1633    #[test]
1634    fn test_stack_persistence() {
1635        let (_temp_dir, repo_path) = create_test_repo();
1636
1637        let stack_id = {
1638            let mut manager = StackManager::new(&repo_path).unwrap();
1639            manager
1640                .create_stack("persistent-stack".to_string(), None, None)
1641                .unwrap()
1642        };
1643
1644        // Create new manager and verify data was loaded
1645        let manager = StackManager::new(&repo_path).unwrap();
1646        assert_eq!(manager.stacks.len(), 1);
1647        let stack = manager.get_stack(&stack_id).unwrap();
1648        assert_eq!(stack.name, "persistent-stack");
1649    }
1650
1651    #[test]
1652    fn test_multiple_stacks() {
1653        let (_temp_dir, repo_path) = create_test_repo();
1654        let mut manager = StackManager::new(&repo_path).unwrap();
1655
1656        let stack1_id = manager
1657            .create_stack("stack-1".to_string(), None, None)
1658            .unwrap();
1659        let stack2_id = manager
1660            .create_stack("stack-2".to_string(), None, None)
1661            .unwrap();
1662
1663        assert_eq!(manager.stacks.len(), 2);
1664
1665        // Second stack should be active (newly created stacks become active)
1666        assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1667        assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1668
1669        // Change active stack
1670        manager.set_active_stack(Some(stack2_id)).unwrap();
1671        assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1672        assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1673    }
1674
1675    #[test]
1676    fn test_delete_stack() {
1677        let (_temp_dir, repo_path) = create_test_repo();
1678        let mut manager = StackManager::new(&repo_path).unwrap();
1679
1680        let stack_id = manager
1681            .create_stack("to-delete".to_string(), None, None)
1682            .unwrap();
1683        assert_eq!(manager.stacks.len(), 1);
1684
1685        let deleted = manager.delete_stack(&stack_id).unwrap();
1686        assert_eq!(deleted.name, "to-delete");
1687        assert_eq!(manager.stacks.len(), 0);
1688        assert!(manager.get_active_stack().is_none());
1689    }
1690
1691    #[test]
1692    fn test_validation() {
1693        let (_temp_dir, repo_path) = create_test_repo();
1694        let mut manager = StackManager::new(&repo_path).unwrap();
1695
1696        manager
1697            .create_stack("valid-stack".to_string(), None, None)
1698            .unwrap();
1699
1700        // Should pass validation
1701        assert!(manager.validate_all().is_ok());
1702    }
1703
1704    #[test]
1705    fn test_duplicate_commit_message_detection() {
1706        let (_temp_dir, repo_path) = create_test_repo();
1707        let mut manager = StackManager::new(&repo_path).unwrap();
1708
1709        // Create a stack
1710        manager
1711            .create_stack("test-stack".to_string(), None, None)
1712            .unwrap();
1713
1714        // Create first commit
1715        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1716        Command::new("git")
1717            .args(["add", "file1.txt"])
1718            .current_dir(&repo_path)
1719            .output()
1720            .unwrap();
1721
1722        Command::new("git")
1723            .args(["commit", "-m", "Add authentication feature"])
1724            .current_dir(&repo_path)
1725            .output()
1726            .unwrap();
1727
1728        let commit1_hash = Command::new("git")
1729            .args(["rev-parse", "HEAD"])
1730            .current_dir(&repo_path)
1731            .output()
1732            .unwrap();
1733        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1734            .trim()
1735            .to_string();
1736
1737        // Push first commit to stack - should succeed
1738        let entry1_id = manager
1739            .push_to_stack(
1740                "feature/auth".to_string(),
1741                commit1_hash,
1742                "Add authentication feature".to_string(),
1743                "main".to_string(),
1744            )
1745            .unwrap();
1746
1747        // Verify first entry was added
1748        assert!(manager
1749            .get_active_stack()
1750            .unwrap()
1751            .get_entry(&entry1_id)
1752            .is_some());
1753
1754        // Create second commit
1755        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1756        Command::new("git")
1757            .args(["add", "file2.txt"])
1758            .current_dir(&repo_path)
1759            .output()
1760            .unwrap();
1761
1762        Command::new("git")
1763            .args(["commit", "-m", "Different commit message"])
1764            .current_dir(&repo_path)
1765            .output()
1766            .unwrap();
1767
1768        let commit2_hash = Command::new("git")
1769            .args(["rev-parse", "HEAD"])
1770            .current_dir(&repo_path)
1771            .output()
1772            .unwrap();
1773        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1774            .trim()
1775            .to_string();
1776
1777        // Try to push second commit with the SAME message - should fail
1778        let result = manager.push_to_stack(
1779            "feature/auth2".to_string(),
1780            commit2_hash.clone(),
1781            "Add authentication feature".to_string(), // Same message as first commit
1782            "main".to_string(),
1783        );
1784
1785        // Should fail with validation error
1786        assert!(result.is_err());
1787        let error = result.unwrap_err();
1788        assert!(matches!(error, CascadeError::Validation(_)));
1789
1790        // Error message should contain helpful information
1791        let error_msg = error.to_string();
1792        assert!(error_msg.contains("Duplicate commit message"));
1793        assert!(error_msg.contains("Add authentication feature"));
1794        assert!(error_msg.contains("šŸ’” Consider using a more specific message"));
1795
1796        // Push with different message - should succeed
1797        let entry2_id = manager
1798            .push_to_stack(
1799                "feature/auth2".to_string(),
1800                commit2_hash,
1801                "Add authentication validation".to_string(), // Different message
1802                "main".to_string(),
1803            )
1804            .unwrap();
1805
1806        // Verify both entries exist
1807        let stack = manager.get_active_stack().unwrap();
1808        assert_eq!(stack.entries.len(), 2);
1809        assert!(stack.get_entry(&entry1_id).is_some());
1810        assert!(stack.get_entry(&entry2_id).is_some());
1811    }
1812
1813    #[test]
1814    fn test_duplicate_message_with_different_case() {
1815        let (_temp_dir, repo_path) = create_test_repo();
1816        let mut manager = StackManager::new(&repo_path).unwrap();
1817
1818        manager
1819            .create_stack("test-stack".to_string(), None, None)
1820            .unwrap();
1821
1822        // Create and push first commit
1823        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1824        Command::new("git")
1825            .args(["add", "file1.txt"])
1826            .current_dir(&repo_path)
1827            .output()
1828            .unwrap();
1829
1830        Command::new("git")
1831            .args(["commit", "-m", "fix bug"])
1832            .current_dir(&repo_path)
1833            .output()
1834            .unwrap();
1835
1836        let commit1_hash = Command::new("git")
1837            .args(["rev-parse", "HEAD"])
1838            .current_dir(&repo_path)
1839            .output()
1840            .unwrap();
1841        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1842            .trim()
1843            .to_string();
1844
1845        manager
1846            .push_to_stack(
1847                "feature/fix1".to_string(),
1848                commit1_hash,
1849                "fix bug".to_string(),
1850                "main".to_string(),
1851            )
1852            .unwrap();
1853
1854        // Create second commit
1855        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1856        Command::new("git")
1857            .args(["add", "file2.txt"])
1858            .current_dir(&repo_path)
1859            .output()
1860            .unwrap();
1861
1862        Command::new("git")
1863            .args(["commit", "-m", "Fix Bug"])
1864            .current_dir(&repo_path)
1865            .output()
1866            .unwrap();
1867
1868        let commit2_hash = Command::new("git")
1869            .args(["rev-parse", "HEAD"])
1870            .current_dir(&repo_path)
1871            .output()
1872            .unwrap();
1873        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1874            .trim()
1875            .to_string();
1876
1877        // Different case should be allowed (case-sensitive comparison)
1878        let result = manager.push_to_stack(
1879            "feature/fix2".to_string(),
1880            commit2_hash,
1881            "Fix Bug".to_string(), // Different case
1882            "main".to_string(),
1883        );
1884
1885        // Should succeed because it's case-sensitive
1886        assert!(result.is_ok());
1887    }
1888
1889    #[test]
1890    fn test_duplicate_message_across_different_stacks() {
1891        let (_temp_dir, repo_path) = create_test_repo();
1892        let mut manager = StackManager::new(&repo_path).unwrap();
1893
1894        // Create first stack and push commit
1895        let stack1_id = manager
1896            .create_stack("stack1".to_string(), None, None)
1897            .unwrap();
1898
1899        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1900        Command::new("git")
1901            .args(["add", "file1.txt"])
1902            .current_dir(&repo_path)
1903            .output()
1904            .unwrap();
1905
1906        Command::new("git")
1907            .args(["commit", "-m", "shared message"])
1908            .current_dir(&repo_path)
1909            .output()
1910            .unwrap();
1911
1912        let commit1_hash = Command::new("git")
1913            .args(["rev-parse", "HEAD"])
1914            .current_dir(&repo_path)
1915            .output()
1916            .unwrap();
1917        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1918            .trim()
1919            .to_string();
1920
1921        manager
1922            .push_to_stack(
1923                "feature/shared1".to_string(),
1924                commit1_hash,
1925                "shared message".to_string(),
1926                "main".to_string(),
1927            )
1928            .unwrap();
1929
1930        // Create second stack
1931        let stack2_id = manager
1932            .create_stack("stack2".to_string(), None, None)
1933            .unwrap();
1934
1935        // Set second stack as active
1936        manager.set_active_stack(Some(stack2_id)).unwrap();
1937
1938        // Create commit for second stack
1939        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1940        Command::new("git")
1941            .args(["add", "file2.txt"])
1942            .current_dir(&repo_path)
1943            .output()
1944            .unwrap();
1945
1946        Command::new("git")
1947            .args(["commit", "-m", "shared message"])
1948            .current_dir(&repo_path)
1949            .output()
1950            .unwrap();
1951
1952        let commit2_hash = Command::new("git")
1953            .args(["rev-parse", "HEAD"])
1954            .current_dir(&repo_path)
1955            .output()
1956            .unwrap();
1957        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1958            .trim()
1959            .to_string();
1960
1961        // Same message in different stack should be allowed
1962        let result = manager.push_to_stack(
1963            "feature/shared2".to_string(),
1964            commit2_hash,
1965            "shared message".to_string(), // Same message but different stack
1966            "main".to_string(),
1967        );
1968
1969        // Should succeed because it's a different stack
1970        assert!(result.is_ok());
1971
1972        // Verify both stacks have entries with the same message
1973        let stack1 = manager.get_stack(&stack1_id).unwrap();
1974        let stack2 = manager.get_stack(&stack2_id).unwrap();
1975
1976        assert_eq!(stack1.entries.len(), 1);
1977        assert_eq!(stack2.entries.len(), 1);
1978        assert_eq!(stack1.entries[0].message, "shared message");
1979        assert_eq!(stack2.entries[0].message, "shared message");
1980    }
1981
1982    #[test]
1983    fn test_duplicate_after_pop() {
1984        let (_temp_dir, repo_path) = create_test_repo();
1985        let mut manager = StackManager::new(&repo_path).unwrap();
1986
1987        manager
1988            .create_stack("test-stack".to_string(), None, None)
1989            .unwrap();
1990
1991        // Create and push first commit
1992        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1993        Command::new("git")
1994            .args(["add", "file1.txt"])
1995            .current_dir(&repo_path)
1996            .output()
1997            .unwrap();
1998
1999        Command::new("git")
2000            .args(["commit", "-m", "temporary message"])
2001            .current_dir(&repo_path)
2002            .output()
2003            .unwrap();
2004
2005        let commit1_hash = Command::new("git")
2006            .args(["rev-parse", "HEAD"])
2007            .current_dir(&repo_path)
2008            .output()
2009            .unwrap();
2010        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
2011            .trim()
2012            .to_string();
2013
2014        manager
2015            .push_to_stack(
2016                "feature/temp".to_string(),
2017                commit1_hash,
2018                "temporary message".to_string(),
2019                "main".to_string(),
2020            )
2021            .unwrap();
2022
2023        // Pop the entry
2024        let popped = manager.pop_from_stack().unwrap();
2025        assert_eq!(popped.message, "temporary message");
2026
2027        // Create new commit
2028        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
2029        Command::new("git")
2030            .args(["add", "file2.txt"])
2031            .current_dir(&repo_path)
2032            .output()
2033            .unwrap();
2034
2035        Command::new("git")
2036            .args(["commit", "-m", "temporary message"])
2037            .current_dir(&repo_path)
2038            .output()
2039            .unwrap();
2040
2041        let commit2_hash = Command::new("git")
2042            .args(["rev-parse", "HEAD"])
2043            .current_dir(&repo_path)
2044            .output()
2045            .unwrap();
2046        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
2047            .trim()
2048            .to_string();
2049
2050        // Should be able to push same message again after popping
2051        let result = manager.push_to_stack(
2052            "feature/temp2".to_string(),
2053            commit2_hash,
2054            "temporary message".to_string(),
2055            "main".to_string(),
2056        );
2057
2058        assert!(result.is_ok());
2059    }
2060}