1#[derive(Debug, Clone, PartialEq)]
9pub enum LineChange {
10 Unchanged(Vec<String>),
12 Deleted(Vec<String>),
14 Inserted(Vec<String>),
16}
17
18impl LineChange {
19 #[allow(dead_code)]
20 fn is_unchanged(&self) -> bool {
21 matches!(self, LineChange::Unchanged(_))
22 }
23}
24
25pub fn diff_lines(base: &[&str], modified: &[&str]) -> Vec<LineChange> {
29 if base.is_empty() && modified.is_empty() {
30 return Vec::new();
31 }
32 if base.is_empty() {
33 return vec![LineChange::Inserted(
34 modified.iter().map(|s| s.to_string()).collect(),
35 )];
36 }
37 if modified.is_empty() {
38 return vec![LineChange::Deleted(
39 base.iter().map(|s| s.to_string()).collect(),
40 )];
41 }
42
43 let m = base.len();
44 let n = modified.len();
45
46 let mut dp = vec![vec![0usize; n + 1]; m + 1];
48 for i in 1..=m {
49 for j in 1..=n {
50 if base[i - 1] == modified[j - 1] {
51 dp[i][j] = dp[i - 1][j - 1] + 1;
52 } else {
53 dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
54 }
55 }
56 }
57
58 let mut changes = Vec::new();
60 let mut i = m;
61 let mut j = n;
62
63 while i > 0 || j > 0 {
64 if i > 0 && j > 0 && base[i - 1] == modified[j - 1] {
65 changes.push(LineChange::Unchanged(vec![base[i - 1].to_string()]));
66 i -= 1;
67 j -= 1;
68 } else if j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j]) {
69 changes.push(LineChange::Inserted(vec![modified[j - 1].to_string()]));
70 j -= 1;
71 } else {
72 changes.push(LineChange::Deleted(vec![base[i - 1].to_string()]));
73 i -= 1;
74 }
75 }
76
77 changes.reverse();
78 coalesce_changes(changes)
79}
80
81fn try_extend_last(result: &mut [LineChange], change: &LineChange) -> bool {
84 let can_merge = match result.last() {
85 Some(LineChange::Unchanged(_)) => matches!(change, LineChange::Unchanged(_)),
86 Some(LineChange::Deleted(_)) => matches!(change, LineChange::Deleted(_)),
87 Some(LineChange::Inserted(_)) => matches!(change, LineChange::Inserted(_)),
88 None => false,
89 };
90 if !can_merge {
91 return false;
92 }
93 let last = result.last_mut().unwrap();
94 match (last, change) {
95 (LineChange::Unchanged(v), LineChange::Unchanged(new)) => {
96 v.extend(new.iter().cloned());
97 true
98 }
99 (LineChange::Deleted(v), LineChange::Deleted(new)) => {
100 v.extend(new.iter().cloned());
101 true
102 }
103 (LineChange::Inserted(v), LineChange::Inserted(new)) => {
104 v.extend(new.iter().cloned());
105 true
106 }
107 _ => false,
108 }
109}
110
111fn coalesce_changes(changes: Vec<LineChange>) -> Vec<LineChange> {
113 let mut result = Vec::new();
114 for change in changes {
115 if !try_extend_last(&mut result, &change) {
116 result.push(change);
117 }
118 }
119 result
120}
121
122#[derive(Debug, Clone)]
124pub struct MergeOutput {
125 pub lines: Vec<String>,
127 pub is_clean: bool,
129 pub auto_merged: usize,
131 pub conflicts: usize,
133}
134
135pub fn three_way_merge_lines(
144 base: &[&str],
145 ours: &[&str],
146 theirs: &[&str],
147 ours_label: &str,
148 theirs_label: &str,
149) -> MergeOutput {
150 if ours == theirs {
152 return MergeOutput {
153 lines: ours.iter().map(|s| s.to_string()).collect(),
154 is_clean: true,
155 auto_merged: 0,
156 conflicts: 0,
157 };
158 }
159 if base == ours {
160 return MergeOutput {
161 lines: theirs.iter().map(|s| s.to_string()).collect(),
162 is_clean: true,
163 auto_merged: 0,
164 conflicts: 0,
165 };
166 }
167 if base == theirs {
168 return MergeOutput {
169 lines: ours.iter().map(|s| s.to_string()).collect(),
170 is_clean: true,
171 auto_merged: 0,
172 conflicts: 0,
173 };
174 }
175
176 let ours_diff = diff_lines(base, ours);
177 let theirs_diff = diff_lines(base, theirs);
178
179 let mut merged = Vec::new();
180 let mut is_clean = true;
181 let mut auto_merged = 0usize;
182 let mut conflicts = 0usize;
183
184 let mut ours_map = build_change_map(base, &ours_diff);
187 let mut theirs_map = build_change_map(base, &theirs_diff);
188
189 let base_len = base.len();
190 let mut i = 0;
191
192 while i < base_len {
193 let ours_action = ours_map.remove(&i);
194 let theirs_action = theirs_map.remove(&i);
195
196 match (ours_action, theirs_action) {
197 (None, None) => {
198 merged.push(base[i].to_string());
200 i += 1;
201 }
202 (Some(a), None) => {
203 apply_action(&mut merged, &a, &mut i);
205 }
206 (None, Some(a)) => {
207 apply_action(&mut merged, &a, &mut i);
209 }
210 (Some(a), Some(b)) => {
211 if a.output_lines() == b.output_lines() {
214 merged.extend(a.output_lines());
216 i += a.advance();
217 auto_merged += 1;
218 } else {
219 is_clean = false;
221 conflicts += 1;
222 merged.push(format!("<<<<<<< {}", ours_label));
223 merged.extend(a.output_lines());
224 merged.push("=======".to_string());
225 merged.extend(b.output_lines());
226 merged.push(format!(">>>>>>> {}", theirs_label));
227 i += a.advance().max(b.advance());
229 }
230 }
231 }
232 }
233
234 let ours_trailing = ours_map.remove(&base_len);
237 let theirs_trailing = theirs_map.remove(&base_len);
238 match (ours_trailing, theirs_trailing) {
239 (None, None) => {}
240 (Some(a), None) | (None, Some(a)) => {
241 merged.extend(a.output_lines());
242 }
243 (Some(a), Some(b)) => {
244 if a.output_lines() == b.output_lines() {
245 merged.extend(a.output_lines());
246 } else {
247 is_clean = false;
248 conflicts += 1;
249 merged.push(format!("<<<<<<< {}", ours_label));
250 merged.extend(a.output_lines());
251 merged.push("=======".to_string());
252 merged.extend(b.output_lines());
253 merged.push(format!(">>>>>>> {}", theirs_label));
254 }
255 }
256 }
257
258 MergeOutput {
259 lines: merged,
260 is_clean,
261 auto_merged,
262 conflicts,
263 }
264}
265
266#[derive(Debug, Clone)]
268enum SideAction {
269 DeleteInsert {
271 deleted: usize,
272 inserted: Vec<String>,
273 },
274 Insert { lines: Vec<String> },
276}
277
278impl SideAction {
279 fn advance(&self) -> usize {
280 match self {
281 SideAction::DeleteInsert { deleted, .. } => *deleted,
282 SideAction::Insert { .. } => 0,
283 }
284 }
285
286 fn output_lines(&self) -> Vec<String> {
287 match self {
288 SideAction::DeleteInsert { inserted, .. } => inserted.clone(),
289 SideAction::Insert { lines } => lines.clone(),
290 }
291 }
292}
293
294fn build_change_map(
299 _base: &[&str],
300 changes: &[LineChange],
301) -> std::collections::HashMap<usize, SideAction> {
302 let mut map = std::collections::HashMap::new();
303 let mut base_idx = 0;
304 let mut last_delete_idx: Option<usize> = None;
307
308 for change in changes {
309 match change {
310 LineChange::Unchanged(lines) => {
311 base_idx += lines.len();
312 last_delete_idx = None;
313 }
314 LineChange::Deleted(lines) => {
315 map.insert(
316 base_idx,
317 SideAction::DeleteInsert {
318 deleted: lines.len(),
319 inserted: Vec::new(),
320 },
321 );
322 last_delete_idx = Some(base_idx);
323 base_idx += lines.len();
324 }
325 LineChange::Inserted(lines) => {
326 if let Some(del_idx) = last_delete_idx {
327 if let Some(SideAction::DeleteInsert { inserted, .. }) = map.get_mut(&del_idx) {
329 inserted.extend(lines.iter().cloned());
330 }
331 } else if let Some(existing) = map.get_mut(&base_idx) {
332 match existing {
334 SideAction::DeleteInsert { inserted, .. } => {
335 inserted.extend(lines.iter().cloned());
336 }
337 SideAction::Insert { lines: v } => {
338 v.extend(lines.iter().cloned());
339 }
340 }
341 } else {
342 map.insert(
343 base_idx,
344 SideAction::Insert {
345 lines: lines.clone(),
346 },
347 );
348 }
349 }
351 }
352 }
353
354 map
355}
356
357fn apply_action(merged: &mut Vec<String>, action: &SideAction, base_idx: &mut usize) {
359 match action {
360 SideAction::DeleteInsert { deleted, inserted } => {
361 merged.extend(inserted.iter().cloned());
362 *base_idx += deleted;
363 }
364 SideAction::Insert { lines } => {
365 merged.extend(lines.iter().cloned());
366 }
367 }
368}
369
370#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_diff_identical() {
380 let base = ["a", "b", "c"];
381 let changes = diff_lines(&base, &base);
382 assert_eq!(changes.len(), 1);
383 assert!(matches!(changes[0], LineChange::Unchanged(_)));
384 }
385
386 #[test]
387 fn test_diff_insert() {
388 let base = ["a", "c"];
389 let modified = ["a", "b", "c"];
390 let changes = diff_lines(&base, &modified);
391 assert_eq!(changes.len(), 3); assert!(matches!(&changes[1], LineChange::Inserted(v) if v == &["b"]));
393 }
394
395 #[test]
396 fn test_diff_delete() {
397 let base = ["a", "b", "c"];
398 let modified = ["a", "c"];
399 let changes = diff_lines(&base, &modified);
400 assert_eq!(changes.len(), 3);
401 assert!(matches!(&changes[1], LineChange::Deleted(v) if v == &["b"]));
402 }
403
404 #[test]
405 fn test_diff_replace() {
406 let base = ["a", "b", "c"];
407 let modified = ["a", "x", "c"];
408 let changes = diff_lines(&base, &modified);
409 assert!(changes.iter().any(|c| matches!(c, LineChange::Deleted(_))));
410 assert!(changes.iter().any(|c| matches!(c, LineChange::Inserted(_))));
411 }
412
413 #[test]
414 fn test_merge_trivial_unchanged() {
415 let base = ["a", "b", "c"];
416 let result = three_way_merge_lines(&base, &base, &base, "ours", "theirs");
417 assert!(result.is_clean);
418 assert_eq!(result.lines, vec!["a", "b", "c"]);
419 }
420
421 #[test]
422 fn test_merge_one_side_changed() {
423 let base = ["a", "b", "c"];
424 let ours = ["a", "X", "c"];
425 let theirs = ["a", "b", "c"];
426 let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
427 assert!(result.is_clean);
428 assert_eq!(result.lines, vec!["a", "X", "c"]);
429 }
430
431 #[test]
432 fn test_merge_both_sides_different_regions() {
433 let base = ["a", "b", "c", "d"];
434 let ours = ["a", "X", "c", "d"]; let theirs = ["a", "b", "c", "Y"]; let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
437 assert!(result.is_clean);
438 assert_eq!(result.lines, vec!["a", "X", "c", "Y"]);
439 }
440
441 #[test]
442 fn test_merge_both_sides_same_change() {
443 let base = ["a", "b", "c"];
444 let ours = ["a", "X", "c"];
445 let theirs = ["a", "X", "c"];
446 let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
447 assert!(result.is_clean);
448 assert_eq!(result.lines, vec!["a", "X", "c"]);
449 }
450
451 #[test]
452 fn test_merge_conflict() {
453 let base = ["a", "b", "c"];
454 let ours = ["a", "X", "c"];
455 let theirs = ["a", "Y", "c"];
456 let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
457 assert!(!result.is_clean);
458 assert_eq!(result.conflicts, 1);
459 let content = result.lines.join("\n");
460 assert!(content.contains("X"));
461 assert!(content.contains("Y"));
462 assert!(content.contains("<<<<<<< ours"));
463 assert!(content.contains(">>>>>>> theirs"));
464 }
465
466 #[test]
467 fn test_merge_conflict_markers_format() {
468 let base = ["line1"];
469 let ours = ["ours_version"];
470 let theirs = ["theirs_version"];
471 let result = three_way_merge_lines(&base, &ours, &theirs, "ours (HEAD)", "theirs");
472 assert!(!result.is_clean);
473 let lines = result.lines;
474 assert_eq!(lines[0], "<<<<<<< ours (HEAD)");
475 assert_eq!(lines[1], "ours_version");
476 assert_eq!(lines[2], "=======");
477 assert_eq!(lines[3], "theirs_version");
478 assert_eq!(lines[4], ">>>>>>> theirs");
479 }
480
481 #[test]
482 fn test_merge_additions_both_sides() {
483 let base = ["a", "c"];
484 let ours = ["a", "b", "c"]; let theirs = ["a", "c", "d"]; let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
487 assert!(result.is_clean);
488 assert!(result.lines.contains(&"b".to_string()));
490 assert!(result.lines.contains(&"d".to_string()));
491 }
492
493 #[test]
494 fn test_merge_empty_base() {
495 let base: [&str; 0] = [];
496 let ours = ["a", "b"];
497 let theirs = ["c", "d"];
498 let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
499 assert!(!result.is_clean);
500 assert_eq!(result.conflicts, 1);
501 }
502
503 #[test]
504 fn test_merge_large_file_different_regions() {
505 let base: Vec<String> = (0..20).map(|i| format!("line {}", i)).collect();
506 let mut ours = base.clone();
507 let mut theirs = base.clone();
508
509 for (i, item) in ours.iter_mut().enumerate().take(5) {
511 *item = format!("OURS {}", i);
512 }
513 for (i, item) in theirs.iter_mut().enumerate().skip(15) {
515 *item = format!("THEIRS {}", i);
516 }
517
518 let base_refs: Vec<&str> = base.iter().map(|s| s.as_str()).collect();
519 let ours_refs: Vec<&str> = ours.iter().map(|s| s.as_str()).collect();
520 let theirs_refs: Vec<&str> = theirs.iter().map(|s| s.as_str()).collect();
521
522 let result = three_way_merge_lines(&base_refs, &ours_refs, &theirs_refs, "ours", "theirs");
523 assert!(result.is_clean, "should auto-merge non-overlapping changes");
524 assert_eq!(result.lines.len(), 20);
525 assert_eq!(result.lines[0], "OURS 0");
526 assert_eq!(result.lines[15], "THEIRS 15");
527 assert_eq!(result.lines[10], "line 10");
529 }
530}