cascade_cli/stack/
manager.rs

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