1use crate::core::diff::{myers_diff, DiffOp};
6use crate::core::{Object, ObjectHash, Tree, TreeEntry};
7use crate::storage::ObjectStore;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum MergeStrategy {
14 Recursive,
15 Ours,
16 Theirs,
17}
18
19impl std::str::FromStr for MergeStrategy {
20 type Err = String;
21
22 fn from_str(s: &str) -> Result<Self, String> {
23 match s {
24 "recursive" => Ok(MergeStrategy::Recursive),
25 "ours" => Ok(MergeStrategy::Ours),
26 "theirs" => Ok(MergeStrategy::Theirs),
27 _ => Err(format!(
28 "Unknown merge strategy: '{}'. Valid: recursive, ours, theirs",
29 s
30 )),
31 }
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct MergeResult {
38 pub tree: Option<ObjectHash>,
40 pub fast_forward: bool,
42 pub has_conflicts: bool,
44 pub file_results: Vec<FileMergeResult>,
46 pub strategy: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct FileMergeResult {
53 pub path: String,
54 pub status: FileMergeStatus,
55 #[serde(skip_serializing_if = "Vec::is_empty")]
57 pub conflicts: Vec<ConflictRegion>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "lowercase")]
63pub enum FileMergeStatus {
64 Clean,
65 Conflict,
66 Added,
67 Deleted,
68 AutoResolved,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ConflictRegion {
75 pub start_line: usize,
76 pub ours: Vec<String>,
77 pub theirs: Vec<String>,
78 pub base: Vec<String>,
79}
80
81pub fn find_merge_base(
83 store: &ObjectStore,
84 commit_a: &ObjectHash,
85 commit_b: &ObjectHash,
86) -> Result<Option<ObjectHash>, String> {
87 let mut visited_a: HashMap<String, usize> = HashMap::new();
89 let mut visited_b: HashMap<String, usize> = HashMap::new();
90 let mut queue_a = vec![commit_a.clone()];
91 let mut queue_b = vec![commit_b.clone()];
92 let mut depth = 0usize;
93
94 while !queue_a.is_empty() || !queue_b.is_empty() {
96 let mut next_a = Vec::new();
98 for hash in &queue_a {
99 let key = hash.to_string();
100 if visited_b.contains_key(&key) {
101 return Ok(Some(hash.clone()));
102 }
103 if visited_a.contains_key(&key) {
104 continue;
105 }
106 visited_a.insert(key, depth);
107
108 if let Ok(Object::Commit(commit)) = store.read(hash) {
109 for parent in &commit.parents {
110 if !visited_a.contains_key(&parent.to_string()) {
111 next_a.push(parent.clone());
112 }
113 }
114 }
115 }
116 queue_a = next_a;
117
118 let mut next_b = Vec::new();
120 for hash in &queue_b {
121 let key = hash.to_string();
122 if visited_a.contains_key(&key) {
123 return Ok(Some(hash.clone()));
124 }
125 if visited_b.contains_key(&key) {
126 continue;
127 }
128 visited_b.insert(key, depth);
129
130 if let Ok(Object::Commit(commit)) = store.read(hash) {
131 for parent in &commit.parents {
132 if !visited_b.contains_key(&parent.to_string()) {
133 next_b.push(parent.clone());
134 }
135 }
136 }
137 }
138 queue_b = next_b;
139 depth += 1;
140
141 if depth > 10000 {
143 return Err("Merge base search exceeded depth limit".to_string());
144 }
145 }
146
147 Ok(None) }
149
150pub fn is_ancestor(
152 store: &ObjectStore,
153 ancestor: &ObjectHash,
154 descendant: &ObjectHash,
155) -> Result<bool, String> {
156 if ancestor.to_string() == descendant.to_string() {
157 return Ok(true);
158 }
159
160 let mut queue = vec![descendant.clone()];
161 let mut visited = std::collections::HashSet::new();
162
163 while let Some(hash) = queue.pop() {
164 let key = hash.to_string();
165 if key == ancestor.to_string() {
166 return Ok(true);
167 }
168 if !visited.insert(key) {
169 continue;
170 }
171 if let Ok(Object::Commit(commit)) = store.read(&hash) {
172 for parent in &commit.parents {
173 queue.push(parent.clone());
174 }
175 }
176 }
177
178 Ok(false)
179}
180
181pub fn merge_trees(
183 store: &ObjectStore,
184 base_tree: Option<&Tree>,
185 ours_tree: &Tree,
186 theirs_tree: &Tree,
187 strategy: MergeStrategy,
188) -> Result<MergeResult, String> {
189 let base_files = match base_tree {
191 Some(t) => crate::core::diff::collect_tree_files(t, store, "")?,
192 None => vec![],
193 };
194 let ours_files = crate::core::diff::collect_tree_files(ours_tree, store, "")?;
195 let theirs_files = crate::core::diff::collect_tree_files(theirs_tree, store, "")?;
196
197 let base_map: HashMap<String, ObjectHash> = base_files.into_iter().collect();
198 let ours_map: HashMap<String, ObjectHash> = ours_files.into_iter().collect();
199 let theirs_map: HashMap<String, ObjectHash> = theirs_files.into_iter().collect();
200
201 let mut all_paths: Vec<String> = ours_map
203 .keys()
204 .chain(theirs_map.keys())
205 .chain(base_map.keys())
206 .cloned()
207 .collect();
208 all_paths.sort();
209 all_paths.dedup();
210
211 let mut file_results = Vec::new();
212 let mut merged_entries: Vec<(String, ObjectHash)> = Vec::new();
213 let mut has_conflicts = false;
214
215 for path in &all_paths {
216 let base_hash = base_map.get(path);
217 let ours_hash = ours_map.get(path);
218 let theirs_hash = theirs_map.get(path);
219
220 let result = merge_file_entry(store, path, base_hash, ours_hash, theirs_hash, strategy)?;
221
222 if result.status == FileMergeStatus::Conflict {
223 has_conflicts = true;
224 }
225
226 match result.status {
228 FileMergeStatus::Clean | FileMergeStatus::AutoResolved => {
229 if let Some(h) = ours_hash {
231 if base_hash.map(|b| b == h).unwrap_or(false) {
232 if let Some(th) = theirs_hash {
234 merged_entries.push((path.clone(), th.clone()));
235 }
236 } else {
237 merged_entries.push((path.clone(), h.clone()));
238 }
239 } else if let Some(th) = theirs_hash {
240 merged_entries.push((path.clone(), th.clone()));
241 }
242 }
243 FileMergeStatus::Added => {
244 if let Some(h) = ours_hash.or(theirs_hash) {
245 merged_entries.push((path.clone(), h.clone()));
246 }
247 }
248 FileMergeStatus::Deleted => {
249 }
251 FileMergeStatus::Conflict => {
252 match strategy {
254 MergeStrategy::Ours => {
255 if let Some(h) = ours_hash {
256 merged_entries.push((path.clone(), h.clone()));
257 }
258 }
259 MergeStrategy::Theirs => {
260 if let Some(h) = theirs_hash {
261 merged_entries.push((path.clone(), h.clone()));
262 }
263 }
264 MergeStrategy::Recursive => {
265 if let Some(h) = ours_hash {
267 merged_entries.push((path.clone(), h.clone()));
268 }
269 }
270 }
271 }
272 }
273
274 file_results.push(result);
275 }
276
277 let tree_hash = if !has_conflicts || strategy != MergeStrategy::Recursive {
279 Some(build_flat_tree(store, &merged_entries)?)
280 } else {
281 None };
283
284 Ok(MergeResult {
285 tree: tree_hash,
286 fast_forward: false,
287 has_conflicts,
288 file_results,
289 strategy: format!("{:?}", strategy).to_lowercase(),
290 })
291}
292
293fn merge_file_entry(
295 store: &ObjectStore,
296 path: &str,
297 base_hash: Option<&ObjectHash>,
298 ours_hash: Option<&ObjectHash>,
299 theirs_hash: Option<&ObjectHash>,
300 strategy: MergeStrategy,
301) -> Result<FileMergeResult, String> {
302 match (base_hash, ours_hash, theirs_hash) {
303 (Some(_), None, None) => Ok(FileMergeResult {
305 path: path.to_string(),
306 status: FileMergeStatus::Deleted,
307 conflicts: vec![],
308 }),
309
310 (None, Some(_), None) => Ok(FileMergeResult {
312 path: path.to_string(),
313 status: FileMergeStatus::Added,
314 conflicts: vec![],
315 }),
316
317 (None, None, Some(_)) => Ok(FileMergeResult {
319 path: path.to_string(),
320 status: FileMergeStatus::Added,
321 conflicts: vec![],
322 }),
323
324 (None, Some(o), Some(t)) => {
326 if o == t {
327 Ok(FileMergeResult {
328 path: path.to_string(),
329 status: FileMergeStatus::Clean,
330 conflicts: vec![],
331 })
332 } else {
333 handle_content_conflict(store, path, None, Some(o), Some(t), strategy)
334 }
335 }
336
337 (Some(b), Some(o), None) => {
339 if o == b {
340 Ok(FileMergeResult {
342 path: path.to_string(),
343 status: FileMergeStatus::Deleted,
344 conflicts: vec![],
345 })
346 } else {
347 Ok(FileMergeResult {
349 path: path.to_string(),
350 status: FileMergeStatus::Conflict,
351 conflicts: vec![ConflictRegion {
352 start_line: 1,
353 ours: vec!["(file modified)".to_string()],
354 theirs: vec!["(file deleted)".to_string()],
355 base: vec!["(file existed)".to_string()],
356 }],
357 })
358 }
359 }
360
361 (Some(b), None, Some(t)) => {
363 if t == b {
364 Ok(FileMergeResult {
366 path: path.to_string(),
367 status: FileMergeStatus::Deleted,
368 conflicts: vec![],
369 })
370 } else {
371 Ok(FileMergeResult {
373 path: path.to_string(),
374 status: FileMergeStatus::Conflict,
375 conflicts: vec![ConflictRegion {
376 start_line: 1,
377 ours: vec!["(file deleted)".to_string()],
378 theirs: vec!["(file modified)".to_string()],
379 base: vec!["(file existed)".to_string()],
380 }],
381 })
382 }
383 }
384
385 (Some(b), Some(o), Some(t)) => {
387 if o == t {
388 Ok(FileMergeResult {
390 path: path.to_string(),
391 status: FileMergeStatus::Clean,
392 conflicts: vec![],
393 })
394 } else if o == b {
395 Ok(FileMergeResult {
397 path: path.to_string(),
398 status: FileMergeStatus::Clean,
399 conflicts: vec![],
400 })
401 } else if t == b {
402 Ok(FileMergeResult {
404 path: path.to_string(),
405 status: FileMergeStatus::Clean,
406 conflicts: vec![],
407 })
408 } else {
409 handle_content_conflict(store, path, Some(b), Some(o), Some(t), strategy)
411 }
412 }
413
414 (None, None, None) => Ok(FileMergeResult {
416 path: path.to_string(),
417 status: FileMergeStatus::Clean,
418 conflicts: vec![],
419 }),
420 }
421}
422
423fn handle_content_conflict(
425 store: &ObjectStore,
426 path: &str,
427 base_hash: Option<&ObjectHash>,
428 ours_hash: Option<&ObjectHash>,
429 theirs_hash: Option<&ObjectHash>,
430 strategy: MergeStrategy,
431) -> Result<FileMergeResult, String> {
432 match strategy {
434 MergeStrategy::Ours => {
435 return Ok(FileMergeResult {
436 path: path.to_string(),
437 status: FileMergeStatus::AutoResolved,
438 conflicts: vec![],
439 });
440 }
441 MergeStrategy::Theirs => {
442 return Ok(FileMergeResult {
443 path: path.to_string(),
444 status: FileMergeStatus::AutoResolved,
445 conflicts: vec![],
446 });
447 }
448 MergeStrategy::Recursive => {}
449 }
450
451 let base_content = match base_hash {
453 Some(h) => read_blob_text(store, h)?,
454 None => String::new(),
455 };
456 let ours_content = match ours_hash {
457 Some(h) => read_blob_text(store, h)?,
458 None => String::new(),
459 };
460 let theirs_content = match theirs_hash {
461 Some(h) => read_blob_text(store, h)?,
462 None => String::new(),
463 };
464
465 let base_lines: Vec<&str> = base_content.lines().collect();
467 let ours_lines: Vec<&str> = ours_content.lines().collect();
468 let theirs_lines: Vec<&str> = theirs_content.lines().collect();
469
470 let merge_result = three_way_merge(&base_lines, &ours_lines, &theirs_lines);
471
472 if merge_result.conflicts.is_empty() {
473 Ok(FileMergeResult {
474 path: path.to_string(),
475 status: FileMergeStatus::AutoResolved,
476 conflicts: vec![],
477 })
478 } else {
479 Ok(FileMergeResult {
480 path: path.to_string(),
481 status: FileMergeStatus::Conflict,
482 conflicts: merge_result.conflicts,
483 })
484 }
485}
486
487struct ThreeWayResult {
489 _merged_lines: Vec<String>,
490 conflicts: Vec<ConflictRegion>,
491}
492
493fn three_way_merge(base: &[&str], ours: &[&str], theirs: &[&str]) -> ThreeWayResult {
495 let ours_ops = myers_diff(base, ours);
497 let theirs_ops = myers_diff(base, theirs);
498
499 let mut ours_changed_lines: std::collections::HashSet<usize> = std::collections::HashSet::new();
501 let mut theirs_changed_lines: std::collections::HashSet<usize> =
502 std::collections::HashSet::new();
503
504 {
505 let mut base_idx = 0usize;
506 for op in &ours_ops {
507 match op {
508 DiffOp::Equal(_) => {
509 base_idx += 1;
510 }
511 DiffOp::Delete(_) => {
512 ours_changed_lines.insert(base_idx);
513 base_idx += 1;
514 }
515 DiffOp::Insert(_) => {
516 ours_changed_lines.insert(base_idx);
517 }
518 }
519 }
520 }
521
522 {
523 let mut base_idx = 0usize;
524 for op in &theirs_ops {
525 match op {
526 DiffOp::Equal(_) => {
527 base_idx += 1;
528 }
529 DiffOp::Delete(_) => {
530 theirs_changed_lines.insert(base_idx);
531 base_idx += 1;
532 }
533 DiffOp::Insert(_) => {
534 theirs_changed_lines.insert(base_idx);
535 }
536 }
537 }
538 }
539
540 let mut conflicts = Vec::new();
542 let overlapping: std::collections::HashSet<usize> = ours_changed_lines
543 .intersection(&theirs_changed_lines)
544 .cloned()
545 .collect();
546
547 if !overlapping.is_empty() {
548 let mut sorted_overlaps: Vec<usize> = overlapping.into_iter().collect();
549 sorted_overlaps.sort();
550
551 let mut regions: Vec<(usize, usize)> = Vec::new();
552 let mut start = sorted_overlaps[0];
553 let mut end = sorted_overlaps[0];
554
555 for &line in &sorted_overlaps[1..] {
556 if line <= end + 1 {
557 end = line;
558 } else {
559 regions.push((start, end));
560 start = line;
561 end = line;
562 }
563 }
564 regions.push((start, end));
565
566 for (start, end) in regions {
567 let base_region: Vec<String> = (start..=end)
568 .filter_map(|i| base.get(i).map(|s| s.to_string()))
569 .collect();
570 let ours_region: Vec<String> = (start..=end)
571 .filter_map(|i| ours.get(i).map(|s| s.to_string()))
572 .collect();
573 let theirs_region: Vec<String> = (start..=end)
574 .filter_map(|i| theirs.get(i).map(|s| s.to_string()))
575 .collect();
576
577 conflicts.push(ConflictRegion {
578 start_line: start + 1,
579 ours: ours_region,
580 theirs: theirs_region,
581 base: base_region,
582 });
583 }
584 }
585
586 let mut merged_lines = Vec::new();
588 let max_len = base.len().max(ours.len()).max(theirs.len());
589
590 for i in 0..max_len {
591 if ours_changed_lines.contains(&i) && !theirs_changed_lines.contains(&i) {
592 if let Some(line) = ours.get(i) {
593 merged_lines.push(line.to_string());
594 }
595 } else if theirs_changed_lines.contains(&i) && !ours_changed_lines.contains(&i) {
596 if let Some(line) = theirs.get(i) {
597 merged_lines.push(line.to_string());
598 }
599 } else if !ours_changed_lines.contains(&i) && !theirs_changed_lines.contains(&i) {
600 if let Some(line) = base.get(i) {
601 merged_lines.push(line.to_string());
602 }
603 }
604 }
605
606 ThreeWayResult {
607 _merged_lines: merged_lines,
608 conflicts,
609 }
610}
611
612fn read_blob_text(store: &ObjectStore, hash: &ObjectHash) -> Result<String, String> {
614 match store.read(hash)? {
615 Object::Blob(b) => String::from_utf8(b.content)
616 .map_err(|_| format!("File {} is binary, cannot merge", hash)),
617 _ => Err(format!("Expected blob object for hash {}", hash)),
618 }
619}
620
621fn build_flat_tree(
623 store: &ObjectStore,
624 entries: &[(String, ObjectHash)],
625) -> Result<ObjectHash, String> {
626 let mut root_entries: Vec<TreeEntry> = Vec::new();
628 let mut subdirs: HashMap<String, Vec<(String, ObjectHash)>> = HashMap::new();
629
630 for (path, hash) in entries {
631 if let Some(sep) = path.find('/') {
632 let dir = &path[..sep];
633 let rest = &path[sep + 1..];
634 subdirs
635 .entry(dir.to_string())
636 .or_default()
637 .push((rest.to_string(), hash.clone()));
638 } else {
639 root_entries.push(TreeEntry {
640 mode: "100644".to_string(),
641 name: path.clone(),
642 hash: hash.clone(),
643 object_type: "blob".to_string(),
644 });
645 }
646 }
647
648 for (dir, sub_entries) in &subdirs {
650 let subtree_hash = build_flat_tree(store, sub_entries)?;
651 root_entries.push(TreeEntry {
652 mode: "040000".to_string(),
653 name: dir.clone(),
654 hash: subtree_hash,
655 object_type: "tree".to_string(),
656 });
657 }
658
659 root_entries.sort_by(|a, b| a.name.cmp(&b.name));
660
661 let tree = Tree {
662 entries: root_entries,
663 };
664 let tree_obj = Object::Tree(tree);
665 store.write(&tree_obj)
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 use std::str::FromStr;
672
673 #[test]
674 fn test_merge_strategy_from_str() {
675 assert_eq!(
676 MergeStrategy::from_str("recursive").unwrap(),
677 MergeStrategy::Recursive
678 );
679 assert_eq!(
680 MergeStrategy::from_str("ours").unwrap(),
681 MergeStrategy::Ours
682 );
683 assert_eq!(
684 MergeStrategy::from_str("theirs").unwrap(),
685 MergeStrategy::Theirs
686 );
687 assert!(MergeStrategy::from_str("invalid").is_err());
688 }
689
690 #[test]
691 fn test_three_way_merge_no_conflict() {
692 let base = vec!["line1", "line2", "line3"];
693 let ours = vec!["line1", "MODIFIED", "line3"];
694 let theirs = vec!["line1", "line2", "line3"];
695 let result = three_way_merge(&base, &ours, &theirs);
696 assert!(result.conflicts.is_empty());
697 }
698
699 #[test]
700 fn test_three_way_merge_both_sides_different_regions() {
701 let base = vec!["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
702 let ours = vec!["A", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
703 let theirs = vec!["a", "b", "c", "d", "e", "f", "g", "h", "i", "J"];
704 let result = three_way_merge(&base, &ours, &theirs);
705 assert!(result.conflicts.is_empty());
706 assert_eq!(
707 result._merged_lines,
708 vec!["A", "b", "c", "d", "e", "f", "g", "h", "i", "J"]
709 );
710 }
711
712 #[test]
713 fn test_three_way_merge_conflict() {
714 let base = vec!["line1", "line2", "line3"];
715 let ours = vec!["line1", "OURS", "line3"];
716 let theirs = vec!["line1", "THEIRS", "line3"];
717 let result = three_way_merge(&base, &ours, &theirs);
718 assert!(!result.conflicts.is_empty());
719 }
720}