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