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 if self.active_stack_id == Some(*stack_id) {
345 self.active_stack_id = None;
346 }
347 self.updated_at = Utc::now();
348 }
349 removed
350 }
351
352 pub fn get_stack(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
354 self.stacks.get(stack_id)
355 }
356
357 pub fn get_stack_mut(&mut self, stack_id: &Uuid) -> Option<&mut StackMetadata> {
359 self.stacks.get_mut(stack_id)
360 }
361
362 pub fn set_active_stack(&mut self, stack_id: Option<Uuid>) {
364 self.active_stack_id = stack_id;
365 self.updated_at = Utc::now();
366 }
367
368 pub fn get_active_stack(&self) -> Option<&StackMetadata> {
370 self.active_stack_id.and_then(|id| self.stacks.get(&id))
371 }
372
373 pub fn add_commit(&mut self, commit_metadata: CommitMetadata) {
375 self.commits
376 .insert(commit_metadata.hash.clone(), commit_metadata);
377 self.updated_at = Utc::now();
378 }
379
380 pub fn remove_commit(&mut self, commit_hash: &str) -> Option<CommitMetadata> {
382 let removed = self.commits.remove(commit_hash);
383 if removed.is_some() {
384 self.updated_at = Utc::now();
385 }
386 removed
387 }
388
389 pub fn get_commit(&self, commit_hash: &str) -> Option<&CommitMetadata> {
391 self.commits.get(commit_hash)
392 }
393
394 pub fn get_all_stacks(&self) -> Vec<&StackMetadata> {
396 self.stacks.values().collect()
397 }
398
399 pub fn get_stack_commits(&self, stack_id: &Uuid) -> Vec<&CommitMetadata> {
401 self.commits
402 .values()
403 .filter(|commit| &commit.stack_id == stack_id)
404 .collect()
405 }
406
407 pub fn find_stack_by_name(&self, name: &str) -> Option<&StackMetadata> {
409 self.stacks.values().find(|stack| stack.name == name)
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn test_commit_metadata() {
419 let stack_id = Uuid::new_v4();
420 let entry_id = Uuid::new_v4();
421
422 let mut commit = CommitMetadata::new(
423 "abc123".to_string(),
424 "Test commit".to_string(),
425 entry_id,
426 stack_id,
427 "feature-branch".to_string(),
428 "main".to_string(),
429 );
430
431 assert_eq!(commit.hash, "abc123");
432 assert_eq!(commit.message, "Test commit");
433 assert_eq!(commit.short_hash(), "abc123");
434 assert!(!commit.is_pushed);
435 assert!(!commit.is_submitted);
436 assert!(!commit.is_merged);
437
438 commit.add_dependency("def456".to_string());
439 assert_eq!(commit.dependencies, vec!["def456"]);
440
441 commit.mark_pushed();
442 assert!(commit.is_pushed);
443
444 commit.mark_submitted("PR-123".to_string());
445 assert!(commit.is_submitted);
446 assert_eq!(commit.pull_request_id, Some("PR-123".to_string()));
447
448 commit.mark_merged(true);
449 assert!(commit.is_merged);
450 }
451
452 #[test]
453 fn test_stack_metadata() {
454 let stack_id = Uuid::new_v4();
455 let mut stack = StackMetadata::new(
456 stack_id,
457 "test-stack".to_string(),
458 "main".to_string(),
459 Some("Test stack".to_string()),
460 );
461
462 assert_eq!(stack.name, "test-stack");
463 assert_eq!(stack.base_branch, "main");
464 assert_eq!(stack.total_commits, 0);
465 assert_eq!(stack.completion_percentage(), 0.0);
466
467 stack.add_branch("feature-1".to_string());
468 stack.add_commit("abc123".to_string());
469 stack.update_stats(2, 1, 0);
470
471 assert_eq!(stack.branches, vec!["feature-1"]);
472 assert_eq!(stack.total_commits, 2);
473 assert_eq!(stack.submitted_commits, 1);
474 assert_eq!(stack.completion_percentage(), 50.0);
475 assert!(!stack.is_complete());
476 assert!(!stack.is_fully_merged());
477
478 stack.update_stats(2, 2, 2);
479 assert!(stack.is_complete());
480 assert!(stack.is_fully_merged());
481 }
482
483 #[test]
484 fn test_repository_metadata() {
485 let mut repo = RepositoryMetadata::new("main".to_string());
486
487 let stack_id = Uuid::new_v4();
488 let stack =
489 StackMetadata::new(stack_id, "test-stack".to_string(), "main".to_string(), None);
490
491 assert!(repo.get_active_stack().is_none());
492 assert_eq!(repo.get_all_stacks().len(), 0);
493
494 repo.add_stack(stack);
495 assert_eq!(repo.get_all_stacks().len(), 1);
496 assert!(repo.get_stack(&stack_id).is_some());
497
498 repo.set_active_stack(Some(stack_id));
499 assert!(repo.get_active_stack().is_some());
500 assert_eq!(repo.get_active_stack().unwrap().stack_id, stack_id);
501
502 let found = repo.find_stack_by_name("test-stack");
503 assert!(found.is_some());
504 assert_eq!(found.unwrap().stack_id, stack_id);
505 }
506}