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