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