cascade_cli/stack/
stack.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use uuid::Uuid;
5
6/// Represents a single entry in a stack
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct StackEntry {
9    /// Unique identifier for this entry
10    pub id: Uuid,
11    /// Branch name for this entry
12    pub branch: String,
13    /// Commit hash
14    pub commit_hash: String,
15    /// Commit message
16    pub message: String,
17    /// Parent entry ID (None for base)
18    pub parent_id: Option<Uuid>,
19    /// Child entry IDs
20    pub children: Vec<Uuid>,
21    /// When this entry was created
22    pub created_at: DateTime<Utc>,
23    /// When this entry was last updated
24    pub updated_at: DateTime<Utc>,
25    /// Whether this entry has been submitted for review
26    pub is_submitted: bool,
27    /// Pull request ID if submitted
28    pub pull_request_id: Option<String>,
29    /// Whether this entry is synced with remote
30    pub is_synced: bool,
31}
32
33/// Represents the status of a stack
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35pub enum StackStatus {
36    /// Stack is clean and ready
37    Clean,
38    /// Stack has uncommitted changes
39    Dirty,
40    /// Stack needs to be synced with remote
41    OutOfSync,
42    /// Stack has conflicts that need resolution
43    Conflicted,
44    /// Stack is being rebased
45    Rebasing,
46    /// Stack needs sync due to new commits on base branch
47    NeedsSync,
48    /// Stack has corrupted or missing commits
49    Corrupted,
50}
51
52/// Represents a complete stack of commits
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct Stack {
55    /// Unique identifier for this stack
56    pub id: Uuid,
57    /// Human-readable name for the stack
58    pub name: String,
59    /// Description of what this stack implements
60    pub description: Option<String>,
61    /// Base branch this stack is built on
62    pub base_branch: String,
63    /// All entries in this stack (ordered)
64    pub entries: Vec<StackEntry>,
65    /// Map of entry ID to entry for quick lookup
66    pub entry_map: HashMap<Uuid, StackEntry>,
67    /// Current status of the stack
68    pub status: StackStatus,
69    /// When this stack was created
70    pub created_at: DateTime<Utc>,
71    /// When this stack was last updated
72    pub updated_at: DateTime<Utc>,
73    /// Whether this stack is active (current working stack)
74    pub is_active: bool,
75}
76
77impl Stack {
78    /// Create a new empty stack
79    pub fn new(name: String, base_branch: String, description: Option<String>) -> Self {
80        let now = Utc::now();
81        Self {
82            id: Uuid::new_v4(),
83            name,
84            description,
85            base_branch,
86            entries: Vec::new(),
87            entry_map: HashMap::new(),
88            status: StackStatus::Clean,
89            created_at: now,
90            updated_at: now,
91            is_active: false,
92        }
93    }
94
95    /// Add a new entry to the top of the stack
96    pub fn push_entry(&mut self, branch: String, commit_hash: String, message: String) -> Uuid {
97        let now = Utc::now();
98        let entry_id = Uuid::new_v4();
99
100        // Find the current top entry to set as parent
101        let parent_id = self.entries.last().map(|entry| entry.id);
102
103        let entry = StackEntry {
104            id: entry_id,
105            branch,
106            commit_hash,
107            message,
108            parent_id,
109            children: Vec::new(),
110            created_at: now,
111            updated_at: now,
112            is_submitted: false,
113            pull_request_id: None,
114            is_synced: false,
115        };
116
117        // Update parent's children if exists
118        if let Some(parent_id) = parent_id {
119            if let Some(parent) = self.entry_map.get_mut(&parent_id) {
120                parent.children.push(entry_id);
121            }
122        }
123
124        // Add to collections
125        self.entries.push(entry.clone());
126        self.entry_map.insert(entry_id, entry);
127        self.updated_at = now;
128
129        entry_id
130    }
131
132    /// Remove the top entry from the stack
133    pub fn pop_entry(&mut self) -> Option<StackEntry> {
134        if let Some(entry) = self.entries.pop() {
135            let entry_id = entry.id;
136            self.entry_map.remove(&entry_id);
137
138            // Update parent's children if exists
139            if let Some(parent_id) = entry.parent_id {
140                if let Some(parent) = self.entry_map.get_mut(&parent_id) {
141                    parent.children.retain(|&id| id != entry_id);
142                }
143            }
144
145            self.updated_at = Utc::now();
146            Some(entry)
147        } else {
148            None
149        }
150    }
151
152    /// Get an entry by ID
153    pub fn get_entry(&self, id: &Uuid) -> Option<&StackEntry> {
154        self.entry_map.get(id)
155    }
156
157    /// Get a mutable entry by ID
158    pub fn get_entry_mut(&mut self, id: &Uuid) -> Option<&mut StackEntry> {
159        self.entry_map.get_mut(id)
160    }
161
162    /// Get the base (first) entry of the stack
163    pub fn get_base_entry(&self) -> Option<&StackEntry> {
164        self.entries.first()
165    }
166
167    /// Get the top (last) entry of the stack
168    pub fn get_top_entry(&self) -> Option<&StackEntry> {
169        self.entries.last()
170    }
171
172    /// Get all entries that are children of the given entry
173    pub fn get_children(&self, entry_id: &Uuid) -> Vec<&StackEntry> {
174        if let Some(entry) = self.get_entry(entry_id) {
175            entry
176                .children
177                .iter()
178                .filter_map(|id| self.get_entry(id))
179                .collect()
180        } else {
181            Vec::new()
182        }
183    }
184
185    /// Get the parent of the given entry
186    pub fn get_parent(&self, entry_id: &Uuid) -> Option<&StackEntry> {
187        if let Some(entry) = self.get_entry(entry_id) {
188            entry
189                .parent_id
190                .and_then(|parent_id| self.get_entry(&parent_id))
191        } else {
192            None
193        }
194    }
195
196    /// Check if the stack is empty
197    pub fn is_empty(&self) -> bool {
198        self.entries.is_empty()
199    }
200
201    /// Get the number of entries in the stack
202    pub fn len(&self) -> usize {
203        self.entries.len()
204    }
205
206    /// Mark an entry as submitted with a pull request ID
207    pub fn mark_entry_submitted(&mut self, entry_id: &Uuid, pull_request_id: String) -> bool {
208        if let Some(entry) = self.get_entry_mut(entry_id) {
209            entry.is_submitted = true;
210            entry.pull_request_id = Some(pull_request_id);
211            entry.updated_at = Utc::now();
212            self.updated_at = Utc::now();
213
214            // Synchronize the entries vector with the updated entry_map
215            self.sync_entries_from_map();
216            true
217        } else {
218            false
219        }
220    }
221
222    /// Synchronize the entries vector with the entry_map (entry_map is source of truth)
223    fn sync_entries_from_map(&mut self) {
224        for entry in &mut self.entries {
225            if let Some(updated_entry) = self.entry_map.get(&entry.id) {
226                *entry = updated_entry.clone();
227            }
228        }
229    }
230
231    /// Force synchronization of entries from entry_map (public method for fixing corrupted data)
232    pub fn repair_data_consistency(&mut self) {
233        self.sync_entries_from_map();
234    }
235
236    /// Mark an entry as synced
237    pub fn mark_entry_synced(&mut self, entry_id: &Uuid) -> bool {
238        if let Some(entry) = self.get_entry_mut(entry_id) {
239            entry.is_synced = true;
240            entry.updated_at = Utc::now();
241            self.updated_at = Utc::now();
242
243            // Synchronize the entries vector with the updated entry_map
244            self.sync_entries_from_map();
245            true
246        } else {
247            false
248        }
249    }
250
251    /// Update stack status
252    pub fn update_status(&mut self, status: StackStatus) {
253        self.status = status;
254        self.updated_at = Utc::now();
255    }
256
257    /// Set this stack as active
258    pub fn set_active(&mut self, active: bool) {
259        self.is_active = active;
260        self.updated_at = Utc::now();
261    }
262
263    /// Get all branch names in this stack
264    pub fn get_branch_names(&self) -> Vec<String> {
265        self.entries
266            .iter()
267            .map(|entry| entry.branch.clone())
268            .collect()
269    }
270
271    /// Validate the stack structure and Git state integrity
272    pub fn validate(&self) -> Result<String, String> {
273        // Validate basic structure
274        if self.entries.is_empty() {
275            return Ok("Empty stack is valid".to_string());
276        }
277
278        // Check parent-child relationships
279        for (i, entry) in self.entries.iter().enumerate() {
280            if i == 0 {
281                // First entry should have no parent
282                if entry.parent_id.is_some() {
283                    return Err(format!(
284                        "First entry {} should not have a parent",
285                        entry.short_hash()
286                    ));
287                }
288            } else {
289                // Other entries should have the previous entry as parent
290                let expected_parent = &self.entries[i - 1];
291                if entry.parent_id != Some(expected_parent.id) {
292                    return Err(format!(
293                        "Entry {} has incorrect parent relationship",
294                        entry.short_hash()
295                    ));
296                }
297            }
298
299            // Check if parent exists in map
300            if let Some(parent_id) = entry.parent_id {
301                if !self.entry_map.contains_key(&parent_id) {
302                    return Err(format!(
303                        "Entry {} references non-existent parent {}",
304                        entry.short_hash(),
305                        parent_id
306                    ));
307                }
308            }
309        }
310
311        // Check that all entries are in the map
312        for entry in &self.entries {
313            if !self.entry_map.contains_key(&entry.id) {
314                return Err(format!(
315                    "Entry {} is not in the entry map",
316                    entry.short_hash()
317                ));
318            }
319        }
320
321        // Check for duplicate IDs
322        let mut seen_ids = std::collections::HashSet::new();
323        for entry in &self.entries {
324            if !seen_ids.insert(entry.id) {
325                return Err(format!("Duplicate entry ID: {}", entry.id));
326            }
327        }
328
329        // Check for duplicate branch names
330        let mut seen_branches = std::collections::HashSet::new();
331        for entry in &self.entries {
332            if !seen_branches.insert(&entry.branch) {
333                return Err(format!("Duplicate branch name: {}", entry.branch));
334            }
335        }
336
337        Ok("Stack validation passed".to_string())
338    }
339
340    /// Validate Git state integrity (requires Git repository access)
341    /// This checks that branch HEADs match the expected commit hashes
342    pub fn validate_git_integrity(
343        &self,
344        git_repo: &crate::git::GitRepository,
345    ) -> Result<String, String> {
346        use tracing::warn;
347
348        let mut issues = Vec::new();
349        let mut warnings = Vec::new();
350
351        for entry in &self.entries {
352            // Check if branch exists
353            if !git_repo.branch_exists(&entry.branch) {
354                issues.push(format!(
355                    "Branch '{}' for entry {} does not exist",
356                    entry.branch,
357                    entry.short_hash()
358                ));
359                continue;
360            }
361
362            // Check if branch HEAD matches stored commit hash
363            match git_repo.get_branch_head(&entry.branch) {
364                Ok(branch_head) => {
365                    if branch_head != entry.commit_hash {
366                        issues.push(format!(
367                            "🚨 BRANCH MODIFICATION DETECTED: Branch '{}' has been manually modified!\n   \
368                             Expected commit: {} (from stack entry)\n   \
369                             Actual commit:   {} (current branch HEAD)\n   \
370                             💡 Someone may have checked out '{}' and added commits.\n   \
371                             This breaks stack integrity!",
372                            entry.branch,
373                            &entry.commit_hash[..8],
374                            &branch_head[..8],
375                            entry.branch
376                        ));
377                    }
378                }
379                Err(e) => {
380                    warnings.push(format!(
381                        "Could not check branch '{}' HEAD: {}",
382                        entry.branch, e
383                    ));
384                }
385            }
386
387            // Check if commit still exists
388            match git_repo.commit_exists(&entry.commit_hash) {
389                Ok(exists) => {
390                    if !exists {
391                        issues.push(format!(
392                            "Commit {} for entry {} no longer exists",
393                            entry.short_hash(),
394                            entry.id
395                        ));
396                    }
397                }
398                Err(e) => {
399                    warnings.push(format!(
400                        "Could not verify commit {} existence: {}",
401                        entry.short_hash(),
402                        e
403                    ));
404                }
405            }
406        }
407
408        // Log warnings
409        for warning in &warnings {
410            warn!("{}", warning);
411        }
412
413        if !issues.is_empty() {
414            Err(format!(
415                "Git integrity validation failed:\n{}{}",
416                issues.join("\n"),
417                if !warnings.is_empty() {
418                    format!("\n\nWarnings:\n{}", warnings.join("\n"))
419                } else {
420                    String::new()
421                }
422            ))
423        } else if !warnings.is_empty() {
424            Ok(format!(
425                "Git integrity validation passed with warnings:\n{}",
426                warnings.join("\n")
427            ))
428        } else {
429            Ok("Git integrity validation passed".to_string())
430        }
431    }
432}
433
434impl StackEntry {
435    /// Check if this entry can be safely modified
436    pub fn can_modify(&self) -> bool {
437        !self.is_submitted && !self.is_synced
438    }
439
440    /// Get a short version of the commit hash
441    pub fn short_hash(&self) -> String {
442        if self.commit_hash.len() >= 8 {
443            self.commit_hash[..8].to_string()
444        } else {
445            self.commit_hash.clone()
446        }
447    }
448
449    /// Get a short version of the commit message
450    pub fn short_message(&self, max_len: usize) -> String {
451        if self.message.len() > max_len {
452            format!("{}...", &self.message[..max_len])
453        } else {
454            self.message.clone()
455        }
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[test]
464    fn test_create_empty_stack() {
465        let stack = Stack::new(
466            "test-stack".to_string(),
467            "main".to_string(),
468            Some("Test stack description".to_string()),
469        );
470
471        assert_eq!(stack.name, "test-stack");
472        assert_eq!(stack.base_branch, "main");
473        assert_eq!(
474            stack.description,
475            Some("Test stack description".to_string())
476        );
477        assert!(stack.is_empty());
478        assert_eq!(stack.len(), 0);
479        assert_eq!(stack.status, StackStatus::Clean);
480        assert!(!stack.is_active);
481    }
482
483    #[test]
484    fn test_push_pop_entries() {
485        let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
486
487        // Push first entry
488        let entry1_id = stack.push_entry(
489            "feature-1".to_string(),
490            "abc123".to_string(),
491            "Add feature 1".to_string(),
492        );
493
494        assert_eq!(stack.len(), 1);
495        assert!(!stack.is_empty());
496
497        let entry1 = stack.get_entry(&entry1_id).unwrap();
498        assert_eq!(entry1.branch, "feature-1");
499        assert_eq!(entry1.commit_hash, "abc123");
500        assert_eq!(entry1.message, "Add feature 1");
501        assert_eq!(entry1.parent_id, None);
502        assert!(entry1.children.is_empty());
503
504        // Push second entry
505        let entry2_id = stack.push_entry(
506            "feature-2".to_string(),
507            "def456".to_string(),
508            "Add feature 2".to_string(),
509        );
510
511        assert_eq!(stack.len(), 2);
512
513        let entry2 = stack.get_entry(&entry2_id).unwrap();
514        assert_eq!(entry2.parent_id, Some(entry1_id));
515
516        // Check parent-child relationship
517        let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
518        assert_eq!(updated_entry1.children, vec![entry2_id]);
519
520        // Pop entry
521        let popped = stack.pop_entry().unwrap();
522        assert_eq!(popped.id, entry2_id);
523        assert_eq!(stack.len(), 1);
524
525        // Check parent's children were updated
526        let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
527        assert!(updated_entry1.children.is_empty());
528    }
529
530    #[test]
531    fn test_stack_navigation() {
532        let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
533
534        let entry1_id = stack.push_entry(
535            "branch1".to_string(),
536            "hash1".to_string(),
537            "msg1".to_string(),
538        );
539        let entry2_id = stack.push_entry(
540            "branch2".to_string(),
541            "hash2".to_string(),
542            "msg2".to_string(),
543        );
544        let entry3_id = stack.push_entry(
545            "branch3".to_string(),
546            "hash3".to_string(),
547            "msg3".to_string(),
548        );
549
550        // Test base and top
551        assert_eq!(stack.get_base_entry().unwrap().id, entry1_id);
552        assert_eq!(stack.get_top_entry().unwrap().id, entry3_id);
553
554        // Test parent/child relationships
555        assert_eq!(stack.get_parent(&entry2_id).unwrap().id, entry1_id);
556        assert_eq!(stack.get_parent(&entry3_id).unwrap().id, entry2_id);
557        assert!(stack.get_parent(&entry1_id).is_none());
558
559        let children_of_1 = stack.get_children(&entry1_id);
560        assert_eq!(children_of_1.len(), 1);
561        assert_eq!(children_of_1[0].id, entry2_id);
562    }
563
564    #[test]
565    fn test_stack_validation() {
566        let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
567
568        // Empty stack should be valid
569        assert!(stack.validate().is_ok());
570
571        // Add some entries
572        stack.push_entry(
573            "branch1".to_string(),
574            "hash1".to_string(),
575            "msg1".to_string(),
576        );
577        stack.push_entry(
578            "branch2".to_string(),
579            "hash2".to_string(),
580            "msg2".to_string(),
581        );
582
583        // Valid stack should pass validation
584        let result = stack.validate();
585        assert!(result.is_ok());
586        assert!(result.unwrap().contains("validation passed"));
587    }
588
589    #[test]
590    fn test_mark_entry_submitted() {
591        let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
592        let entry_id = stack.push_entry(
593            "branch1".to_string(),
594            "hash1".to_string(),
595            "msg1".to_string(),
596        );
597
598        assert!(!stack.get_entry(&entry_id).unwrap().is_submitted);
599        assert!(stack
600            .get_entry(&entry_id)
601            .unwrap()
602            .pull_request_id
603            .is_none());
604
605        assert!(stack.mark_entry_submitted(&entry_id, "PR-123".to_string()));
606
607        let entry = stack.get_entry(&entry_id).unwrap();
608        assert!(entry.is_submitted);
609        assert_eq!(entry.pull_request_id, Some("PR-123".to_string()));
610    }
611
612    #[test]
613    fn test_branch_names() {
614        let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
615
616        assert!(stack.get_branch_names().is_empty());
617
618        stack.push_entry(
619            "feature-1".to_string(),
620            "hash1".to_string(),
621            "msg1".to_string(),
622        );
623        stack.push_entry(
624            "feature-2".to_string(),
625            "hash2".to_string(),
626            "msg2".to_string(),
627        );
628
629        let branches = stack.get_branch_names();
630        assert_eq!(branches, vec!["feature-1", "feature-2"]);
631    }
632}