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