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