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 entries: Vec<StackEntry>,
65 pub entry_map: HashMap<Uuid, StackEntry>,
67 pub status: StackStatus,
69 pub created_at: DateTime<Utc>,
71 pub updated_at: DateTime<Utc>,
73 pub is_active: bool,
75}
76
77impl Stack {
78 pub fn new(name: String, base_branch: String, description: Option<String>) -> Self {
80 let now = Utc::now();
81 Self {
82 id: Uuid::new_v4(),
83 name,
84 description,
85 base_branch,
86 entries: Vec::new(),
87 entry_map: HashMap::new(),
88 status: StackStatus::Clean,
89 created_at: now,
90 updated_at: now,
91 is_active: false,
92 }
93 }
94
95 pub fn push_entry(&mut self, branch: String, commit_hash: String, message: String) -> Uuid {
97 let now = Utc::now();
98 let entry_id = Uuid::new_v4();
99
100 let parent_id = self.entries.last().map(|entry| entry.id);
102
103 let entry = StackEntry {
104 id: entry_id,
105 branch,
106 commit_hash,
107 message,
108 parent_id,
109 children: Vec::new(),
110 created_at: now,
111 updated_at: now,
112 is_submitted: false,
113 pull_request_id: None,
114 is_synced: false,
115 };
116
117 if let Some(parent_id) = parent_id {
119 if let Some(parent) = self.entry_map.get_mut(&parent_id) {
120 parent.children.push(entry_id);
121 }
122 }
123
124 self.entries.push(entry.clone());
126 self.entry_map.insert(entry_id, entry);
127 self.updated_at = now;
128
129 entry_id
130 }
131
132 pub fn pop_entry(&mut self) -> Option<StackEntry> {
134 if let Some(entry) = self.entries.pop() {
135 let entry_id = entry.id;
136 self.entry_map.remove(&entry_id);
137
138 if let Some(parent_id) = entry.parent_id {
140 if let Some(parent) = self.entry_map.get_mut(&parent_id) {
141 parent.children.retain(|&id| id != entry_id);
142 }
143 }
144
145 self.updated_at = Utc::now();
146 Some(entry)
147 } else {
148 None
149 }
150 }
151
152 pub fn get_entry(&self, id: &Uuid) -> Option<&StackEntry> {
154 self.entry_map.get(id)
155 }
156
157 pub fn get_entry_mut(&mut self, id: &Uuid) -> Option<&mut StackEntry> {
159 self.entry_map.get_mut(id)
160 }
161
162 pub fn get_base_entry(&self) -> Option<&StackEntry> {
164 self.entries.first()
165 }
166
167 pub fn get_top_entry(&self) -> Option<&StackEntry> {
169 self.entries.last()
170 }
171
172 pub fn get_children(&self, entry_id: &Uuid) -> Vec<&StackEntry> {
174 if let Some(entry) = self.get_entry(entry_id) {
175 entry
176 .children
177 .iter()
178 .filter_map(|id| self.get_entry(id))
179 .collect()
180 } else {
181 Vec::new()
182 }
183 }
184
185 pub fn get_parent(&self, entry_id: &Uuid) -> Option<&StackEntry> {
187 if let Some(entry) = self.get_entry(entry_id) {
188 entry
189 .parent_id
190 .and_then(|parent_id| self.get_entry(&parent_id))
191 } else {
192 None
193 }
194 }
195
196 pub fn is_empty(&self) -> bool {
198 self.entries.is_empty()
199 }
200
201 pub fn len(&self) -> usize {
203 self.entries.len()
204 }
205
206 pub fn mark_entry_submitted(&mut self, entry_id: &Uuid, pull_request_id: String) -> bool {
208 if let Some(entry) = self.get_entry_mut(entry_id) {
209 entry.is_submitted = true;
210 entry.pull_request_id = Some(pull_request_id);
211 entry.updated_at = Utc::now();
212 self.updated_at = Utc::now();
213
214 self.sync_entries_from_map();
216 true
217 } else {
218 false
219 }
220 }
221
222 fn sync_entries_from_map(&mut self) {
224 for entry in &mut self.entries {
225 if let Some(updated_entry) = self.entry_map.get(&entry.id) {
226 *entry = updated_entry.clone();
227 }
228 }
229 }
230
231 pub fn repair_data_consistency(&mut self) {
233 self.sync_entries_from_map();
234 }
235
236 pub fn mark_entry_synced(&mut self, entry_id: &Uuid) -> bool {
238 if let Some(entry) = self.get_entry_mut(entry_id) {
239 entry.is_synced = true;
240 entry.updated_at = Utc::now();
241 self.updated_at = Utc::now();
242
243 self.sync_entries_from_map();
245 true
246 } else {
247 false
248 }
249 }
250
251 pub fn update_status(&mut self, status: StackStatus) {
253 self.status = status;
254 self.updated_at = Utc::now();
255 }
256
257 pub fn set_active(&mut self, active: bool) {
259 self.is_active = active;
260 self.updated_at = Utc::now();
261 }
262
263 pub fn get_branch_names(&self) -> Vec<String> {
265 self.entries
266 .iter()
267 .map(|entry| entry.branch.clone())
268 .collect()
269 }
270
271 pub fn validate(&self) -> Result<String, String> {
273 if self.entries.is_empty() {
275 return Ok("Empty stack is valid".to_string());
276 }
277
278 for (i, entry) in self.entries.iter().enumerate() {
280 if i == 0 {
281 if entry.parent_id.is_some() {
283 return Err(format!(
284 "First entry {} should not have a parent",
285 entry.short_hash()
286 ));
287 }
288 } else {
289 let expected_parent = &self.entries[i - 1];
291 if entry.parent_id != Some(expected_parent.id) {
292 return Err(format!(
293 "Entry {} has incorrect parent relationship",
294 entry.short_hash()
295 ));
296 }
297 }
298
299 if let Some(parent_id) = entry.parent_id {
301 if !self.entry_map.contains_key(&parent_id) {
302 return Err(format!(
303 "Entry {} references non-existent parent {}",
304 entry.short_hash(),
305 parent_id
306 ));
307 }
308 }
309 }
310
311 for entry in &self.entries {
313 if !self.entry_map.contains_key(&entry.id) {
314 return Err(format!(
315 "Entry {} is not in the entry map",
316 entry.short_hash()
317 ));
318 }
319 }
320
321 let mut seen_ids = std::collections::HashSet::new();
323 for entry in &self.entries {
324 if !seen_ids.insert(entry.id) {
325 return Err(format!("Duplicate entry ID: {}", entry.id));
326 }
327 }
328
329 let mut seen_branches = std::collections::HashSet::new();
331 for entry in &self.entries {
332 if !seen_branches.insert(&entry.branch) {
333 return Err(format!("Duplicate branch name: {}", entry.branch));
334 }
335 }
336
337 Ok("Stack validation passed".to_string())
338 }
339
340 pub fn validate_git_integrity(
343 &self,
344 git_repo: &crate::git::GitRepository,
345 ) -> Result<String, String> {
346 use tracing::warn;
347
348 let mut issues = Vec::new();
349 let mut warnings = Vec::new();
350
351 for entry in &self.entries {
352 if !git_repo.branch_exists(&entry.branch) {
354 issues.push(format!(
355 "Branch '{}' for entry {} does not exist",
356 entry.branch,
357 entry.short_hash()
358 ));
359 continue;
360 }
361
362 match git_repo.get_branch_head(&entry.branch) {
364 Ok(branch_head) => {
365 if branch_head != entry.commit_hash {
366 issues.push(format!(
367 "🚨 BRANCH MODIFICATION DETECTED: Branch '{}' has been manually modified!\n \
368 Expected commit: {} (from stack entry)\n \
369 Actual commit: {} (current branch HEAD)\n \
370 💡 Someone may have checked out '{}' and added commits.\n \
371 This breaks stack integrity!",
372 entry.branch,
373 &entry.commit_hash[..8],
374 &branch_head[..8],
375 entry.branch
376 ));
377 }
378 }
379 Err(e) => {
380 warnings.push(format!(
381 "Could not check branch '{}' HEAD: {}",
382 entry.branch, e
383 ));
384 }
385 }
386
387 match git_repo.commit_exists(&entry.commit_hash) {
389 Ok(exists) => {
390 if !exists {
391 issues.push(format!(
392 "Commit {} for entry {} no longer exists",
393 entry.short_hash(),
394 entry.id
395 ));
396 }
397 }
398 Err(e) => {
399 warnings.push(format!(
400 "Could not verify commit {} existence: {}",
401 entry.short_hash(),
402 e
403 ));
404 }
405 }
406 }
407
408 for warning in &warnings {
410 warn!("{}", warning);
411 }
412
413 if !issues.is_empty() {
414 Err(format!(
415 "Git integrity validation failed:\n{}{}",
416 issues.join("\n"),
417 if !warnings.is_empty() {
418 format!("\n\nWarnings:\n{}", warnings.join("\n"))
419 } else {
420 String::new()
421 }
422 ))
423 } else if !warnings.is_empty() {
424 Ok(format!(
425 "Git integrity validation passed with warnings:\n{}",
426 warnings.join("\n")
427 ))
428 } else {
429 Ok("Git integrity validation passed".to_string())
430 }
431 }
432}
433
434impl StackEntry {
435 pub fn can_modify(&self) -> bool {
437 !self.is_submitted && !self.is_synced
438 }
439
440 pub fn short_hash(&self) -> String {
442 if self.commit_hash.len() >= 8 {
443 self.commit_hash[..8].to_string()
444 } else {
445 self.commit_hash.clone()
446 }
447 }
448
449 pub fn short_message(&self, max_len: usize) -> String {
451 if self.message.len() > max_len {
452 format!("{}...", &self.message[..max_len])
453 } else {
454 self.message.clone()
455 }
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn test_create_empty_stack() {
465 let stack = Stack::new(
466 "test-stack".to_string(),
467 "main".to_string(),
468 Some("Test stack description".to_string()),
469 );
470
471 assert_eq!(stack.name, "test-stack");
472 assert_eq!(stack.base_branch, "main");
473 assert_eq!(
474 stack.description,
475 Some("Test stack description".to_string())
476 );
477 assert!(stack.is_empty());
478 assert_eq!(stack.len(), 0);
479 assert_eq!(stack.status, StackStatus::Clean);
480 assert!(!stack.is_active);
481 }
482
483 #[test]
484 fn test_push_pop_entries() {
485 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
486
487 let entry1_id = stack.push_entry(
489 "feature-1".to_string(),
490 "abc123".to_string(),
491 "Add feature 1".to_string(),
492 );
493
494 assert_eq!(stack.len(), 1);
495 assert!(!stack.is_empty());
496
497 let entry1 = stack.get_entry(&entry1_id).unwrap();
498 assert_eq!(entry1.branch, "feature-1");
499 assert_eq!(entry1.commit_hash, "abc123");
500 assert_eq!(entry1.message, "Add feature 1");
501 assert_eq!(entry1.parent_id, None);
502 assert!(entry1.children.is_empty());
503
504 let entry2_id = stack.push_entry(
506 "feature-2".to_string(),
507 "def456".to_string(),
508 "Add feature 2".to_string(),
509 );
510
511 assert_eq!(stack.len(), 2);
512
513 let entry2 = stack.get_entry(&entry2_id).unwrap();
514 assert_eq!(entry2.parent_id, Some(entry1_id));
515
516 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
518 assert_eq!(updated_entry1.children, vec![entry2_id]);
519
520 let popped = stack.pop_entry().unwrap();
522 assert_eq!(popped.id, entry2_id);
523 assert_eq!(stack.len(), 1);
524
525 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
527 assert!(updated_entry1.children.is_empty());
528 }
529
530 #[test]
531 fn test_stack_navigation() {
532 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
533
534 let entry1_id = stack.push_entry(
535 "branch1".to_string(),
536 "hash1".to_string(),
537 "msg1".to_string(),
538 );
539 let entry2_id = stack.push_entry(
540 "branch2".to_string(),
541 "hash2".to_string(),
542 "msg2".to_string(),
543 );
544 let entry3_id = stack.push_entry(
545 "branch3".to_string(),
546 "hash3".to_string(),
547 "msg3".to_string(),
548 );
549
550 assert_eq!(stack.get_base_entry().unwrap().id, entry1_id);
552 assert_eq!(stack.get_top_entry().unwrap().id, entry3_id);
553
554 assert_eq!(stack.get_parent(&entry2_id).unwrap().id, entry1_id);
556 assert_eq!(stack.get_parent(&entry3_id).unwrap().id, entry2_id);
557 assert!(stack.get_parent(&entry1_id).is_none());
558
559 let children_of_1 = stack.get_children(&entry1_id);
560 assert_eq!(children_of_1.len(), 1);
561 assert_eq!(children_of_1[0].id, entry2_id);
562 }
563
564 #[test]
565 fn test_stack_validation() {
566 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
567
568 assert!(stack.validate().is_ok());
570
571 stack.push_entry(
573 "branch1".to_string(),
574 "hash1".to_string(),
575 "msg1".to_string(),
576 );
577 stack.push_entry(
578 "branch2".to_string(),
579 "hash2".to_string(),
580 "msg2".to_string(),
581 );
582
583 let result = stack.validate();
585 assert!(result.is_ok());
586 assert!(result.unwrap().contains("validation passed"));
587 }
588
589 #[test]
590 fn test_mark_entry_submitted() {
591 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
592 let entry_id = stack.push_entry(
593 "branch1".to_string(),
594 "hash1".to_string(),
595 "msg1".to_string(),
596 );
597
598 assert!(!stack.get_entry(&entry_id).unwrap().is_submitted);
599 assert!(stack
600 .get_entry(&entry_id)
601 .unwrap()
602 .pull_request_id
603 .is_none());
604
605 assert!(stack.mark_entry_submitted(&entry_id, "PR-123".to_string()));
606
607 let entry = stack.get_entry(&entry_id).unwrap();
608 assert!(entry.is_submitted);
609 assert_eq!(entry.pull_request_id, Some("PR-123".to_string()));
610 }
611
612 #[test]
613 fn test_branch_names() {
614 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
615
616 assert!(stack.get_branch_names().is_empty());
617
618 stack.push_entry(
619 "feature-1".to_string(),
620 "hash1".to_string(),
621 "msg1".to_string(),
622 );
623 stack.push_entry(
624 "feature-2".to_string(),
625 "hash2".to_string(),
626 "msg2".to_string(),
627 );
628
629 let branches = stack.get_branch_names();
630 assert_eq!(branches, vec!["feature-1", "feature-2"]);
631 }
632}