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 get_base_entry(&self) -> Option<&StackEntry> {
167 self.entries.first()
168 }
169
170 pub fn get_top_entry(&self) -> Option<&StackEntry> {
172 self.entries.last()
173 }
174
175 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 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 pub fn is_empty(&self) -> bool {
201 self.entries.is_empty()
202 }
203
204 pub fn len(&self) -> usize {
206 self.entries.len()
207 }
208
209 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 self.sync_entries_from_map();
219 true
220 } else {
221 false
222 }
223 }
224
225 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 pub fn repair_data_consistency(&mut self) {
236 self.sync_entries_from_map();
237 }
238
239 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 self.sync_entries_from_map();
248 true
249 } else {
250 false
251 }
252 }
253
254 pub fn update_status(&mut self, status: StackStatus) {
256 self.status = status;
257 self.updated_at = Utc::now();
258 }
259
260 pub fn set_active(&mut self, active: bool) {
262 self.is_active = active;
263 self.updated_at = Utc::now();
264 }
265
266 pub fn get_branch_names(&self) -> Vec<String> {
268 self.entries
269 .iter()
270 .map(|entry| entry.branch.clone())
271 .collect()
272 }
273
274 pub fn validate(&self) -> Result<String, String> {
276 if self.entries.is_empty() {
278 return Ok("Empty stack is valid".to_string());
279 }
280
281 for (i, entry) in self.entries.iter().enumerate() {
283 if i == 0 {
284 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 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 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 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 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 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 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 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 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 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 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 pub fn can_modify(&self) -> bool {
440 !self.is_submitted && !self.is_synced
441 }
442
443 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 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 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 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 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
521 assert_eq!(updated_entry1.children, vec![entry2_id]);
522
523 let popped = stack.pop_entry().unwrap();
525 assert_eq!(popped.id, entry2_id);
526 assert_eq!(stack.len(), 1);
527
528 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 assert_eq!(stack.get_base_entry().unwrap().id, entry1_id);
555 assert_eq!(stack.get_top_entry().unwrap().id, entry3_id);
556
557 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 assert!(stack.validate().is_ok());
573
574 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 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}