cascade_cli/stack/
manager.rs

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