1use crate::error::{CollabError, Result};
4use serde::{Deserialize, Serialize};
5use similar::{ChangeTag, TextDiff};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum MergeStrategy {
11 Ours,
13 Theirs,
15 Auto,
17 Manual,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ConflictResolution {
24 pub has_conflicts: bool,
26 pub resolved: serde_json::Value,
28 pub conflicts: Vec<Conflict>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Conflict {
35 pub path: String,
37 pub ours: serde_json::Value,
39 pub theirs: serde_json::Value,
41 pub base: Option<serde_json::Value>,
43}
44
45pub struct ConflictResolver {
47 default_strategy: MergeStrategy,
49}
50
51impl ConflictResolver {
52 #[must_use]
54 pub const fn new(default_strategy: MergeStrategy) -> Self {
55 Self { default_strategy }
56 }
57
58 pub fn resolve(
64 &self,
65 base: Option<&serde_json::Value>,
66 ours: &serde_json::Value,
67 theirs: &serde_json::Value,
68 strategy: Option<MergeStrategy>,
69 ) -> Result<ConflictResolution> {
70 let strategy = strategy.unwrap_or(self.default_strategy);
71
72 if ours == theirs {
74 return Ok(ConflictResolution {
75 has_conflicts: false,
76 resolved: ours.clone(),
77 conflicts: Vec::new(),
78 });
79 }
80
81 match strategy {
82 MergeStrategy::Ours => Ok(ConflictResolution {
83 has_conflicts: false,
84 resolved: ours.clone(),
85 conflicts: Vec::new(),
86 }),
87 MergeStrategy::Theirs => Ok(ConflictResolution {
88 has_conflicts: false,
89 resolved: theirs.clone(),
90 conflicts: Vec::new(),
91 }),
92 MergeStrategy::Auto => self.auto_merge(base, ours, theirs),
93 MergeStrategy::Manual => {
94 let conflicts = self.detect_conflicts("", base, ours, theirs);
96 Ok(ConflictResolution {
97 has_conflicts: !conflicts.is_empty(),
98 resolved: ours.clone(), conflicts,
100 })
101 }
102 }
103 }
104
105 #[allow(clippy::unnecessary_wraps, clippy::unused_self)]
107 fn auto_merge(
108 &self,
109 base: Option<&serde_json::Value>,
110 ours: &serde_json::Value,
111 theirs: &serde_json::Value,
112 ) -> Result<ConflictResolution> {
113 match (ours, theirs) {
115 (serde_json::Value::Object(ours_obj), serde_json::Value::Object(theirs_obj)) => {
116 let mut resolved = serde_json::Map::new();
117 let mut conflicts = Vec::new();
118
119 let base_obj = base.and_then(|b| b.as_object());
120
121 let all_keys: std::collections::HashSet<_> =
123 ours_obj.keys().chain(theirs_obj.keys()).collect();
124
125 for key in all_keys {
126 let ours_val = ours_obj.get(key);
127 let theirs_val = theirs_obj.get(key);
128 let base_val = base_obj.and_then(|b| b.get(key));
129
130 match (ours_val, theirs_val) {
131 (Some(o), Some(t)) if o == t => {
132 resolved.insert(key.clone(), o.clone());
134 }
135 (Some(o), Some(t)) => {
136 if let Some(base_val) = base_val {
138 if o == base_val {
139 resolved.insert(key.clone(), t.clone());
141 } else if t == base_val {
142 resolved.insert(key.clone(), o.clone());
144 } else {
145 conflicts.push(Conflict {
147 path: key.clone(),
148 ours: o.clone(),
149 theirs: t.clone(),
150 base: Some(base_val.clone()),
151 });
152 resolved.insert(key.clone(), o.clone()); }
154 } else {
155 conflicts.push(Conflict {
157 path: key.clone(),
158 ours: o.clone(),
159 theirs: t.clone(),
160 base: None,
161 });
162 resolved.insert(key.clone(), o.clone());
163 }
164 }
165 (Some(o), None) => {
166 resolved.insert(key.clone(), o.clone());
168 }
169 (None, Some(t)) => {
170 resolved.insert(key.clone(), t.clone());
172 }
173 (None, None) => unreachable!(),
174 }
175 }
176
177 Ok(ConflictResolution {
178 has_conflicts: !conflicts.is_empty(),
179 resolved: serde_json::Value::Object(resolved),
180 conflicts,
181 })
182 }
183 _ => {
184 Ok(ConflictResolution {
186 has_conflicts: true,
187 resolved: ours.clone(),
188 conflicts: vec![Conflict {
189 path: String::new(),
190 ours: ours.clone(),
191 theirs: theirs.clone(),
192 base: base.cloned(),
193 }],
194 })
195 }
196 }
197 }
198
199 #[allow(clippy::only_used_in_recursion)]
201 fn detect_conflicts(
202 &self,
203 path: &str,
204 base: Option<&serde_json::Value>,
205 ours: &serde_json::Value,
206 theirs: &serde_json::Value,
207 ) -> Vec<Conflict> {
208 let mut conflicts = Vec::new();
209
210 if ours == theirs {
211 return conflicts;
212 }
213
214 match (ours, theirs) {
215 (serde_json::Value::Object(ours_obj), serde_json::Value::Object(theirs_obj)) => {
216 let base_obj = base.and_then(|b| b.as_object());
217 let all_keys: std::collections::HashSet<_> =
218 ours_obj.keys().chain(theirs_obj.keys()).collect();
219
220 for key in all_keys {
221 let new_path = if path.is_empty() {
222 key.clone()
223 } else {
224 format!("{path}.{key}")
225 };
226
227 let ours_val = ours_obj.get(key);
228 let theirs_val = theirs_obj.get(key);
229 let base_val = base_obj.and_then(|b| b.get(key));
230
231 if let (Some(o), Some(t)) = (ours_val, theirs_val) {
232 conflicts.extend(self.detect_conflicts(&new_path, base_val, o, t));
233 } else if ours_val != theirs_val {
234 conflicts.push(Conflict {
235 path: new_path,
236 ours: ours_val.cloned().unwrap_or(serde_json::Value::Null),
237 theirs: theirs_val.cloned().unwrap_or(serde_json::Value::Null),
238 base: base_val.cloned(),
239 });
240 }
241 }
242 }
243 _ => {
244 conflicts.push(Conflict {
245 path: path.to_string(),
246 ours: ours.clone(),
247 theirs: theirs.clone(),
248 base: base.cloned(),
249 });
250 }
251 }
252
253 conflicts
254 }
255
256 #[allow(clippy::similar_names)]
267 pub fn merge_text(&self, base: &str, ours: &str, theirs: &str) -> Result<String> {
268 if ours == theirs {
269 return Ok(ours.to_string());
270 }
271
272 let base_lines: Vec<&str> = base.lines().collect();
274 let ours_lines: Vec<&str> = ours.lines().collect();
275 let theirs_lines: Vec<&str> = theirs.lines().collect();
276
277 let diff_ours = TextDiff::from_lines(base, ours);
279 let diff_theirs = TextDiff::from_lines(base, theirs);
280
281 let ours_changes = Self::collect_line_changes(&diff_ours);
283 let theirs_changes = Self::collect_line_changes(&diff_theirs);
284
285 let mut result = String::new();
286 let mut has_conflict = false;
287
288 for (i, base_line) in base_lines.iter().enumerate() {
290 let ours_changed = ours_changes.get(&i);
291 let theirs_changed = theirs_changes.get(&i);
292
293 match (ours_changed, theirs_changed) {
294 (None, None) => {
295 result.push_str(base_line);
297 result.push('\n');
298 }
299 (Some(ours_replacement), None) => {
300 for line in ours_replacement {
302 result.push_str(line);
303 result.push('\n');
304 }
305 }
306 (None, Some(theirs_replacement)) => {
307 for line in theirs_replacement {
309 result.push_str(line);
310 result.push('\n');
311 }
312 }
313 (Some(ours_replacement), Some(theirs_replacement)) => {
314 if ours_replacement == theirs_replacement {
315 for line in ours_replacement {
317 result.push_str(line);
318 result.push('\n');
319 }
320 } else {
321 has_conflict = true;
323 }
324 }
325 }
326 }
327
328 for line in ours_lines.iter().skip(base_lines.len()) {
330 result.push_str(line);
331 result.push('\n');
332 }
333 for line in theirs_lines.iter().skip(base_lines.len()) {
335 result.push_str(line);
336 result.push('\n');
337 }
338
339 if has_conflict {
340 Err(CollabError::ConflictDetected("Text merge conflict".to_string()))
341 } else {
342 Ok(result)
343 }
344 }
345
346 fn collect_line_changes<'a>(
351 diff: &TextDiff<'a, 'a, 'a, str>,
352 ) -> std::collections::HashMap<usize, Vec<&'a str>> {
353 let mut changes: std::collections::HashMap<usize, Vec<&str>> =
354 std::collections::HashMap::new();
355 let mut base_idx: usize = 0;
356
357 for change in diff.iter_all_changes() {
358 match change.tag() {
359 ChangeTag::Equal => {
360 base_idx += 1;
361 }
362 ChangeTag::Delete => {
363 changes.entry(base_idx).or_default();
365 base_idx += 1;
366 }
367 ChangeTag::Insert => {
368 let idx = if base_idx > 0 { base_idx - 1 } else { 0 };
370 changes.entry(idx).or_default().push(change.value().trim_end_matches('\n'));
371 }
372 }
373 }
374
375 changes
376 }
377}
378
379impl Default for ConflictResolver {
380 fn default() -> Self {
381 Self::new(MergeStrategy::Auto)
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use serde_json::json;
389
390 #[test]
391 fn test_no_conflict() {
392 let resolver = ConflictResolver::default();
393 let value = json!({"key": "value"});
394
395 let result = resolver.resolve(None, &value, &value, None).unwrap();
396
397 assert!(!result.has_conflicts);
398 assert_eq!(result.resolved, value);
399 assert!(result.conflicts.is_empty());
400 }
401
402 #[test]
403 fn test_strategy_ours() {
404 let resolver = ConflictResolver::default();
405 let ours = json!({"key": "ours"});
406 let theirs = json!({"key": "theirs"});
407
408 let result = resolver.resolve(None, &ours, &theirs, Some(MergeStrategy::Ours)).unwrap();
409
410 assert!(!result.has_conflicts);
411 assert_eq!(result.resolved, ours);
412 }
413
414 #[test]
415 fn test_strategy_theirs() {
416 let resolver = ConflictResolver::default();
417 let ours = json!({"key": "ours"});
418 let theirs = json!({"key": "theirs"});
419
420 let result = resolver.resolve(None, &ours, &theirs, Some(MergeStrategy::Theirs)).unwrap();
421
422 assert!(!result.has_conflicts);
423 assert_eq!(result.resolved, theirs);
424 }
425
426 #[test]
427 fn test_auto_merge_no_base() {
428 let resolver = ConflictResolver::default();
429 let ours = json!({"key1": "value1"});
430 let theirs = json!({"key2": "value2"});
431
432 let result = resolver.resolve(None, &ours, &theirs, Some(MergeStrategy::Auto)).unwrap();
433
434 assert!(!result.has_conflicts);
436 assert_eq!(result.resolved["key1"], "value1");
437 assert_eq!(result.resolved["key2"], "value2");
438 }
439
440 #[test]
441 fn test_auto_merge_with_base() {
442 let resolver = ConflictResolver::default();
443 let base = json!({"key": "base"});
444 let ours = json!({"key": "ours"});
445 let theirs = json!({"key": "base"}); let result = resolver
448 .resolve(Some(&base), &ours, &theirs, Some(MergeStrategy::Auto))
449 .unwrap();
450
451 assert!(!result.has_conflicts);
452 assert_eq!(result.resolved["key"], "ours");
453 }
454
455 #[test]
456 fn test_conflict_detection() {
457 let resolver = ConflictResolver::default();
458 let base = json!({"key": "base"});
459 let ours = json!({"key": "ours"});
460 let theirs = json!({"key": "theirs"});
461
462 let result = resolver
463 .resolve(Some(&base), &ours, &theirs, Some(MergeStrategy::Auto))
464 .unwrap();
465
466 assert!(result.has_conflicts);
467 assert_eq!(result.conflicts.len(), 1);
468 assert_eq!(result.conflicts[0].path, "key");
469 }
470}