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 pub pull_request_id: Option<String>,
31 pub created_at: DateTime<Utc>,
33 pub updated_at: DateTime<Utc>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct StackMetadata {
40 pub stack_id: Uuid,
42 pub name: String,
44 pub description: Option<String>,
46 pub base_branch: String,
48 pub current_branch: Option<String>,
50 pub total_commits: usize,
52 pub submitted_commits: usize,
54 pub merged_commits: usize,
56 pub branches: Vec<String>,
58 pub commit_hashes: Vec<String>,
60 pub has_conflicts: bool,
62 pub is_up_to_date: bool,
64 pub last_sync: Option<DateTime<Utc>>,
66 pub created_at: DateTime<Utc>,
68 pub updated_at: DateTime<Utc>,
70}
71
72impl CommitMetadata {
73 pub fn new(
75 hash: String,
76 message: String,
77 stack_entry_id: Uuid,
78 stack_id: Uuid,
79 branch: String,
80 source_branch: String,
81 ) -> Self {
82 let now = Utc::now();
83 Self {
84 hash,
85 message,
86 stack_entry_id,
87 stack_id,
88 branch,
89 source_branch,
90 dependencies: Vec::new(),
91 dependents: Vec::new(),
92 is_pushed: false,
93 is_submitted: false,
94 pull_request_id: None,
95 created_at: now,
96 updated_at: now,
97 }
98 }
99
100 pub fn add_dependency(&mut self, commit_hash: String) {
102 if !self.dependencies.contains(&commit_hash) {
103 self.dependencies.push(commit_hash);
104 self.updated_at = Utc::now();
105 }
106 }
107
108 pub fn add_dependent(&mut self, commit_hash: String) {
110 if !self.dependents.contains(&commit_hash) {
111 self.dependents.push(commit_hash);
112 self.updated_at = Utc::now();
113 }
114 }
115
116 pub fn mark_pushed(&mut self) {
118 self.is_pushed = true;
119 self.updated_at = Utc::now();
120 }
121
122 pub fn mark_submitted(&mut self, pull_request_id: String) {
124 self.is_submitted = true;
125 self.pull_request_id = Some(pull_request_id);
126 self.updated_at = Utc::now();
127 }
128
129 pub fn short_hash(&self) -> String {
131 if self.hash.len() >= 8 {
132 self.hash[..8].to_string()
133 } else {
134 self.hash.clone()
135 }
136 }
137}
138
139impl StackMetadata {
140 pub fn new(
142 stack_id: Uuid,
143 name: String,
144 base_branch: String,
145 description: Option<String>,
146 ) -> Self {
147 let now = Utc::now();
148 Self {
149 stack_id,
150 name,
151 description,
152 base_branch,
153 current_branch: None,
154 total_commits: 0,
155 submitted_commits: 0,
156 merged_commits: 0,
157 branches: Vec::new(),
158 commit_hashes: Vec::new(),
159 has_conflicts: false,
160 is_up_to_date: true,
161 last_sync: None,
162 created_at: now,
163 updated_at: now,
164 }
165 }
166
167 pub fn update_stats(&mut self, total: usize, submitted: usize, merged: usize) {
169 self.total_commits = total;
170 self.submitted_commits = submitted;
171 self.merged_commits = merged;
172 self.updated_at = Utc::now();
173 }
174
175 pub fn add_branch(&mut self, branch: String) {
177 if !self.branches.contains(&branch) {
178 self.branches.push(branch);
179 self.updated_at = Utc::now();
180 }
181 }
182
183 pub fn remove_branch(&mut self, branch: &str) {
185 if let Some(pos) = self.branches.iter().position(|b| b == branch) {
186 self.branches.remove(pos);
187 self.updated_at = Utc::now();
188 }
189 }
190
191 pub fn set_current_branch(&mut self, branch: Option<String>) {
193 self.current_branch = branch;
194 self.updated_at = Utc::now();
195 }
196
197 pub fn add_commit(&mut self, commit_hash: String) {
199 if !self.commit_hashes.contains(&commit_hash) {
200 self.commit_hashes.push(commit_hash);
201 self.total_commits = self.commit_hashes.len();
202 self.updated_at = Utc::now();
203 }
204 }
205
206 pub fn remove_commit(&mut self, commit_hash: &str) {
208 if let Some(pos) = self.commit_hashes.iter().position(|h| h == commit_hash) {
209 self.commit_hashes.remove(pos);
210 self.total_commits = self.commit_hashes.len();
211 self.updated_at = Utc::now();
212 }
213 }
214
215 pub fn set_conflicts(&mut self, has_conflicts: bool) {
217 self.has_conflicts = has_conflicts;
218 self.updated_at = Utc::now();
219 }
220
221 pub fn set_up_to_date(&mut self, is_up_to_date: bool) {
223 self.is_up_to_date = is_up_to_date;
224 if is_up_to_date {
225 self.last_sync = Some(Utc::now());
226 }
227 self.updated_at = Utc::now();
228 }
229
230 pub fn completion_percentage(&self) -> f64 {
232 if self.total_commits == 0 {
233 0.0
234 } else {
235 (self.submitted_commits as f64 / self.total_commits as f64) * 100.0
236 }
237 }
238
239 pub fn merge_percentage(&self) -> f64 {
241 if self.total_commits == 0 {
242 0.0
243 } else {
244 (self.merged_commits as f64 / self.total_commits as f64) * 100.0
245 }
246 }
247
248 pub fn is_complete(&self) -> bool {
250 self.total_commits > 0 && self.submitted_commits == self.total_commits
251 }
252
253 pub fn is_fully_merged(&self) -> bool {
255 self.total_commits > 0 && self.merged_commits == self.total_commits
256 }
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct EditModeState {
262 pub is_active: bool,
263 pub target_entry_id: Option<Uuid>,
264 pub target_stack_id: Option<Uuid>,
265 pub original_commit_hash: String,
266 pub started_at: DateTime<Utc>,
267}
268
269impl EditModeState {
270 pub fn new(stack_id: Uuid, entry_id: Uuid, commit_hash: String) -> Self {
272 Self {
273 is_active: true,
274 target_entry_id: Some(entry_id),
275 target_stack_id: Some(stack_id),
276 original_commit_hash: commit_hash,
277 started_at: Utc::now(),
278 }
279 }
280
281 pub fn clear() -> Self {
283 Self {
284 is_active: false,
285 target_entry_id: None,
286 target_stack_id: None,
287 original_commit_hash: String::new(),
288 started_at: Utc::now(),
289 }
290 }
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct RepositoryMetadata {
296 pub stacks: HashMap<Uuid, StackMetadata>,
298 pub commits: HashMap<String, CommitMetadata>,
300 pub active_stack_id: Option<Uuid>,
302 pub default_base_branch: String,
304 pub edit_mode: Option<EditModeState>,
306 pub updated_at: DateTime<Utc>,
308}
309
310impl RepositoryMetadata {
311 pub fn new(default_base_branch: String) -> Self {
313 Self {
314 stacks: HashMap::new(),
315 commits: HashMap::new(),
316 active_stack_id: None,
317 default_base_branch,
318 edit_mode: None,
319 updated_at: Utc::now(),
320 }
321 }
322
323 pub fn add_stack(&mut self, stack_metadata: StackMetadata) {
325 self.stacks.insert(stack_metadata.stack_id, stack_metadata);
326 self.updated_at = Utc::now();
327 }
328
329 pub fn remove_stack(&mut self, stack_id: &Uuid) -> Option<StackMetadata> {
331 let removed = self.stacks.remove(stack_id);
332 if removed.is_some() {
333 if self.active_stack_id == Some(*stack_id) {
335 self.active_stack_id = None;
336 }
337 self.updated_at = Utc::now();
338 }
339 removed
340 }
341
342 pub fn get_stack(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
344 self.stacks.get(stack_id)
345 }
346
347 pub fn get_stack_mut(&mut self, stack_id: &Uuid) -> Option<&mut StackMetadata> {
349 self.stacks.get_mut(stack_id)
350 }
351
352 pub fn set_active_stack(&mut self, stack_id: Option<Uuid>) {
354 self.active_stack_id = stack_id;
355 self.updated_at = Utc::now();
356 }
357
358 pub fn get_active_stack(&self) -> Option<&StackMetadata> {
360 self.active_stack_id.and_then(|id| self.stacks.get(&id))
361 }
362
363 pub fn add_commit(&mut self, commit_metadata: CommitMetadata) {
365 self.commits
366 .insert(commit_metadata.hash.clone(), commit_metadata);
367 self.updated_at = Utc::now();
368 }
369
370 pub fn remove_commit(&mut self, commit_hash: &str) -> Option<CommitMetadata> {
372 let removed = self.commits.remove(commit_hash);
373 if removed.is_some() {
374 self.updated_at = Utc::now();
375 }
376 removed
377 }
378
379 pub fn get_commit(&self, commit_hash: &str) -> Option<&CommitMetadata> {
381 self.commits.get(commit_hash)
382 }
383
384 pub fn get_all_stacks(&self) -> Vec<&StackMetadata> {
386 self.stacks.values().collect()
387 }
388
389 pub fn get_stack_commits(&self, stack_id: &Uuid) -> Vec<&CommitMetadata> {
391 self.commits
392 .values()
393 .filter(|commit| &commit.stack_id == stack_id)
394 .collect()
395 }
396
397 pub fn find_stack_by_name(&self, name: &str) -> Option<&StackMetadata> {
399 self.stacks.values().find(|stack| stack.name == name)
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_commit_metadata() {
409 let stack_id = Uuid::new_v4();
410 let entry_id = Uuid::new_v4();
411
412 let mut commit = CommitMetadata::new(
413 "abc123".to_string(),
414 "Test commit".to_string(),
415 entry_id,
416 stack_id,
417 "feature-branch".to_string(),
418 "main".to_string(),
419 );
420
421 assert_eq!(commit.hash, "abc123");
422 assert_eq!(commit.message, "Test commit");
423 assert_eq!(commit.short_hash(), "abc123");
424 assert!(!commit.is_pushed);
425 assert!(!commit.is_submitted);
426
427 commit.add_dependency("def456".to_string());
428 assert_eq!(commit.dependencies, vec!["def456"]);
429
430 commit.mark_pushed();
431 assert!(commit.is_pushed);
432
433 commit.mark_submitted("PR-123".to_string());
434 assert!(commit.is_submitted);
435 assert_eq!(commit.pull_request_id, Some("PR-123".to_string()));
436 }
437
438 #[test]
439 fn test_stack_metadata() {
440 let stack_id = Uuid::new_v4();
441 let mut stack = StackMetadata::new(
442 stack_id,
443 "test-stack".to_string(),
444 "main".to_string(),
445 Some("Test stack".to_string()),
446 );
447
448 assert_eq!(stack.name, "test-stack");
449 assert_eq!(stack.base_branch, "main");
450 assert_eq!(stack.total_commits, 0);
451 assert_eq!(stack.completion_percentage(), 0.0);
452
453 stack.add_branch("feature-1".to_string());
454 stack.add_commit("abc123".to_string());
455 stack.update_stats(2, 1, 0);
456
457 assert_eq!(stack.branches, vec!["feature-1"]);
458 assert_eq!(stack.total_commits, 2);
459 assert_eq!(stack.submitted_commits, 1);
460 assert_eq!(stack.completion_percentage(), 50.0);
461 assert!(!stack.is_complete());
462 assert!(!stack.is_fully_merged());
463
464 stack.update_stats(2, 2, 2);
465 assert!(stack.is_complete());
466 assert!(stack.is_fully_merged());
467 }
468
469 #[test]
470 fn test_repository_metadata() {
471 let mut repo = RepositoryMetadata::new("main".to_string());
472
473 let stack_id = Uuid::new_v4();
474 let stack =
475 StackMetadata::new(stack_id, "test-stack".to_string(), "main".to_string(), None);
476
477 assert!(repo.get_active_stack().is_none());
478 assert_eq!(repo.get_all_stacks().len(), 0);
479
480 repo.add_stack(stack);
481 assert_eq!(repo.get_all_stacks().len(), 1);
482 assert!(repo.get_stack(&stack_id).is_some());
483
484 repo.set_active_stack(Some(stack_id));
485 assert!(repo.get_active_stack().is_some());
486 assert_eq!(repo.get_active_stack().unwrap().stack_id, stack_id);
487
488 let found = repo.find_stack_by_name("test-stack");
489 assert!(found.is_some());
490 assert_eq!(found.unwrap().stack_id, stack_id);
491 }
492}