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 Some(last) = result.last_mut() else {
94 return false;
95 };
96 match (last, change) {
97 (LineChange::Unchanged(v), LineChange::Unchanged(new)) => {
98 v.extend(new.iter().cloned());
99 true
100 }
101 (LineChange::Deleted(v), LineChange::Deleted(new)) => {
102 v.extend(new.iter().cloned());
103 true
104 }
105 (LineChange::Inserted(v), LineChange::Inserted(new)) => {
106 v.extend(new.iter().cloned());
107 true
108 }
109 _ => false,
110 }
111}
112
113fn coalesce_changes(changes: Vec<LineChange>) -> Vec<LineChange> {
115 let mut result = Vec::new();
116 for change in changes {
117 if !try_extend_last(&mut result, &change) {
118 result.push(change);
119 }
120 }
121 result
122}
123
124#[derive(Debug, Clone)]
126pub struct MergeOutput {
127 pub lines: Vec<String>,
129 pub is_clean: bool,
131 pub auto_merged: usize,
133 pub conflicts: usize,
135}
136
137pub fn three_way_merge_lines(
146 base: &[&str],
147 ours: &[&str],
148 theirs: &[&str],
149 ours_label: &str,
150 theirs_label: &str,
151) -> MergeOutput {
152 if ours == theirs {
154 return MergeOutput {
155 lines: ours.iter().map(|s| s.to_string()).collect(),
156 is_clean: true,
157 auto_merged: 0,
158 conflicts: 0,
159 };
160 }
161 if base == ours {
162 return MergeOutput {
163 lines: theirs.iter().map(|s| s.to_string()).collect(),
164 is_clean: true,
165 auto_merged: 0,
166 conflicts: 0,
167 };
168 }
169 if base == theirs {
170 return MergeOutput {
171 lines: ours.iter().map(|s| s.to_string()).collect(),
172 is_clean: true,
173 auto_merged: 0,
174 conflicts: 0,
175 };
176 }
177
178 let ours_diff = diff_lines(base, ours);
179 let theirs_diff = diff_lines(base, theirs);
180
181 let mut merged = Vec::new();
182 let mut is_clean = true;
183 let mut auto_merged = 0usize;
184 let mut conflicts = 0usize;
185
186 let mut ours_map = build_change_map(base, &ours_diff);
189 let mut theirs_map = build_change_map(base, &theirs_diff);
190
191 let base_len = base.len();
192 let mut i = 0;
193
194 while i < base_len {
195 let ours_action = ours_map.remove(&i);
196 let theirs_action = theirs_map.remove(&i);
197
198 match (ours_action, theirs_action) {
199 (None, None) => {
200 merged.push(base[i].to_string());
202 i += 1;
203 }
204 (Some(a), None) => {
205 apply_action(&mut merged, &a, &mut i);
207 }
208 (None, Some(a)) => {
209 apply_action(&mut merged, &a, &mut i);
211 }
212 (Some(a), Some(b)) => {
213 if a.output_lines() == b.output_lines() {
216 merged.extend(a.output_lines());
218 i += a.advance();
219 auto_merged += 1;
220 } else {
221 is_clean = false;
223 conflicts += 1;
224 merged.push(format!("<<<<<<< {}", ours_label));
225 merged.extend(a.output_lines());
226 merged.push("=======".to_string());
227 merged.extend(b.output_lines());
228 merged.push(format!(">>>>>>> {}", theirs_label));
229 i += a.advance().max(b.advance());
231 }
232 }
233 }
234 }
235
236 let ours_trailing = ours_map.remove(&base_len);
239 let theirs_trailing = theirs_map.remove(&base_len);
240 match (ours_trailing, theirs_trailing) {
241 (None, None) => {}
242 (Some(a), None) | (None, Some(a)) => {
243 merged.extend(a.output_lines());
244 }
245 (Some(a), Some(b)) => {
246 if a.output_lines() == b.output_lines() {
247 merged.extend(a.output_lines());
248 } else {
249 is_clean = false;
250 conflicts += 1;
251 merged.push(format!("<<<<<<< {}", ours_label));
252 merged.extend(a.output_lines());
253 merged.push("=======".to_string());
254 merged.extend(b.output_lines());
255 merged.push(format!(">>>>>>> {}", theirs_label));
256 }
257 }
258 }
259
260 MergeOutput {
261 lines: merged,
262 is_clean,
263 auto_merged,
264 conflicts,
265 }
266}
267
268#[derive(Debug, Clone)]
270enum SideAction {
271 DeleteInsert {
273 deleted: usize,
274 inserted: Vec<String>,
275 },
276 Insert { lines: Vec<String> },
278}
279
280impl SideAction {
281 fn advance(&self) -> usize {
282 match self {
283 SideAction::DeleteInsert { deleted, .. } => *deleted,
284 SideAction::Insert { .. } => 0,
285 }
286 }
287
288 fn output_lines(&self) -> Vec<String> {
289 match self {
290 SideAction::DeleteInsert { inserted, .. } => inserted.clone(),
291 SideAction::Insert { lines } => lines.clone(),
292 }
293 }
294}
295
296fn build_change_map(
301 _base: &[&str],
302 changes: &[LineChange],
303) -> std::collections::HashMap<usize, SideAction> {
304 let mut map = std::collections::HashMap::new();
305 let mut base_idx = 0;
306 let mut last_delete_idx: Option<usize> = None;
309
310 for change in changes {
311 match change {
312 LineChange::Unchanged(lines) => {
313 base_idx += lines.len();
314 last_delete_idx = None;
315 }
316 LineChange::Deleted(lines) => {
317 map.insert(
318 base_idx,
319 SideAction::DeleteInsert {
320 deleted: lines.len(),
321 inserted: Vec::new(),
322 },
323 );
324 last_delete_idx = Some(base_idx);
325 base_idx += lines.len();
326 }
327 LineChange::Inserted(lines) => {
328 if let Some(del_idx) = last_delete_idx {
329 if let Some(SideAction::DeleteInsert { inserted, .. }) = map.get_mut(&del_idx) {
331 inserted.extend(lines.iter().cloned());
332 }
333 } else if let Some(existing) = map.get_mut(&base_idx) {
334 match existing {
336 SideAction::DeleteInsert { inserted, .. } => {
337 inserted.extend(lines.iter().cloned());
338 }
339 SideAction::Insert { lines: v } => {
340 v.extend(lines.iter().cloned());
341 }
342 }
343 } else {
344 map.insert(
345 base_idx,
346 SideAction::Insert {
347 lines: lines.clone(),
348 },
349 );
350 }
351 }
353 }
354 }
355
356 map
357}
358
359fn apply_action(merged: &mut Vec<String>, action: &SideAction, base_idx: &mut usize) {
361 match action {
362 SideAction::DeleteInsert { deleted, inserted } => {
363 merged.extend(inserted.iter().cloned());
364 *base_idx += deleted;
365 }
366 SideAction::Insert { lines } => {
367 merged.extend(lines.iter().cloned());
368 }
369 }
370}
371
372#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_diff_identical() {
382 let base = ["a", "b", "c"];
383 let changes = diff_lines(&base, &base);
384 assert_eq!(changes.len(), 1);
385 assert!(matches!(changes[0], LineChange::Unchanged(_)));
386 }
387
388 #[test]
389 fn test_diff_insert() {
390 let base = ["a", "c"];
391 let modified = ["a", "b", "c"];
392 let changes = diff_lines(&base, &modified);
393 assert_eq!(changes.len(), 3); assert!(matches!(&changes[1], LineChange::Inserted(v) if v == &["b"]));
395 }
396
397 #[test]
398 fn test_diff_delete() {
399 let base = ["a", "b", "c"];
400 let modified = ["a", "c"];
401 let changes = diff_lines(&base, &modified);
402 assert_eq!(changes.len(), 3);
403 assert!(matches!(&changes[1], LineChange::Deleted(v) if v == &["b"]));
404 }
405
406 #[test]
407 fn test_diff_replace() {
408 let base = ["a", "b", "c"];
409 let modified = ["a", "x", "c"];
410 let changes = diff_lines(&base, &modified);
411 assert!(changes.iter().any(|c| matches!(c, LineChange::Deleted(_))));
412 assert!(changes.iter().any(|c| matches!(c, LineChange::Inserted(_))));
413 }
414
415 #[test]
416 fn test_merge_trivial_unchanged() {
417 let base = ["a", "b", "c"];
418 let result = three_way_merge_lines(&base, &base, &base, "ours", "theirs");
419 assert!(result.is_clean);
420 assert_eq!(result.lines, vec!["a", "b", "c"]);
421 }
422
423 #[test]
424 fn test_merge_one_side_changed() {
425 let base = ["a", "b", "c"];
426 let ours = ["a", "X", "c"];
427 let theirs = ["a", "b", "c"];
428 let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
429 assert!(result.is_clean);
430 assert_eq!(result.lines, vec!["a", "X", "c"]);
431 }
432
433 #[test]
434 fn test_merge_both_sides_different_regions() {
435 let base = ["a", "b", "c", "d"];
436 let ours = ["a", "X", "c", "d"]; let theirs = ["a", "b", "c", "Y"]; let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
439 assert!(result.is_clean);
440 assert_eq!(result.lines, vec!["a", "X", "c", "Y"]);
441 }
442
443 #[test]
444 fn test_merge_both_sides_same_change() {
445 let base = ["a", "b", "c"];
446 let ours = ["a", "X", "c"];
447 let theirs = ["a", "X", "c"];
448 let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
449 assert!(result.is_clean);
450 assert_eq!(result.lines, vec!["a", "X", "c"]);
451 }
452
453 #[test]
454 fn test_merge_conflict() {
455 let base = ["a", "b", "c"];
456 let ours = ["a", "X", "c"];
457 let theirs = ["a", "Y", "c"];
458 let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
459 assert!(!result.is_clean);
460 assert_eq!(result.conflicts, 1);
461 let content = result.lines.join("\n");
462 assert!(content.contains("X"));
463 assert!(content.contains("Y"));
464 assert!(content.contains("<<<<<<< ours"));
465 assert!(content.contains(">>>>>>> theirs"));
466 }
467
468 #[test]
469 fn test_merge_conflict_markers_format() {
470 let base = ["line1"];
471 let ours = ["ours_version"];
472 let theirs = ["theirs_version"];
473 let result = three_way_merge_lines(&base, &ours, &theirs, "ours (HEAD)", "theirs");
474 assert!(!result.is_clean);
475 let lines = result.lines;
476 assert_eq!(lines[0], "<<<<<<< ours (HEAD)");
477 assert_eq!(lines[1], "ours_version");
478 assert_eq!(lines[2], "=======");
479 assert_eq!(lines[3], "theirs_version");
480 assert_eq!(lines[4], ">>>>>>> theirs");
481 }
482
483 #[test]
484 fn test_merge_additions_both_sides() {
485 let base = ["a", "c"];
486 let ours = ["a", "b", "c"]; let theirs = ["a", "c", "d"]; let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
489 assert!(result.is_clean);
490 assert!(result.lines.contains(&"b".to_string()));
492 assert!(result.lines.contains(&"d".to_string()));
493 }
494
495 #[test]
496 fn test_merge_empty_base() {
497 let base: [&str; 0] = [];
498 let ours = ["a", "b"];
499 let theirs = ["c", "d"];
500 let result = three_way_merge_lines(&base, &ours, &theirs, "ours", "theirs");
501 assert!(!result.is_clean);
502 assert_eq!(result.conflicts, 1);
503 }
504
505 #[test]
506 fn test_merge_large_file_different_regions() {
507 let base: Vec<String> = (0..20).map(|i| format!("line {}", i)).collect();
508 let mut ours = base.clone();
509 let mut theirs = base.clone();
510
511 for (i, item) in ours.iter_mut().enumerate().take(5) {
513 *item = format!("OURS {}", i);
514 }
515 for (i, item) in theirs.iter_mut().enumerate().skip(15) {
517 *item = format!("THEIRS {}", i);
518 }
519
520 let base_refs: Vec<&str> = base.iter().map(|s| s.as_str()).collect();
521 let ours_refs: Vec<&str> = ours.iter().map(|s| s.as_str()).collect();
522 let theirs_refs: Vec<&str> = theirs.iter().map(|s| s.as_str()).collect();
523
524 let result = three_way_merge_lines(&base_refs, &ours_refs, &theirs_refs, "ours", "theirs");
525 assert!(result.is_clean, "should auto-merge non-overlapping changes");
526 assert_eq!(result.lines.len(), 20);
527 assert_eq!(result.lines[0], "OURS 0");
528 assert_eq!(result.lines[15], "THEIRS 15");
529 assert_eq!(result.lines[10], "line 10");
531 }
532}