cascade_cli/stack/
manager.rs

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