1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct StackEntry {
9 pub id: Uuid,
11 pub branch: String,
13 pub commit_hash: String,
15 pub message: String,
17 pub parent_id: Option<Uuid>,
19 pub children: Vec<Uuid>,
21 pub created_at: DateTime<Utc>,
23 pub updated_at: DateTime<Utc>,
25 pub is_submitted: bool,
27 pub pull_request_id: Option<String>,
29 pub is_synced: bool,
31 #[serde(default)]
33 pub is_merged: bool,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub enum StackStatus {
39 Clean,
41 Dirty,
43 OutOfSync,
45 Conflicted,
47 Rebasing,
49 NeedsSync,
51 Corrupted,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct Stack {
58 pub id: Uuid,
60 pub name: String,
62 pub description: Option<String>,
64 pub base_branch: String,
66 pub working_branch: Option<String>,
68 pub entries: Vec<StackEntry>,
70 pub entry_map: HashMap<Uuid, StackEntry>,
72 pub status: StackStatus,
74 pub created_at: DateTime<Utc>,
76 pub updated_at: DateTime<Utc>,
78 pub is_active: bool,
80}
81
82impl Stack {
83 pub fn new(name: String, base_branch: String, description: Option<String>) -> Self {
85 let now = Utc::now();
86 Self {
87 id: Uuid::new_v4(),
88 name,
89 description,
90 base_branch,
91 working_branch: None,
92 entries: Vec::new(),
93 entry_map: HashMap::new(),
94 status: StackStatus::Clean,
95 created_at: now,
96 updated_at: now,
97 is_active: false,
98 }
99 }
100
101 pub fn push_entry(&mut self, branch: String, commit_hash: String, message: String) -> Uuid {
103 let now = Utc::now();
104 let entry_id = Uuid::new_v4();
105
106 let parent_id = self.entries.last().map(|entry| entry.id);
108
109 let entry = StackEntry {
110 id: entry_id,
111 branch,
112 commit_hash,
113 message,
114 parent_id,
115 children: Vec::new(),
116 created_at: now,
117 updated_at: now,
118 is_submitted: false,
119 pull_request_id: None,
120 is_synced: false,
121 is_merged: false,
122 };
123
124 if let Some(parent_id) = parent_id {
126 if let Some(parent) = self.entry_map.get_mut(&parent_id) {
127 parent.children.push(entry_id);
128 }
129 }
130
131 self.entries.push(entry.clone());
133 self.entry_map.insert(entry_id, entry);
134 self.updated_at = now;
135
136 entry_id
137 }
138
139 pub fn pop_entry(&mut self) -> Option<StackEntry> {
141 if let Some(entry) = self.entries.pop() {
142 let entry_id = entry.id;
143 self.entry_map.remove(&entry_id);
144
145 if let Some(parent_id) = entry.parent_id {
147 if let Some(parent) = self.entry_map.get_mut(&parent_id) {
148 parent.children.retain(|&id| id != entry_id);
149 }
150 }
151
152 self.updated_at = Utc::now();
153 Some(entry)
154 } else {
155 None
156 }
157 }
158
159 pub fn get_entry(&self, id: &Uuid) -> Option<&StackEntry> {
161 self.entry_map.get(id)
162 }
163
164 pub fn get_entry_mut(&mut self, id: &Uuid) -> Option<&mut StackEntry> {
166 self.entry_map.get_mut(id)
167 }
168
169 pub fn update_entry_commit_hash(
172 &mut self,
173 entry_id: &Uuid,
174 new_commit_hash: String,
175 ) -> Result<(), String> {
176 let updated_in_vec = self
178 .entries
179 .iter_mut()
180 .find(|e| e.id == *entry_id)
181 .map(|entry| {
182 entry.commit_hash = new_commit_hash.clone();
183 })
184 .is_some();
185
186 let updated_in_map = self
188 .entry_map
189 .get_mut(entry_id)
190 .map(|entry| {
191 entry.commit_hash = new_commit_hash;
192 })
193 .is_some();
194
195 if updated_in_vec && updated_in_map {
196 Ok(())
197 } else {
198 Err(format!("Entry {} not found", entry_id))
199 }
200 }
201
202 pub fn get_base_entry(&self) -> Option<&StackEntry> {
204 self.entries.first()
205 }
206
207 pub fn get_top_entry(&self) -> Option<&StackEntry> {
209 self.entries.last()
210 }
211
212 pub fn get_children(&self, entry_id: &Uuid) -> Vec<&StackEntry> {
214 if let Some(entry) = self.get_entry(entry_id) {
215 entry
216 .children
217 .iter()
218 .filter_map(|id| self.get_entry(id))
219 .collect()
220 } else {
221 Vec::new()
222 }
223 }
224
225 pub fn get_parent(&self, entry_id: &Uuid) -> Option<&StackEntry> {
227 if let Some(entry) = self.get_entry(entry_id) {
228 entry
229 .parent_id
230 .and_then(|parent_id| self.get_entry(&parent_id))
231 } else {
232 None
233 }
234 }
235
236 pub fn is_empty(&self) -> bool {
238 self.entries.is_empty()
239 }
240
241 pub fn len(&self) -> usize {
243 self.entries.len()
244 }
245
246 pub fn mark_entry_submitted(&mut self, entry_id: &Uuid, pull_request_id: String) -> bool {
248 if let Some(entry) = self.get_entry_mut(entry_id) {
249 entry.is_submitted = true;
250 entry.pull_request_id = Some(pull_request_id);
251 entry.updated_at = Utc::now();
252 entry.is_merged = false;
253 self.updated_at = Utc::now();
254
255 self.sync_entries_from_map();
257 true
258 } else {
259 false
260 }
261 }
262
263 fn sync_entries_from_map(&mut self) {
265 for entry in &mut self.entries {
266 if let Some(updated_entry) = self.entry_map.get(&entry.id) {
267 *entry = updated_entry.clone();
268 }
269 }
270 }
271
272 pub fn repair_data_consistency(&mut self) {
274 self.sync_entries_from_map();
275 }
276
277 pub fn mark_entry_synced(&mut self, entry_id: &Uuid) -> bool {
279 if let Some(entry) = self.get_entry_mut(entry_id) {
280 entry.is_synced = true;
281 entry.updated_at = Utc::now();
282 self.updated_at = Utc::now();
283
284 self.sync_entries_from_map();
286 true
287 } else {
288 false
289 }
290 }
291
292 pub fn mark_entry_merged(&mut self, entry_id: &Uuid, merged: bool) -> bool {
294 if let Some(entry) = self.get_entry_mut(entry_id) {
295 entry.is_merged = merged;
296 entry.updated_at = Utc::now();
297 self.updated_at = Utc::now();
298 self.sync_entries_from_map();
299 true
300 } else {
301 false
302 }
303 }
304
305 pub fn update_status(&mut self, status: StackStatus) {
307 self.status = status;
308 self.updated_at = Utc::now();
309 }
310
311 pub fn set_active(&mut self, active: bool) {
313 self.is_active = active;
314 self.updated_at = Utc::now();
315 }
316
317 pub fn get_branch_names(&self) -> Vec<String> {
319 self.entries
320 .iter()
321 .map(|entry| entry.branch.clone())
322 .collect()
323 }
324
325 pub fn validate(&self) -> Result<String, String> {
327 if self.entries.is_empty() {
329 return Ok("Empty stack is valid".to_string());
330 }
331
332 for (i, entry) in self.entries.iter().enumerate() {
334 if i == 0 {
335 if entry.parent_id.is_some() {
337 return Err(format!(
338 "First entry {} should not have a parent",
339 entry.short_hash()
340 ));
341 }
342 } else {
343 let expected_parent = &self.entries[i - 1];
345 if entry.parent_id != Some(expected_parent.id) {
346 return Err(format!(
347 "Entry {} has incorrect parent relationship",
348 entry.short_hash()
349 ));
350 }
351 }
352
353 if let Some(parent_id) = entry.parent_id {
355 if !self.entry_map.contains_key(&parent_id) {
356 return Err(format!(
357 "Entry {} references non-existent parent {}",
358 entry.short_hash(),
359 parent_id
360 ));
361 }
362 }
363 }
364
365 for entry in &self.entries {
367 if !self.entry_map.contains_key(&entry.id) {
368 return Err(format!(
369 "Entry {} is not in the entry map",
370 entry.short_hash()
371 ));
372 }
373 }
374
375 let mut seen_ids = std::collections::HashSet::new();
377 for entry in &self.entries {
378 if !seen_ids.insert(entry.id) {
379 return Err(format!("Duplicate entry ID: {}", entry.id));
380 }
381 }
382
383 let mut seen_branches = std::collections::HashSet::new();
385 for entry in &self.entries {
386 if !seen_branches.insert(&entry.branch) {
387 return Err(format!("Duplicate branch name: {}", entry.branch));
388 }
389 }
390
391 Ok("Stack validation passed".to_string())
392 }
393
394 pub fn validate_git_integrity(
397 &self,
398 git_repo: &crate::git::GitRepository,
399 ) -> Result<String, String> {
400 use tracing::warn;
401
402 let mut issues = Vec::new();
403 let mut warnings = Vec::new();
404
405 for entry in &self.entries {
406 if !git_repo.branch_exists(&entry.branch) {
408 issues.push(format!(
409 "Branch '{}' for entry {} does not exist",
410 entry.branch,
411 entry.short_hash()
412 ));
413 continue;
414 }
415
416 match git_repo.get_branch_head(&entry.branch) {
418 Ok(branch_head) => {
419 if branch_head != entry.commit_hash {
420 issues.push(format!(
421 "Branch '{}' has diverged from stack metadata\n \
422 Expected commit: {} (from stack entry)\n \
423 Actual commit: {} (current branch HEAD)\n \
424 The branch may have been modified outside of cascade",
425 entry.branch,
426 &entry.commit_hash[..8],
427 &branch_head[..8]
428 ));
429 }
430 }
431 Err(e) => {
432 warnings.push(format!(
433 "Could not check branch '{}' HEAD: {}",
434 entry.branch, e
435 ));
436 }
437 }
438
439 match git_repo.commit_exists(&entry.commit_hash) {
441 Ok(exists) => {
442 if !exists {
443 issues.push(format!(
444 "Commit {} for entry {} no longer exists",
445 entry.short_hash(),
446 entry.id
447 ));
448 }
449 }
450 Err(e) => {
451 warnings.push(format!(
452 "Could not verify commit {} existence: {}",
453 entry.short_hash(),
454 e
455 ));
456 }
457 }
458 }
459
460 for warning in &warnings {
462 warn!("{}", warning);
463 }
464
465 if !issues.is_empty() {
466 Err(format!(
467 "Git integrity validation failed:\n{}{}",
468 issues.join("\n"),
469 if !warnings.is_empty() {
470 format!("\n\nWarnings:\n{}", warnings.join("\n"))
471 } else {
472 String::new()
473 }
474 ))
475 } else if !warnings.is_empty() {
476 Ok(format!(
477 "Git integrity validation passed with warnings:\n{}",
478 warnings.join("\n")
479 ))
480 } else {
481 Ok("Git integrity validation passed".to_string())
482 }
483 }
484}
485
486impl StackEntry {
487 pub fn can_modify(&self) -> bool {
489 !self.is_submitted && !self.is_synced && !self.is_merged
490 }
491
492 pub fn short_hash(&self) -> String {
494 if self.commit_hash.len() >= 8 {
495 self.commit_hash[..8].to_string()
496 } else {
497 self.commit_hash.clone()
498 }
499 }
500
501 pub fn short_message(&self, max_len: usize) -> String {
503 let trimmed = self.message.trim();
504 if trimmed.len() > max_len {
505 format!("{}...", &trimmed[..max_len])
506 } else {
507 trimmed.to_string()
508 }
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn test_create_empty_stack() {
518 let stack = Stack::new(
519 "test-stack".to_string(),
520 "main".to_string(),
521 Some("Test stack description".to_string()),
522 );
523
524 assert_eq!(stack.name, "test-stack");
525 assert_eq!(stack.base_branch, "main");
526 assert_eq!(
527 stack.description,
528 Some("Test stack description".to_string())
529 );
530 assert!(stack.is_empty());
531 assert_eq!(stack.len(), 0);
532 assert_eq!(stack.status, StackStatus::Clean);
533 assert!(!stack.is_active);
534 }
535
536 #[test]
537 fn test_push_pop_entries() {
538 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
539
540 let entry1_id = stack.push_entry(
542 "feature-1".to_string(),
543 "abc123".to_string(),
544 "Add feature 1".to_string(),
545 );
546
547 assert_eq!(stack.len(), 1);
548 assert!(!stack.is_empty());
549
550 let entry1 = stack.get_entry(&entry1_id).unwrap();
551 assert_eq!(entry1.branch, "feature-1");
552 assert_eq!(entry1.commit_hash, "abc123");
553 assert_eq!(entry1.message, "Add feature 1");
554 assert_eq!(entry1.parent_id, None);
555 assert!(entry1.children.is_empty());
556
557 let entry2_id = stack.push_entry(
559 "feature-2".to_string(),
560 "def456".to_string(),
561 "Add feature 2".to_string(),
562 );
563
564 assert_eq!(stack.len(), 2);
565
566 let entry2 = stack.get_entry(&entry2_id).unwrap();
567 assert_eq!(entry2.parent_id, Some(entry1_id));
568
569 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
571 assert_eq!(updated_entry1.children, vec![entry2_id]);
572
573 let popped = stack.pop_entry().unwrap();
575 assert_eq!(popped.id, entry2_id);
576 assert_eq!(stack.len(), 1);
577
578 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
580 assert!(updated_entry1.children.is_empty());
581 }
582
583 #[test]
584 fn test_stack_navigation() {
585 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
586
587 let entry1_id = stack.push_entry(
588 "branch1".to_string(),
589 "hash1".to_string(),
590 "msg1".to_string(),
591 );
592 let entry2_id = stack.push_entry(
593 "branch2".to_string(),
594 "hash2".to_string(),
595 "msg2".to_string(),
596 );
597 let entry3_id = stack.push_entry(
598 "branch3".to_string(),
599 "hash3".to_string(),
600 "msg3".to_string(),
601 );
602
603 assert_eq!(stack.get_base_entry().unwrap().id, entry1_id);
605 assert_eq!(stack.get_top_entry().unwrap().id, entry3_id);
606
607 assert_eq!(stack.get_parent(&entry2_id).unwrap().id, entry1_id);
609 assert_eq!(stack.get_parent(&entry3_id).unwrap().id, entry2_id);
610 assert!(stack.get_parent(&entry1_id).is_none());
611
612 let children_of_1 = stack.get_children(&entry1_id);
613 assert_eq!(children_of_1.len(), 1);
614 assert_eq!(children_of_1[0].id, entry2_id);
615 }
616
617 #[test]
618 fn test_stack_validation() {
619 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
620
621 assert!(stack.validate().is_ok());
623
624 stack.push_entry(
626 "branch1".to_string(),
627 "hash1".to_string(),
628 "msg1".to_string(),
629 );
630 stack.push_entry(
631 "branch2".to_string(),
632 "hash2".to_string(),
633 "msg2".to_string(),
634 );
635
636 let result = stack.validate();
638 assert!(result.is_ok());
639 assert!(result.unwrap().contains("validation passed"));
640 }
641
642 #[test]
643 fn test_mark_entry_submitted() {
644 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
645 let entry_id = stack.push_entry(
646 "branch1".to_string(),
647 "hash1".to_string(),
648 "msg1".to_string(),
649 );
650
651 assert!(!stack.get_entry(&entry_id).unwrap().is_submitted);
652 assert!(stack
653 .get_entry(&entry_id)
654 .unwrap()
655 .pull_request_id
656 .is_none());
657
658 assert!(stack.mark_entry_submitted(&entry_id, "PR-123".to_string()));
659
660 let entry = stack.get_entry(&entry_id).unwrap();
661 assert!(entry.is_submitted);
662 assert_eq!(entry.pull_request_id, Some("PR-123".to_string()));
663 }
664
665 #[test]
666 fn test_mark_entry_merged() {
667 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
668 let entry_id = stack.push_entry(
669 "branch1".to_string(),
670 "hash1".to_string(),
671 "msg1".to_string(),
672 );
673
674 assert!(!stack.get_entry(&entry_id).unwrap().is_merged);
675 assert!(stack.mark_entry_merged(&entry_id, true));
676 let merged_entry = stack.get_entry(&entry_id).unwrap();
677 assert!(merged_entry.is_merged);
678 assert!(!merged_entry.can_modify());
679
680 assert!(stack.mark_entry_merged(&entry_id, false));
681 assert!(!stack.get_entry(&entry_id).unwrap().is_merged);
682 }
683
684 #[test]
685 fn test_branch_names() {
686 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
687
688 assert!(stack.get_branch_names().is_empty());
689
690 stack.push_entry(
691 "feature-1".to_string(),
692 "hash1".to_string(),
693 "msg1".to_string(),
694 );
695 stack.push_entry(
696 "feature-2".to_string(),
697 "hash2".to_string(),
698 "msg2".to_string(),
699 );
700
701 let branches = stack.get_branch_names();
702 assert_eq!(branches, vec!["feature-1", "feature-2"]);
703 }
704}