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    /// Update merged state for a stack entry
659    pub fn set_entry_merged(
660        &mut self,
661        stack_id: &Uuid,
662        entry_id: &Uuid,
663        merged: bool,
664    ) -> Result<()> {
665        let stack = self
666            .stacks
667            .get_mut(stack_id)
668            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
669
670        let current_entry = stack
671            .entry_map
672            .get(entry_id)
673            .cloned()
674            .ok_or_else(|| CascadeError::config(format!("Entry {entry_id} not found")))?;
675
676        if current_entry.is_merged == merged {
677            return Ok(());
678        }
679
680        if !stack.mark_entry_merged(entry_id, merged) {
681            return Err(CascadeError::config(format!(
682                "Entry {entry_id} not found in stack {stack_id}"
683            )));
684        }
685
686        // Update commit metadata using the stored commit hash
687        if let Some(commit_meta) = self.metadata.commits.get_mut(&current_entry.commit_hash) {
688            commit_meta.mark_merged(merged);
689        }
690
691        // Update stack metadata statistics
692        if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
693            let submitted_count = stack.entries.iter().filter(|e| e.is_submitted).count();
694            let merged_count = stack.entries.iter().filter(|e| e.is_merged).count();
695            stack_meta.update_stats(stack.entries.len(), submitted_count, merged_count);
696        }
697
698        self.save_to_disk()?;
699
700        Ok(())
701    }
702
703    /// Repair data consistency issues in all stacks
704    pub fn repair_all_stacks(&mut self) -> Result<()> {
705        for stack in self.stacks.values_mut() {
706            stack.repair_data_consistency();
707        }
708        self.save_to_disk()?;
709        Ok(())
710    }
711
712    /// Get all stacks
713    pub fn get_all_stacks(&self) -> Vec<&Stack> {
714        self.stacks.values().collect()
715    }
716
717    /// Get stack metadata
718    pub fn get_stack_metadata(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
719        self.metadata.get_stack(stack_id)
720    }
721
722    /// Get repository metadata
723    pub fn get_repository_metadata(&self) -> &RepositoryMetadata {
724        &self.metadata
725    }
726
727    /// Get the Git repository
728    pub fn git_repo(&self) -> &GitRepository {
729        &self.repo
730    }
731
732    /// Get the repository path
733    pub fn repo_path(&self) -> &Path {
734        &self.repo_path
735    }
736
737    // Edit mode management methods
738
739    /// Check if currently in edit mode
740    pub fn is_in_edit_mode(&self) -> bool {
741        self.metadata
742            .edit_mode
743            .as_ref()
744            .map(|edit_state| edit_state.is_active)
745            .unwrap_or(false)
746    }
747
748    /// Get current edit mode information
749    pub fn get_edit_mode_info(&self) -> Option<&super::metadata::EditModeState> {
750        self.metadata.edit_mode.as_ref()
751    }
752
753    /// Enter edit mode for a specific stack entry
754    pub fn enter_edit_mode(&mut self, stack_id: Uuid, entry_id: Uuid) -> Result<()> {
755        // Get the commit hash first to avoid borrow checker issues
756        let commit_hash = {
757            let stack = self
758                .get_stack(&stack_id)
759                .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
760
761            let entry = stack.get_entry(&entry_id).ok_or_else(|| {
762                CascadeError::config(format!("Entry {entry_id} not found in stack"))
763            })?;
764
765            entry.commit_hash.clone()
766        };
767
768        // If already in edit mode, exit the current one first
769        if self.is_in_edit_mode() {
770            self.exit_edit_mode()?;
771        }
772
773        // Create new edit mode state
774        let edit_state = super::metadata::EditModeState::new(stack_id, entry_id, commit_hash);
775
776        self.metadata.edit_mode = Some(edit_state);
777        self.save_to_disk()?;
778
779        debug!(
780            "Entered edit mode for entry {} in stack {}",
781            entry_id, stack_id
782        );
783        Ok(())
784    }
785
786    /// Exit edit mode
787    pub fn exit_edit_mode(&mut self) -> Result<()> {
788        if !self.is_in_edit_mode() {
789            return Err(CascadeError::config("Not currently in edit mode"));
790        }
791
792        // Clear edit mode state
793        self.metadata.edit_mode = None;
794        self.save_to_disk()?;
795
796        debug!("Exited edit mode");
797        Ok(())
798    }
799
800    /// Sync stack with Git repository state
801    pub fn sync_stack(&mut self, stack_id: &Uuid) -> Result<()> {
802        let stack = self
803            .stacks
804            .get_mut(stack_id)
805            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
806
807        // šŸ†• ENHANCED: Check Git integrity first (branch HEAD matches stored commits)
808        if let Err(integrity_error) = stack.validate_git_integrity(&self.repo) {
809            stack.update_status(StackStatus::Corrupted);
810            return Err(CascadeError::branch(format!(
811                "Stack '{}' Git integrity check failed:\n{}",
812                stack.name, integrity_error
813            )));
814        }
815
816        // Check if all commits still exist
817        let mut missing_commits = Vec::new();
818        for entry in &stack.entries {
819            if !self.repo.commit_exists(&entry.commit_hash)? {
820                missing_commits.push(entry.commit_hash.clone());
821            }
822        }
823
824        if !missing_commits.is_empty() {
825            stack.update_status(StackStatus::Corrupted);
826            return Err(CascadeError::branch(format!(
827                "Stack {} has missing commits: {}",
828                stack.name,
829                missing_commits.join(", ")
830            )));
831        }
832
833        // Check if base branch exists and has new commits (try to fetch from remote if not local)
834        if !self.repo.branch_exists_or_fetch(&stack.base_branch)? {
835            return Err(CascadeError::branch(format!(
836                "Base branch '{}' does not exist locally or remotely. Check the branch name or switch to a different base.",
837                stack.base_branch
838            )));
839        }
840
841        let _base_hash = self.repo.get_branch_head(&stack.base_branch)?;
842
843        // Check if any stack entries are missing their commits
844        let mut corrupted_entry = None;
845        for entry in &stack.entries {
846            if !self.repo.commit_exists(&entry.commit_hash)? {
847                corrupted_entry = Some((entry.commit_hash.clone(), entry.branch.clone()));
848                break;
849            }
850        }
851
852        if let Some((commit_hash, branch)) = corrupted_entry {
853            stack.update_status(StackStatus::Corrupted);
854            return Err(CascadeError::branch(format!(
855                "Commit {commit_hash} from stack entry '{branch}' no longer exists"
856            )));
857        }
858
859        // Compare base branch with the earliest commit in the stack
860        let needs_sync = if let Some(first_entry) = stack.entries.first() {
861            // Get commits between base and first entry
862            match self
863                .repo
864                .get_commits_between(&stack.base_branch, &first_entry.commit_hash)
865            {
866                Ok(commits) => !commits.is_empty(), // If there are commits, we need to sync
867                Err(_) => true,                     // If we can't compare, assume we need to sync
868            }
869        } else {
870            false // Empty stack is always clean
871        };
872
873        // Update stack status based on sync needs
874        if needs_sync {
875            stack.update_status(StackStatus::NeedsSync);
876            debug!(
877                "Stack '{}' needs sync - new commits on base branch",
878                stack.name
879            );
880        } else {
881            stack.update_status(StackStatus::Clean);
882            debug!("Stack '{}' is clean", stack.name);
883        }
884
885        // Update metadata
886        if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
887            stack_meta.set_up_to_date(true);
888        }
889
890        self.save_to_disk()?;
891
892        Ok(())
893    }
894
895    /// List all stacks with their status
896    pub fn list_stacks(&self) -> Vec<(Uuid, &str, &StackStatus, usize, Option<&str>)> {
897        self.stacks
898            .values()
899            .map(|stack| {
900                (
901                    stack.id,
902                    stack.name.as_str(),
903                    &stack.status,
904                    stack.entries.len(),
905                    if stack.is_active {
906                        Some("active")
907                    } else {
908                        None
909                    },
910                )
911            })
912            .collect()
913    }
914
915    /// Get all stacks as Stack objects for TUI
916    pub fn get_all_stacks_objects(&self) -> Result<Vec<Stack>> {
917        let mut stacks: Vec<Stack> = self.stacks.values().cloned().collect();
918        stacks.sort_by(|a, b| a.name.cmp(&b.name));
919        Ok(stacks)
920    }
921
922    /// Validate all stacks including Git integrity
923    pub fn validate_all(&self) -> Result<()> {
924        for stack in self.stacks.values() {
925            // Basic structure validation
926            stack.validate().map_err(|e| {
927                CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
928            })?;
929
930            // Git integrity validation
931            stack.validate_git_integrity(&self.repo).map_err(|e| {
932                CascadeError::config(format!(
933                    "Stack '{}' Git integrity validation failed: {}",
934                    stack.name, e
935                ))
936            })?;
937        }
938        Ok(())
939    }
940
941    /// Validate a specific stack including Git integrity
942    pub fn validate_stack(&self, stack_id: &Uuid) -> Result<()> {
943        let stack = self
944            .stacks
945            .get(stack_id)
946            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
947
948        // Basic structure validation
949        stack.validate().map_err(|e| {
950            CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
951        })?;
952
953        // Git integrity validation
954        stack.validate_git_integrity(&self.repo).map_err(|e| {
955            CascadeError::config(format!(
956                "Stack '{}' Git integrity validation failed: {}",
957                stack.name, e
958            ))
959        })?;
960
961        Ok(())
962    }
963
964    /// Save all data to disk
965    pub fn save_to_disk(&self) -> Result<()> {
966        // Ensure config directory exists
967        if !self.config_dir.exists() {
968            fs::create_dir_all(&self.config_dir).map_err(|e| {
969                CascadeError::config(format!("Failed to create config directory: {e}"))
970            })?;
971        }
972
973        // Save stacks atomically
974        crate::utils::atomic_file::write_json(&self.stacks_file, &self.stacks)?;
975
976        // Save metadata atomically
977        crate::utils::atomic_file::write_json(&self.metadata_file, &self.metadata)?;
978
979        Ok(())
980    }
981
982    /// Load data from disk
983    fn load_from_disk(&mut self) -> Result<()> {
984        // Load stacks if file exists
985        if self.stacks_file.exists() {
986            let stacks_content = fs::read_to_string(&self.stacks_file)
987                .map_err(|e| CascadeError::config(format!("Failed to read stacks file: {e}")))?;
988
989            self.stacks = serde_json::from_str(&stacks_content)
990                .map_err(|e| CascadeError::config(format!("Failed to parse stacks file: {e}")))?;
991        }
992
993        // Load metadata if file exists
994        if self.metadata_file.exists() {
995            let metadata_content = fs::read_to_string(&self.metadata_file)
996                .map_err(|e| CascadeError::config(format!("Failed to read metadata file: {e}")))?;
997
998            self.metadata = serde_json::from_str(&metadata_content)
999                .map_err(|e| CascadeError::config(format!("Failed to parse metadata file: {e}")))?;
1000        }
1001
1002        Ok(())
1003    }
1004
1005    /// Check if the user has changed branches since the stack was activated
1006    /// Returns true if branch change detected and user wants to proceed
1007    pub fn check_for_branch_change(&mut self) -> Result<bool> {
1008        // Extract stack information first to avoid borrow conflicts
1009        let (stack_id, stack_name, stored_branch) = {
1010            if let Some(active_stack) = self.get_active_stack() {
1011                let stack_id = active_stack.id;
1012                let stack_name = active_stack.name.clone();
1013                let stored_branch = if let Some(stack_meta) = self.metadata.get_stack(&stack_id) {
1014                    stack_meta.current_branch.clone()
1015                } else {
1016                    None
1017                };
1018                (Some(stack_id), stack_name, stored_branch)
1019            } else {
1020                (None, String::new(), None)
1021            }
1022        };
1023
1024        // If no active stack, nothing to check
1025        let Some(stack_id) = stack_id else {
1026            return Ok(true);
1027        };
1028
1029        let current_branch = self.repo.get_current_branch().ok();
1030
1031        // Check if branch has changed
1032        if stored_branch.as_ref() != current_branch.as_ref() {
1033            Output::warning("Branch change detected!");
1034            Output::sub_item(format!(
1035                "Stack '{}' was active on: {}",
1036                stack_name,
1037                stored_branch.as_deref().unwrap_or("unknown")
1038            ));
1039            Output::sub_item(format!(
1040                "Current branch: {}",
1041                current_branch.as_deref().unwrap_or("unknown")
1042            ));
1043            Output::spacing();
1044
1045            let options = vec![
1046                format!("Keep stack '{stack_name}' active (continue with stack workflow)"),
1047                "Deactivate stack (use normal Git workflow)".to_string(),
1048                "Switch to a different stack".to_string(),
1049                "Cancel and stay on current workflow".to_string(),
1050            ];
1051
1052            let choice = Select::with_theme(&ColorfulTheme::default())
1053                .with_prompt("What would you like to do?")
1054                .default(0)
1055                .items(&options)
1056                .interact()
1057                .map_err(|e| CascadeError::config(format!("Failed to get user choice: {e}")))?;
1058
1059            match choice {
1060                0 => {
1061                    // Update the tracked branch and continue
1062                    if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
1063                        stack_meta.set_current_branch(current_branch);
1064                    }
1065                    self.save_to_disk()?;
1066                    Output::success(format!(
1067                        "Continuing with stack '{stack_name}' on current branch"
1068                    ));
1069                    return Ok(true);
1070                }
1071                1 => {
1072                    // Deactivate the stack
1073                    self.set_active_stack(None)?;
1074                    Output::success(format!(
1075                        "Deactivated stack '{stack_name}' - using normal Git workflow"
1076                    ));
1077                    return Ok(false);
1078                }
1079                2 => {
1080                    // Show available stacks
1081                    let stacks = self.get_all_stacks();
1082                    if stacks.len() <= 1 {
1083                        Output::warning("No other stacks available. Deactivating current stack.");
1084                        self.set_active_stack(None)?;
1085                        return Ok(false);
1086                    }
1087
1088                    Output::spacing();
1089                    Output::info("Available stacks:");
1090                    for (i, stack) in stacks.iter().enumerate() {
1091                        if stack.id != stack_id {
1092                            Output::numbered_item(i + 1, &stack.name);
1093                        }
1094                    }
1095                    let stack_name_input: String = Input::with_theme(&ColorfulTheme::default())
1096                        .with_prompt("Enter stack name")
1097                        .validate_with(|input: &String| -> std::result::Result<(), &str> {
1098                            if input.trim().is_empty() {
1099                                Err("Stack name cannot be empty")
1100                            } else {
1101                                Ok(())
1102                            }
1103                        })
1104                        .interact_text()
1105                        .map_err(|e| {
1106                            CascadeError::config(format!("Failed to get user input: {e}"))
1107                        })?;
1108                    let stack_name_input = stack_name_input.trim();
1109
1110                    if let Err(e) = self.set_active_stack_by_name(stack_name_input) {
1111                        Output::warning(format!("{e}"));
1112                        Output::sub_item("Deactivating stack instead.");
1113                        self.set_active_stack(None)?;
1114                        return Ok(false);
1115                    } else {
1116                        Output::success(format!("Switched to stack '{stack_name_input}'"));
1117                        return Ok(true);
1118                    }
1119                }
1120                3 => {
1121                    Output::info("Cancelled - no changes made");
1122                    return Ok(false);
1123                }
1124                _ => {
1125                    Output::info("Invalid choice - no changes made");
1126                    return Ok(false);
1127                }
1128            }
1129        }
1130
1131        // No branch change detected
1132        Ok(true)
1133    }
1134
1135    /// Handle Git integrity issues with multiple user-friendly options
1136    /// Provides non-destructive choices for dealing with branch modifications
1137    pub fn handle_branch_modifications(
1138        &mut self,
1139        stack_id: &Uuid,
1140        auto_mode: Option<String>,
1141    ) -> Result<()> {
1142        let stack = self
1143            .stacks
1144            .get_mut(stack_id)
1145            .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
1146
1147        debug!("Checking Git integrity for stack '{}'", stack.name);
1148
1149        // Detect all modifications
1150        let mut modifications = Vec::new();
1151        for entry in &stack.entries {
1152            if !self.repo.branch_exists(&entry.branch) {
1153                modifications.push(BranchModification::Missing {
1154                    branch: entry.branch.clone(),
1155                    entry_id: entry.id,
1156                    expected_commit: entry.commit_hash.clone(),
1157                });
1158            } else if let Ok(branch_head) = self.repo.get_branch_head(&entry.branch) {
1159                if branch_head != entry.commit_hash {
1160                    // Get extra commits and their messages
1161                    let extra_commits = self
1162                        .repo
1163                        .get_commits_between(&entry.commit_hash, &branch_head)?;
1164                    let mut extra_messages = Vec::new();
1165                    for commit in &extra_commits {
1166                        if let Some(message) = commit.message() {
1167                            let first_line =
1168                                message.lines().next().unwrap_or("(no message)").to_string();
1169                            extra_messages.push(format!(
1170                                "{}: {}",
1171                                &commit.id().to_string()[..8],
1172                                first_line
1173                            ));
1174                        }
1175                    }
1176
1177                    modifications.push(BranchModification::ExtraCommits {
1178                        branch: entry.branch.clone(),
1179                        entry_id: entry.id,
1180                        expected_commit: entry.commit_hash.clone(),
1181                        actual_commit: branch_head,
1182                        extra_commit_count: extra_commits.len(),
1183                        extra_commit_messages: extra_messages,
1184                    });
1185                }
1186            }
1187        }
1188
1189        if modifications.is_empty() {
1190            // Silent success - no issues to report
1191            return Ok(());
1192        }
1193
1194        // Show detected modifications
1195        println!();
1196        Output::section(format!("Branch modifications detected in '{}'", stack.name));
1197        for (i, modification) in modifications.iter().enumerate() {
1198            match modification {
1199                BranchModification::Missing { branch, .. } => {
1200                    Output::numbered_item(i + 1, format!("Branch '{branch}' is missing"));
1201                }
1202                BranchModification::ExtraCommits {
1203                    branch,
1204                    expected_commit,
1205                    actual_commit,
1206                    extra_commit_count,
1207                    extra_commit_messages,
1208                    ..
1209                } => {
1210                    println!(
1211                        "   {}. Branch '{}' has {} extra commit(s)",
1212                        i + 1,
1213                        branch,
1214                        extra_commit_count
1215                    );
1216                    println!(
1217                        "      Expected: {} | Actual: {}",
1218                        &expected_commit[..8],
1219                        &actual_commit[..8]
1220                    );
1221
1222                    // Show extra commit messages (first few only)
1223                    for (j, message) in extra_commit_messages.iter().enumerate() {
1224                        match j.cmp(&3) {
1225                            std::cmp::Ordering::Less => {
1226                                Output::sub_item(format!("     + {message}"));
1227                            }
1228                            std::cmp::Ordering::Equal => {
1229                                Output::sub_item(format!(
1230                                    "     + ... and {} more",
1231                                    extra_commit_count - 3
1232                                ));
1233                                break;
1234                            }
1235                            std::cmp::Ordering::Greater => {
1236                                break;
1237                            }
1238                        }
1239                    }
1240                }
1241            }
1242        }
1243        Output::spacing();
1244
1245        // Auto mode handling
1246        if let Some(mode) = auto_mode {
1247            return self.apply_auto_fix(stack_id, &modifications, &mode);
1248        }
1249
1250        // Interactive mode - ask user for each modification
1251        let mut handled_count = 0;
1252        let mut skipped_count = 0;
1253        for modification in modifications.iter() {
1254            let was_skipped = self.handle_single_modification(stack_id, modification)?;
1255            if was_skipped {
1256                skipped_count += 1;
1257            } else {
1258                handled_count += 1;
1259            }
1260        }
1261
1262        self.save_to_disk()?;
1263
1264        // Show appropriate summary based on what was done
1265        if skipped_count == 0 {
1266            Output::success("All branch modifications resolved");
1267        } else if handled_count > 0 {
1268            Output::warning(format!(
1269                "Resolved {} modification(s), {} skipped",
1270                handled_count, skipped_count
1271            ));
1272        } else {
1273            Output::warning("All modifications skipped - integrity issues remain");
1274        }
1275
1276        Ok(())
1277    }
1278
1279    /// Handle a single branch modification interactively
1280    /// Returns true if the modification was skipped, false if handled
1281    fn handle_single_modification(
1282        &mut self,
1283        stack_id: &Uuid,
1284        modification: &BranchModification,
1285    ) -> Result<bool> {
1286        match modification {
1287            BranchModification::Missing {
1288                branch,
1289                expected_commit,
1290                ..
1291            } => {
1292                Output::info(format!("Missing branch '{branch}'"));
1293                Output::sub_item(format!(
1294                    "Will create the branch at commit {}",
1295                    &expected_commit[..8]
1296                ));
1297
1298                self.repo.create_branch(branch, Some(expected_commit))?;
1299                Output::success(format!("Created branch '{branch}'"));
1300                Ok(false) // Not skipped
1301            }
1302
1303            BranchModification::ExtraCommits {
1304                branch,
1305                entry_id,
1306                expected_commit,
1307                extra_commit_count,
1308                ..
1309            } => {
1310                println!();
1311                Output::info(format!(
1312                    "Branch '{}' has {} extra commit(s)",
1313                    branch, extra_commit_count
1314                ));
1315                let options = vec![
1316                    "Incorporate - Update stack entry to include extra commits",
1317                    "Split - Create new stack entry for extra commits",
1318                    "Reset - Remove extra commits (DESTRUCTIVE)",
1319                    "Skip - Leave as-is for now",
1320                ];
1321
1322                let choice = Select::with_theme(&ColorfulTheme::default())
1323                    .with_prompt("Choose how to handle extra commits")
1324                    .default(0)
1325                    .items(&options)
1326                    .interact()
1327                    .map_err(|e| CascadeError::config(format!("Failed to get user choice: {e}")))?;
1328
1329                match choice {
1330                    0 => {
1331                        self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1332                        Ok(false) // Not skipped
1333                    }
1334                    1 => {
1335                        self.split_extra_commits(stack_id, *entry_id, branch)?;
1336                        Ok(false) // Not skipped
1337                    }
1338                    2 => {
1339                        self.reset_branch_destructive(branch, expected_commit)?;
1340                        Ok(false) // Not skipped
1341                    }
1342                    3 => {
1343                        Output::warning(format!("Skipped '{branch}' - integrity issue remains"));
1344                        Ok(true) // Skipped
1345                    }
1346                    _ => {
1347                        Output::warning(format!("Invalid choice - skipped '{branch}'"));
1348                        Ok(true) // Skipped
1349                    }
1350                }
1351            }
1352        }
1353    }
1354
1355    /// Apply automatic fix based on mode
1356    fn apply_auto_fix(
1357        &mut self,
1358        stack_id: &Uuid,
1359        modifications: &[BranchModification],
1360        mode: &str,
1361    ) -> Result<()> {
1362        Output::info(format!("šŸ¤– Applying automatic fix mode: {mode}"));
1363
1364        for modification in modifications {
1365            match (modification, mode) {
1366                (
1367                    BranchModification::Missing {
1368                        branch,
1369                        expected_commit,
1370                        ..
1371                    },
1372                    _,
1373                ) => {
1374                    self.repo.create_branch(branch, Some(expected_commit))?;
1375                    Output::success(format!("Created missing branch '{branch}'"));
1376                }
1377
1378                (
1379                    BranchModification::ExtraCommits {
1380                        branch, entry_id, ..
1381                    },
1382                    "incorporate",
1383                ) => {
1384                    self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1385                }
1386
1387                (
1388                    BranchModification::ExtraCommits {
1389                        branch, entry_id, ..
1390                    },
1391                    "split",
1392                ) => {
1393                    self.split_extra_commits(stack_id, *entry_id, branch)?;
1394                }
1395
1396                (
1397                    BranchModification::ExtraCommits {
1398                        branch,
1399                        expected_commit,
1400                        ..
1401                    },
1402                    "reset",
1403                ) => {
1404                    self.reset_branch_destructive(branch, expected_commit)?;
1405                }
1406
1407                _ => {
1408                    return Err(CascadeError::config(format!(
1409                        "Unknown auto-fix mode '{mode}'. Use: incorporate, split, reset"
1410                    )));
1411                }
1412            }
1413        }
1414
1415        self.save_to_disk()?;
1416        Output::success(format!("Auto-fix completed for mode: {mode}"));
1417        Ok(())
1418    }
1419
1420    /// Incorporate extra commits into the existing stack entry (update commit hash)
1421    fn incorporate_extra_commits(
1422        &mut self,
1423        stack_id: &Uuid,
1424        entry_id: Uuid,
1425        branch: &str,
1426    ) -> Result<()> {
1427        let stack = self
1428            .stacks
1429            .get_mut(stack_id)
1430            .ok_or_else(|| CascadeError::config(format!("Stack not found: {}", stack_id)))?;
1431
1432        // Find entry and get info we need before mutation
1433        let entry_info = stack
1434            .entries
1435            .iter()
1436            .find(|e| e.id == entry_id)
1437            .map(|e| (e.commit_hash.clone(), e.id));
1438
1439        if let Some((old_commit_hash, entry_id)) = entry_info {
1440            let new_head = self.repo.get_branch_head(branch)?;
1441            let old_commit = old_commit_hash[..8].to_string();
1442
1443            // Get the extra commits for message update
1444            let extra_commits = self.repo.get_commits_between(&old_commit_hash, &new_head)?;
1445
1446            // Update the entry to point to the new HEAD using safe wrapper
1447            // Note: We intentionally do NOT append commit messages here
1448            // The entry's message should describe the feature/change, not list all commits
1449            stack
1450                .update_entry_commit_hash(&entry_id, new_head.clone())
1451                .map_err(CascadeError::config)?;
1452
1453            Output::success(format!(
1454                "Incorporated {} commit(s) into entry '{}'",
1455                extra_commits.len(),
1456                &new_head[..8]
1457            ));
1458            Output::sub_item(format!("Updated: {} -> {}", old_commit, &new_head[..8]));
1459        }
1460
1461        Ok(())
1462    }
1463
1464    /// Split extra commits into a new stack entry
1465    fn split_extra_commits(&mut self, stack_id: &Uuid, entry_id: Uuid, branch: &str) -> Result<()> {
1466        let stack = self
1467            .stacks
1468            .get_mut(stack_id)
1469            .ok_or_else(|| CascadeError::config(format!("Stack not found: {}", stack_id)))?;
1470        let new_head = self.repo.get_branch_head(branch)?;
1471
1472        // Find the position of the current entry
1473        let entry_position = stack
1474            .entries
1475            .iter()
1476            .position(|e| e.id == entry_id)
1477            .ok_or_else(|| CascadeError::config("Entry not found in stack"))?;
1478
1479        // Create a new branch name for the split
1480        let base_name = branch.trim_end_matches(|c: char| c.is_ascii_digit() || c == '-');
1481        let new_branch = format!("{base_name}-continued");
1482
1483        // Create new branch at the current HEAD
1484        self.repo.create_branch(&new_branch, Some(&new_head))?;
1485
1486        // Get extra commits for message creation
1487        let original_entry = &stack.entries[entry_position];
1488        let original_commit_hash = original_entry.commit_hash.clone(); // Clone to avoid borrowing issue
1489        let extra_commits = self
1490            .repo
1491            .get_commits_between(&original_commit_hash, &new_head)?;
1492
1493        // Create commit message from extra commits
1494        let mut extra_messages = Vec::new();
1495        for commit in &extra_commits {
1496            if let Some(message) = commit.message() {
1497                let first_line = message.lines().next().unwrap_or("").to_string();
1498                extra_messages.push(first_line);
1499            }
1500        }
1501
1502        let new_message = if extra_messages.len() == 1 {
1503            extra_messages[0].clone()
1504        } else {
1505            format!("Combined changes:\n• {}", extra_messages.join("\n• "))
1506        };
1507
1508        // Create new stack entry manually (no constructor method exists)
1509        let now = Utc::now();
1510        let new_entry = crate::stack::StackEntry {
1511            id: uuid::Uuid::new_v4(),
1512            branch: new_branch.clone(),
1513            commit_hash: new_head,
1514            message: new_message,
1515            parent_id: Some(entry_id), // Parent is the current entry
1516            children: Vec::new(),
1517            created_at: now,
1518            updated_at: now,
1519            is_submitted: false,
1520            pull_request_id: None,
1521            is_synced: false,
1522            is_merged: false,
1523        };
1524
1525        // Insert the new entry after the current one
1526        stack.entries.insert(entry_position + 1, new_entry);
1527
1528        // Reset the original branch to its expected commit
1529        self.repo
1530            .reset_branch_to_commit(branch, &original_commit_hash)?;
1531
1532        println!(
1533            "   āœ… Split {} commit(s) into new entry '{}'",
1534            extra_commits.len(),
1535            new_branch
1536        );
1537        println!("      Original branch '{branch}' reset to expected commit");
1538
1539        Ok(())
1540    }
1541
1542    /// Reset branch to expected commit (destructive - loses extra work)
1543    fn reset_branch_destructive(&self, branch: &str, expected_commit: &str) -> Result<()> {
1544        self.repo.reset_branch_to_commit(branch, expected_commit)?;
1545        Output::warning(format!(
1546            "Reset branch '{}' to {} (extra commits lost)",
1547            branch,
1548            &expected_commit[..8]
1549        ));
1550        Ok(())
1551    }
1552}
1553
1554#[cfg(test)]
1555mod tests {
1556    use super::*;
1557    use std::process::Command;
1558    use tempfile::TempDir;
1559
1560    fn create_test_repo() -> (TempDir, PathBuf) {
1561        let temp_dir = TempDir::new().unwrap();
1562        let repo_path = temp_dir.path().to_path_buf();
1563
1564        // Initialize git repository
1565        Command::new("git")
1566            .args(["init"])
1567            .current_dir(&repo_path)
1568            .output()
1569            .unwrap();
1570
1571        // Configure git
1572        Command::new("git")
1573            .args(["config", "user.name", "Test User"])
1574            .current_dir(&repo_path)
1575            .output()
1576            .unwrap();
1577
1578        Command::new("git")
1579            .args(["config", "user.email", "test@example.com"])
1580            .current_dir(&repo_path)
1581            .output()
1582            .unwrap();
1583
1584        // Create an initial commit
1585        std::fs::write(repo_path.join("README.md"), "# Test Repo").unwrap();
1586        Command::new("git")
1587            .args(["add", "."])
1588            .current_dir(&repo_path)
1589            .output()
1590            .unwrap();
1591
1592        Command::new("git")
1593            .args(["commit", "-m", "Initial commit"])
1594            .current_dir(&repo_path)
1595            .output()
1596            .unwrap();
1597
1598        // Initialize cascade
1599        crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
1600            .unwrap();
1601
1602        (temp_dir, repo_path)
1603    }
1604
1605    #[test]
1606    fn test_create_stack_manager() {
1607        let (_temp_dir, repo_path) = create_test_repo();
1608        let manager = StackManager::new(&repo_path).unwrap();
1609
1610        assert_eq!(manager.stacks.len(), 0);
1611        assert!(manager.get_active_stack().is_none());
1612    }
1613
1614    #[test]
1615    fn test_create_and_manage_stack() {
1616        let (_temp_dir, repo_path) = create_test_repo();
1617        let mut manager = StackManager::new(&repo_path).unwrap();
1618
1619        // Create a stack using the default branch
1620        let stack_id = manager
1621            .create_stack(
1622                "test-stack".to_string(),
1623                None, // Use default branch
1624                Some("Test stack description".to_string()),
1625            )
1626            .unwrap();
1627
1628        // Verify stack was created
1629        assert_eq!(manager.stacks.len(), 1);
1630        let stack = manager.get_stack(&stack_id).unwrap();
1631        assert_eq!(stack.name, "test-stack");
1632        // Should use the default branch (which gets set from the Git repo)
1633        assert!(!stack.base_branch.is_empty());
1634        assert!(stack.is_active);
1635
1636        // Verify it's the active stack
1637        let active = manager.get_active_stack().unwrap();
1638        assert_eq!(active.id, stack_id);
1639
1640        // Test get by name
1641        let found = manager.get_stack_by_name("test-stack").unwrap();
1642        assert_eq!(found.id, stack_id);
1643    }
1644
1645    #[test]
1646    fn test_stack_persistence() {
1647        let (_temp_dir, repo_path) = create_test_repo();
1648
1649        let stack_id = {
1650            let mut manager = StackManager::new(&repo_path).unwrap();
1651            manager
1652                .create_stack("persistent-stack".to_string(), None, None)
1653                .unwrap()
1654        };
1655
1656        // Create new manager and verify data was loaded
1657        let manager = StackManager::new(&repo_path).unwrap();
1658        assert_eq!(manager.stacks.len(), 1);
1659        let stack = manager.get_stack(&stack_id).unwrap();
1660        assert_eq!(stack.name, "persistent-stack");
1661    }
1662
1663    #[test]
1664    fn test_multiple_stacks() {
1665        let (_temp_dir, repo_path) = create_test_repo();
1666        let mut manager = StackManager::new(&repo_path).unwrap();
1667
1668        let stack1_id = manager
1669            .create_stack("stack-1".to_string(), None, None)
1670            .unwrap();
1671        let stack2_id = manager
1672            .create_stack("stack-2".to_string(), None, None)
1673            .unwrap();
1674
1675        assert_eq!(manager.stacks.len(), 2);
1676
1677        // Second stack should be active (newly created stacks become active)
1678        assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1679        assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1680
1681        // Change active stack
1682        manager.set_active_stack(Some(stack2_id)).unwrap();
1683        assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1684        assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1685    }
1686
1687    #[test]
1688    fn test_delete_stack() {
1689        let (_temp_dir, repo_path) = create_test_repo();
1690        let mut manager = StackManager::new(&repo_path).unwrap();
1691
1692        let stack_id = manager
1693            .create_stack("to-delete".to_string(), None, None)
1694            .unwrap();
1695        assert_eq!(manager.stacks.len(), 1);
1696
1697        let deleted = manager.delete_stack(&stack_id).unwrap();
1698        assert_eq!(deleted.name, "to-delete");
1699        assert_eq!(manager.stacks.len(), 0);
1700        assert!(manager.get_active_stack().is_none());
1701    }
1702
1703    #[test]
1704    fn test_validation() {
1705        let (_temp_dir, repo_path) = create_test_repo();
1706        let mut manager = StackManager::new(&repo_path).unwrap();
1707
1708        manager
1709            .create_stack("valid-stack".to_string(), None, None)
1710            .unwrap();
1711
1712        // Should pass validation
1713        assert!(manager.validate_all().is_ok());
1714    }
1715
1716    #[test]
1717    fn test_duplicate_commit_message_detection() {
1718        let (_temp_dir, repo_path) = create_test_repo();
1719        let mut manager = StackManager::new(&repo_path).unwrap();
1720
1721        // Create a stack
1722        manager
1723            .create_stack("test-stack".to_string(), None, None)
1724            .unwrap();
1725
1726        // Create first commit
1727        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1728        Command::new("git")
1729            .args(["add", "file1.txt"])
1730            .current_dir(&repo_path)
1731            .output()
1732            .unwrap();
1733
1734        Command::new("git")
1735            .args(["commit", "-m", "Add authentication feature"])
1736            .current_dir(&repo_path)
1737            .output()
1738            .unwrap();
1739
1740        let commit1_hash = Command::new("git")
1741            .args(["rev-parse", "HEAD"])
1742            .current_dir(&repo_path)
1743            .output()
1744            .unwrap();
1745        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1746            .trim()
1747            .to_string();
1748
1749        // Push first commit to stack - should succeed
1750        let entry1_id = manager
1751            .push_to_stack(
1752                "feature/auth".to_string(),
1753                commit1_hash,
1754                "Add authentication feature".to_string(),
1755                "main".to_string(),
1756            )
1757            .unwrap();
1758
1759        // Verify first entry was added
1760        assert!(manager
1761            .get_active_stack()
1762            .unwrap()
1763            .get_entry(&entry1_id)
1764            .is_some());
1765
1766        // Create second commit
1767        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1768        Command::new("git")
1769            .args(["add", "file2.txt"])
1770            .current_dir(&repo_path)
1771            .output()
1772            .unwrap();
1773
1774        Command::new("git")
1775            .args(["commit", "-m", "Different commit message"])
1776            .current_dir(&repo_path)
1777            .output()
1778            .unwrap();
1779
1780        let commit2_hash = Command::new("git")
1781            .args(["rev-parse", "HEAD"])
1782            .current_dir(&repo_path)
1783            .output()
1784            .unwrap();
1785        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1786            .trim()
1787            .to_string();
1788
1789        // Try to push second commit with the SAME message - should fail
1790        let result = manager.push_to_stack(
1791            "feature/auth2".to_string(),
1792            commit2_hash.clone(),
1793            "Add authentication feature".to_string(), // Same message as first commit
1794            "main".to_string(),
1795        );
1796
1797        // Should fail with validation error
1798        assert!(result.is_err());
1799        let error = result.unwrap_err();
1800        assert!(matches!(error, CascadeError::Validation(_)));
1801
1802        // Error message should contain helpful information
1803        let error_msg = error.to_string();
1804        assert!(error_msg.contains("Duplicate commit message"));
1805        assert!(error_msg.contains("Add authentication feature"));
1806        assert!(error_msg.contains("šŸ’” Consider using a more specific message"));
1807
1808        // Push with different message - should succeed
1809        let entry2_id = manager
1810            .push_to_stack(
1811                "feature/auth2".to_string(),
1812                commit2_hash,
1813                "Add authentication validation".to_string(), // Different message
1814                "main".to_string(),
1815            )
1816            .unwrap();
1817
1818        // Verify both entries exist
1819        let stack = manager.get_active_stack().unwrap();
1820        assert_eq!(stack.entries.len(), 2);
1821        assert!(stack.get_entry(&entry1_id).is_some());
1822        assert!(stack.get_entry(&entry2_id).is_some());
1823    }
1824
1825    #[test]
1826    fn test_duplicate_message_with_different_case() {
1827        let (_temp_dir, repo_path) = create_test_repo();
1828        let mut manager = StackManager::new(&repo_path).unwrap();
1829
1830        manager
1831            .create_stack("test-stack".to_string(), None, None)
1832            .unwrap();
1833
1834        // Create and push first commit
1835        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1836        Command::new("git")
1837            .args(["add", "file1.txt"])
1838            .current_dir(&repo_path)
1839            .output()
1840            .unwrap();
1841
1842        Command::new("git")
1843            .args(["commit", "-m", "fix bug"])
1844            .current_dir(&repo_path)
1845            .output()
1846            .unwrap();
1847
1848        let commit1_hash = Command::new("git")
1849            .args(["rev-parse", "HEAD"])
1850            .current_dir(&repo_path)
1851            .output()
1852            .unwrap();
1853        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1854            .trim()
1855            .to_string();
1856
1857        manager
1858            .push_to_stack(
1859                "feature/fix1".to_string(),
1860                commit1_hash,
1861                "fix bug".to_string(),
1862                "main".to_string(),
1863            )
1864            .unwrap();
1865
1866        // Create second commit
1867        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1868        Command::new("git")
1869            .args(["add", "file2.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 commit2_hash = Command::new("git")
1881            .args(["rev-parse", "HEAD"])
1882            .current_dir(&repo_path)
1883            .output()
1884            .unwrap();
1885        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1886            .trim()
1887            .to_string();
1888
1889        // Different case should be allowed (case-sensitive comparison)
1890        let result = manager.push_to_stack(
1891            "feature/fix2".to_string(),
1892            commit2_hash,
1893            "Fix Bug".to_string(), // Different case
1894            "main".to_string(),
1895        );
1896
1897        // Should succeed because it's case-sensitive
1898        assert!(result.is_ok());
1899    }
1900
1901    #[test]
1902    fn test_duplicate_message_across_different_stacks() {
1903        let (_temp_dir, repo_path) = create_test_repo();
1904        let mut manager = StackManager::new(&repo_path).unwrap();
1905
1906        // Create first stack and push commit
1907        let stack1_id = manager
1908            .create_stack("stack1".to_string(), None, None)
1909            .unwrap();
1910
1911        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1912        Command::new("git")
1913            .args(["add", "file1.txt"])
1914            .current_dir(&repo_path)
1915            .output()
1916            .unwrap();
1917
1918        Command::new("git")
1919            .args(["commit", "-m", "shared message"])
1920            .current_dir(&repo_path)
1921            .output()
1922            .unwrap();
1923
1924        let commit1_hash = Command::new("git")
1925            .args(["rev-parse", "HEAD"])
1926            .current_dir(&repo_path)
1927            .output()
1928            .unwrap();
1929        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1930            .trim()
1931            .to_string();
1932
1933        manager
1934            .push_to_stack(
1935                "feature/shared1".to_string(),
1936                commit1_hash,
1937                "shared message".to_string(),
1938                "main".to_string(),
1939            )
1940            .unwrap();
1941
1942        // Create second stack
1943        let stack2_id = manager
1944            .create_stack("stack2".to_string(), None, None)
1945            .unwrap();
1946
1947        // Set second stack as active
1948        manager.set_active_stack(Some(stack2_id)).unwrap();
1949
1950        // Create commit for second stack
1951        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1952        Command::new("git")
1953            .args(["add", "file2.txt"])
1954            .current_dir(&repo_path)
1955            .output()
1956            .unwrap();
1957
1958        Command::new("git")
1959            .args(["commit", "-m", "shared message"])
1960            .current_dir(&repo_path)
1961            .output()
1962            .unwrap();
1963
1964        let commit2_hash = Command::new("git")
1965            .args(["rev-parse", "HEAD"])
1966            .current_dir(&repo_path)
1967            .output()
1968            .unwrap();
1969        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1970            .trim()
1971            .to_string();
1972
1973        // Same message in different stack should be allowed
1974        let result = manager.push_to_stack(
1975            "feature/shared2".to_string(),
1976            commit2_hash,
1977            "shared message".to_string(), // Same message but different stack
1978            "main".to_string(),
1979        );
1980
1981        // Should succeed because it's a different stack
1982        assert!(result.is_ok());
1983
1984        // Verify both stacks have entries with the same message
1985        let stack1 = manager.get_stack(&stack1_id).unwrap();
1986        let stack2 = manager.get_stack(&stack2_id).unwrap();
1987
1988        assert_eq!(stack1.entries.len(), 1);
1989        assert_eq!(stack2.entries.len(), 1);
1990        assert_eq!(stack1.entries[0].message, "shared message");
1991        assert_eq!(stack2.entries[0].message, "shared message");
1992    }
1993
1994    #[test]
1995    fn test_duplicate_after_pop() {
1996        let (_temp_dir, repo_path) = create_test_repo();
1997        let mut manager = StackManager::new(&repo_path).unwrap();
1998
1999        manager
2000            .create_stack("test-stack".to_string(), None, None)
2001            .unwrap();
2002
2003        // Create and push first commit
2004        std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
2005        Command::new("git")
2006            .args(["add", "file1.txt"])
2007            .current_dir(&repo_path)
2008            .output()
2009            .unwrap();
2010
2011        Command::new("git")
2012            .args(["commit", "-m", "temporary message"])
2013            .current_dir(&repo_path)
2014            .output()
2015            .unwrap();
2016
2017        let commit1_hash = Command::new("git")
2018            .args(["rev-parse", "HEAD"])
2019            .current_dir(&repo_path)
2020            .output()
2021            .unwrap();
2022        let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
2023            .trim()
2024            .to_string();
2025
2026        manager
2027            .push_to_stack(
2028                "feature/temp".to_string(),
2029                commit1_hash,
2030                "temporary message".to_string(),
2031                "main".to_string(),
2032            )
2033            .unwrap();
2034
2035        // Pop the entry
2036        let popped = manager.pop_from_stack().unwrap();
2037        assert_eq!(popped.message, "temporary message");
2038
2039        // Create new commit
2040        std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
2041        Command::new("git")
2042            .args(["add", "file2.txt"])
2043            .current_dir(&repo_path)
2044            .output()
2045            .unwrap();
2046
2047        Command::new("git")
2048            .args(["commit", "-m", "temporary message"])
2049            .current_dir(&repo_path)
2050            .output()
2051            .unwrap();
2052
2053        let commit2_hash = Command::new("git")
2054            .args(["rev-parse", "HEAD"])
2055            .current_dir(&repo_path)
2056            .output()
2057            .unwrap();
2058        let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
2059            .trim()
2060            .to_string();
2061
2062        // Should be able to push same message again after popping
2063        let result = manager.push_to_stack(
2064            "feature/temp2".to_string(),
2065            commit2_hash,
2066            "temporary message".to_string(),
2067            "main".to_string(),
2068        );
2069
2070        assert!(result.is_ok());
2071    }
2072}