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