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