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 true
214 } else {
215 false
216 }
217 }
218
219 pub fn mark_entry_synced(&mut self, entry_id: &Uuid) -> bool {
221 if let Some(entry) = self.get_entry_mut(entry_id) {
222 entry.is_synced = true;
223 entry.updated_at = Utc::now();
224 self.updated_at = Utc::now();
225 true
226 } else {
227 false
228 }
229 }
230
231 pub fn update_status(&mut self, status: StackStatus) {
233 self.status = status;
234 self.updated_at = Utc::now();
235 }
236
237 pub fn set_active(&mut self, active: bool) {
239 self.is_active = active;
240 self.updated_at = Utc::now();
241 }
242
243 pub fn get_branch_names(&self) -> Vec<String> {
245 self.entries
246 .iter()
247 .map(|entry| entry.branch.clone())
248 .collect()
249 }
250
251 pub fn validate(&self) -> Result<(), String> {
253 for entry in &self.entries {
255 if !self.entry_map.contains_key(&entry.id) {
256 return Err(format!("Entry {} not found in entry map", entry.id));
257 }
258 }
259
260 if self.entry_map.len() != self.entries.len() {
262 return Err("Entry map and vector have different sizes".to_string());
263 }
264
265 for entry in &self.entries {
267 if let Some(parent_id) = entry.parent_id {
268 if let Some(parent) = self.entry_map.get(&parent_id) {
269 if !parent.children.contains(&entry.id) {
270 return Err(format!(
271 "Parent {} doesn't reference child {}",
272 parent_id, entry.id
273 ));
274 }
275 } else {
276 return Err(format!(
277 "Parent {} not found for entry {}",
278 parent_id, entry.id
279 ));
280 }
281 }
282
283 for child_id in &entry.children {
284 if let Some(child) = self.entry_map.get(child_id) {
285 if child.parent_id != Some(entry.id) {
286 return Err(format!(
287 "Child {} doesn't reference parent {}",
288 child_id, entry.id
289 ));
290 }
291 } else {
292 return Err(format!(
293 "Child {} not found for entry {}",
294 child_id, entry.id
295 ));
296 }
297 }
298 }
299
300 Ok(())
301 }
302}
303
304impl StackEntry {
305 pub fn can_modify(&self) -> bool {
307 !self.is_submitted && !self.is_synced
308 }
309
310 pub fn short_hash(&self) -> String {
312 if self.commit_hash.len() >= 8 {
313 self.commit_hash[..8].to_string()
314 } else {
315 self.commit_hash.clone()
316 }
317 }
318
319 pub fn short_message(&self, max_len: usize) -> String {
321 if self.message.len() > max_len {
322 format!("{}...", &self.message[..max_len])
323 } else {
324 self.message.clone()
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_create_empty_stack() {
335 let stack = Stack::new(
336 "test-stack".to_string(),
337 "main".to_string(),
338 Some("Test stack description".to_string()),
339 );
340
341 assert_eq!(stack.name, "test-stack");
342 assert_eq!(stack.base_branch, "main");
343 assert_eq!(
344 stack.description,
345 Some("Test stack description".to_string())
346 );
347 assert!(stack.is_empty());
348 assert_eq!(stack.len(), 0);
349 assert_eq!(stack.status, StackStatus::Clean);
350 assert!(!stack.is_active);
351 }
352
353 #[test]
354 fn test_push_pop_entries() {
355 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
356
357 let entry1_id = stack.push_entry(
359 "feature-1".to_string(),
360 "abc123".to_string(),
361 "Add feature 1".to_string(),
362 );
363
364 assert_eq!(stack.len(), 1);
365 assert!(!stack.is_empty());
366
367 let entry1 = stack.get_entry(&entry1_id).unwrap();
368 assert_eq!(entry1.branch, "feature-1");
369 assert_eq!(entry1.commit_hash, "abc123");
370 assert_eq!(entry1.message, "Add feature 1");
371 assert_eq!(entry1.parent_id, None);
372 assert!(entry1.children.is_empty());
373
374 let entry2_id = stack.push_entry(
376 "feature-2".to_string(),
377 "def456".to_string(),
378 "Add feature 2".to_string(),
379 );
380
381 assert_eq!(stack.len(), 2);
382
383 let entry2 = stack.get_entry(&entry2_id).unwrap();
384 assert_eq!(entry2.parent_id, Some(entry1_id));
385
386 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
388 assert_eq!(updated_entry1.children, vec![entry2_id]);
389
390 let popped = stack.pop_entry().unwrap();
392 assert_eq!(popped.id, entry2_id);
393 assert_eq!(stack.len(), 1);
394
395 let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
397 assert!(updated_entry1.children.is_empty());
398 }
399
400 #[test]
401 fn test_stack_navigation() {
402 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
403
404 let entry1_id = stack.push_entry(
405 "branch1".to_string(),
406 "hash1".to_string(),
407 "msg1".to_string(),
408 );
409 let entry2_id = stack.push_entry(
410 "branch2".to_string(),
411 "hash2".to_string(),
412 "msg2".to_string(),
413 );
414 let entry3_id = stack.push_entry(
415 "branch3".to_string(),
416 "hash3".to_string(),
417 "msg3".to_string(),
418 );
419
420 assert_eq!(stack.get_base_entry().unwrap().id, entry1_id);
422 assert_eq!(stack.get_top_entry().unwrap().id, entry3_id);
423
424 assert_eq!(stack.get_parent(&entry2_id).unwrap().id, entry1_id);
426 assert_eq!(stack.get_parent(&entry3_id).unwrap().id, entry2_id);
427 assert!(stack.get_parent(&entry1_id).is_none());
428
429 let children_of_1 = stack.get_children(&entry1_id);
430 assert_eq!(children_of_1.len(), 1);
431 assert_eq!(children_of_1[0].id, entry2_id);
432 }
433
434 #[test]
435 fn test_stack_validation() {
436 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
437
438 assert!(stack.validate().is_ok());
440
441 stack.push_entry(
443 "branch1".to_string(),
444 "hash1".to_string(),
445 "msg1".to_string(),
446 );
447 stack.push_entry(
448 "branch2".to_string(),
449 "hash2".to_string(),
450 "msg2".to_string(),
451 );
452
453 assert!(stack.validate().is_ok());
455 }
456
457 #[test]
458 fn test_mark_entry_submitted() {
459 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
460 let entry_id = stack.push_entry(
461 "branch1".to_string(),
462 "hash1".to_string(),
463 "msg1".to_string(),
464 );
465
466 assert!(!stack.get_entry(&entry_id).unwrap().is_submitted);
467 assert!(stack
468 .get_entry(&entry_id)
469 .unwrap()
470 .pull_request_id
471 .is_none());
472
473 assert!(stack.mark_entry_submitted(&entry_id, "PR-123".to_string()));
474
475 let entry = stack.get_entry(&entry_id).unwrap();
476 assert!(entry.is_submitted);
477 assert_eq!(entry.pull_request_id, Some("PR-123".to_string()));
478 }
479
480 #[test]
481 fn test_branch_names() {
482 let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
483
484 assert!(stack.get_branch_names().is_empty());
485
486 stack.push_entry(
487 "feature-1".to_string(),
488 "hash1".to_string(),
489 "msg1".to_string(),
490 );
491 stack.push_entry(
492 "feature-2".to_string(),
493 "hash2".to_string(),
494 "msg2".to_string(),
495 );
496
497 let branches = stack.get_branch_names();
498 assert_eq!(branches, vec!["feature-1", "feature-2"]);
499 }
500}