cascade_cli/stack/
manager.rs

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