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