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