Skip to main content

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