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(
36 &self,
37 source_workspace_id: Uuid,
38 target_workspace_id: Uuid,
39 ) -> Result<Option<Uuid>> {
40 let fork = sqlx::query!(
42 r#"
43 SELECT fork_point_commit_id as "fork_point_commit_id: Uuid"
44 FROM workspace_forks
45 WHERE source_workspace_id = ? AND forked_workspace_id = ?
46 "#,
47 source_workspace_id,
48 target_workspace_id
49 )
50 .fetch_optional(&self.db)
51 .await?;
52
53 if let Some(fork) = fork {
54 if let Some(commit_id) = fork.fork_point_commit_id {
55 return Ok(Some(commit_id));
56 }
57 }
58
59 let fork = sqlx::query!(
61 r#"
62 SELECT fork_point_commit_id as "fork_point_commit_id: Uuid"
63 FROM workspace_forks
64 WHERE source_workspace_id = ? AND forked_workspace_id = ?
65 "#,
66 target_workspace_id,
67 source_workspace_id
68 )
69 .fetch_optional(&self.db)
70 .await?;
71
72 if let Some(fork) = fork {
73 if let Some(commit_id) = fork.fork_point_commit_id {
74 return Ok(Some(commit_id));
75 }
76 }
77
78 let source_commits =
81 self.version_control.get_history(source_workspace_id, Some(1000)).await?;
82 let target_commits =
83 self.version_control.get_history(target_workspace_id, Some(1000)).await?;
84
85 let _source_commit_ids: std::collections::HashSet<Uuid> =
87 source_commits.iter().map(|c| c.id).collect();
88 let target_commit_ids: std::collections::HashSet<Uuid> =
89 target_commits.iter().map(|c| c.id).collect();
90
91 for source_commit in &source_commits {
94 if target_commit_ids.contains(&source_commit.id) {
95 return Ok(Some(source_commit.id));
96 }
97 }
98
99 if let (Some(source_latest), Some(target_latest)) =
102 (source_commits.first(), target_commits.first())
103 {
104 let source_ancestors = self.build_ancestor_set(source_latest.id).await?;
106 let target_ancestors = self.build_ancestor_set(target_latest.id).await?;
107
108 for ancestor in &source_ancestors {
110 if target_ancestors.contains(ancestor) {
111 return Ok(Some(*ancestor));
112 }
113 }
114 }
115
116 Ok(None)
118 }
119
120 pub async fn merge_workspaces(
130 &self,
131 source_workspace_id: Uuid,
132 target_workspace_id: Uuid,
133 _user_id: Uuid,
134 ) -> Result<(Value, Vec<MergeConflict>)> {
135 let source_commit =
137 self.version_control.get_latest_commit(source_workspace_id).await?.ok_or_else(
138 || CollabError::Internal("Source workspace has no commits".to_string()),
139 )?;
140
141 let target_commit =
142 self.version_control.get_latest_commit(target_workspace_id).await?.ok_or_else(
143 || CollabError::Internal("Target workspace has no commits".to_string()),
144 )?;
145
146 let base_commit_id = self
148 .find_common_ancestor(source_workspace_id, target_workspace_id)
149 .await?
150 .ok_or_else(|| {
151 CollabError::Internal(
152 "Cannot find common ancestor. Workspaces must be related by fork.".to_string(),
153 )
154 })?;
155
156 let base_commit = self.version_control.get_commit(base_commit_id).await?;
157
158 let (merged_state, conflicts) = self.three_way_merge(
160 &base_commit.snapshot,
161 &source_commit.snapshot,
162 &target_commit.snapshot,
163 )?;
164
165 let mut merge = WorkspaceMerge::new(
167 source_workspace_id,
168 target_workspace_id,
169 base_commit_id,
170 source_commit.id,
171 target_commit.id,
172 );
173
174 if conflicts.is_empty() {
175 merge.status = MergeStatus::Completed;
176 } else {
177 merge.status = MergeStatus::Conflict;
178 merge.conflict_data = Some(serde_json::to_value(&conflicts)?);
179 }
180
181 let status_str = match merge.status {
183 MergeStatus::Pending => "pending",
184 MergeStatus::InProgress => "in_progress",
185 MergeStatus::Completed => "completed",
186 MergeStatus::Conflict => "conflict",
187 MergeStatus::Cancelled => "cancelled",
188 };
189 let conflict_data_str =
190 merge.conflict_data.as_ref().map(serde_json::to_string).transpose()?;
191 let merged_at_str = merge.merged_at.map(|dt| dt.to_rfc3339());
192 let created_at_str = merge.created_at.to_rfc3339();
193
194 sqlx::query!(
195 r#"
196 INSERT INTO workspace_merges (
197 id, source_workspace_id, target_workspace_id,
198 base_commit_id, source_commit_id, target_commit_id,
199 merge_commit_id, status, conflict_data, merged_by, merged_at, created_at
200 )
201 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
202 "#,
203 merge.id,
204 merge.source_workspace_id,
205 merge.target_workspace_id,
206 merge.base_commit_id,
207 merge.source_commit_id,
208 merge.target_commit_id,
209 merge.merge_commit_id,
210 status_str,
211 conflict_data_str,
212 merge.merged_by,
213 merged_at_str,
214 created_at_str
215 )
216 .execute(&self.db)
217 .await?;
218
219 Ok((merged_state, conflicts))
220 }
221
222 fn three_way_merge(
230 &self,
231 base: &Value,
232 source: &Value,
233 target: &Value,
234 ) -> Result<(Value, Vec<MergeConflict>)> {
235 let mut merged = target.clone();
236 let mut conflicts = Vec::new();
237
238 self.merge_value("", base, source, target, &mut merged, &mut conflicts)?;
239
240 Ok((merged, conflicts))
241 }
242
243 #[allow(clippy::only_used_in_recursion, clippy::too_many_lines)]
245 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(
396 &self,
397 merge_id: Uuid,
398 user_id: Uuid,
399 resolved_state: Value,
400 message: String,
401 ) -> Result<Uuid> {
402 let merge = self.get_merge(merge_id).await?;
404
405 if merge.status != MergeStatus::Conflict && merge.status != MergeStatus::Pending {
406 return Err(CollabError::InvalidInput(
407 "Merge is not in a state that can be completed".to_string(),
408 ));
409 }
410
411 let next_version =
413 match self.version_control.get_latest_commit(merge.target_workspace_id).await? {
414 Some(latest) => latest.version + 1,
415 None => 1,
416 };
417
418 let merge_commit = self
420 .version_control
421 .create_commit(
422 merge.target_workspace_id,
423 user_id,
424 message,
425 Some(merge.target_commit_id),
426 next_version,
427 resolved_state.clone(),
428 serde_json::json!({
429 "type": "merge",
430 "source_workspace_id": merge.source_workspace_id,
431 "source_commit_id": merge.source_commit_id,
432 }),
433 )
434 .await?;
435
436 let now = Utc::now();
438 sqlx::query!(
439 r#"
440 UPDATE workspace_merges
441 SET merge_commit_id = ?, status = ?, merged_by = ?, merged_at = ?
442 WHERE id = ?
443 "#,
444 merge_commit.id,
445 MergeStatus::Completed,
446 user_id,
447 now,
448 merge_id
449 )
450 .execute(&self.db)
451 .await?;
452
453 Ok(merge_commit.id)
454 }
455
456 pub async fn get_merge(&self, merge_id: Uuid) -> Result<WorkspaceMerge> {
462 let merge_id_str = merge_id.to_string();
463 let row = sqlx::query!(
464 r#"
465 SELECT
466 id,
467 source_workspace_id,
468 target_workspace_id,
469 base_commit_id,
470 source_commit_id,
471 target_commit_id,
472 merge_commit_id,
473 status,
474 conflict_data,
475 merged_by,
476 merged_at,
477 created_at
478 FROM workspace_merges
479 WHERE id = ?
480 "#,
481 merge_id_str
482 )
483 .fetch_optional(&self.db)
484 .await?
485 .ok_or_else(|| CollabError::Internal(format!("Merge not found: {merge_id}")))?;
486
487 Ok(WorkspaceMerge {
488 id: Uuid::parse_str(&row.id)
489 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
490 source_workspace_id: Uuid::parse_str(&row.source_workspace_id)
491 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
492 target_workspace_id: Uuid::parse_str(&row.target_workspace_id)
493 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
494 base_commit_id: Uuid::parse_str(&row.base_commit_id)
495 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
496 source_commit_id: Uuid::parse_str(&row.source_commit_id)
497 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
498 target_commit_id: Uuid::parse_str(&row.target_commit_id)
499 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
500 merge_commit_id: row.merge_commit_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
501 status: serde_json::from_str(&row.status)
502 .map_err(|e| CollabError::Internal(format!("Invalid status: {e}")))?,
503 conflict_data: row.conflict_data.as_ref().and_then(|s| serde_json::from_str(s).ok()),
504 merged_by: row.merged_by.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
505 merged_at: row
506 .merged_at
507 .as_ref()
508 .map(|s| {
509 chrono::DateTime::parse_from_rfc3339(s)
510 .map(|dt| dt.with_timezone(&Utc))
511 .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))
512 })
513 .transpose()?,
514 created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
515 .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
516 .with_timezone(&Utc),
517 })
518 }
519
520 pub async fn list_merges(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMerge>> {
526 let rows = sqlx::query!(
527 r#"
528 SELECT
529 id as "id: Uuid",
530 source_workspace_id as "source_workspace_id: Uuid",
531 target_workspace_id as "target_workspace_id: Uuid",
532 base_commit_id as "base_commit_id: Uuid",
533 source_commit_id as "source_commit_id: Uuid",
534 target_commit_id as "target_commit_id: Uuid",
535 merge_commit_id as "merge_commit_id: Uuid",
536 status,
537 conflict_data,
538 merged_by as "merged_by: Uuid",
539 merged_at,
540 created_at
541 FROM workspace_merges
542 WHERE source_workspace_id = ? OR target_workspace_id = ?
543 ORDER BY created_at DESC
544 "#,
545 workspace_id,
546 workspace_id
547 )
548 .fetch_all(&self.db)
549 .await?;
550
551 let merges: Result<Vec<WorkspaceMerge>> = rows
552 .into_iter()
553 .map(|row| {
554 let status = match row.status.as_str() {
555 "pending" => MergeStatus::Pending,
556 "in_progress" => MergeStatus::InProgress,
557 "completed" => MergeStatus::Completed,
558 "conflict" => MergeStatus::Conflict,
559 "cancelled" => MergeStatus::Cancelled,
560 other => return Err(CollabError::Internal(format!("Invalid status: {other}"))),
561 };
562 Ok(WorkspaceMerge {
563 id: row.id,
564 source_workspace_id: row.source_workspace_id,
565 target_workspace_id: row.target_workspace_id,
566 base_commit_id: row.base_commit_id,
567 source_commit_id: row.source_commit_id,
568 target_commit_id: row.target_commit_id,
569 merge_commit_id: row.merge_commit_id,
570 status,
571 conflict_data: row
572 .conflict_data
573 .as_ref()
574 .and_then(|s| serde_json::from_str(s).ok()),
575 merged_by: row.merged_by,
576 merged_at: row
577 .merged_at
578 .as_ref()
579 .map(|s| {
580 chrono::DateTime::parse_from_rfc3339(s)
581 .map(|dt| dt.with_timezone(&Utc))
582 .map_err(|e| {
583 CollabError::Internal(format!("Invalid timestamp: {e}"))
584 })
585 })
586 .transpose()?,
587 created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
588 .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
589 .with_timezone(&Utc),
590 })
591 })
592 .collect();
593 let merges = merges?;
594
595 Ok(merges)
596 }
597
598 async fn build_ancestor_set(&self, commit_id: Uuid) -> Result<std::collections::HashSet<Uuid>> {
600 let mut ancestors = std::collections::HashSet::new();
601 let mut current_id = Some(commit_id);
602 let mut visited = std::collections::HashSet::new();
603
604 let max_depth = 1000;
606 let mut depth = 0;
607
608 while let Some(id) = current_id {
609 if visited.contains(&id) || depth > max_depth {
610 break; }
612 visited.insert(id);
613 ancestors.insert(id);
614
615 match self.version_control.get_commit(id).await {
617 Ok(commit) => {
618 current_id = commit.parent_id;
619 depth += 1;
620 }
621 Err(_) => break, }
623 }
624
625 Ok(ancestors)
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632 use serde_json::json;
633 use sqlx::SqlitePool;
634
635 async fn setup_test_db() -> Pool<Sqlite> {
636 let pool = SqlitePool::connect(":memory:").await.unwrap();
637
638 sqlx::query(
640 r"
641 CREATE TABLE IF NOT EXISTS workspace_forks (
642 id TEXT PRIMARY KEY,
643 source_workspace_id TEXT NOT NULL,
644 forked_workspace_id TEXT NOT NULL,
645 fork_point_commit_id TEXT,
646 created_at TEXT NOT NULL,
647 created_by TEXT NOT NULL
648 )
649 ",
650 )
651 .execute(&pool)
652 .await
653 .unwrap();
654
655 sqlx::query(
657 r"
658 CREATE TABLE IF NOT EXISTS workspace_merges (
659 id TEXT PRIMARY KEY,
660 source_workspace_id TEXT NOT NULL,
661 target_workspace_id TEXT NOT NULL,
662 base_commit_id TEXT NOT NULL,
663 source_commit_id TEXT NOT NULL,
664 target_commit_id TEXT NOT NULL,
665 merge_commit_id TEXT,
666 status TEXT NOT NULL,
667 conflict_data TEXT,
668 merged_by TEXT,
669 merged_at TEXT,
670 created_at TEXT NOT NULL
671 )
672 ",
673 )
674 .execute(&pool)
675 .await
676 .unwrap();
677
678 sqlx::query(
680 r"
681 CREATE TABLE IF NOT EXISTS commits (
682 id TEXT PRIMARY KEY,
683 workspace_id TEXT NOT NULL,
684 user_id TEXT NOT NULL,
685 message TEXT NOT NULL,
686 parent_id TEXT,
687 version INTEGER NOT NULL,
688 snapshot TEXT NOT NULL,
689 metadata TEXT,
690 created_at TEXT NOT NULL
691 )
692 ",
693 )
694 .execute(&pool)
695 .await
696 .unwrap();
697
698 pool
699 }
700
701 #[tokio::test]
702 async fn test_merge_service_new() {
703 let pool = setup_test_db().await;
704 let _service = MergeService::new(pool);
705 }
707
708 #[test]
709 fn test_three_way_merge_no_changes() {
710 let pool_fut = setup_test_db();
711 let rt = tokio::runtime::Runtime::new().unwrap();
712 let pool = rt.block_on(pool_fut);
713 let service = MergeService::new(pool);
714
715 let base = json!({"key": "value"});
716 let source = json!({"key": "value"});
717 let target = json!({"key": "value"});
718
719 let result = service.three_way_merge(&base, &source, &target);
720 assert!(result.is_ok());
721
722 let (merged, conflicts) = result.unwrap();
723 assert_eq!(merged, target);
724 assert!(conflicts.is_empty());
725 }
726
727 #[test]
728 fn test_three_way_merge_source_change() {
729 let pool_fut = setup_test_db();
730 let rt = tokio::runtime::Runtime::new().unwrap();
731 let pool = rt.block_on(pool_fut);
732 let service = MergeService::new(pool);
733
734 let base = json!({"key": "value"});
735 let source = json!({"key": "new_value"});
736 let target = json!({"key": "value"});
737
738 let result = service.three_way_merge(&base, &source, &target);
739 assert!(result.is_ok());
740
741 let (merged, conflicts) = result.unwrap();
742 assert_eq!(merged, source);
743 assert!(conflicts.is_empty());
744 }
745
746 #[test]
747 fn test_three_way_merge_target_change() {
748 let pool_fut = setup_test_db();
749 let rt = tokio::runtime::Runtime::new().unwrap();
750 let pool = rt.block_on(pool_fut);
751 let service = MergeService::new(pool);
752
753 let base = json!({"key": "value"});
754 let source = json!({"key": "value"});
755 let target = json!({"key": "new_value"});
756
757 let result = service.three_way_merge(&base, &source, &target);
758 assert!(result.is_ok());
759
760 let (merged, conflicts) = result.unwrap();
761 assert_eq!(merged, target);
762 assert!(conflicts.is_empty());
763 }
764
765 #[test]
766 fn test_three_way_merge_both_changed_same() {
767 let pool_fut = setup_test_db();
768 let rt = tokio::runtime::Runtime::new().unwrap();
769 let pool = rt.block_on(pool_fut);
770 let service = MergeService::new(pool);
771
772 let base = json!({"key": "value"});
773 let source = json!({"key": "new_value"});
774 let target = json!({"key": "new_value"});
775
776 let result = service.three_way_merge(&base, &source, &target);
777 assert!(result.is_ok());
778
779 let (merged, conflicts) = result.unwrap();
780 assert_eq!(merged, source);
781 assert!(conflicts.is_empty());
782 }
783
784 #[test]
785 fn test_three_way_merge_conflict() {
786 let pool_fut = setup_test_db();
787 let rt = tokio::runtime::Runtime::new().unwrap();
788 let pool = rt.block_on(pool_fut);
789 let service = MergeService::new(pool);
790
791 let base = json!({"key": "value"});
792 let source = json!({"key": "source_value"});
793 let target = json!({"key": "target_value"});
794
795 let result = service.three_way_merge(&base, &source, &target);
796 assert!(result.is_ok());
797
798 let (merged, conflicts) = result.unwrap();
799 assert_eq!(merged, target); assert_eq!(conflicts.len(), 1);
801 assert_eq!(conflicts[0].path, "key");
802 assert_eq!(conflicts[0].conflict_type, ConflictType::Modified);
803 }
804
805 #[test]
806 fn test_three_way_merge_object_add_source() {
807 let pool_fut = setup_test_db();
808 let rt = tokio::runtime::Runtime::new().unwrap();
809 let pool = rt.block_on(pool_fut);
810 let service = MergeService::new(pool);
811
812 let base = json!({});
813 let source = json!({"new_key": "value"});
814 let target = json!({});
815
816 let result = service.three_way_merge(&base, &source, &target);
817 assert!(result.is_ok());
818
819 let (merged, conflicts) = result.unwrap();
820 assert_eq!(merged.get("new_key"), Some(&json!("value")));
821 assert!(conflicts.is_empty());
822 }
823
824 #[test]
825 fn test_three_way_merge_object_add_target() {
826 let pool_fut = setup_test_db();
827 let rt = tokio::runtime::Runtime::new().unwrap();
828 let pool = rt.block_on(pool_fut);
829 let service = MergeService::new(pool);
830
831 let base = json!({});
832 let source = json!({});
833 let target = json!({"new_key": "value"});
834
835 let result = service.three_way_merge(&base, &source, &target);
836 assert!(result.is_ok());
837
838 let (merged, conflicts) = result.unwrap();
839 assert_eq!(merged.get("new_key"), Some(&json!("value")));
840 assert!(conflicts.is_empty());
841 }
842
843 #[test]
844 fn test_three_way_merge_both_added_different() {
845 let pool_fut = setup_test_db();
846 let rt = tokio::runtime::Runtime::new().unwrap();
847 let pool = rt.block_on(pool_fut);
848 let service = MergeService::new(pool);
849
850 let base = json!({});
851 let source = json!({"key": "source_value"});
852 let target = json!({"key": "target_value"});
853
854 let result = service.three_way_merge(&base, &source, &target);
855 assert!(result.is_ok());
856
857 let (_merged, conflicts) = result.unwrap();
858 assert_eq!(conflicts.len(), 1);
859 assert_eq!(conflicts[0].conflict_type, ConflictType::BothAdded);
860 }
861
862 #[test]
863 fn test_three_way_merge_nested_objects() {
864 let pool_fut = setup_test_db();
865 let rt = tokio::runtime::Runtime::new().unwrap();
866 let pool = rt.block_on(pool_fut);
867 let service = MergeService::new(pool);
868
869 let base = json!({
870 "parent": {
871 "child": "value"
872 }
873 });
874 let source = json!({
875 "parent": {
876 "child": "new_value"
877 }
878 });
879 let target = json!({
880 "parent": {
881 "child": "value"
882 }
883 });
884
885 let result = service.three_way_merge(&base, &source, &target);
886 assert!(result.is_ok());
887
888 let (merged, conflicts) = result.unwrap();
889 assert_eq!(merged["parent"]["child"], json!("new_value"));
890 assert!(conflicts.is_empty());
891 }
892
893 #[test]
894 fn test_three_way_merge_arrays_no_conflict() {
895 let pool_fut = setup_test_db();
896 let rt = tokio::runtime::Runtime::new().unwrap();
897 let pool = rt.block_on(pool_fut);
898 let service = MergeService::new(pool);
899
900 let base = json!([1, 2, 3]);
901 let source = json!([1, 2, 3]);
902 let target = json!([1, 2, 3]);
903
904 let result = service.three_way_merge(&base, &source, &target);
905 assert!(result.is_ok());
906
907 let (merged, conflicts) = result.unwrap();
908 assert_eq!(merged, target);
909 assert!(conflicts.is_empty());
910 }
911
912 #[test]
913 fn test_three_way_merge_arrays_conflict() {
914 let pool_fut = setup_test_db();
915 let rt = tokio::runtime::Runtime::new().unwrap();
916 let pool = rt.block_on(pool_fut);
917 let service = MergeService::new(pool);
918
919 let base = json!([1, 2, 3]);
920 let source = json!([1, 2, 4]);
921 let target = json!([1, 2, 5]);
922
923 let result = service.three_way_merge(&base, &source, &target);
924 assert!(result.is_ok());
925
926 let (merged, conflicts) = result.unwrap();
927 assert_eq!(merged, target);
928 assert_eq!(conflicts.len(), 1);
929 }
930
931 #[test]
932 fn test_workspace_merge_new() {
933 let source_ws = Uuid::new_v4();
934 let target_ws = Uuid::new_v4();
935 let base_commit = Uuid::new_v4();
936 let source_commit = Uuid::new_v4();
937 let target_commit = Uuid::new_v4();
938
939 let merge =
940 WorkspaceMerge::new(source_ws, target_ws, base_commit, source_commit, target_commit);
941
942 assert_eq!(merge.source_workspace_id, source_ws);
943 assert_eq!(merge.target_workspace_id, target_ws);
944 assert_eq!(merge.base_commit_id, base_commit);
945 assert_eq!(merge.source_commit_id, source_commit);
946 assert_eq!(merge.target_commit_id, target_commit);
947 assert_eq!(merge.status, MergeStatus::Pending);
948 assert!(merge.merge_commit_id.is_none());
949 }
950
951 #[test]
952 fn test_merge_conflict_types() {
953 assert_eq!(ConflictType::Modified, ConflictType::Modified);
954 assert_eq!(ConflictType::BothAdded, ConflictType::BothAdded);
955 assert_eq!(ConflictType::DeletedModified, ConflictType::DeletedModified);
956
957 assert_ne!(ConflictType::Modified, ConflictType::BothAdded);
958 }
959
960 #[test]
961 fn test_merge_status_equality() {
962 assert_eq!(MergeStatus::Pending, MergeStatus::Pending);
963 assert_eq!(MergeStatus::Conflict, MergeStatus::Conflict);
964 assert_eq!(MergeStatus::Completed, MergeStatus::Completed);
965
966 assert_ne!(MergeStatus::Pending, MergeStatus::Completed);
967 }
968}