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 merge_commit = self
399 .version_control
400 .create_commit(
401 merge.target_workspace_id,
402 user_id,
403 message,
404 Some(merge.target_commit_id),
405 0, resolved_state.clone(),
408 serde_json::json!({
409 "type": "merge",
410 "source_workspace_id": merge.source_workspace_id,
411 "source_commit_id": merge.source_commit_id,
412 }),
413 )
414 .await?;
415
416 let now = Utc::now();
418 sqlx::query!(
419 r#"
420 UPDATE workspace_merges
421 SET merge_commit_id = ?, status = ?, merged_by = ?, merged_at = ?
422 WHERE id = ?
423 "#,
424 merge_commit.id,
425 MergeStatus::Completed,
426 user_id,
427 now,
428 merge_id
429 )
430 .execute(&self.db)
431 .await?;
432
433 Ok(merge_commit.id)
434 }
435
436 pub async fn get_merge(&self, merge_id: Uuid) -> Result<WorkspaceMerge> {
438 let merge_id_str = merge_id.to_string();
439 let row = sqlx::query!(
440 r#"
441 SELECT
442 id,
443 source_workspace_id,
444 target_workspace_id,
445 base_commit_id,
446 source_commit_id,
447 target_commit_id,
448 merge_commit_id,
449 status,
450 conflict_data,
451 merged_by,
452 merged_at,
453 created_at
454 FROM workspace_merges
455 WHERE id = ?
456 "#,
457 merge_id_str
458 )
459 .fetch_optional(&self.db)
460 .await?
461 .ok_or_else(|| CollabError::Internal(format!("Merge not found: {merge_id}")))?;
462
463 Ok(WorkspaceMerge {
464 id: Uuid::parse_str(&row.id)
465 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
466 source_workspace_id: Uuid::parse_str(&row.source_workspace_id)
467 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
468 target_workspace_id: Uuid::parse_str(&row.target_workspace_id)
469 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
470 base_commit_id: Uuid::parse_str(&row.base_commit_id)
471 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
472 source_commit_id: Uuid::parse_str(&row.source_commit_id)
473 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
474 target_commit_id: Uuid::parse_str(&row.target_commit_id)
475 .map_err(|e| CollabError::Internal(format!("Invalid UUID: {e}")))?,
476 merge_commit_id: row.merge_commit_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
477 status: serde_json::from_str(&row.status)
478 .map_err(|e| CollabError::Internal(format!("Invalid status: {e}")))?,
479 conflict_data: row.conflict_data.as_ref().and_then(|s| serde_json::from_str(s).ok()),
480 merged_by: row.merged_by.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
481 merged_at: row
482 .merged_at
483 .as_ref()
484 .map(|s| {
485 chrono::DateTime::parse_from_rfc3339(s)
486 .map(|dt| dt.with_timezone(&Utc))
487 .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))
488 })
489 .transpose()?,
490 created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
491 .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
492 .with_timezone(&Utc),
493 })
494 }
495
496 pub async fn list_merges(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMerge>> {
498 let rows = sqlx::query!(
499 r#"
500 SELECT
501 id as "id: Uuid",
502 source_workspace_id as "source_workspace_id: Uuid",
503 target_workspace_id as "target_workspace_id: Uuid",
504 base_commit_id as "base_commit_id: Uuid",
505 source_commit_id as "source_commit_id: Uuid",
506 target_commit_id as "target_commit_id: Uuid",
507 merge_commit_id as "merge_commit_id: Uuid",
508 status,
509 conflict_data,
510 merged_by as "merged_by: Uuid",
511 merged_at,
512 created_at
513 FROM workspace_merges
514 WHERE source_workspace_id = ? OR target_workspace_id = ?
515 ORDER BY created_at DESC
516 "#,
517 workspace_id,
518 workspace_id
519 )
520 .fetch_all(&self.db)
521 .await?;
522
523 let merges: Result<Vec<WorkspaceMerge>> = rows
524 .into_iter()
525 .map(|row| {
526 let status = match row.status.as_str() {
527 "pending" => MergeStatus::Pending,
528 "in_progress" => MergeStatus::InProgress,
529 "completed" => MergeStatus::Completed,
530 "conflict" => MergeStatus::Conflict,
531 "cancelled" => MergeStatus::Cancelled,
532 other => return Err(CollabError::Internal(format!("Invalid status: {other}"))),
533 };
534 Ok(WorkspaceMerge {
535 id: row.id,
536 source_workspace_id: row.source_workspace_id,
537 target_workspace_id: row.target_workspace_id,
538 base_commit_id: row.base_commit_id,
539 source_commit_id: row.source_commit_id,
540 target_commit_id: row.target_commit_id,
541 merge_commit_id: row.merge_commit_id,
542 status,
543 conflict_data: row
544 .conflict_data
545 .as_ref()
546 .and_then(|s| serde_json::from_str(s).ok()),
547 merged_by: row.merged_by,
548 merged_at: row
549 .merged_at
550 .as_ref()
551 .map(|s| {
552 chrono::DateTime::parse_from_rfc3339(s)
553 .map(|dt| dt.with_timezone(&Utc))
554 .map_err(|e| {
555 CollabError::Internal(format!("Invalid timestamp: {e}"))
556 })
557 })
558 .transpose()?,
559 created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)
560 .map_err(|e| CollabError::Internal(format!("Invalid timestamp: {e}")))?
561 .with_timezone(&Utc),
562 })
563 })
564 .collect();
565 let merges = merges?;
566
567 Ok(merges)
568 }
569
570 async fn build_ancestor_set(&self, commit_id: Uuid) -> Result<std::collections::HashSet<Uuid>> {
572 let mut ancestors = std::collections::HashSet::new();
573 let mut current_id = Some(commit_id);
574 let mut visited = std::collections::HashSet::new();
575
576 let max_depth = 1000;
578 let mut depth = 0;
579
580 while let Some(id) = current_id {
581 if visited.contains(&id) || depth > max_depth {
582 break; }
584 visited.insert(id);
585 ancestors.insert(id);
586
587 match self.version_control.get_commit(id).await {
589 Ok(commit) => {
590 current_id = commit.parent_id;
591 depth += 1;
592 }
593 Err(_) => break, }
595 }
596
597 Ok(ancestors)
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use serde_json::json;
605 use sqlx::SqlitePool;
606
607 async fn setup_test_db() -> Pool<Sqlite> {
608 let pool = SqlitePool::connect(":memory:").await.unwrap();
609
610 sqlx::query(
612 r#"
613 CREATE TABLE IF NOT EXISTS workspace_forks (
614 id TEXT PRIMARY KEY,
615 source_workspace_id TEXT NOT NULL,
616 forked_workspace_id TEXT NOT NULL,
617 fork_point_commit_id TEXT,
618 created_at TEXT NOT NULL,
619 created_by TEXT NOT NULL
620 )
621 "#,
622 )
623 .execute(&pool)
624 .await
625 .unwrap();
626
627 sqlx::query(
629 r#"
630 CREATE TABLE IF NOT EXISTS workspace_merges (
631 id TEXT PRIMARY KEY,
632 source_workspace_id TEXT NOT NULL,
633 target_workspace_id TEXT NOT NULL,
634 base_commit_id TEXT NOT NULL,
635 source_commit_id TEXT NOT NULL,
636 target_commit_id TEXT NOT NULL,
637 merge_commit_id TEXT,
638 status TEXT NOT NULL,
639 conflict_data TEXT,
640 merged_by TEXT,
641 merged_at TEXT,
642 created_at TEXT NOT NULL
643 )
644 "#,
645 )
646 .execute(&pool)
647 .await
648 .unwrap();
649
650 sqlx::query(
652 r#"
653 CREATE TABLE IF NOT EXISTS commits (
654 id TEXT PRIMARY KEY,
655 workspace_id TEXT NOT NULL,
656 user_id TEXT NOT NULL,
657 message TEXT NOT NULL,
658 parent_id TEXT,
659 version INTEGER NOT NULL,
660 snapshot TEXT NOT NULL,
661 metadata TEXT,
662 created_at TEXT NOT NULL
663 )
664 "#,
665 )
666 .execute(&pool)
667 .await
668 .unwrap();
669
670 pool
671 }
672
673 #[tokio::test]
674 async fn test_merge_service_new() {
675 let pool = setup_test_db().await;
676 let service = MergeService::new(pool);
677
678 assert!(true);
681 }
682
683 #[test]
684 fn test_three_way_merge_no_changes() {
685 let pool_fut = setup_test_db();
686 let rt = tokio::runtime::Runtime::new().unwrap();
687 let pool = rt.block_on(pool_fut);
688 let service = MergeService::new(pool);
689
690 let base = json!({"key": "value"});
691 let source = json!({"key": "value"});
692 let target = json!({"key": "value"});
693
694 let result = service.three_way_merge(&base, &source, &target);
695 assert!(result.is_ok());
696
697 let (merged, conflicts) = result.unwrap();
698 assert_eq!(merged, target);
699 assert!(conflicts.is_empty());
700 }
701
702 #[test]
703 fn test_three_way_merge_source_change() {
704 let pool_fut = setup_test_db();
705 let rt = tokio::runtime::Runtime::new().unwrap();
706 let pool = rt.block_on(pool_fut);
707 let service = MergeService::new(pool);
708
709 let base = json!({"key": "value"});
710 let source = json!({"key": "new_value"});
711 let target = json!({"key": "value"});
712
713 let result = service.three_way_merge(&base, &source, &target);
714 assert!(result.is_ok());
715
716 let (merged, conflicts) = result.unwrap();
717 assert_eq!(merged, source);
718 assert!(conflicts.is_empty());
719 }
720
721 #[test]
722 fn test_three_way_merge_target_change() {
723 let pool_fut = setup_test_db();
724 let rt = tokio::runtime::Runtime::new().unwrap();
725 let pool = rt.block_on(pool_fut);
726 let service = MergeService::new(pool);
727
728 let base = json!({"key": "value"});
729 let source = json!({"key": "value"});
730 let target = json!({"key": "new_value"});
731
732 let result = service.three_way_merge(&base, &source, &target);
733 assert!(result.is_ok());
734
735 let (merged, conflicts) = result.unwrap();
736 assert_eq!(merged, target);
737 assert!(conflicts.is_empty());
738 }
739
740 #[test]
741 fn test_three_way_merge_both_changed_same() {
742 let pool_fut = setup_test_db();
743 let rt = tokio::runtime::Runtime::new().unwrap();
744 let pool = rt.block_on(pool_fut);
745 let service = MergeService::new(pool);
746
747 let base = json!({"key": "value"});
748 let source = json!({"key": "new_value"});
749 let target = json!({"key": "new_value"});
750
751 let result = service.three_way_merge(&base, &source, &target);
752 assert!(result.is_ok());
753
754 let (merged, conflicts) = result.unwrap();
755 assert_eq!(merged, source);
756 assert!(conflicts.is_empty());
757 }
758
759 #[test]
760 fn test_three_way_merge_conflict() {
761 let pool_fut = setup_test_db();
762 let rt = tokio::runtime::Runtime::new().unwrap();
763 let pool = rt.block_on(pool_fut);
764 let service = MergeService::new(pool);
765
766 let base = json!({"key": "value"});
767 let source = json!({"key": "source_value"});
768 let target = json!({"key": "target_value"});
769
770 let result = service.three_way_merge(&base, &source, &target);
771 assert!(result.is_ok());
772
773 let (merged, conflicts) = result.unwrap();
774 assert_eq!(merged, target); assert_eq!(conflicts.len(), 1);
776 assert_eq!(conflicts[0].path, "key");
777 assert_eq!(conflicts[0].conflict_type, ConflictType::Modified);
778 }
779
780 #[test]
781 fn test_three_way_merge_object_add_source() {
782 let pool_fut = setup_test_db();
783 let rt = tokio::runtime::Runtime::new().unwrap();
784 let pool = rt.block_on(pool_fut);
785 let service = MergeService::new(pool);
786
787 let base = json!({});
788 let source = json!({"new_key": "value"});
789 let target = json!({});
790
791 let result = service.three_way_merge(&base, &source, &target);
792 assert!(result.is_ok());
793
794 let (merged, conflicts) = result.unwrap();
795 assert_eq!(merged.get("new_key"), Some(&json!("value")));
796 assert!(conflicts.is_empty());
797 }
798
799 #[test]
800 fn test_three_way_merge_object_add_target() {
801 let pool_fut = setup_test_db();
802 let rt = tokio::runtime::Runtime::new().unwrap();
803 let pool = rt.block_on(pool_fut);
804 let service = MergeService::new(pool);
805
806 let base = json!({});
807 let source = json!({});
808 let target = json!({"new_key": "value"});
809
810 let result = service.three_way_merge(&base, &source, &target);
811 assert!(result.is_ok());
812
813 let (merged, conflicts) = result.unwrap();
814 assert_eq!(merged.get("new_key"), Some(&json!("value")));
815 assert!(conflicts.is_empty());
816 }
817
818 #[test]
819 fn test_three_way_merge_both_added_different() {
820 let pool_fut = setup_test_db();
821 let rt = tokio::runtime::Runtime::new().unwrap();
822 let pool = rt.block_on(pool_fut);
823 let service = MergeService::new(pool);
824
825 let base = json!({});
826 let source = json!({"key": "source_value"});
827 let target = json!({"key": "target_value"});
828
829 let result = service.three_way_merge(&base, &source, &target);
830 assert!(result.is_ok());
831
832 let (merged, conflicts) = result.unwrap();
833 assert_eq!(conflicts.len(), 1);
834 assert_eq!(conflicts[0].conflict_type, ConflictType::BothAdded);
835 }
836
837 #[test]
838 fn test_three_way_merge_nested_objects() {
839 let pool_fut = setup_test_db();
840 let rt = tokio::runtime::Runtime::new().unwrap();
841 let pool = rt.block_on(pool_fut);
842 let service = MergeService::new(pool);
843
844 let base = json!({
845 "parent": {
846 "child": "value"
847 }
848 });
849 let source = json!({
850 "parent": {
851 "child": "new_value"
852 }
853 });
854 let target = json!({
855 "parent": {
856 "child": "value"
857 }
858 });
859
860 let result = service.three_way_merge(&base, &source, &target);
861 assert!(result.is_ok());
862
863 let (merged, conflicts) = result.unwrap();
864 assert_eq!(merged["parent"]["child"], json!("new_value"));
865 assert!(conflicts.is_empty());
866 }
867
868 #[test]
869 fn test_three_way_merge_arrays_no_conflict() {
870 let pool_fut = setup_test_db();
871 let rt = tokio::runtime::Runtime::new().unwrap();
872 let pool = rt.block_on(pool_fut);
873 let service = MergeService::new(pool);
874
875 let base = json!([1, 2, 3]);
876 let source = json!([1, 2, 3]);
877 let target = json!([1, 2, 3]);
878
879 let result = service.three_way_merge(&base, &source, &target);
880 assert!(result.is_ok());
881
882 let (merged, conflicts) = result.unwrap();
883 assert_eq!(merged, target);
884 assert!(conflicts.is_empty());
885 }
886
887 #[test]
888 fn test_three_way_merge_arrays_conflict() {
889 let pool_fut = setup_test_db();
890 let rt = tokio::runtime::Runtime::new().unwrap();
891 let pool = rt.block_on(pool_fut);
892 let service = MergeService::new(pool);
893
894 let base = json!([1, 2, 3]);
895 let source = json!([1, 2, 4]);
896 let target = json!([1, 2, 5]);
897
898 let result = service.three_way_merge(&base, &source, &target);
899 assert!(result.is_ok());
900
901 let (merged, conflicts) = result.unwrap();
902 assert_eq!(merged, target);
903 assert_eq!(conflicts.len(), 1);
904 }
905
906 #[test]
907 fn test_workspace_merge_new() {
908 let source_ws = Uuid::new_v4();
909 let target_ws = Uuid::new_v4();
910 let base_commit = Uuid::new_v4();
911 let source_commit = Uuid::new_v4();
912 let target_commit = Uuid::new_v4();
913
914 let merge =
915 WorkspaceMerge::new(source_ws, target_ws, base_commit, source_commit, target_commit);
916
917 assert_eq!(merge.source_workspace_id, source_ws);
918 assert_eq!(merge.target_workspace_id, target_ws);
919 assert_eq!(merge.base_commit_id, base_commit);
920 assert_eq!(merge.source_commit_id, source_commit);
921 assert_eq!(merge.target_commit_id, target_commit);
922 assert_eq!(merge.status, MergeStatus::Pending);
923 assert!(merge.merge_commit_id.is_none());
924 }
925
926 #[test]
927 fn test_merge_conflict_types() {
928 assert_eq!(ConflictType::Modified, ConflictType::Modified);
929 assert_eq!(ConflictType::BothAdded, ConflictType::BothAdded);
930 assert_eq!(ConflictType::DeletedModified, ConflictType::DeletedModified);
931
932 assert_ne!(ConflictType::Modified, ConflictType::BothAdded);
933 }
934
935 #[test]
936 fn test_merge_status_equality() {
937 assert_eq!(MergeStatus::Pending, MergeStatus::Pending);
938 assert_eq!(MergeStatus::Conflict, MergeStatus::Conflict);
939 assert_eq!(MergeStatus::Completed, MergeStatus::Completed);
940
941 assert_ne!(MergeStatus::Pending, MergeStatus::Completed);
942 }
943}