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 This commonly happens after 'ca entry amend' without --restack\n \
425 Run 'ca validate' and choose 'Incorporate' to update metadata",
426 entry.branch,
427 &entry.commit_hash[..8],
428 &branch_head[..8]
429 ));
430 }
431 }
432 Err(e) => {
433 warnings.push(format!(
434 "Could not check branch '{}' HEAD: {}",
435 entry.branch, e
436 ));
437 }
438 }
439
440 match git_repo.commit_exists(&entry.commit_hash) {
442 Ok(exists) => {
443 if !exists {
444 issues.push(format!(
445 "Commit {} for entry {} no longer exists",
446 entry.short_hash(),
447 entry.id
448 ));
449 }
450 }
451 Err(e) => {
452 warnings.push(format!(
453 "Could not verify commit {} existence: {}",
454 entry.short_hash(),
455 e
456 ));
457 }
458 }
459 }
460
461 for warning in &warnings {
463 warn!("{}", warning);
464 }
465
466 if !issues.is_empty() {
467 Err(format!(
468 "Git integrity validation failed:\n{}{}",
469 issues.join("\n"),
470 if !warnings.is_empty() {
471 format!("\n\nWarnings:\n{}", warnings.join("\n"))
472 } else {
473 String::new()
474 }
475 ))
476 } else if !warnings.is_empty() {
477 Ok(format!(
478 "Git integrity validation passed with warnings:\n{}",
479 warnings.join("\n")
480 ))
481 } else {
482 Ok("Git integrity validation passed".to_string())
483 }
484 }
485}
486
487impl StackEntry {
488 pub fn can_modify(&self) -> bool {
490 !self.is_submitted && !self.is_synced && !self.is_merged
491 }
492
493 pub fn short_hash(&self) -> String {
495 if self.commit_hash.len() >= 8 {
496 self.commit_hash[..8].to_string()
497 } else {
498 self.commit_hash.clone()
499 }
500 }
501
502 pub fn short_message(&self, max_len: usize) -> String {
504 let trimmed = self.message.trim();
505 if trimmed.len() > max_len {
506 format!("{}...", &trimmed[..max_len])
507 } else {
508 trimmed.to_string()
509 }
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516
517 #[test]
518 fn test_create_empty_stack() {
519 let stack = Stack::new(
520 "test-stack".to_string(),
521 "main".to_string(),
522 Some("Test stack description".to_string()),
523 );
524
525 assert_eq!(stack.name, "test-stack");
526 assert_eq!(stack.base_branch, "main");
527 assert_eq!(
528 stack.description,
529 Some("Test stack description".to_string())
530 );
531 assert!(stack.is_empty());
532 assert_eq!(stack.len(), 0);
533 assert_eq!(stack.status, StackStatus::Clean);
534 assert!(!stack.is_active);
535 }
536
537 #[test]
538 fn test_push_pop_entries() {
539 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
540
541 let entry1_id = stack.push_entry(
543 "feature-1".to_string(),
544 "abc123".to_string(),
545 "Add feature 1".to_string(),
546 );
547
548 assert_eq!(stack.len(), 1);
549 assert!(!stack.is_empty());
550
551 let entry1 = stack.get_entry(&entry1_id).unwrap();
552 assert_eq!(entry1.branch, "feature-1");
553 assert_eq!(entry1.commit_hash, "abc123");
554 assert_eq!(entry1.message, "Add feature 1");
555 assert_eq!(entry1.parent_id, None);
556 assert!(entry1.children.is_empty());
557
558 let entry2_id = stack.push_entry(
560 "feature-2".to_string(),
561 "def456".to_string(),
562 "Add feature 2".to_string(),
563 );
564
565 assert_eq!(stack.len(), 2);
566
567 let entry2 = stack.get_entry(&entry2_id).unwrap();
568 assert_eq!(entry2.parent_id, Some(entry1_id));
569
570 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
572 assert_eq!(updated_entry1.children, vec![entry2_id]);
573
574 let popped = stack.pop_entry().unwrap();
576 assert_eq!(popped.id, entry2_id);
577 assert_eq!(stack.len(), 1);
578
579 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
581 assert!(updated_entry1.children.is_empty());
582 }
583
584 #[test]
585 fn test_stack_navigation() {
586 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
587
588 let entry1_id = stack.push_entry(
589 "branch1".to_string(),
590 "hash1".to_string(),
591 "msg1".to_string(),
592 );
593 let entry2_id = stack.push_entry(
594 "branch2".to_string(),
595 "hash2".to_string(),
596 "msg2".to_string(),
597 );
598 let entry3_id = stack.push_entry(
599 "branch3".to_string(),
600 "hash3".to_string(),
601 "msg3".to_string(),
602 );
603
604 assert_eq!(stack.get_base_entry().unwrap().id, entry1_id);
606 assert_eq!(stack.get_top_entry().unwrap().id, entry3_id);
607
608 assert_eq!(stack.get_parent(&entry2_id).unwrap().id, entry1_id);
610 assert_eq!(stack.get_parent(&entry3_id).unwrap().id, entry2_id);
611 assert!(stack.get_parent(&entry1_id).is_none());
612
613 let children_of_1 = stack.get_children(&entry1_id);
614 assert_eq!(children_of_1.len(), 1);
615 assert_eq!(children_of_1[0].id, entry2_id);
616 }
617
618 #[test]
619 fn test_stack_validation() {
620 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
621
622 assert!(stack.validate().is_ok());
624
625 stack.push_entry(
627 "branch1".to_string(),
628 "hash1".to_string(),
629 "msg1".to_string(),
630 );
631 stack.push_entry(
632 "branch2".to_string(),
633 "hash2".to_string(),
634 "msg2".to_string(),
635 );
636
637 let result = stack.validate();
639 assert!(result.is_ok());
640 assert!(result.unwrap().contains("validation passed"));
641 }
642
643 #[test]
644 fn test_mark_entry_submitted() {
645 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
646 let entry_id = stack.push_entry(
647 "branch1".to_string(),
648 "hash1".to_string(),
649 "msg1".to_string(),
650 );
651
652 assert!(!stack.get_entry(&entry_id).unwrap().is_submitted);
653 assert!(stack
654 .get_entry(&entry_id)
655 .unwrap()
656 .pull_request_id
657 .is_none());
658
659 assert!(stack.mark_entry_submitted(&entry_id, "PR-123".to_string()));
660
661 let entry = stack.get_entry(&entry_id).unwrap();
662 assert!(entry.is_submitted);
663 assert_eq!(entry.pull_request_id, Some("PR-123".to_string()));
664 }
665
666 #[test]
667 fn test_mark_entry_merged() {
668 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
669 let entry_id = stack.push_entry(
670 "branch1".to_string(),
671 "hash1".to_string(),
672 "msg1".to_string(),
673 );
674
675 assert!(!stack.get_entry(&entry_id).unwrap().is_merged);
676 assert!(stack.mark_entry_merged(&entry_id, true));
677 let merged_entry = stack.get_entry(&entry_id).unwrap();
678 assert!(merged_entry.is_merged);
679 assert!(!merged_entry.can_modify());
680
681 assert!(stack.mark_entry_merged(&entry_id, false));
682 assert!(!stack.get_entry(&entry_id).unwrap().is_merged);
683 }
684
685 #[test]
686 fn test_branch_names() {
687 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
688
689 assert!(stack.get_branch_names().is_empty());
690
691 stack.push_entry(
692 "feature-1".to_string(),
693 "hash1".to_string(),
694 "msg1".to_string(),
695 );
696 stack.push_entry(
697 "feature-2".to_string(),
698 "hash2".to_string(),
699 "msg2".to_string(),
700 );
701
702 let branches = stack.get_branch_names();
703 assert_eq!(branches, vec!["feature-1", "feature-2"]);
704 }
705}