Skip to main content

oxihuman_core/
three_way_merge.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Three-way text merge algorithm stub.
6//!
7//! Given a common ancestor (`base`) and two diverged versions (`ours` / `theirs`),
8//! produces a merged result or reports conflicts where both sides changed the same region.
9
10/// Outcome of merging a single region.
11#[derive(Debug, Clone, PartialEq)]
12pub enum MergeRegion {
13    /// Both sides agree — use the common text.
14    Common(Vec<String>),
15    /// Only one side changed.
16    Ours(Vec<String>),
17    /// Only the other side changed.
18    Theirs(Vec<String>),
19    /// Both sides changed differently — a conflict.
20    Conflict {
21        ours: Vec<String>,
22        theirs: Vec<String>,
23    },
24}
25
26/// The result of a three-way merge.
27#[derive(Debug, Clone)]
28pub struct MergeResult {
29    pub regions: Vec<MergeRegion>,
30}
31
32impl MergeResult {
33    pub fn new() -> Self {
34        Self {
35            regions: Vec::new(),
36        }
37    }
38
39    pub fn has_conflicts(&self) -> bool {
40        self.regions
41            .iter()
42            .any(|r| matches!(r, MergeRegion::Conflict { .. }))
43    }
44
45    pub fn conflict_count(&self) -> usize {
46        self.regions
47            .iter()
48            .filter(|r| matches!(r, MergeRegion::Conflict { .. }))
49            .count()
50    }
51
52    pub fn region_count(&self) -> usize {
53        self.regions.len()
54    }
55
56    /// Collect all merged lines (conflict regions represented by both sides).
57    pub fn collect_lines(&self, prefer_ours: bool) -> Vec<String> {
58        let mut out = Vec::new();
59        for r in &self.regions {
60            match r {
61                MergeRegion::Common(ls) => out.extend(ls.iter().cloned()),
62                MergeRegion::Ours(ls) => out.extend(ls.iter().cloned()),
63                MergeRegion::Theirs(ls) => out.extend(ls.iter().cloned()),
64                MergeRegion::Conflict { ours, theirs } => {
65                    if prefer_ours {
66                        out.extend(ours.iter().cloned());
67                    } else {
68                        out.extend(theirs.iter().cloned());
69                    }
70                }
71            }
72        }
73        out
74    }
75}
76
77impl Default for MergeResult {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83/// Perform a three-way merge on line slices.
84pub fn three_way_merge(base: &[&str], ours: &[&str], theirs: &[&str]) -> MergeResult {
85    let mut result = MergeResult::new();
86    let n = base.len().max(ours.len()).max(theirs.len());
87
88    let mut bi = 0usize;
89    let mut oi = 0usize;
90    let mut ti = 0usize;
91
92    while bi < base.len() || oi < ours.len() || ti < theirs.len() {
93        let b = base.get(bi).copied();
94        let o = ours.get(oi).copied();
95        let t = theirs.get(ti).copied();
96
97        match (b, o, t) {
98            (Some(bv), Some(ov), Some(tv)) if bv == ov && bv == tv => {
99                /* Unchanged on both sides */
100                result
101                    .regions
102                    .push(MergeRegion::Common(vec![bv.to_string()]));
103                bi += 1;
104                oi += 1;
105                ti += 1;
106            }
107            (Some(bv), Some(ov), Some(tv)) if bv != ov && bv == tv => {
108                /* Only ours changed */
109                result.regions.push(MergeRegion::Ours(vec![ov.to_string()]));
110                bi += 1;
111                oi += 1;
112                ti += 1;
113            }
114            (Some(bv), Some(ov), Some(tv)) if bv == ov && bv != tv => {
115                /* Only theirs changed */
116                result
117                    .regions
118                    .push(MergeRegion::Theirs(vec![tv.to_string()]));
119                bi += 1;
120                oi += 1;
121                ti += 1;
122            }
123            (_, Some(ov), Some(tv)) => {
124                /* Conflict or both changed */
125                result.regions.push(MergeRegion::Conflict {
126                    ours: vec![ov.to_string()],
127                    theirs: vec![tv.to_string()],
128                });
129                if bi < base.len() {
130                    bi += 1;
131                }
132                oi += 1;
133                ti += 1;
134            }
135            (_, Some(ov), None) => {
136                result.regions.push(MergeRegion::Ours(vec![ov.to_string()]));
137                if bi < base.len() {
138                    bi += 1;
139                }
140                oi += 1;
141            }
142            (_, None, Some(tv)) => {
143                result
144                    .regions
145                    .push(MergeRegion::Theirs(vec![tv.to_string()]));
146                if bi < base.len() {
147                    bi += 1;
148                }
149                ti += 1;
150            }
151            _ => break,
152        }
153    }
154    let _ = n;
155    result
156}
157
158/// Check whether all regions in a merge result are conflict-free.
159pub fn is_clean_merge(result: &MergeResult) -> bool {
160    !result.has_conflicts()
161}
162
163/// Count non-conflicting merged lines.
164pub fn clean_line_count(result: &MergeResult) -> usize {
165    result
166        .regions
167        .iter()
168        .map(|r| match r {
169            MergeRegion::Common(ls) | MergeRegion::Ours(ls) | MergeRegion::Theirs(ls) => ls.len(),
170            MergeRegion::Conflict { ours, theirs } => ours.len() + theirs.len(),
171        })
172        .sum()
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_no_change_is_clean() {
181        let lines = ["a", "b", "c"];
182        let r = three_way_merge(&lines, &lines, &lines);
183        assert!(r.is_clean_merge_check());
184    }
185
186    /* Helper extension for tests */
187    impl MergeResult {
188        fn is_clean_merge_check(&self) -> bool {
189            is_clean_merge(self)
190        }
191    }
192
193    #[test]
194    fn test_ours_change() {
195        let base = ["a"];
196        let ours = ["b"];
197        let theirs = ["a"];
198        let r = three_way_merge(&base, &ours, &theirs);
199        assert!(!r.has_conflicts());
200        assert!(r.regions.iter().any(|r| matches!(r, MergeRegion::Ours(_))));
201    }
202
203    #[test]
204    fn test_theirs_change() {
205        let base = ["a"];
206        let ours = ["a"];
207        let theirs = ["c"];
208        let r = three_way_merge(&base, &ours, &theirs);
209        assert!(!r.has_conflicts());
210    }
211
212    #[test]
213    fn test_conflict_detected() {
214        let base = ["a"];
215        let ours = ["b"];
216        let theirs = ["c"];
217        let r = three_way_merge(&base, &ours, &theirs);
218        assert!(r.has_conflicts());
219        assert_eq!(r.conflict_count(), 1);
220    }
221
222    #[test]
223    fn test_collect_lines_prefer_ours() {
224        let base = ["a"];
225        let ours = ["b"];
226        let theirs = ["c"];
227        let r = three_way_merge(&base, &ours, &theirs);
228        let lines = r.collect_lines(true);
229        assert_eq!(lines, vec!["b"]);
230    }
231
232    #[test]
233    fn test_collect_lines_prefer_theirs() {
234        let base = ["a"];
235        let ours = ["b"];
236        let theirs = ["c"];
237        let r = three_way_merge(&base, &ours, &theirs);
238        let lines = r.collect_lines(false);
239        assert_eq!(lines, vec!["c"]);
240    }
241
242    #[test]
243    fn test_region_count() {
244        let base = ["a", "b"];
245        let ours = ["a", "x"];
246        let theirs = ["a", "y"];
247        let r = three_way_merge(&base, &ours, &theirs);
248        assert!(r.region_count() > 0);
249    }
250
251    #[test]
252    fn test_clean_line_count_nonzero() {
253        let base = ["a"];
254        let r = three_way_merge(&base, &base, &base);
255        assert!(clean_line_count(&r) > 0);
256    }
257
258    #[test]
259    fn test_default() {
260        let r = MergeResult::default();
261        assert_eq!(r.region_count(), 0);
262    }
263}