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 remove_entry_at(&mut self, index: usize) -> Option<StackEntry> {
161 if index >= self.entries.len() {
162 return None;
163 }
164
165 let entry = self.entries.remove(index);
166 let entry_id = entry.id;
167 self.entry_map.remove(&entry_id);
168
169 for &child_id in &entry.children {
171 if let Some(child) = self.entry_map.get_mut(&child_id) {
172 child.parent_id = entry.parent_id;
173 }
174 }
175
176 if let Some(parent_id) = entry.parent_id {
178 if let Some(parent) = self.entry_map.get_mut(&parent_id) {
179 parent.children.retain(|&id| id != entry_id);
180 for &child_id in &entry.children {
181 if !parent.children.contains(&child_id) {
182 parent.children.push(child_id);
183 }
184 }
185 }
186 }
187
188 self.sync_entries_from_map();
189 self.updated_at = Utc::now();
190 Some(entry)
191 }
192
193 pub fn get_entry(&self, id: &Uuid) -> Option<&StackEntry> {
195 self.entry_map.get(id)
196 }
197
198 pub fn get_entry_mut(&mut self, id: &Uuid) -> Option<&mut StackEntry> {
200 self.entry_map.get_mut(id)
201 }
202
203 pub fn update_entry_commit_hash(
206 &mut self,
207 entry_id: &Uuid,
208 new_commit_hash: String,
209 ) -> Result<(), String> {
210 let updated_in_vec = self
212 .entries
213 .iter_mut()
214 .find(|e| e.id == *entry_id)
215 .map(|entry| {
216 entry.commit_hash = new_commit_hash.clone();
217 })
218 .is_some();
219
220 let updated_in_map = self
222 .entry_map
223 .get_mut(entry_id)
224 .map(|entry| {
225 entry.commit_hash = new_commit_hash;
226 })
227 .is_some();
228
229 if updated_in_vec && updated_in_map {
230 Ok(())
231 } else {
232 Err(format!("Entry {} not found", entry_id))
233 }
234 }
235
236 pub fn get_base_entry(&self) -> Option<&StackEntry> {
238 self.entries.first()
239 }
240
241 pub fn get_top_entry(&self) -> Option<&StackEntry> {
243 self.entries.last()
244 }
245
246 pub fn get_children(&self, entry_id: &Uuid) -> Vec<&StackEntry> {
248 if let Some(entry) = self.get_entry(entry_id) {
249 entry
250 .children
251 .iter()
252 .filter_map(|id| self.get_entry(id))
253 .collect()
254 } else {
255 Vec::new()
256 }
257 }
258
259 pub fn get_parent(&self, entry_id: &Uuid) -> Option<&StackEntry> {
261 if let Some(entry) = self.get_entry(entry_id) {
262 entry
263 .parent_id
264 .and_then(|parent_id| self.get_entry(&parent_id))
265 } else {
266 None
267 }
268 }
269
270 pub fn is_empty(&self) -> bool {
272 self.entries.is_empty()
273 }
274
275 pub fn len(&self) -> usize {
277 self.entries.len()
278 }
279
280 pub fn mark_entry_submitted(&mut self, entry_id: &Uuid, pull_request_id: String) -> bool {
282 if let Some(entry) = self.get_entry_mut(entry_id) {
283 entry.is_submitted = true;
284 entry.pull_request_id = Some(pull_request_id);
285 entry.updated_at = Utc::now();
286 entry.is_merged = false;
287 self.updated_at = Utc::now();
288
289 self.sync_entries_from_map();
291 true
292 } else {
293 false
294 }
295 }
296
297 fn sync_entries_from_map(&mut self) {
299 for entry in &mut self.entries {
300 if let Some(updated_entry) = self.entry_map.get(&entry.id) {
301 *entry = updated_entry.clone();
302 }
303 }
304 }
305
306 pub fn repair_data_consistency(&mut self) {
308 self.sync_entries_from_map();
309 }
310
311 pub fn mark_entry_synced(&mut self, entry_id: &Uuid) -> bool {
313 if let Some(entry) = self.get_entry_mut(entry_id) {
314 entry.is_synced = true;
315 entry.updated_at = Utc::now();
316 self.updated_at = Utc::now();
317
318 self.sync_entries_from_map();
320 true
321 } else {
322 false
323 }
324 }
325
326 pub fn mark_entry_merged(&mut self, entry_id: &Uuid, merged: bool) -> bool {
328 if let Some(entry) = self.get_entry_mut(entry_id) {
329 entry.is_merged = merged;
330 entry.updated_at = Utc::now();
331 self.updated_at = Utc::now();
332 self.sync_entries_from_map();
333 true
334 } else {
335 false
336 }
337 }
338
339 pub fn update_status(&mut self, status: StackStatus) {
341 self.status = status;
342 self.updated_at = Utc::now();
343 }
344
345 pub fn set_active(&mut self, active: bool) {
347 self.is_active = active;
348 self.updated_at = Utc::now();
349 }
350
351 pub fn get_branch_names(&self) -> Vec<String> {
353 self.entries
354 .iter()
355 .map(|entry| entry.branch.clone())
356 .collect()
357 }
358
359 pub fn validate(&self) -> Result<String, String> {
361 if self.entries.is_empty() {
363 return Ok("Empty stack is valid".to_string());
364 }
365
366 for (i, entry) in self.entries.iter().enumerate() {
368 if i == 0 {
369 if entry.parent_id.is_some() {
371 return Err(format!(
372 "First entry {} should not have a parent",
373 entry.short_hash()
374 ));
375 }
376 } else {
377 let expected_parent = &self.entries[i - 1];
379 if entry.parent_id != Some(expected_parent.id) {
380 return Err(format!(
381 "Entry {} has incorrect parent relationship",
382 entry.short_hash()
383 ));
384 }
385 }
386
387 if let Some(parent_id) = entry.parent_id {
389 if !self.entry_map.contains_key(&parent_id) {
390 return Err(format!(
391 "Entry {} references non-existent parent {}",
392 entry.short_hash(),
393 parent_id
394 ));
395 }
396 }
397 }
398
399 for entry in &self.entries {
401 if !self.entry_map.contains_key(&entry.id) {
402 return Err(format!(
403 "Entry {} is not in the entry map",
404 entry.short_hash()
405 ));
406 }
407 }
408
409 let mut seen_ids = std::collections::HashSet::new();
411 for entry in &self.entries {
412 if !seen_ids.insert(entry.id) {
413 return Err(format!("Duplicate entry ID: {}", entry.id));
414 }
415 }
416
417 let mut seen_branches = std::collections::HashSet::new();
419 for entry in &self.entries {
420 if !seen_branches.insert(&entry.branch) {
421 return Err(format!("Duplicate branch name: {}", entry.branch));
422 }
423 }
424
425 Ok("Stack validation passed".to_string())
426 }
427
428 pub fn validate_git_integrity(
431 &self,
432 git_repo: &crate::git::GitRepository,
433 ) -> Result<String, String> {
434 use tracing::warn;
435
436 let mut issues = Vec::new();
437 let mut warnings = Vec::new();
438
439 for entry in &self.entries {
440 if !git_repo.branch_exists(&entry.branch) {
442 issues.push(format!(
443 "Branch '{}' for entry {} does not exist",
444 entry.branch,
445 entry.short_hash()
446 ));
447 continue;
448 }
449
450 match git_repo.get_branch_head(&entry.branch) {
452 Ok(branch_head) => {
453 if branch_head != entry.commit_hash {
454 issues.push(format!(
455 "Branch '{}' has diverged from stack metadata\n \
456 Expected commit: {} (from stack entry)\n \
457 Actual commit: {} (current branch HEAD)\n \
458 This commonly happens after 'ca entry amend' without --restack\n \
459 Run 'ca validate' and choose 'Incorporate' to update metadata",
460 entry.branch,
461 &entry.commit_hash[..8],
462 &branch_head[..8]
463 ));
464 }
465 }
466 Err(e) => {
467 warnings.push(format!(
468 "Could not check branch '{}' HEAD: {}",
469 entry.branch, e
470 ));
471 }
472 }
473
474 match git_repo.commit_exists(&entry.commit_hash) {
476 Ok(exists) => {
477 if !exists {
478 issues.push(format!(
479 "Commit {} for entry {} no longer exists",
480 entry.short_hash(),
481 entry.id
482 ));
483 }
484 }
485 Err(e) => {
486 warnings.push(format!(
487 "Could not verify commit {} existence: {}",
488 entry.short_hash(),
489 e
490 ));
491 }
492 }
493 }
494
495 for warning in &warnings {
497 warn!("{}", warning);
498 }
499
500 if !issues.is_empty() {
501 Err(format!(
502 "Git integrity validation failed:\n{}{}",
503 issues.join("\n"),
504 if !warnings.is_empty() {
505 format!("\n\nWarnings:\n{}", warnings.join("\n"))
506 } else {
507 String::new()
508 }
509 ))
510 } else if !warnings.is_empty() {
511 Ok(format!(
512 "Git integrity validation passed with warnings:\n{}",
513 warnings.join("\n")
514 ))
515 } else {
516 Ok("Git integrity validation passed".to_string())
517 }
518 }
519}
520
521impl StackEntry {
522 pub fn can_modify(&self) -> bool {
524 !self.is_submitted && !self.is_synced && !self.is_merged
525 }
526
527 pub fn short_hash(&self) -> String {
529 if self.commit_hash.len() >= 8 {
530 self.commit_hash[..8].to_string()
531 } else {
532 self.commit_hash.clone()
533 }
534 }
535
536 pub fn short_message(&self, max_len: usize) -> String {
538 let trimmed = self.message.trim();
539 if trimmed.len() > max_len {
540 format!("{}...", &trimmed[..max_len])
541 } else {
542 trimmed.to_string()
543 }
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 #[test]
552 fn test_create_empty_stack() {
553 let stack = Stack::new(
554 "test-stack".to_string(),
555 "main".to_string(),
556 Some("Test stack description".to_string()),
557 );
558
559 assert_eq!(stack.name, "test-stack");
560 assert_eq!(stack.base_branch, "main");
561 assert_eq!(
562 stack.description,
563 Some("Test stack description".to_string())
564 );
565 assert!(stack.is_empty());
566 assert_eq!(stack.len(), 0);
567 assert_eq!(stack.status, StackStatus::Clean);
568 assert!(!stack.is_active);
569 }
570
571 #[test]
572 fn test_push_pop_entries() {
573 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
574
575 let entry1_id = stack.push_entry(
577 "feature-1".to_string(),
578 "abc123".to_string(),
579 "Add feature 1".to_string(),
580 );
581
582 assert_eq!(stack.len(), 1);
583 assert!(!stack.is_empty());
584
585 let entry1 = stack.get_entry(&entry1_id).unwrap();
586 assert_eq!(entry1.branch, "feature-1");
587 assert_eq!(entry1.commit_hash, "abc123");
588 assert_eq!(entry1.message, "Add feature 1");
589 assert_eq!(entry1.parent_id, None);
590 assert!(entry1.children.is_empty());
591
592 let entry2_id = stack.push_entry(
594 "feature-2".to_string(),
595 "def456".to_string(),
596 "Add feature 2".to_string(),
597 );
598
599 assert_eq!(stack.len(), 2);
600
601 let entry2 = stack.get_entry(&entry2_id).unwrap();
602 assert_eq!(entry2.parent_id, Some(entry1_id));
603
604 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
606 assert_eq!(updated_entry1.children, vec![entry2_id]);
607
608 let popped = stack.pop_entry().unwrap();
610 assert_eq!(popped.id, entry2_id);
611 assert_eq!(stack.len(), 1);
612
613 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
615 assert!(updated_entry1.children.is_empty());
616 }
617
618 #[test]
619 fn test_stack_navigation() {
620 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
621
622 let entry1_id = stack.push_entry(
623 "branch1".to_string(),
624 "hash1".to_string(),
625 "msg1".to_string(),
626 );
627 let entry2_id = stack.push_entry(
628 "branch2".to_string(),
629 "hash2".to_string(),
630 "msg2".to_string(),
631 );
632 let entry3_id = stack.push_entry(
633 "branch3".to_string(),
634 "hash3".to_string(),
635 "msg3".to_string(),
636 );
637
638 assert_eq!(stack.get_base_entry().unwrap().id, entry1_id);
640 assert_eq!(stack.get_top_entry().unwrap().id, entry3_id);
641
642 assert_eq!(stack.get_parent(&entry2_id).unwrap().id, entry1_id);
644 assert_eq!(stack.get_parent(&entry3_id).unwrap().id, entry2_id);
645 assert!(stack.get_parent(&entry1_id).is_none());
646
647 let children_of_1 = stack.get_children(&entry1_id);
648 assert_eq!(children_of_1.len(), 1);
649 assert_eq!(children_of_1[0].id, entry2_id);
650 }
651
652 #[test]
653 fn test_stack_validation() {
654 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
655
656 assert!(stack.validate().is_ok());
658
659 stack.push_entry(
661 "branch1".to_string(),
662 "hash1".to_string(),
663 "msg1".to_string(),
664 );
665 stack.push_entry(
666 "branch2".to_string(),
667 "hash2".to_string(),
668 "msg2".to_string(),
669 );
670
671 let result = stack.validate();
673 assert!(result.is_ok());
674 assert!(result.unwrap().contains("validation passed"));
675 }
676
677 #[test]
678 fn test_mark_entry_submitted() {
679 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
680 let entry_id = stack.push_entry(
681 "branch1".to_string(),
682 "hash1".to_string(),
683 "msg1".to_string(),
684 );
685
686 assert!(!stack.get_entry(&entry_id).unwrap().is_submitted);
687 assert!(stack
688 .get_entry(&entry_id)
689 .unwrap()
690 .pull_request_id
691 .is_none());
692
693 assert!(stack.mark_entry_submitted(&entry_id, "PR-123".to_string()));
694
695 let entry = stack.get_entry(&entry_id).unwrap();
696 assert!(entry.is_submitted);
697 assert_eq!(entry.pull_request_id, Some("PR-123".to_string()));
698 }
699
700 #[test]
701 fn test_mark_entry_merged() {
702 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
703 let entry_id = stack.push_entry(
704 "branch1".to_string(),
705 "hash1".to_string(),
706 "msg1".to_string(),
707 );
708
709 assert!(!stack.get_entry(&entry_id).unwrap().is_merged);
710 assert!(stack.mark_entry_merged(&entry_id, true));
711 let merged_entry = stack.get_entry(&entry_id).unwrap();
712 assert!(merged_entry.is_merged);
713 assert!(!merged_entry.can_modify());
714
715 assert!(stack.mark_entry_merged(&entry_id, false));
716 assert!(!stack.get_entry(&entry_id).unwrap().is_merged);
717 }
718
719 #[test]
720 fn test_branch_names() {
721 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
722
723 assert!(stack.get_branch_names().is_empty());
724
725 stack.push_entry(
726 "feature-1".to_string(),
727 "hash1".to_string(),
728 "msg1".to_string(),
729 );
730 stack.push_entry(
731 "feature-2".to_string(),
732 "hash2".to_string(),
733 "msg2".to_string(),
734 );
735
736 let branches = stack.get_branch_names();
737 assert_eq!(branches, vec!["feature-1", "feature-2"]);
738 }
739}