Skip to main content

oxihuman_core/
merge_conflict_resolver.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! 3-way merge conflict resolver.
6
7/// Result of a 3-way merge for one region.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum MergeResult {
10    Clean(Vec<String>),
11    Conflict {
12        ours: Vec<String>,
13        theirs: Vec<String>,
14    },
15}
16
17/// Configuration for merge resolution.
18pub struct MergeConfig {
19    pub label_ours: String,
20    pub label_theirs: String,
21    pub auto_resolve: bool,
22}
23
24impl MergeConfig {
25    pub fn new(label_ours: &str, label_theirs: &str) -> Self {
26        MergeConfig {
27            label_ours: label_ours.to_string(),
28            label_theirs: label_theirs.to_string(),
29            auto_resolve: false,
30        }
31    }
32}
33
34impl Default for MergeConfig {
35    fn default() -> Self {
36        Self::new("ours", "theirs")
37    }
38}
39
40/// Perform a 3-way merge of lines.
41pub fn three_way_merge(base: &[&str], ours: &[&str], theirs: &[&str]) -> MergeResult {
42    /* If both sides match base, return base */
43    if ours == base && theirs == base {
44        return MergeResult::Clean(base.iter().map(|s| s.to_string()).collect());
45    }
46    /* If only ours changed, take ours */
47    if theirs == base {
48        return MergeResult::Clean(ours.iter().map(|s| s.to_string()).collect());
49    }
50    /* If only theirs changed, take theirs */
51    if ours == base {
52        return MergeResult::Clean(theirs.iter().map(|s| s.to_string()).collect());
53    }
54    /* Both changed — conflict */
55    MergeResult::Conflict {
56        ours: ours.iter().map(|s| s.to_string()).collect(),
57        theirs: theirs.iter().map(|s| s.to_string()).collect(),
58    }
59}
60
61/// Count conflicts in a merge result list.
62pub fn count_conflicts(results: &[MergeResult]) -> usize {
63    results
64        .iter()
65        .filter(|r| matches!(r, MergeResult::Conflict { .. }))
66        .count()
67}
68
69/// Format a conflict block with conflict markers.
70pub fn format_conflict(cfg: &MergeConfig, ours: &[String], theirs: &[String]) -> Vec<String> {
71    let mut out = Vec::new();
72    out.push(format!("<<<<<<< {}", cfg.label_ours));
73    out.extend(ours.iter().cloned());
74    out.push("=======".to_string());
75    out.extend(theirs.iter().cloned());
76    out.push(format!(">>>>>>> {}", cfg.label_theirs));
77    out
78}
79
80/// Auto-resolve by preferring ours.
81pub fn auto_resolve_ours(result: MergeResult) -> Vec<String> {
82    match result {
83        MergeResult::Clean(lines) => lines,
84        MergeResult::Conflict { ours, .. } => ours,
85    }
86}
87
88/// Auto-resolve by preferring theirs.
89pub fn auto_resolve_theirs(result: MergeResult) -> Vec<String> {
90    match result {
91        MergeResult::Clean(lines) => lines,
92        MergeResult::Conflict { theirs, .. } => theirs,
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_no_change() {
102        let r = three_way_merge(&["a"], &["a"], &["a"]);
103        assert_eq!(r, MergeResult::Clean(vec!["a".to_string()]));
104    }
105
106    #[test]
107    fn test_ours_only_change() {
108        let r = three_way_merge(&["a"], &["b"], &["a"]);
109        assert_eq!(r, MergeResult::Clean(vec!["b".to_string()]));
110    }
111
112    #[test]
113    fn test_theirs_only_change() {
114        let r = three_way_merge(&["a"], &["a"], &["c"]);
115        assert_eq!(r, MergeResult::Clean(vec!["c".to_string()]));
116    }
117
118    #[test]
119    fn test_conflict() {
120        let r = three_way_merge(&["a"], &["b"], &["c"]);
121        assert!(matches!(r, MergeResult::Conflict { .. }));
122    }
123
124    #[test]
125    fn test_count_conflicts() {
126        let results = vec![
127            MergeResult::Clean(vec![]),
128            MergeResult::Conflict {
129                ours: vec![],
130                theirs: vec![],
131            },
132        ];
133        assert_eq!(count_conflicts(&results), 1);
134    }
135
136    #[test]
137    fn test_format_conflict() {
138        let cfg = MergeConfig::default();
139        let lines = format_conflict(&cfg, &["ours".to_string()], &["theirs".to_string()]);
140        assert!(lines[0].contains("ours"));
141        assert!(lines[lines.len() - 1].contains("theirs"));
142    }
143
144    #[test]
145    fn test_auto_resolve_ours() {
146        let r = MergeResult::Conflict {
147            ours: vec!["mine".to_string()],
148            theirs: vec!["yours".to_string()],
149        };
150        let resolved = auto_resolve_ours(r);
151        assert_eq!(resolved, vec!["mine".to_string()]);
152    }
153
154    #[test]
155    fn test_auto_resolve_theirs() {
156        let r = MergeResult::Conflict {
157            ours: vec!["mine".to_string()],
158            theirs: vec!["yours".to_string()],
159        };
160        let resolved = auto_resolve_theirs(r);
161        assert_eq!(resolved, vec!["yours".to_string()]);
162    }
163
164    #[test]
165    fn test_merge_config_default() {
166        let cfg = MergeConfig::default();
167        assert_eq!(cfg.label_ours, "ours");
168        assert_eq!(cfg.label_theirs, "theirs");
169    }
170
171    #[test]
172    fn test_clean_result_is_clean() {
173        let r = three_way_merge(&["x", "y"], &["x", "y"], &["x", "y"]);
174        assert!(matches!(r, MergeResult::Clean(_)));
175    }
176}