1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use std::collections::HashMap;
10use std::fs;
11use std::io::Write;
12use std::path::Path;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Commit {
17 pub id: String,
18 pub parent_id: Option<String>,
19 pub author: String,
20 pub email: String,
21 pub message: String,
22 pub timestamp: DateTime<Utc>,
23 pub content_hash: String,
24 pub metadata: HashMap<String, String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Branch {
30 pub name: String,
31 pub head_commit_id: String,
32 pub created_at: DateTime<Utc>,
33 pub created_by: String,
34 pub protected: bool,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Diff {
40 pub from_commit: String,
41 pub to_commit: String,
42 pub changes: Vec<DiffChange>,
43 pub stats: DiffStats,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct DiffChange {
49 pub path: String,
50 pub change_type: DiffChangeType,
51 pub old_value: Option<serde_json::Value>,
52 pub new_value: Option<serde_json::Value>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(rename_all = "lowercase")]
58pub enum DiffChangeType {
59 Added,
60 Modified,
61 Deleted,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct DiffStats {
67 pub additions: usize,
68 pub deletions: usize,
69 pub modifications: usize,
70}
71
72#[derive(Debug)]
74pub struct VersionControlRepository {
75 orchestration_id: String,
76 storage_path: String,
77 branches: HashMap<String, Branch>,
78 commits: HashMap<String, Commit>,
79 current_branch: String,
80}
81
82impl VersionControlRepository {
83 pub fn new(orchestration_id: String, storage_path: String) -> Result<Self, String> {
85 fs::create_dir_all(&storage_path).map_err(|e| e.to_string())?;
87
88 let mut repo = Self {
89 orchestration_id,
90 storage_path: storage_path.clone(),
91 branches: HashMap::new(),
92 commits: HashMap::new(),
93 current_branch: "main".to_string(),
94 };
95
96 if repo.branches.is_empty() {
98 let initial_commit = Commit {
99 id: Self::generate_commit_id("initial", ""),
100 parent_id: None,
101 author: "System".to_string(),
102 email: "system@mockforge".to_string(),
103 message: "Initial commit".to_string(),
104 timestamp: Utc::now(),
105 content_hash: "".to_string(),
106 metadata: HashMap::new(),
107 };
108
109 let main_branch = Branch {
110 name: "main".to_string(),
111 head_commit_id: initial_commit.id.clone(),
112 created_at: Utc::now(),
113 created_by: "System".to_string(),
114 protected: true,
115 };
116
117 repo.commits.insert(initial_commit.id.clone(), initial_commit);
118 repo.branches.insert("main".to_string(), main_branch);
119 }
120
121 repo.save()?;
123
124 Ok(repo)
125 }
126
127 pub fn load(orchestration_id: String, storage_path: String) -> Result<Self, String> {
129 let repo_file = Path::new(&storage_path).join("repository.json");
130
131 if !repo_file.exists() {
132 return Self::new(orchestration_id, storage_path);
133 }
134
135 let content = fs::read_to_string(&repo_file).map_err(|e| e.to_string())?;
136 let repo: Self = serde_json::from_str(&content).map_err(|e| e.to_string())?;
137
138 Ok(repo)
139 }
140
141 fn save(&self) -> Result<(), String> {
143 let repo_file = Path::new(&self.storage_path).join("repository.json");
144 let content = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
145 let mut file = fs::File::create(repo_file).map_err(|e| e.to_string())?;
146 file.write_all(content.as_bytes()).map_err(|e| e.to_string())?;
147 Ok(())
148 }
149
150 pub fn commit(
152 &mut self,
153 author: String,
154 email: String,
155 message: String,
156 content: &serde_json::Value,
157 ) -> Result<Commit, String> {
158 let content_hash = Self::hash_content(content);
159 let parent_id = self.get_current_head()?;
160
161 let commit = Commit {
162 id: Self::generate_commit_id(&author, &message),
163 parent_id: Some(parent_id),
164 author,
165 email,
166 message,
167 timestamp: Utc::now(),
168 content_hash: content_hash.clone(),
169 metadata: HashMap::new(),
170 };
171
172 let content_file = Path::new(&self.storage_path)
174 .join("contents")
175 .join(format!("{}.json", content_hash));
176
177 let parent =
178 content_file.parent().ok_or_else(|| "Invalid content file path".to_string())?;
179 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
180 let content_str = serde_json::to_string_pretty(content).map_err(|e| e.to_string())?;
181 let mut file = fs::File::create(content_file).map_err(|e| e.to_string())?;
182 file.write_all(content_str.as_bytes()).map_err(|e| e.to_string())?;
183
184 if let Some(branch) = self.branches.get_mut(&self.current_branch) {
186 branch.head_commit_id = commit.id.clone();
187 }
188
189 self.commits.insert(commit.id.clone(), commit.clone());
190 self.save()?;
191
192 Ok(commit)
193 }
194
195 pub fn create_branch(
197 &mut self,
198 name: String,
199 from_commit: Option<String>,
200 ) -> Result<Branch, String> {
201 if self.branches.contains_key(&name) {
202 return Err(format!("Branch '{}' already exists", name));
203 }
204
205 let head_commit_id = match from_commit {
206 Some(commit_id) => commit_id,
207 None => self.get_current_head()?,
208 };
209
210 let branch = Branch {
211 name: name.clone(),
212 head_commit_id,
213 created_at: Utc::now(),
214 created_by: "user".to_string(),
215 protected: false,
216 };
217
218 self.branches.insert(name, branch.clone());
219 self.save()?;
220
221 Ok(branch)
222 }
223
224 pub fn checkout(&mut self, branch_name: String) -> Result<(), String> {
226 if !self.branches.contains_key(&branch_name) {
227 return Err(format!("Branch '{}' does not exist", branch_name));
228 }
229
230 self.current_branch = branch_name;
231 self.save()?;
232
233 Ok(())
234 }
235
236 pub fn diff(&self, from_commit: String, to_commit: String) -> Result<Diff, String> {
238 let from_content = self.get_commit_content(&from_commit)?;
239 let to_content = self.get_commit_content(&to_commit)?;
240
241 let changes = Self::compute_diff(&from_content, &to_content, "");
242
243 let stats = DiffStats {
244 additions: changes.iter().filter(|c| c.change_type == DiffChangeType::Added).count(),
245 deletions: changes.iter().filter(|c| c.change_type == DiffChangeType::Deleted).count(),
246 modifications: changes
247 .iter()
248 .filter(|c| c.change_type == DiffChangeType::Modified)
249 .count(),
250 };
251
252 Ok(Diff {
253 from_commit,
254 to_commit,
255 changes,
256 stats,
257 })
258 }
259
260 pub fn history(&self, max_count: Option<usize>) -> Result<Vec<Commit>, String> {
262 let mut commits = Vec::new();
263 let mut current_id = Some(self.get_current_head()?);
264
265 let limit = max_count.unwrap_or(usize::MAX);
266
267 while let Some(id) = current_id {
268 if commits.len() >= limit {
269 break;
270 }
271
272 if let Some(commit) = self.commits.get(&id) {
273 commits.push(commit.clone());
274 current_id = commit.parent_id.clone();
275 } else {
276 break;
277 }
278 }
279
280 Ok(commits)
281 }
282
283 pub fn get_commit_content(&self, commit_id: &str) -> Result<serde_json::Value, String> {
285 let commit = self
286 .commits
287 .get(commit_id)
288 .ok_or_else(|| format!("Commit '{}' not found", commit_id))?;
289
290 let content_file = Path::new(&self.storage_path)
291 .join("contents")
292 .join(format!("{}.json", commit.content_hash));
293
294 let content = fs::read_to_string(&content_file).map_err(|e| e.to_string())?;
295 serde_json::from_str(&content).map_err(|e| e.to_string())
296 }
297
298 fn get_current_head(&self) -> Result<String, String> {
300 self.branches
301 .get(&self.current_branch)
302 .map(|b| b.head_commit_id.clone())
303 .ok_or_else(|| "Current branch not found".to_string())
304 }
305
306 fn generate_commit_id(author: &str, message: &str) -> String {
308 let data = format!("{}{}{}", author, message, Utc::now().timestamp_millis());
309 let mut hasher = Sha256::new();
310 hasher.update(data.as_bytes());
311 format!("{:x}", hasher.finalize())[..16].to_string()
312 }
313
314 fn hash_content(content: &serde_json::Value) -> String {
316 let content_str = serde_json::to_string(content).unwrap();
317 let mut hasher = Sha256::new();
318 hasher.update(content_str.as_bytes());
319 format!("{:x}", hasher.finalize())[..16].to_string()
320 }
321
322 fn compute_diff(
324 from: &serde_json::Value,
325 to: &serde_json::Value,
326 path: &str,
327 ) -> Vec<DiffChange> {
328 let mut changes = Vec::new();
329
330 match (from, to) {
331 (serde_json::Value::Object(from_obj), serde_json::Value::Object(to_obj)) => {
332 for (key, to_value) in to_obj {
334 let new_path = if path.is_empty() {
335 key.clone()
336 } else {
337 format!("{}.{}", path, key)
338 };
339
340 if let Some(from_value) = from_obj.get(key) {
341 if from_value != to_value {
342 if from_value.is_object() && to_value.is_object() {
343 changes.extend(Self::compute_diff(from_value, to_value, &new_path));
344 } else {
345 changes.push(DiffChange {
346 path: new_path,
347 change_type: DiffChangeType::Modified,
348 old_value: Some(from_value.clone()),
349 new_value: Some(to_value.clone()),
350 });
351 }
352 }
353 } else {
354 changes.push(DiffChange {
355 path: new_path,
356 change_type: DiffChangeType::Added,
357 old_value: None,
358 new_value: Some(to_value.clone()),
359 });
360 }
361 }
362
363 for (key, from_value) in from_obj {
365 if !to_obj.contains_key(key) {
366 let new_path = if path.is_empty() {
367 key.clone()
368 } else {
369 format!("{}.{}", path, key)
370 };
371
372 changes.push(DiffChange {
373 path: new_path,
374 change_type: DiffChangeType::Deleted,
375 old_value: Some(from_value.clone()),
376 new_value: None,
377 });
378 }
379 }
380 }
381 _ => {
382 if from != to {
383 changes.push(DiffChange {
384 path: path.to_string(),
385 change_type: DiffChangeType::Modified,
386 old_value: Some(from.clone()),
387 new_value: Some(to.clone()),
388 });
389 }
390 }
391 }
392
393 changes
394 }
395
396 pub fn list_branches(&self) -> Vec<Branch> {
398 self.branches.values().cloned().collect()
399 }
400
401 pub fn current_branch(&self) -> &str {
403 &self.current_branch
404 }
405}
406
407impl Serialize for VersionControlRepository {
408 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
409 where
410 S: serde::Serializer,
411 {
412 use serde::ser::SerializeStruct;
413 let mut state = serializer.serialize_struct("VersionControlRepository", 5)?;
414 state.serialize_field("orchestration_id", &self.orchestration_id)?;
415 state.serialize_field("storage_path", &self.storage_path)?;
416 state.serialize_field("branches", &self.branches)?;
417 state.serialize_field("commits", &self.commits)?;
418 state.serialize_field("current_branch", &self.current_branch)?;
419 state.end()
420 }
421}
422
423impl<'de> Deserialize<'de> for VersionControlRepository {
424 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
425 where
426 D: serde::Deserializer<'de>,
427 {
428 #[derive(Deserialize)]
429 struct RepoData {
430 orchestration_id: String,
431 storage_path: String,
432 branches: HashMap<String, Branch>,
433 commits: HashMap<String, Commit>,
434 current_branch: String,
435 }
436
437 let data = RepoData::deserialize(deserializer)?;
438
439 Ok(VersionControlRepository {
440 orchestration_id: data.orchestration_id,
441 storage_path: data.storage_path,
442 branches: data.branches,
443 commits: data.commits,
444 current_branch: data.current_branch,
445 })
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use tempfile::tempdir;
453
454 #[test]
455 fn test_repository_creation() {
456 let temp_dir = tempdir().unwrap();
457 let repo = VersionControlRepository::new(
458 "test-orch".to_string(),
459 temp_dir.path().to_str().unwrap().to_string(),
460 )
461 .unwrap();
462
463 assert_eq!(repo.current_branch(), "main");
464 assert_eq!(repo.list_branches().len(), 1);
465 }
466
467 #[test]
468 fn test_commit() {
469 let temp_dir = tempdir().unwrap();
470 let mut repo = VersionControlRepository::new(
471 "test-orch".to_string(),
472 temp_dir.path().to_str().unwrap().to_string(),
473 )
474 .unwrap();
475
476 let content = serde_json::json!({
477 "name": "Test Orchestration",
478 "steps": []
479 });
480
481 let commit = repo
482 .commit(
483 "Test User".to_string(),
484 "test@example.com".to_string(),
485 "Initial orchestration".to_string(),
486 &content,
487 )
488 .unwrap();
489
490 assert_eq!(commit.author, "Test User");
491 assert_eq!(repo.history(None).unwrap().len(), 2); }
493
494 #[test]
495 fn test_branching() {
496 let temp_dir = tempdir().unwrap();
497 let mut repo = VersionControlRepository::new(
498 "test-orch".to_string(),
499 temp_dir.path().to_str().unwrap().to_string(),
500 )
501 .unwrap();
502
503 repo.create_branch("feature-1".to_string(), None).unwrap();
504 assert_eq!(repo.list_branches().len(), 2);
505
506 repo.checkout("feature-1".to_string()).unwrap();
507 assert_eq!(repo.current_branch(), "feature-1");
508 }
509
510 #[test]
511 fn test_diff() {
512 let temp_dir = tempdir().unwrap();
513 let mut repo = VersionControlRepository::new(
514 "test-orch".to_string(),
515 temp_dir.path().to_str().unwrap().to_string(),
516 )
517 .unwrap();
518
519 let content1 = serde_json::json!({
520 "name": "Test Orchestration",
521 "steps": []
522 });
523
524 let commit1 = repo
525 .commit(
526 "User".to_string(),
527 "user@example.com".to_string(),
528 "First commit".to_string(),
529 &content1,
530 )
531 .unwrap();
532
533 let content2 = serde_json::json!({
534 "name": "Test Orchestration Updated",
535 "steps": [{"name": "step1"}]
536 });
537
538 let commit2 = repo
539 .commit(
540 "User".to_string(),
541 "user@example.com".to_string(),
542 "Second commit".to_string(),
543 &content2,
544 )
545 .unwrap();
546
547 let diff = repo.diff(commit1.id, commit2.id).unwrap();
548 assert!(diff.stats.modifications > 0 || diff.stats.additions > 0);
549 }
550}