oxihuman_core/
three_way_merge.rs1#![allow(dead_code)]
4
5#[derive(Debug, Clone, PartialEq)]
12pub enum MergeRegion {
13 Common(Vec<String>),
15 Ours(Vec<String>),
17 Theirs(Vec<String>),
19 Conflict {
21 ours: Vec<String>,
22 theirs: Vec<String>,
23 },
24}
25
26#[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 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
83pub 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 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 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 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 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
158pub fn is_clean_merge(result: &MergeResult) -> bool {
160 !result.has_conflicts()
161}
162
163pub 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 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}