mockforge_collab/
conflict.rs

1//! Conflict resolution for concurrent edits
2
3use crate::error::{CollabError, Result};
4use serde::{Deserialize, Serialize};
5use similar::{ChangeTag, TextDiff};
6
7/// Merge strategy for conflict resolution
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum MergeStrategy {
11    /// Keep local changes (ours)
12    Ours,
13    /// Keep remote changes (theirs)
14    Theirs,
15    /// Attempt automatic merge
16    Auto,
17    /// Manual resolution required
18    Manual,
19}
20
21/// Conflict resolution result
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ConflictResolution {
24    /// Whether conflicts were detected
25    pub has_conflicts: bool,
26    /// Resolved content
27    pub resolved: serde_json::Value,
28    /// List of conflicts (if manual resolution needed)
29    pub conflicts: Vec<Conflict>,
30}
31
32/// A conflict between two versions
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Conflict {
35    /// Path to the conflicting field
36    pub path: String,
37    /// Local value
38    pub ours: serde_json::Value,
39    /// Remote value
40    pub theirs: serde_json::Value,
41    /// Common ancestor value (if available)
42    pub base: Option<serde_json::Value>,
43}
44
45/// Conflict resolver
46pub struct ConflictResolver {
47    /// Default merge strategy
48    default_strategy: MergeStrategy,
49}
50
51impl ConflictResolver {
52    /// Create a new conflict resolver
53    pub fn new(default_strategy: MergeStrategy) -> Self {
54        Self { default_strategy }
55    }
56
57    /// Resolve conflicts between two versions
58    pub fn resolve(
59        &self,
60        base: Option<&serde_json::Value>,
61        ours: &serde_json::Value,
62        theirs: &serde_json::Value,
63        strategy: Option<MergeStrategy>,
64    ) -> Result<ConflictResolution> {
65        let strategy = strategy.unwrap_or(self.default_strategy);
66
67        // If values are identical, no conflict
68        if ours == theirs {
69            return Ok(ConflictResolution {
70                has_conflicts: false,
71                resolved: ours.clone(),
72                conflicts: Vec::new(),
73            });
74        }
75
76        match strategy {
77            MergeStrategy::Ours => Ok(ConflictResolution {
78                has_conflicts: false,
79                resolved: ours.clone(),
80                conflicts: Vec::new(),
81            }),
82            MergeStrategy::Theirs => Ok(ConflictResolution {
83                has_conflicts: false,
84                resolved: theirs.clone(),
85                conflicts: Vec::new(),
86            }),
87            MergeStrategy::Auto => self.auto_merge(base, ours, theirs),
88            MergeStrategy::Manual => {
89                // Detect all conflicts and return for manual resolution
90                let conflicts = self.detect_conflicts("", base, ours, theirs);
91                Ok(ConflictResolution {
92                    has_conflicts: !conflicts.is_empty(),
93                    resolved: ours.clone(), // Default to ours
94                    conflicts,
95                })
96            }
97        }
98    }
99
100    /// Attempt automatic merge
101    fn auto_merge(
102        &self,
103        base: Option<&serde_json::Value>,
104        ours: &serde_json::Value,
105        theirs: &serde_json::Value,
106    ) -> Result<ConflictResolution> {
107        // For JSON objects, try field-by-field merge
108        match (ours, theirs) {
109            (serde_json::Value::Object(ours_obj), serde_json::Value::Object(theirs_obj)) => {
110                let mut resolved = serde_json::Map::new();
111                let mut conflicts = Vec::new();
112
113                let base_obj = base.and_then(|b| b.as_object());
114
115                // Merge all keys
116                let all_keys: std::collections::HashSet<_> =
117                    ours_obj.keys().chain(theirs_obj.keys()).collect();
118
119                for key in all_keys {
120                    let ours_val = ours_obj.get(key);
121                    let theirs_val = theirs_obj.get(key);
122                    let base_val = base_obj.and_then(|b| b.get(key));
123
124                    match (ours_val, theirs_val) {
125                        (Some(o), Some(t)) if o == t => {
126                            // No conflict, values are the same
127                            resolved.insert(key.clone(), o.clone());
128                        }
129                        (Some(o), Some(t)) => {
130                            // Check if only one side changed from base
131                            if let Some(base_val) = base_val {
132                                if o == base_val {
133                                    // Only theirs changed
134                                    resolved.insert(key.clone(), t.clone());
135                                } else if t == base_val {
136                                    // Only ours changed
137                                    resolved.insert(key.clone(), o.clone());
138                                } else {
139                                    // Both changed - conflict
140                                    conflicts.push(Conflict {
141                                        path: key.clone(),
142                                        ours: o.clone(),
143                                        theirs: t.clone(),
144                                        base: Some(base_val.clone()),
145                                    });
146                                    resolved.insert(key.clone(), o.clone()); // Default to ours
147                                }
148                            } else {
149                                // No base - conflict
150                                conflicts.push(Conflict {
151                                    path: key.clone(),
152                                    ours: o.clone(),
153                                    theirs: t.clone(),
154                                    base: None,
155                                });
156                                resolved.insert(key.clone(), o.clone());
157                            }
158                        }
159                        (Some(o), None) => {
160                            // Only in ours
161                            resolved.insert(key.clone(), o.clone());
162                        }
163                        (None, Some(t)) => {
164                            // Only in theirs
165                            resolved.insert(key.clone(), t.clone());
166                        }
167                        (None, None) => unreachable!(),
168                    }
169                }
170
171                Ok(ConflictResolution {
172                    has_conflicts: !conflicts.is_empty(),
173                    resolved: serde_json::Value::Object(resolved),
174                    conflicts,
175                })
176            }
177            _ => {
178                // For non-objects, treat as conflict
179                Ok(ConflictResolution {
180                    has_conflicts: true,
181                    resolved: ours.clone(),
182                    conflicts: vec![Conflict {
183                        path: String::new(),
184                        ours: ours.clone(),
185                        theirs: theirs.clone(),
186                        base: base.cloned(),
187                    }],
188                })
189            }
190        }
191    }
192
193    /// Detect all conflicts recursively
194    fn detect_conflicts(
195        &self,
196        path: &str,
197        base: Option<&serde_json::Value>,
198        ours: &serde_json::Value,
199        theirs: &serde_json::Value,
200    ) -> Vec<Conflict> {
201        let mut conflicts = Vec::new();
202
203        if ours == theirs {
204            return conflicts;
205        }
206
207        match (ours, theirs) {
208            (serde_json::Value::Object(ours_obj), serde_json::Value::Object(theirs_obj)) => {
209                let base_obj = base.and_then(|b| b.as_object());
210                let all_keys: std::collections::HashSet<_> =
211                    ours_obj.keys().chain(theirs_obj.keys()).collect();
212
213                for key in all_keys {
214                    let new_path = if path.is_empty() {
215                        key.clone()
216                    } else {
217                        format!("{}.{}", path, key)
218                    };
219
220                    let ours_val = ours_obj.get(key);
221                    let theirs_val = theirs_obj.get(key);
222                    let base_val = base_obj.and_then(|b| b.get(key));
223
224                    if let (Some(o), Some(t)) = (ours_val, theirs_val) {
225                        conflicts.extend(self.detect_conflicts(&new_path, base_val, o, t));
226                    } else if ours_val != theirs_val {
227                        conflicts.push(Conflict {
228                            path: new_path,
229                            ours: ours_val.cloned().unwrap_or(serde_json::Value::Null),
230                            theirs: theirs_val.cloned().unwrap_or(serde_json::Value::Null),
231                            base: base_val.cloned(),
232                        });
233                    }
234                }
235            }
236            _ => {
237                conflicts.push(Conflict {
238                    path: path.to_string(),
239                    ours: ours.clone(),
240                    theirs: theirs.clone(),
241                    base: base.cloned(),
242                });
243            }
244        }
245
246        conflicts
247    }
248
249    /// Merge text with three-way merge algorithm
250    pub fn merge_text(&self, base: &str, ours: &str, theirs: &str) -> Result<String> {
251        if ours == theirs {
252            return Ok(ours.to_string());
253        }
254
255        // Simple line-based three-way merge
256        let diff_ours = TextDiff::from_lines(base, ours);
257        let _diff_theirs = TextDiff::from_lines(base, theirs);
258
259        let mut result = String::new();
260        let has_conflict = false;
261
262        // This is a simplified merge - a real implementation would be more sophisticated
263        for change in diff_ours.iter_all_changes() {
264            match change.tag() {
265                ChangeTag::Equal => result.push_str(change.value()),
266                ChangeTag::Delete => {}
267                ChangeTag::Insert => result.push_str(change.value()),
268            }
269        }
270
271        if has_conflict {
272            Err(CollabError::ConflictDetected("Text merge conflict".to_string()))
273        } else {
274            Ok(result)
275        }
276    }
277}
278
279impl Default for ConflictResolver {
280    fn default() -> Self {
281        Self::new(MergeStrategy::Auto)
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use serde_json::json;
289
290    #[test]
291    fn test_no_conflict() {
292        let resolver = ConflictResolver::default();
293        let value = json!({"key": "value"});
294
295        let result = resolver.resolve(None, &value, &value, None).unwrap();
296
297        assert!(!result.has_conflicts);
298        assert_eq!(result.resolved, value);
299        assert!(result.conflicts.is_empty());
300    }
301
302    #[test]
303    fn test_strategy_ours() {
304        let resolver = ConflictResolver::default();
305        let ours = json!({"key": "ours"});
306        let theirs = json!({"key": "theirs"});
307
308        let result = resolver.resolve(None, &ours, &theirs, Some(MergeStrategy::Ours)).unwrap();
309
310        assert!(!result.has_conflicts);
311        assert_eq!(result.resolved, ours);
312    }
313
314    #[test]
315    fn test_strategy_theirs() {
316        let resolver = ConflictResolver::default();
317        let ours = json!({"key": "ours"});
318        let theirs = json!({"key": "theirs"});
319
320        let result = resolver.resolve(None, &ours, &theirs, Some(MergeStrategy::Theirs)).unwrap();
321
322        assert!(!result.has_conflicts);
323        assert_eq!(result.resolved, theirs);
324    }
325
326    #[test]
327    fn test_auto_merge_no_base() {
328        let resolver = ConflictResolver::default();
329        let ours = json!({"key1": "value1"});
330        let theirs = json!({"key2": "value2"});
331
332        let result = resolver.resolve(None, &ours, &theirs, Some(MergeStrategy::Auto)).unwrap();
333
334        // Should merge both keys
335        assert!(!result.has_conflicts);
336        assert_eq!(result.resolved["key1"], "value1");
337        assert_eq!(result.resolved["key2"], "value2");
338    }
339
340    #[test]
341    fn test_auto_merge_with_base() {
342        let resolver = ConflictResolver::default();
343        let base = json!({"key": "base"});
344        let ours = json!({"key": "ours"});
345        let theirs = json!({"key": "base"}); // Only ours changed
346
347        let result = resolver
348            .resolve(Some(&base), &ours, &theirs, Some(MergeStrategy::Auto))
349            .unwrap();
350
351        assert!(!result.has_conflicts);
352        assert_eq!(result.resolved["key"], "ours");
353    }
354
355    #[test]
356    fn test_conflict_detection() {
357        let resolver = ConflictResolver::default();
358        let base = json!({"key": "base"});
359        let ours = json!({"key": "ours"});
360        let theirs = json!({"key": "theirs"});
361
362        let result = resolver
363            .resolve(Some(&base), &ours, &theirs, Some(MergeStrategy::Auto))
364            .unwrap();
365
366        assert!(result.has_conflicts);
367        assert_eq!(result.conflicts.len(), 1);
368        assert_eq!(result.conflicts[0].path, "key");
369    }
370}