1use crate::error::{CollabError, Result};
4use crate::history::VersionControl;
5use crate::models::{ConflictType, MergeConflict, MergeStatus, WorkspaceMerge};
6use chrono::Utc;
7use serde_json::Value;
8use sqlx::{Pool, Sqlite};
9use uuid::Uuid;
10
11pub struct MergeService {
13 db: Pool<Sqlite>,
14 version_control: VersionControl,
15}
16
17impl MergeService {
18 #[must_use]
20 pub fn new(db: Pool<Sqlite>) -> Self {
21 Self {
22 db: db.clone(),
23 version_control: VersionControl::new(db),
24 }
25 }
26
27 pub async fn find_common_ancestor(
32 &self,
33 source_workspace_id: Uuid,
34 target_workspace_id: Uuid,
35 ) -> Result<Option<Uuid>> {
36 let source_ws_id_str = source_workspace_id.to_string();
38 let target_ws_id_str = target_workspace_id.to_string();
39 let fork = sqlx::query!(
40 r#"
41 SELECT fork_point_commit_id
42 FROM workspace_forks
43 WHERE source_workspace_id = ? AND forked_workspace_id = ?
44 "#,
45 source_ws_id_str,
46 target_ws_id_str
47 )
48 .fetch_optional(&self.db)
49 .await?;
50
51 if let Some(fork) = fork {
52 if let Some(commit_id_str) = fork.fork_point_commit_id.as_ref() {
53 if let Ok(commit_id) = Uuid::parse_str(commit_id_str) {
54 return Ok(Some(commit_id));
55 }
56 }
57 }
58
59 let target_ws_id_str2 = target_workspace_id.to_string();
61 let source_ws_id_str2 = source_workspace_id.to_string();
62 let fork = sqlx::query!(
63 r#"
64 SELECT fork_point_commit_id
65 FROM workspace_forks
66 WHERE source_workspace_id = ? AND forked_workspace_id = ?
67 "#,
68 target_ws_id_str2,
69 source_ws_id_str2
70 )
71 .fetch_optional(&self.db)
72 .await?;
73
74 if let Some(fork) = fork {
75 if let Some(commit_id_str) = fork.fork_point_commit_id.as_ref() {
76 if let Ok(commit_id) = Uuid::parse_str(commit_id_str) {
77 return Ok(Some(commit_id));
78 }
79 }
80 }
81
82 let source_commits =
85 self.version_control.get_history(source_workspace_id, Some(1000)).await?;
86 let target_commits =
87 self.version_control.get_history(target_workspace_id, Some(1000)).await?;
88
89 let source_commit_ids: std::collections::HashSet<Uuid> =
91 source_commits.iter().map(|c| c.id).collect();
92 let target_commit_ids: std::collections::HashSet<Uuid> =
93 target_commits.iter().map(|c| c.id).collect();
94
95 for source_commit in &source_commits {
98 if target_commit_ids.contains(&source_commit.id) {
99 return Ok(Some(source_commit.id));
100 }
101 }
102
103 if let (Some(source_latest), Some(target_latest)) =
106 (source_commits.first(), target_commits.first())
107 {
108 let source_ancestors = self.build_ancestor_set(source_latest.id).await?;
110 let target_ancestors = self.build_ancestor_set(target_latest.id).await?;
111
112 for ancestor in &source_ancestors {
114 if target_ancestors.contains(ancestor) {
115 return Ok(Some(*ancestor));
116 }
117 }
118 }
119
120 Ok(None)
122 }
123
124 pub async fn merge_workspaces(
129 &self,
130 source_workspace_id: Uuid,
131 target_workspace_id: Uuid,
132 user_id: Uuid,
133 ) -> Result<(Value, Vec<MergeConflict>)> {
134 let source_commit =
136 self.version_control.get_latest_commit(source_workspace_id).await?.ok_or_else(
137 || CollabError::Internal("Source workspace has no commits".to_string()),
138 )?;
139
140 let target_commit =
141 self.version_control.get_latest_commit(target_workspace_id).await?.ok_or_else(
142 || CollabError::Internal("Target workspace has no commits".to_string()),
143 )?;
144
145 let base_commit_id = self
147 .find_common_ancestor(source_workspace_id, target_workspace_id)
148 .await?
149 .ok_or_else(|| {
150 CollabError::Internal(
151 "Cannot find common ancestor. Workspaces must be related by fork.".to_string(),
152 )
153 })?;
154
155 let base_commit = self.version_control.get_commit(base_commit_id).await?;
156
157 let (merged_state, conflicts) = self.three_way_merge(
159 &base_commit.snapshot,
160 &source_commit.snapshot,
161 &target_commit.snapshot,
162 )?;
163
164 let mut merge = WorkspaceMerge::new(
166 source_workspace_id,
167 target_workspace_id,
168 base_commit_id,
169 source_commit.id,
170 target_commit.id,
171 );
172
173 if conflicts.is_empty() {
174 merge.status = MergeStatus::Completed;
175 } else {
176 merge.status = MergeStatus::Conflict;
177 merge.conflict_data = Some(serde_json::to_value(&conflicts)?);
178 }
179
180 let merge_id_str = merge.id.to_string();
182 let source_ws_id_str = merge.source_workspace_id.to_string();
183 let target_ws_id_str = merge.target_workspace_id.to_string();
184 let base_commit_id_str = merge.base_commit_id.to_string();
185 let source_commit_id_str = merge.source_commit_id.to_string();
186 let target_commit_id_str = merge.target_commit_id.to_string();
187 let merge_commit_id_str = merge.merge_commit_id.map(|id| id.to_string());
188 let status_str = serde_json::to_string(&merge.status)?;
189 let conflict_data_str =
190 merge.conflict_data.as_ref().map(serde_json::to_string).transpose()?;
191 let merged_by_str = merge.merged_by.map(|id| id.to_string());
192 let merged_at_str = merge.merged_at.map(|dt| dt.to_rfc3339());
193 let created_at_str = merge.created_at.to_rfc3339();
194
195 sqlx::query!(
196 r#"
197 INSERT INTO workspace_merges (
198 id, source_workspace_id, target_workspace_id,
199 base_commit_id, source_commit_id, target_commit_id,
200 merge_commit_id, status, conflict_data, merged_by, merged_at, created_at
201 )
202 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
203 "#,
204 merge_id_str,
205 source_ws_id_str,
206 target_ws_id_str,
207 base_commit_id_str,
208 source_commit_id_str,
209 target_commit_id_str,
210 merge_commit_id_str,
211 status_str,
212 conflict_data_str,
213 merged_by_str,
214 merged_at_str,
215 created_at_str
216 )
217 .execute(&self.db)
218 .await?;
219
220 Ok((merged_state, conflicts))
221 }
222
223 fn three_way_merge(
231 &self,
232 base: &Value,
233 source: &Value,
234 target: &Value,
235 ) -> Result<(Value, Vec<MergeConflict>)> {
236 let mut merged = target.clone();
237 let mut conflicts = Vec::new();
238
239 self.merge_value("", base, source, target, &mut merged, &mut conflicts)?;
240
241 Ok((merged, conflicts))
242 }
243
244 fn merge_value(
246 &self,
247 path: &str,
248 base: &Value,
249 source: &Value,
250 target: &Value,
251 merged: &mut Value,
252 conflicts: &mut Vec<MergeConflict>,
253 ) -> Result<()> {
254 match (base, source, target) {
255 (b, s, t) if b == s && s == t => {
257 }
259
260 (b, s, t) if b == s && t != b => {
262 }
264
265 (b, s, t) if b == t && s != b => {
267 *merged = source.clone();
268 }
269
270 (b, s, t) if s == t && s != b => {
272 *merged = source.clone();
273 }
274
275 (Value::Object(base_obj), Value::Object(source_obj), Value::Object(target_obj)) => {
277 if let Value::Object(merged_obj) = merged {
278 let all_keys: std::collections::HashSet<_> =
280 base_obj.keys().chain(source_obj.keys()).chain(target_obj.keys()).collect();
281
282 for key in all_keys {
283 let base_val = base_obj.get(key);
284 let source_val = source_obj.get(key);
285 let target_val = target_obj.get(key);
286
287 let new_path = if path.is_empty() {
288 key.clone()
289 } else {
290 format!("{path}.{key}")
291 };
292
293 match (base_val, source_val, target_val) {
294 (None, Some(s), None) => {
296 merged_obj.insert(key.clone(), s.clone());
297 }
298 (None, None, Some(t)) => {
300 merged_obj.insert(key.clone(), t.clone());
301 }
302 (None, Some(s), Some(t)) if s != t => {
304 conflicts.push(MergeConflict {
305 path: new_path.clone(),
306 base_value: None,
307 source_value: Some(s.clone()),
308 target_value: Some(t.clone()),
309 conflict_type: ConflictType::BothAdded,
310 });
311 }
313 (None, Some(s), Some(t)) if s == t => {
315 merged_obj.insert(key.clone(), s.clone());
316 }
317 (Some(b), Some(s), Some(t)) => {
319 if let Some(merged_val) = merged_obj.get_mut(key) {
320 self.merge_value(&new_path, b, s, t, merged_val, conflicts)?;
321 }
322 }
323 (Some(b), None, Some(t)) if b == t => {
325 merged_obj.remove(key);
326 }
327 (Some(b), Some(s), None) if b == s => {
329 merged_obj.remove(key);
330 }
331 (Some(b), None, Some(_t)) => {
333 conflicts.push(MergeConflict {
334 path: new_path.clone(),
335 base_value: Some(b.clone()),
336 source_value: source_val.cloned(),
337 target_value: target_val.cloned(),
338 conflict_type: ConflictType::DeletedModified,
339 });
340 }
341 (Some(b), Some(_s), None) => {
343 conflicts.push(MergeConflict {
344 path: new_path.clone(),
345 base_value: Some(b.clone()),
346 source_value: source_val.cloned(),
347 target_value: target_val.cloned(),
348 conflict_type: ConflictType::DeletedModified,
349 });
350 }
351 _ => {}
352 }
353 }
354 }
355 }
356
357 (Value::Array(base_arr), Value::Array(source_arr), Value::Array(target_arr)) => {
359 if (base_arr != source_arr || base_arr != target_arr) && source_arr != target_arr {
360 conflicts.push(MergeConflict {
361 path: path.to_string(),
362 base_value: Some(base.clone()),
363 source_value: Some(source.clone()),
364 target_value: Some(target.clone()),
365 conflict_type: ConflictType::Modified,
366 });
367 }
368 }
369
370 (b, s, t) if s != t && s != b && t != b => {
372 conflicts.push(MergeConflict {
373 path: path.to_string(),
374 base_value: Some(b.clone()),
375 source_value: Some(s.clone()),
376 target_value: Some(t.clone()),
377 conflict_type: ConflictType::Modified,
378 });
379 }
381
382 _ => {
383 }
385 }
386
387 Ok(())
388 }
389
390 pub async fn complete_merge(
392 &self,
393 merge_id: Uuid,
394 user_id: Uuid,
395 resolved_state: Value,
396 message: String,
397 ) -> Result<Uuid> {
398 let merge = self.get_merge(merge_id).await?;
400
401 if merge.status != MergeStatus::Conflict && merge.status != MergeStatus::Pending {
402 return Err(CollabError::InvalidInput(
403 "Merge is not in a state that can be completed".to_string(),
404 ));
405 }
406
407 let merge_commit = self
409 .version_control
410 .create_commit(
411 merge.target_workspace_id,
412 user_id,
413 message,
414 Some(merge.target_commit_id),
415 0, resolved_state.clone(),
418 serde_json::json!({
419 "type": "merge",
420 "source_workspace_id": merge.source_workspace_id,
421 "source_commit_id": merge.source_commit_id,
422 }),
423 )
424 .await?;
425
426 let now = Utc::now();
428 sqlx::query!(
429 r#"
430 UPDATE workspace_merges
431 SET merge_commit_id = ?, status = ?, merged_by = ?, merged_at = ?
432 WHERE id = ?
433 "#,
434 merge_commit.id,
435 MergeStatus::Completed,
436 user_id,
437 now,
438 merge_id
439 )
440 .execute(&self.db)
441 .await?;
442
443 Ok(merge_commit.id)
444 }
445
446 pub async fn get_merge(&self, merge_id: Uuid) -> Result<WorkspaceMerge> {
448 let merge_id_str = merge_id.to_string();
449 let row = sqlx::query!(
450 r#"
451 SELECT
452 id,
453 source_workspace_id,
454 target_workspace_id,
455 base_commit_id,
456 source_commit_id,
457 target_commit_id,
458 merge_commit_id,
459 status,
460 conflict_data,
461 merged_by,
462 merged_at,
463 created_at
464 FROM workspace_merges
465 WHERE id = ?
466 "#,
467 merge_id_str
468 )
469 .fetch_optional(&self.db)
470 .await?
471 .ok_or_else(|| CollabError::Internal(format!("Merge not found: {merge_id}")))?;
472
473 Ok(WorkspaceMerge {
474 id: Uuid::parse_str(&row.id)
475 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
476 source_workspace_id: Uuid::parse_str(&row.source_workspace_id)
477 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
478 target_workspace_id: Uuid::parse_str(&row.target_workspace_id)
479 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
480 base_commit_id: Uuid::parse_str(&row.base_commit_id)
481 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
482 source_commit_id: Uuid::parse_str(&row.source_commit_id)
483 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
484 target_commit_id: Uuid::parse_str(&row.target_commit_id)
485 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
486 merge_commit_id: row.merge_commit_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
487 status: serde_json::from_str(&row.status)
488 .map_err(|e| CollabError::Internal(format!("Invalid status: {e}")))?,
489 conflict_data: row.conflict_data.as_ref().and_then(|s| serde_json::from_str(s).ok()),
490 merged_by: row.merged_by.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
491 merged_at: row
492 .merged_at
493 .as_ref()
494 .map(|s| {
495 chrono::DateTime::parse_from_rfc3339(s)
496 .map(|dt| dt.with_timezone(&Utc))
497 .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))
498 })
499 .transpose()?,
500 created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
501 .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
502 .with_timezone(&Utc),
503 })
504 }
505
506 pub async fn list_merges(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMerge>> {
508 let workspace_id_str = workspace_id.to_string();
509 let rows = sqlx::query!(
510 r#"
511 SELECT
512 id,
513 source_workspace_id,
514 target_workspace_id,
515 base_commit_id,
516 source_commit_id,
517 target_commit_id,
518 merge_commit_id,
519 status,
520 conflict_data,
521 merged_by,
522 merged_at,
523 created_at
524 FROM workspace_merges
525 WHERE source_workspace_id = ? OR target_workspace_id = ?
526 ORDER BY created_at DESC
527 "#,
528 workspace_id_str,
529 workspace_id_str
530 )
531 .fetch_all(&self.db)
532 .await?;
533
534 let merges: Result<Vec<WorkspaceMerge>> = rows
535 .into_iter()
536 .map(|row| {
537 Ok(WorkspaceMerge {
538 id: Uuid::parse_str(&row.id)
539 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
540 source_workspace_id: Uuid::parse_str(&row.source_workspace_id)
541 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
542 target_workspace_id: Uuid::parse_str(&row.target_workspace_id)
543 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
544 base_commit_id: Uuid::parse_str(&row.base_commit_id)
545 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
546 source_commit_id: Uuid::parse_str(&row.source_commit_id)
547 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
548 target_commit_id: Uuid::parse_str(&row.target_commit_id)
549 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
550 merge_commit_id: row
551 .merge_commit_id
552 .as_ref()
553 .and_then(|s| Uuid::parse_str(s).ok()),
554 status: serde_json::from_str(&row.status)
555 .map_err(|e| CollabError::Internal(format!("Invalid status: {e}")))?,
556 conflict_data: row
557 .conflict_data
558 .as_ref()
559 .and_then(|s| serde_json::from_str(s).ok()),
560 merged_by: row.merged_by.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
561 merged_at: row
562 .merged_at
563 .as_ref()
564 .map(|s| {
565 chrono::DateTime::parse_from_rfc3339(s)
566 .map(|dt| dt.with_timezone(&Utc))
567 .map_err(|e| {
568 CollabError::Internal(format!("Invalid timestamp: {e}"))
569 })
570 })
571 .transpose()?,
572 created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
573 .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
574 .with_timezone(&Utc),
575 })
576 })
577 .collect();
578 let merges = merges?;
579
580 Ok(merges)
581 }
582
583 async fn build_ancestor_set(&self, commit_id: Uuid) -> Result<std::collections::HashSet<Uuid>> {
585 let mut ancestors = std::collections::HashSet::new();
586 let mut current_id = Some(commit_id);
587 let mut visited = std::collections::HashSet::new();
588
589 let max_depth = 1000;
591 let mut depth = 0;
592
593 while let Some(id) = current_id {
594 if visited.contains(&id) || depth > max_depth {
595 break; }
597 visited.insert(id);
598 ancestors.insert(id);
599
600 match self.version_control.get_commit(id).await {
602 Ok(commit) => {
603 current_id = commit.parent_id;
604 depth += 1;
605 }
606 Err(_) => break, }
608 }
609
610 Ok(ancestors)
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use serde_json::json;
618 use sqlx::SqlitePool;
619
620 async fn setup_test_db() -> Pool<Sqlite> {
621 let pool = SqlitePool::connect(":memory:").await.unwrap();
622
623 sqlx::query(
625 r#"
626 CREATE TABLE IF NOT EXISTS workspace_forks (
627 id TEXT PRIMARY KEY,
628 source_workspace_id TEXT NOT NULL,
629 forked_workspace_id TEXT NOT NULL,
630 fork_point_commit_id TEXT,
631 created_at TEXT NOT NULL,
632 created_by TEXT NOT NULL
633 )
634 "#,
635 )
636 .execute(&pool)
637 .await
638 .unwrap();
639
640 sqlx::query(
642 r#"
643 CREATE TABLE IF NOT EXISTS workspace_merges (
644 id TEXT PRIMARY KEY,
645 source_workspace_id TEXT NOT NULL,
646 target_workspace_id TEXT NOT NULL,
647 base_commit_id TEXT NOT NULL,
648 source_commit_id TEXT NOT NULL,
649 target_commit_id TEXT NOT NULL,
650 merge_commit_id TEXT,
651 status TEXT NOT NULL,
652 conflict_data TEXT,
653 merged_by TEXT,
654 merged_at TEXT,
655 created_at TEXT NOT NULL
656 )
657 "#,
658 )
659 .execute(&pool)
660 .await
661 .unwrap();
662
663 sqlx::query(
665 r#"
666 CREATE TABLE IF NOT EXISTS commits (
667 id TEXT PRIMARY KEY,
668 workspace_id TEXT NOT NULL,
669 user_id TEXT NOT NULL,
670 message TEXT NOT NULL,
671 parent_id TEXT,
672 version INTEGER NOT NULL,
673 snapshot TEXT NOT NULL,
674 metadata TEXT,
675 created_at TEXT NOT NULL
676 )
677 "#,
678 )
679 .execute(&pool)
680 .await
681 .unwrap();
682
683 pool
684 }
685
686 #[tokio::test]
687 async fn test_merge_service_new() {
688 let pool = setup_test_db().await;
689 let service = MergeService::new(pool);
690
691 assert!(true);
694 }
695
696 #[test]
697 fn test_three_way_merge_no_changes() {
698 let pool_fut = setup_test_db();
699 let rt = tokio::runtime::Runtime::new().unwrap();
700 let pool = rt.block_on(pool_fut);
701 let service = MergeService::new(pool);
702
703 let base = json!({"key": "value"});
704 let source = json!({"key": "value"});
705 let target = json!({"key": "value"});
706
707 let result = service.three_way_merge(&base, &source, &target);
708 assert!(result.is_ok());
709
710 let (merged, conflicts) = result.unwrap();
711 assert_eq!(merged, target);
712 assert!(conflicts.is_empty());
713 }
714
715 #[test]
716 fn test_three_way_merge_source_change() {
717 let pool_fut = setup_test_db();
718 let rt = tokio::runtime::Runtime::new().unwrap();
719 let pool = rt.block_on(pool_fut);
720 let service = MergeService::new(pool);
721
722 let base = json!({"key": "value"});
723 let source = json!({"key": "new_value"});
724 let target = json!({"key": "value"});
725
726 let result = service.three_way_merge(&base, &source, &target);
727 assert!(result.is_ok());
728
729 let (merged, conflicts) = result.unwrap();
730 assert_eq!(merged, source);
731 assert!(conflicts.is_empty());
732 }
733
734 #[test]
735 fn test_three_way_merge_target_change() {
736 let pool_fut = setup_test_db();
737 let rt = tokio::runtime::Runtime::new().unwrap();
738 let pool = rt.block_on(pool_fut);
739 let service = MergeService::new(pool);
740
741 let base = json!({"key": "value"});
742 let source = json!({"key": "value"});
743 let target = json!({"key": "new_value"});
744
745 let result = service.three_way_merge(&base, &source, &target);
746 assert!(result.is_ok());
747
748 let (merged, conflicts) = result.unwrap();
749 assert_eq!(merged, target);
750 assert!(conflicts.is_empty());
751 }
752
753 #[test]
754 fn test_three_way_merge_both_changed_same() {
755 let pool_fut = setup_test_db();
756 let rt = tokio::runtime::Runtime::new().unwrap();
757 let pool = rt.block_on(pool_fut);
758 let service = MergeService::new(pool);
759
760 let base = json!({"key": "value"});
761 let source = json!({"key": "new_value"});
762 let target = json!({"key": "new_value"});
763
764 let result = service.three_way_merge(&base, &source, &target);
765 assert!(result.is_ok());
766
767 let (merged, conflicts) = result.unwrap();
768 assert_eq!(merged, source);
769 assert!(conflicts.is_empty());
770 }
771
772 #[test]
773 fn test_three_way_merge_conflict() {
774 let pool_fut = setup_test_db();
775 let rt = tokio::runtime::Runtime::new().unwrap();
776 let pool = rt.block_on(pool_fut);
777 let service = MergeService::new(pool);
778
779 let base = json!({"key": "value"});
780 let source = json!({"key": "source_value"});
781 let target = json!({"key": "target_value"});
782
783 let result = service.three_way_merge(&base, &source, &target);
784 assert!(result.is_ok());
785
786 let (merged, conflicts) = result.unwrap();
787 assert_eq!(merged, target); assert_eq!(conflicts.len(), 1);
789 assert_eq!(conflicts[0].path, "key");
790 assert_eq!(conflicts[0].conflict_type, ConflictType::Modified);
791 }
792
793 #[test]
794 fn test_three_way_merge_object_add_source() {
795 let pool_fut = setup_test_db();
796 let rt = tokio::runtime::Runtime::new().unwrap();
797 let pool = rt.block_on(pool_fut);
798 let service = MergeService::new(pool);
799
800 let base = json!({});
801 let source = json!({"new_key": "value"});
802 let target = json!({});
803
804 let result = service.three_way_merge(&base, &source, &target);
805 assert!(result.is_ok());
806
807 let (merged, conflicts) = result.unwrap();
808 assert_eq!(merged.get("new_key"), Some(&json!("value")));
809 assert!(conflicts.is_empty());
810 }
811
812 #[test]
813 fn test_three_way_merge_object_add_target() {
814 let pool_fut = setup_test_db();
815 let rt = tokio::runtime::Runtime::new().unwrap();
816 let pool = rt.block_on(pool_fut);
817 let service = MergeService::new(pool);
818
819 let base = json!({});
820 let source = json!({});
821 let target = json!({"new_key": "value"});
822
823 let result = service.three_way_merge(&base, &source, &target);
824 assert!(result.is_ok());
825
826 let (merged, conflicts) = result.unwrap();
827 assert_eq!(merged.get("new_key"), Some(&json!("value")));
828 assert!(conflicts.is_empty());
829 }
830
831 #[test]
832 fn test_three_way_merge_both_added_different() {
833 let pool_fut = setup_test_db();
834 let rt = tokio::runtime::Runtime::new().unwrap();
835 let pool = rt.block_on(pool_fut);
836 let service = MergeService::new(pool);
837
838 let base = json!({});
839 let source = json!({"key": "source_value"});
840 let target = json!({"key": "target_value"});
841
842 let result = service.three_way_merge(&base, &source, &target);
843 assert!(result.is_ok());
844
845 let (merged, conflicts) = result.unwrap();
846 assert_eq!(conflicts.len(), 1);
847 assert_eq!(conflicts[0].conflict_type, ConflictType::BothAdded);
848 }
849
850 #[test]
851 fn test_three_way_merge_nested_objects() {
852 let pool_fut = setup_test_db();
853 let rt = tokio::runtime::Runtime::new().unwrap();
854 let pool = rt.block_on(pool_fut);
855 let service = MergeService::new(pool);
856
857 let base = json!({
858 "parent": {
859 "child": "value"
860 }
861 });
862 let source = json!({
863 "parent": {
864 "child": "new_value"
865 }
866 });
867 let target = json!({
868 "parent": {
869 "child": "value"
870 }
871 });
872
873 let result = service.three_way_merge(&base, &source, &target);
874 assert!(result.is_ok());
875
876 let (merged, conflicts) = result.unwrap();
877 assert_eq!(merged["parent"]["child"], json!("new_value"));
878 assert!(conflicts.is_empty());
879 }
880
881 #[test]
882 fn test_three_way_merge_arrays_no_conflict() {
883 let pool_fut = setup_test_db();
884 let rt = tokio::runtime::Runtime::new().unwrap();
885 let pool = rt.block_on(pool_fut);
886 let service = MergeService::new(pool);
887
888 let base = json!([1, 2, 3]);
889 let source = json!([1, 2, 3]);
890 let target = json!([1, 2, 3]);
891
892 let result = service.three_way_merge(&base, &source, &target);
893 assert!(result.is_ok());
894
895 let (merged, conflicts) = result.unwrap();
896 assert_eq!(merged, target);
897 assert!(conflicts.is_empty());
898 }
899
900 #[test]
901 fn test_three_way_merge_arrays_conflict() {
902 let pool_fut = setup_test_db();
903 let rt = tokio::runtime::Runtime::new().unwrap();
904 let pool = rt.block_on(pool_fut);
905 let service = MergeService::new(pool);
906
907 let base = json!([1, 2, 3]);
908 let source = json!([1, 2, 4]);
909 let target = json!([1, 2, 5]);
910
911 let result = service.three_way_merge(&base, &source, &target);
912 assert!(result.is_ok());
913
914 let (merged, conflicts) = result.unwrap();
915 assert_eq!(merged, target);
916 assert_eq!(conflicts.len(), 1);
917 }
918
919 #[test]
920 fn test_workspace_merge_new() {
921 let source_ws = Uuid::new_v4();
922 let target_ws = Uuid::new_v4();
923 let base_commit = Uuid::new_v4();
924 let source_commit = Uuid::new_v4();
925 let target_commit = Uuid::new_v4();
926
927 let merge =
928 WorkspaceMerge::new(source_ws, target_ws, base_commit, source_commit, target_commit);
929
930 assert_eq!(merge.source_workspace_id, source_ws);
931 assert_eq!(merge.target_workspace_id, target_ws);
932 assert_eq!(merge.base_commit_id, base_commit);
933 assert_eq!(merge.source_commit_id, source_commit);
934 assert_eq!(merge.target_commit_id, target_commit);
935 assert_eq!(merge.status, MergeStatus::Pending);
936 assert!(merge.merge_commit_id.is_none());
937 }
938
939 #[test]
940 fn test_merge_conflict_types() {
941 assert_eq!(ConflictType::Modified, ConflictType::Modified);
942 assert_eq!(ConflictType::BothAdded, ConflictType::BothAdded);
943 assert_eq!(ConflictType::DeletedModified, ConflictType::DeletedModified);
944
945 assert_ne!(ConflictType::Modified, ConflictType::BothAdded);
946 }
947
948 #[test]
949 fn test_merge_status_equality() {
950 assert_eq!(MergeStatus::Pending, MergeStatus::Pending);
951 assert_eq!(MergeStatus::Conflict, MergeStatus::Conflict);
952 assert_eq!(MergeStatus::Completed, MergeStatus::Completed);
953
954 assert_ne!(MergeStatus::Pending, MergeStatus::Completed);
955 }
956}