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}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35pub enum StackStatus {
36 Clean,
38 Dirty,
40 OutOfSync,
42 Conflicted,
44 Rebasing,
46 NeedsSync,
48 Corrupted,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct Stack {
55 pub id: Uuid,
57 pub name: String,
59 pub description: Option<String>,
61 pub base_branch: String,
63 pub working_branch: Option<String>,
65 pub entries: Vec<StackEntry>,
67 pub entry_map: HashMap<Uuid, StackEntry>,
69 pub status: StackStatus,
71 pub created_at: DateTime<Utc>,
73 pub updated_at: DateTime<Utc>,
75 pub is_active: bool,
77}
78
79impl Stack {
80 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 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 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 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 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 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 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 pub fn get_entry(&self, id: &Uuid) -> Option<&StackEntry> {
157 self.entry_map.get(id)
158 }
159
160 pub fn get_entry_mut(&mut self, id: &Uuid) -> Option<&mut StackEntry> {
162 self.entry_map.get_mut(id)
163 }
164
165 pub fn update_entry_commit_hash(
168 &mut self,
169 entry_id: &Uuid,
170 new_commit_hash: String,
171 ) -> Result<(), String> {
172 let updated_in_vec = self
174 .entries
175 .iter_mut()
176 .find(|e| e.id == *entry_id)
177 .map(|entry| {
178 entry.commit_hash = new_commit_hash.clone();
179 })
180 .is_some();
181
182 let updated_in_map = self
184 .entry_map
185 .get_mut(entry_id)
186 .map(|entry| {
187 entry.commit_hash = new_commit_hash;
188 })
189 .is_some();
190
191 if updated_in_vec && updated_in_map {
192 Ok(())
193 } else {
194 Err(format!("Entry {} not found", entry_id))
195 }
196 }
197
198 pub fn get_base_entry(&self) -> Option<&StackEntry> {
200 self.entries.first()
201 }
202
203 pub fn get_top_entry(&self) -> Option<&StackEntry> {
205 self.entries.last()
206 }
207
208 pub fn get_children(&self, entry_id: &Uuid) -> Vec<&StackEntry> {
210 if let Some(entry) = self.get_entry(entry_id) {
211 entry
212 .children
213 .iter()
214 .filter_map(|id| self.get_entry(id))
215 .collect()
216 } else {
217 Vec::new()
218 }
219 }
220
221 pub fn get_parent(&self, entry_id: &Uuid) -> Option<&StackEntry> {
223 if let Some(entry) = self.get_entry(entry_id) {
224 entry
225 .parent_id
226 .and_then(|parent_id| self.get_entry(&parent_id))
227 } else {
228 None
229 }
230 }
231
232 pub fn is_empty(&self) -> bool {
234 self.entries.is_empty()
235 }
236
237 pub fn len(&self) -> usize {
239 self.entries.len()
240 }
241
242 pub fn mark_entry_submitted(&mut self, entry_id: &Uuid, pull_request_id: String) -> bool {
244 if let Some(entry) = self.get_entry_mut(entry_id) {
245 entry.is_submitted = true;
246 entry.pull_request_id = Some(pull_request_id);
247 entry.updated_at = Utc::now();
248 self.updated_at = Utc::now();
249
250 self.sync_entries_from_map();
252 true
253 } else {
254 false
255 }
256 }
257
258 fn sync_entries_from_map(&mut self) {
260 for entry in &mut self.entries {
261 if let Some(updated_entry) = self.entry_map.get(&entry.id) {
262 *entry = updated_entry.clone();
263 }
264 }
265 }
266
267 pub fn repair_data_consistency(&mut self) {
269 self.sync_entries_from_map();
270 }
271
272 pub fn mark_entry_synced(&mut self, entry_id: &Uuid) -> bool {
274 if let Some(entry) = self.get_entry_mut(entry_id) {
275 entry.is_synced = true;
276 entry.updated_at = Utc::now();
277 self.updated_at = Utc::now();
278
279 self.sync_entries_from_map();
281 true
282 } else {
283 false
284 }
285 }
286
287 pub fn update_status(&mut self, status: StackStatus) {
289 self.status = status;
290 self.updated_at = Utc::now();
291 }
292
293 pub fn set_active(&mut self, active: bool) {
295 self.is_active = active;
296 self.updated_at = Utc::now();
297 }
298
299 pub fn get_branch_names(&self) -> Vec<String> {
301 self.entries
302 .iter()
303 .map(|entry| entry.branch.clone())
304 .collect()
305 }
306
307 pub fn validate(&self) -> Result<String, String> {
309 if self.entries.is_empty() {
311 return Ok("Empty stack is valid".to_string());
312 }
313
314 for (i, entry) in self.entries.iter().enumerate() {
316 if i == 0 {
317 if entry.parent_id.is_some() {
319 return Err(format!(
320 "First entry {} should not have a parent",
321 entry.short_hash()
322 ));
323 }
324 } else {
325 let expected_parent = &self.entries[i - 1];
327 if entry.parent_id != Some(expected_parent.id) {
328 return Err(format!(
329 "Entry {} has incorrect parent relationship",
330 entry.short_hash()
331 ));
332 }
333 }
334
335 if let Some(parent_id) = entry.parent_id {
337 if !self.entry_map.contains_key(&parent_id) {
338 return Err(format!(
339 "Entry {} references non-existent parent {}",
340 entry.short_hash(),
341 parent_id
342 ));
343 }
344 }
345 }
346
347 for entry in &self.entries {
349 if !self.entry_map.contains_key(&entry.id) {
350 return Err(format!(
351 "Entry {} is not in the entry map",
352 entry.short_hash()
353 ));
354 }
355 }
356
357 let mut seen_ids = std::collections::HashSet::new();
359 for entry in &self.entries {
360 if !seen_ids.insert(entry.id) {
361 return Err(format!("Duplicate entry ID: {}", entry.id));
362 }
363 }
364
365 let mut seen_branches = std::collections::HashSet::new();
367 for entry in &self.entries {
368 if !seen_branches.insert(&entry.branch) {
369 return Err(format!("Duplicate branch name: {}", entry.branch));
370 }
371 }
372
373 Ok("Stack validation passed".to_string())
374 }
375
376 pub fn validate_git_integrity(
379 &self,
380 git_repo: &crate::git::GitRepository,
381 ) -> Result<String, String> {
382 use tracing::warn;
383
384 let mut issues = Vec::new();
385 let mut warnings = Vec::new();
386
387 for entry in &self.entries {
388 if !git_repo.branch_exists(&entry.branch) {
390 issues.push(format!(
391 "Branch '{}' for entry {} does not exist",
392 entry.branch,
393 entry.short_hash()
394 ));
395 continue;
396 }
397
398 match git_repo.get_branch_head(&entry.branch) {
400 Ok(branch_head) => {
401 if branch_head != entry.commit_hash {
402 issues.push(format!(
403 "Branch '{}' has diverged from stack metadata\n \
404 Expected commit: {} (from stack entry)\n \
405 Actual commit: {} (current branch HEAD)\n \
406 The branch may have been modified outside of cascade",
407 entry.branch,
408 &entry.commit_hash[..8],
409 &branch_head[..8]
410 ));
411 }
412 }
413 Err(e) => {
414 warnings.push(format!(
415 "Could not check branch '{}' HEAD: {}",
416 entry.branch, e
417 ));
418 }
419 }
420
421 match git_repo.commit_exists(&entry.commit_hash) {
423 Ok(exists) => {
424 if !exists {
425 issues.push(format!(
426 "Commit {} for entry {} no longer exists",
427 entry.short_hash(),
428 entry.id
429 ));
430 }
431 }
432 Err(e) => {
433 warnings.push(format!(
434 "Could not verify commit {} existence: {}",
435 entry.short_hash(),
436 e
437 ));
438 }
439 }
440 }
441
442 for warning in &warnings {
444 warn!("{}", warning);
445 }
446
447 if !issues.is_empty() {
448 Err(format!(
449 "Git integrity validation failed:\n{}{}",
450 issues.join("\n"),
451 if !warnings.is_empty() {
452 format!("\n\nWarnings:\n{}", warnings.join("\n"))
453 } else {
454 String::new()
455 }
456 ))
457 } else if !warnings.is_empty() {
458 Ok(format!(
459 "Git integrity validation passed with warnings:\n{}",
460 warnings.join("\n")
461 ))
462 } else {
463 Ok("Git integrity validation passed".to_string())
464 }
465 }
466}
467
468impl StackEntry {
469 pub fn can_modify(&self) -> bool {
471 !self.is_submitted && !self.is_synced
472 }
473
474 pub fn short_hash(&self) -> String {
476 if self.commit_hash.len() >= 8 {
477 self.commit_hash[..8].to_string()
478 } else {
479 self.commit_hash.clone()
480 }
481 }
482
483 pub fn short_message(&self, max_len: usize) -> String {
485 let trimmed = self.message.trim();
486 if trimmed.len() > max_len {
487 format!("{}...", &trimmed[..max_len])
488 } else {
489 trimmed.to_string()
490 }
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 #[test]
499 fn test_create_empty_stack() {
500 let stack = Stack::new(
501 "test-stack".to_string(),
502 "main".to_string(),
503 Some("Test stack description".to_string()),
504 );
505
506 assert_eq!(stack.name, "test-stack");
507 assert_eq!(stack.base_branch, "main");
508 assert_eq!(
509 stack.description,
510 Some("Test stack description".to_string())
511 );
512 assert!(stack.is_empty());
513 assert_eq!(stack.len(), 0);
514 assert_eq!(stack.status, StackStatus::Clean);
515 assert!(!stack.is_active);
516 }
517
518 #[test]
519 fn test_push_pop_entries() {
520 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
521
522 let entry1_id = stack.push_entry(
524 "feature-1".to_string(),
525 "abc123".to_string(),
526 "Add feature 1".to_string(),
527 );
528
529 assert_eq!(stack.len(), 1);
530 assert!(!stack.is_empty());
531
532 let entry1 = stack.get_entry(&entry1_id).unwrap();
533 assert_eq!(entry1.branch, "feature-1");
534 assert_eq!(entry1.commit_hash, "abc123");
535 assert_eq!(entry1.message, "Add feature 1");
536 assert_eq!(entry1.parent_id, None);
537 assert!(entry1.children.is_empty());
538
539 let entry2_id = stack.push_entry(
541 "feature-2".to_string(),
542 "def456".to_string(),
543 "Add feature 2".to_string(),
544 );
545
546 assert_eq!(stack.len(), 2);
547
548 let entry2 = stack.get_entry(&entry2_id).unwrap();
549 assert_eq!(entry2.parent_id, Some(entry1_id));
550
551 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
553 assert_eq!(updated_entry1.children, vec![entry2_id]);
554
555 let popped = stack.pop_entry().unwrap();
557 assert_eq!(popped.id, entry2_id);
558 assert_eq!(stack.len(), 1);
559
560 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
562 assert!(updated_entry1.children.is_empty());
563 }
564
565 #[test]
566 fn test_stack_navigation() {
567 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
568
569 let entry1_id = stack.push_entry(
570 "branch1".to_string(),
571 "hash1".to_string(),
572 "msg1".to_string(),
573 );
574 let entry2_id = stack.push_entry(
575 "branch2".to_string(),
576 "hash2".to_string(),
577 "msg2".to_string(),
578 );
579 let entry3_id = stack.push_entry(
580 "branch3".to_string(),
581 "hash3".to_string(),
582 "msg3".to_string(),
583 );
584
585 assert_eq!(stack.get_base_entry().unwrap().id, entry1_id);
587 assert_eq!(stack.get_top_entry().unwrap().id, entry3_id);
588
589 assert_eq!(stack.get_parent(&entry2_id).unwrap().id, entry1_id);
591 assert_eq!(stack.get_parent(&entry3_id).unwrap().id, entry2_id);
592 assert!(stack.get_parent(&entry1_id).is_none());
593
594 let children_of_1 = stack.get_children(&entry1_id);
595 assert_eq!(children_of_1.len(), 1);
596 assert_eq!(children_of_1[0].id, entry2_id);
597 }
598
599 #[test]
600 fn test_stack_validation() {
601 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
602
603 assert!(stack.validate().is_ok());
605
606 stack.push_entry(
608 "branch1".to_string(),
609 "hash1".to_string(),
610 "msg1".to_string(),
611 );
612 stack.push_entry(
613 "branch2".to_string(),
614 "hash2".to_string(),
615 "msg2".to_string(),
616 );
617
618 let result = stack.validate();
620 assert!(result.is_ok());
621 assert!(result.unwrap().contains("validation passed"));
622 }
623
624 #[test]
625 fn test_mark_entry_submitted() {
626 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
627 let entry_id = stack.push_entry(
628 "branch1".to_string(),
629 "hash1".to_string(),
630 "msg1".to_string(),
631 );
632
633 assert!(!stack.get_entry(&entry_id).unwrap().is_submitted);
634 assert!(stack
635 .get_entry(&entry_id)
636 .unwrap()
637 .pull_request_id
638 .is_none());
639
640 assert!(stack.mark_entry_submitted(&entry_id, "PR-123".to_string()));
641
642 let entry = stack.get_entry(&entry_id).unwrap();
643 assert!(entry.is_submitted);
644 assert_eq!(entry.pull_request_id, Some("PR-123".to_string()));
645 }
646
647 #[test]
648 fn test_branch_names() {
649 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
650
651 assert!(stack.get_branch_names().is_empty());
652
653 stack.push_entry(
654 "feature-1".to_string(),
655 "hash1".to_string(),
656 "msg1".to_string(),
657 );
658 stack.push_entry(
659 "feature-2".to_string(),
660 "hash2".to_string(),
661 "msg2".to_string(),
662 );
663
664 let branches = stack.get_branch_names();
665 assert_eq!(branches, vec!["feature-1", "feature-2"]);
666 }
667}