1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CommitMetadata {
9 pub hash: String,
11 pub message: String,
13 pub stack_entry_id: Uuid,
15 pub stack_id: Uuid,
17 pub branch: String,
19 pub source_branch: String,
21 pub dependencies: Vec<String>,
23 pub dependents: Vec<String>,
25 pub is_pushed: bool,
27 pub is_submitted: bool,
29 #[serde(default)]
31 pub is_merged: bool,
32 pub pull_request_id: Option<String>,
34 pub created_at: DateTime<Utc>,
36 pub updated_at: DateTime<Utc>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct StackMetadata {
43 pub stack_id: Uuid,
45 pub name: String,
47 pub description: Option<String>,
49 pub base_branch: String,
51 pub current_branch: Option<String>,
53 pub total_commits: usize,
55 pub submitted_commits: usize,
57 pub merged_commits: usize,
59 pub branches: Vec<String>,
61 pub commit_hashes: Vec<String>,
63 pub has_conflicts: bool,
65 pub is_up_to_date: bool,
67 pub last_sync: Option<DateTime<Utc>>,
69 pub created_at: DateTime<Utc>,
71 pub updated_at: DateTime<Utc>,
73}
74
75impl CommitMetadata {
76 pub fn new(
78 hash: String,
79 message: String,
80 stack_entry_id: Uuid,
81 stack_id: Uuid,
82 branch: String,
83 source_branch: String,
84 ) -> Self {
85 let now = Utc::now();
86 Self {
87 hash,
88 message,
89 stack_entry_id,
90 stack_id,
91 branch,
92 source_branch,
93 dependencies: Vec::new(),
94 dependents: Vec::new(),
95 is_pushed: false,
96 is_submitted: false,
97 is_merged: false,
98 pull_request_id: None,
99 created_at: now,
100 updated_at: now,
101 }
102 }
103
104 pub fn add_dependency(&mut self, commit_hash: String) {
106 if !self.dependencies.contains(&commit_hash) {
107 self.dependencies.push(commit_hash);
108 self.updated_at = Utc::now();
109 }
110 }
111
112 pub fn add_dependent(&mut self, commit_hash: String) {
114 if !self.dependents.contains(&commit_hash) {
115 self.dependents.push(commit_hash);
116 self.updated_at = Utc::now();
117 }
118 }
119
120 pub fn mark_pushed(&mut self) {
122 self.is_pushed = true;
123 self.updated_at = Utc::now();
124 }
125
126 pub fn mark_submitted(&mut self, pull_request_id: String) {
128 self.is_submitted = true;
129 self.pull_request_id = Some(pull_request_id);
130 self.updated_at = Utc::now();
131 }
132
133 pub fn mark_merged(&mut self, merged: bool) {
135 self.is_merged = merged;
136 self.updated_at = Utc::now();
137 }
138
139 pub fn short_hash(&self) -> String {
141 if self.hash.len() >= 8 {
142 self.hash[..8].to_string()
143 } else {
144 self.hash.clone()
145 }
146 }
147}
148
149impl StackMetadata {
150 pub fn new(
152 stack_id: Uuid,
153 name: String,
154 base_branch: String,
155 description: Option<String>,
156 ) -> Self {
157 let now = Utc::now();
158 Self {
159 stack_id,
160 name,
161 description,
162 base_branch,
163 current_branch: None,
164 total_commits: 0,
165 submitted_commits: 0,
166 merged_commits: 0,
167 branches: Vec::new(),
168 commit_hashes: Vec::new(),
169 has_conflicts: false,
170 is_up_to_date: true,
171 last_sync: None,
172 created_at: now,
173 updated_at: now,
174 }
175 }
176
177 pub fn update_stats(&mut self, total: usize, submitted: usize, merged: usize) {
179 self.total_commits = total;
180 self.submitted_commits = submitted;
181 self.merged_commits = merged;
182 self.updated_at = Utc::now();
183 }
184
185 pub fn add_branch(&mut self, branch: String) {
187 if !self.branches.contains(&branch) {
188 self.branches.push(branch);
189 self.updated_at = Utc::now();
190 }
191 }
192
193 pub fn remove_branch(&mut self, branch: &str) {
195 if let Some(pos) = self.branches.iter().position(|b| b == branch) {
196 self.branches.remove(pos);
197 self.updated_at = Utc::now();
198 }
199 }
200
201 pub fn set_current_branch(&mut self, branch: Option<String>) {
203 self.current_branch = branch;
204 self.updated_at = Utc::now();
205 }
206
207 pub fn add_commit(&mut self, commit_hash: String) {
209 if !self.commit_hashes.contains(&commit_hash) {
210 self.commit_hashes.push(commit_hash);
211 self.total_commits = self.commit_hashes.len();
212 self.updated_at = Utc::now();
213 }
214 }
215
216 pub fn remove_commit(&mut self, commit_hash: &str) {
218 if let Some(pos) = self.commit_hashes.iter().position(|h| h == commit_hash) {
219 self.commit_hashes.remove(pos);
220 self.total_commits = self.commit_hashes.len();
221 self.updated_at = Utc::now();
222 }
223 }
224
225 pub fn set_conflicts(&mut self, has_conflicts: bool) {
227 self.has_conflicts = has_conflicts;
228 self.updated_at = Utc::now();
229 }
230
231 pub fn set_up_to_date(&mut self, is_up_to_date: bool) {
233 self.is_up_to_date = is_up_to_date;
234 if is_up_to_date {
235 self.last_sync = Some(Utc::now());
236 }
237 self.updated_at = Utc::now();
238 }
239
240 pub fn completion_percentage(&self) -> f64 {
242 if self.total_commits == 0 {
243 0.0
244 } else {
245 (self.submitted_commits as f64 / self.total_commits as f64) * 100.0
246 }
247 }
248
249 pub fn merge_percentage(&self) -> f64 {
251 if self.total_commits == 0 {
252 0.0
253 } else {
254 (self.merged_commits as f64 / self.total_commits as f64) * 100.0
255 }
256 }
257
258 pub fn is_complete(&self) -> bool {
260 self.total_commits > 0 && self.submitted_commits == self.total_commits
261 }
262
263 pub fn is_fully_merged(&self) -> bool {
265 self.total_commits > 0 && self.merged_commits == self.total_commits
266 }
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct EditModeState {
272 pub is_active: bool,
273 pub target_entry_id: Option<Uuid>,
274 pub target_stack_id: Option<Uuid>,
275 pub original_commit_hash: String,
276 pub started_at: DateTime<Utc>,
277}
278
279impl EditModeState {
280 pub fn new(stack_id: Uuid, entry_id: Uuid, commit_hash: String) -> Self {
282 Self {
283 is_active: true,
284 target_entry_id: Some(entry_id),
285 target_stack_id: Some(stack_id),
286 original_commit_hash: commit_hash,
287 started_at: Utc::now(),
288 }
289 }
290
291 pub fn clear() -> Self {
293 Self {
294 is_active: false,
295 target_entry_id: None,
296 target_stack_id: None,
297 original_commit_hash: String::new(),
298 started_at: Utc::now(),
299 }
300 }
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct RepositoryMetadata {
306 pub stacks: HashMap<Uuid, StackMetadata>,
308 pub commits: HashMap<String, CommitMetadata>,
310 pub active_stack_id: Option<Uuid>,
312 pub default_base_branch: String,
314 pub edit_mode: Option<EditModeState>,
316 pub updated_at: DateTime<Utc>,
318}
319
320impl RepositoryMetadata {
321 pub fn new(default_base_branch: String) -> Self {
323 Self {
324 stacks: HashMap::new(),
325 commits: HashMap::new(),
326 active_stack_id: None,
327 default_base_branch,
328 edit_mode: None,
329 updated_at: Utc::now(),
330 }
331 }
332
333 pub fn add_stack(&mut self, stack_metadata: StackMetadata) {
335 self.stacks.insert(stack_metadata.stack_id, stack_metadata);
336 self.updated_at = Utc::now();
337 }
338
339 pub fn remove_stack(&mut self, stack_id: &Uuid) -> Option<StackMetadata> {
341 let removed = self.stacks.remove(stack_id);
342 if removed.is_some() {
343 self.updated_at = Utc::now();
344 }
345 removed
346 }
347
348 pub fn get_stack(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
350 self.stacks.get(stack_id)
351 }
352
353 pub fn get_stack_mut(&mut self, stack_id: &Uuid) -> Option<&mut StackMetadata> {
355 self.stacks.get_mut(stack_id)
356 }
357
358 pub fn set_active_stack(&mut self, stack_id: Option<Uuid>) {
360 self.active_stack_id = stack_id;
361 self.updated_at = Utc::now();
362 }
363
364 pub fn get_active_stack(&self) -> Option<&StackMetadata> {
366 self.active_stack_id.and_then(|id| self.stacks.get(&id))
367 }
368
369 pub fn add_commit(&mut self, commit_metadata: CommitMetadata) {
371 self.commits
372 .insert(commit_metadata.hash.clone(), commit_metadata);
373 self.updated_at = Utc::now();
374 }
375
376 pub fn remove_commit(&mut self, commit_hash: &str) -> Option<CommitMetadata> {
378 let removed = self.commits.remove(commit_hash);
379 if removed.is_some() {
380 self.updated_at = Utc::now();
381 }
382 removed
383 }
384
385 pub fn get_commit(&self, commit_hash: &str) -> Option<&CommitMetadata> {
387 self.commits.get(commit_hash)
388 }
389
390 pub fn get_all_stacks(&self) -> Vec<&StackMetadata> {
392 self.stacks.values().collect()
393 }
394
395 pub fn get_stack_commits(&self, stack_id: &Uuid) -> Vec<&CommitMetadata> {
397 self.commits
398 .values()
399 .filter(|commit| &commit.stack_id == stack_id)
400 .collect()
401 }
402
403 pub fn find_stack_by_name(&self, name: &str) -> Option<&StackMetadata> {
405 self.stacks.values().find(|stack| stack.name == name)
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_commit_metadata() {
415 let stack_id = Uuid::new_v4();
416 let entry_id = Uuid::new_v4();
417
418 let mut commit = CommitMetadata::new(
419 "abc123".to_string(),
420 "Test commit".to_string(),
421 entry_id,
422 stack_id,
423 "feature-branch".to_string(),
424 "main".to_string(),
425 );
426
427 assert_eq!(commit.hash, "abc123");
428 assert_eq!(commit.message, "Test commit");
429 assert_eq!(commit.short_hash(), "abc123");
430 assert!(!commit.is_pushed);
431 assert!(!commit.is_submitted);
432 assert!(!commit.is_merged);
433
434 commit.add_dependency("def456".to_string());
435 assert_eq!(commit.dependencies, vec!["def456"]);
436
437 commit.mark_pushed();
438 assert!(commit.is_pushed);
439
440 commit.mark_submitted("PR-123".to_string());
441 assert!(commit.is_submitted);
442 assert_eq!(commit.pull_request_id, Some("PR-123".to_string()));
443
444 commit.mark_merged(true);
445 assert!(commit.is_merged);
446 }
447
448 #[test]
449 fn test_stack_metadata() {
450 let stack_id = Uuid::new_v4();
451 let mut stack = StackMetadata::new(
452 stack_id,
453 "test-stack".to_string(),
454 "main".to_string(),
455 Some("Test stack".to_string()),
456 );
457
458 assert_eq!(stack.name, "test-stack");
459 assert_eq!(stack.base_branch, "main");
460 assert_eq!(stack.total_commits, 0);
461 assert_eq!(stack.completion_percentage(), 0.0);
462
463 stack.add_branch("feature-1".to_string());
464 stack.add_commit("abc123".to_string());
465 stack.update_stats(2, 1, 0);
466
467 assert_eq!(stack.branches, vec!["feature-1"]);
468 assert_eq!(stack.total_commits, 2);
469 assert_eq!(stack.submitted_commits, 1);
470 assert_eq!(stack.completion_percentage(), 50.0);
471 assert!(!stack.is_complete());
472 assert!(!stack.is_fully_merged());
473
474 stack.update_stats(2, 2, 2);
475 assert!(stack.is_complete());
476 assert!(stack.is_fully_merged());
477 }
478
479 #[test]
480 fn test_repository_metadata() {
481 let mut repo = RepositoryMetadata::new("main".to_string());
482
483 let stack_id = Uuid::new_v4();
484 let stack =
485 StackMetadata::new(stack_id, "test-stack".to_string(), "main".to_string(), None);
486
487 assert!(repo.get_active_stack().is_none());
488 assert_eq!(repo.get_all_stacks().len(), 0);
489
490 repo.add_stack(stack);
491 assert_eq!(repo.get_all_stacks().len(), 1);
492 assert!(repo.get_stack(&stack_id).is_some());
493
494 repo.set_active_stack(Some(stack_id));
495 assert!(repo.get_active_stack().is_some());
496 assert_eq!(repo.get_active_stack().unwrap().stack_id, stack_id);
497
498 let found = repo.find_stack_by_name("test-stack");
499 assert!(found.is_some());
500 assert_eq!(found.unwrap().stack_id, stack_id);
501 }
502}