298 lines
14 KiB
Python
298 lines
14 KiB
Python
|
|
# SOURCE: https://github.com/spyder-ide/three-merge/blob/master/three_merge/merge.py
|
||
|
|
from typing import Optional
|
||
|
|
from diff_match_patch import diff_match_patch
|
||
|
|
|
||
|
|
# Constants
|
||
|
|
DIFFER = diff_match_patch()
|
||
|
|
DIFFER.Diff_Timeout = 0.1
|
||
|
|
DIFFER.Diff_EditCost = 4
|
||
|
|
PRESERVED = 0
|
||
|
|
DELETION = -1
|
||
|
|
ADDITION = 1
|
||
|
|
|
||
|
|
|
||
|
|
def merge(
|
||
|
|
source: Optional[str], target: Optional[str], base: Optional[str]
|
||
|
|
) -> Optional[str]:
|
||
|
|
if source is None or target is None or base is None:
|
||
|
|
return None
|
||
|
|
|
||
|
|
diff1_l = DIFFER.diff_main(base, source)
|
||
|
|
diff2_l = DIFFER.diff_main(base, target)
|
||
|
|
|
||
|
|
DIFFER.diff_cleanupEfficiency(diff1_l)
|
||
|
|
DIFFER.diff_cleanupEfficiency(diff2_l)
|
||
|
|
|
||
|
|
diff1 = iter(diff1_l)
|
||
|
|
diff2 = iter(diff2_l)
|
||
|
|
|
||
|
|
composed_text = []
|
||
|
|
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
|
||
|
|
prev_source_text = ""
|
||
|
|
prev_target_text = ""
|
||
|
|
|
||
|
|
while source is not None and target is not None:
|
||
|
|
source_status, source_text = source
|
||
|
|
target_status, target_text = target
|
||
|
|
if source_status == PRESERVED and target_status == PRESERVED:
|
||
|
|
# Base is preserved for both source and target
|
||
|
|
if len(source_text) > len(target_text):
|
||
|
|
# Addition performed by target
|
||
|
|
advance = True
|
||
|
|
composed_text.append(target_text)
|
||
|
|
tempdiff = DIFFER.diff_main(target_text, source_text)
|
||
|
|
_, invariant = tempdiff[1]
|
||
|
|
# _, (_, invariant) = DIFFER.diff_main(target_text, source_text)
|
||
|
|
prev_target_text = target[1]
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
while invariant != "" and target is not None:
|
||
|
|
# Apply target changes until invariant is preserved
|
||
|
|
# target = next(diff2, None)
|
||
|
|
target_status, target_text = target
|
||
|
|
if target_status == DELETION:
|
||
|
|
if len(target_text) > len(invariant):
|
||
|
|
target_text = target_text[len(invariant) :]
|
||
|
|
invariant = ""
|
||
|
|
target = (target_status, target_text) # type: ignore
|
||
|
|
else:
|
||
|
|
invariant = invariant[len(target_text) :]
|
||
|
|
prev_target_text = target[1]
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
elif target_status == ADDITION:
|
||
|
|
composed_text.append(target_text)
|
||
|
|
prev_target_text = target[1]
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
else:
|
||
|
|
# Recompute invariant and advance source
|
||
|
|
if len(invariant) > len(target_text):
|
||
|
|
assert invariant[: len(target_text)] == target_text
|
||
|
|
source = (source_status, invariant[len(target_text) :]) # type: ignore
|
||
|
|
composed_text.append(target_text)
|
||
|
|
invariant = ""
|
||
|
|
advance = False
|
||
|
|
prev_target_text = target[1]
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
else:
|
||
|
|
target_text = target_text[len(invariant) :]
|
||
|
|
composed_text.append(invariant)
|
||
|
|
invariant = ""
|
||
|
|
target = (target_status, target_text) # type: ignore
|
||
|
|
if advance:
|
||
|
|
prev_source_text = source[1] # type: ignore
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
elif len(source_text) < len(target_text):
|
||
|
|
# Addition performed by source
|
||
|
|
advance = True
|
||
|
|
composed_text.append(source_text)
|
||
|
|
tempdiff = DIFFER.diff_main(target_text, source_text)
|
||
|
|
_, invariant = tempdiff[1] if len(tempdiff) > 1 else tempdiff[0]
|
||
|
|
# _, (_, invariant) = DIFFER.diff_main(source_text, target_text)
|
||
|
|
prev_source_text = source[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
while invariant != "" and target is not None and source is not None:
|
||
|
|
# Apply source changes until invariant is preserved
|
||
|
|
source_status, source_text = source
|
||
|
|
if source_status == DELETION:
|
||
|
|
if len(source_text) > len(invariant):
|
||
|
|
source_text = source_text[len(invariant) :]
|
||
|
|
invariant = ""
|
||
|
|
source = (source_status, source_text) # type: ignore
|
||
|
|
else:
|
||
|
|
invariant = invariant[len(source_text) :]
|
||
|
|
prev_source_text = source[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
elif source_status == ADDITION:
|
||
|
|
composed_text.append(source_text)
|
||
|
|
prev_source_text = source[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
else:
|
||
|
|
# Recompute invariant and advance source
|
||
|
|
# invariant = invariant[:len(source_text)]
|
||
|
|
if len(invariant) > len(source_text):
|
||
|
|
assert invariant[: len(source_text)] == source_text
|
||
|
|
target = (target_status, invariant[len(source_text) :]) # type: ignore
|
||
|
|
composed_text.append(source_text)
|
||
|
|
invariant = ""
|
||
|
|
advance = False
|
||
|
|
prev_source_text = source[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
else:
|
||
|
|
source_text = source_text[len(invariant) :]
|
||
|
|
composed_text.append(invariant)
|
||
|
|
invariant = ""
|
||
|
|
source = (source_status, source_text) # type: ignore
|
||
|
|
if advance:
|
||
|
|
prev_target_text = target[1] # type: ignore
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
else:
|
||
|
|
# Source and target are equal
|
||
|
|
composed_text.append(source_text)
|
||
|
|
prev_source_text = source[1]
|
||
|
|
prev_target_text = target[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
elif source_status == ADDITION and target_status == PRESERVED:
|
||
|
|
# Source is adding text
|
||
|
|
composed_text.append(source_text)
|
||
|
|
prev_source_text = source[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
elif source_status == PRESERVED and target_status == ADDITION:
|
||
|
|
# Target is adding text
|
||
|
|
composed_text.append(target_text)
|
||
|
|
prev_target_text = target[1]
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
elif source_status == DELETION and target_status == PRESERVED:
|
||
|
|
if len(target_text) > len(source_text):
|
||
|
|
# Take target text, remove the corresponding part from source
|
||
|
|
target_text = target_text[len(source_text) :]
|
||
|
|
# composed_text.append(target_text)
|
||
|
|
# source = diff1.pop(0)
|
||
|
|
target = (target_status, target_text) # type: ignore
|
||
|
|
prev_source_text = source[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
elif len(target_text) <= len(source_text):
|
||
|
|
source_text = source_text[len(target_text) :]
|
||
|
|
source = (source_status, source_text) # type: ignore
|
||
|
|
prev_target_text = target[1]
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
elif source_status == PRESERVED and target_status == DELETION:
|
||
|
|
if len(source_text) > len(target_text):
|
||
|
|
# Take source text, remove the corresponding part from target
|
||
|
|
source_text = source_text[len(target_text) :]
|
||
|
|
source = (source_status, source_text) # type: ignore
|
||
|
|
prev_target_text = target[1]
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
elif len(source_text) <= len(target_text):
|
||
|
|
# Advance to next source
|
||
|
|
target_text = target_text[len(source_text) :]
|
||
|
|
target = (target_status, target_text) # type: ignore
|
||
|
|
prev_source_text = source[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
elif source_status == DELETION and target_status == ADDITION:
|
||
|
|
# Merge conflict
|
||
|
|
# Err on the side of deletion. Do not add anything
|
||
|
|
# composed_text.append("<<<<<<< ++ {0} ".format(target_text))
|
||
|
|
# composed_text.append("======= -- {0} ".format(source_text))
|
||
|
|
# composed_text.append(">>>>>>>")
|
||
|
|
prev_source_text = source[1]
|
||
|
|
prev_target_text = target[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
if target is not None:
|
||
|
|
target_status, target_text = target
|
||
|
|
if target_text.startswith(source_text):
|
||
|
|
target_text = target_text[len(source_text) :]
|
||
|
|
target = (target_status, target_text) # type: ignore
|
||
|
|
elif source_status == ADDITION and target_status == DELETION:
|
||
|
|
# Merge conflict
|
||
|
|
# Err on the side of deletion. Do not add anything
|
||
|
|
# composed_text.append("<<<<<<< ++ {0} ".format(source_text))
|
||
|
|
# composed_text.append("======= -- {0} ".format(target_text))
|
||
|
|
# composed_text.append(">>>>>>>")
|
||
|
|
prev_source_text = source[1]
|
||
|
|
prev_target_text = target[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
if source is not None:
|
||
|
|
source_status, source_text = source
|
||
|
|
if source_text.startswith(target_text):
|
||
|
|
source_text = source_text[len(target_text) :]
|
||
|
|
source = (source_status, source_text) # type: ignore
|
||
|
|
elif source_status == ADDITION and target_status == ADDITION:
|
||
|
|
# Possible merge conflict
|
||
|
|
if len(source_text) >= len(target_text):
|
||
|
|
if source_text.startswith(target_text):
|
||
|
|
composed_text.append(source_text)
|
||
|
|
else:
|
||
|
|
# Merge conflict
|
||
|
|
# Insert text that has highest distance from original
|
||
|
|
# we assume original is last operation
|
||
|
|
source_dist = DIFFER.diff_levenshtein(
|
||
|
|
DIFFER.diff_main(source_text, prev_source_text)
|
||
|
|
)
|
||
|
|
target_dist = DIFFER.diff_levenshtein(
|
||
|
|
DIFFER.diff_main(target_text, prev_target_text)
|
||
|
|
)
|
||
|
|
if source_dist > target_dist:
|
||
|
|
composed_text.append(source_text)
|
||
|
|
else:
|
||
|
|
composed_text.append(target_text)
|
||
|
|
else:
|
||
|
|
if target_text.startswith(source_text):
|
||
|
|
composed_text.append(target_text)
|
||
|
|
else:
|
||
|
|
# Merge conflict
|
||
|
|
# Insert text that has highest distance from original
|
||
|
|
source_dist = DIFFER.diff_levenshtein(
|
||
|
|
DIFFER.diff_main(source_text, prev_source_text)
|
||
|
|
)
|
||
|
|
target_dist = DIFFER.diff_levenshtein(
|
||
|
|
DIFFER.diff_main(target_text, prev_target_text)
|
||
|
|
)
|
||
|
|
if source_dist > target_dist:
|
||
|
|
composed_text.append(source_text)
|
||
|
|
else:
|
||
|
|
composed_text.append(target_text)
|
||
|
|
prev_source_text = source[1]
|
||
|
|
prev_target_text = target[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
elif source_status == DELETION and target_status == DELETION:
|
||
|
|
# Possible merge conflict
|
||
|
|
merge_conflict = False
|
||
|
|
if len(source_text) > len(target_text):
|
||
|
|
if source_text.startswith(target_text):
|
||
|
|
# Peek target to delete preserved text
|
||
|
|
source_text = source_text[len(target_text) :]
|
||
|
|
source = (source_status, source_text) # type: ignore
|
||
|
|
prev_target_text = target[1]
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
else:
|
||
|
|
merge_conflict = True
|
||
|
|
elif len(target_text) > len(source_text):
|
||
|
|
if target_text.startswith(source_text):
|
||
|
|
target_text = target_text[len(source_text) :]
|
||
|
|
target = (target_status, target_text) # type: ignore
|
||
|
|
prev_source_text = source[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
else:
|
||
|
|
merge_conflict = True
|
||
|
|
else:
|
||
|
|
if target_text == source_text:
|
||
|
|
# Both source and target remove the same text
|
||
|
|
prev_source_text = source[1]
|
||
|
|
prev_target_text = target[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
else:
|
||
|
|
merge_conflict = True
|
||
|
|
|
||
|
|
# Don't handle double deletion scenario
|
||
|
|
if merge_conflict:
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
# composed_text.append("<<<<<<< -- {0} ".format(source_text))
|
||
|
|
# composed_text.append("======= -- {0} ".format(target_text))
|
||
|
|
# composed_text.append(">>>>>>>")
|
||
|
|
|
||
|
|
while source is not None:
|
||
|
|
source_status, source_text = source
|
||
|
|
# assert source_status == ADDITION or source_status == PRESERVED
|
||
|
|
if source_status == ADDITION:
|
||
|
|
composed_text.append(source_text)
|
||
|
|
prev_source_text = source[1]
|
||
|
|
source = next(diff1, None) # type: ignore
|
||
|
|
|
||
|
|
while target is not None:
|
||
|
|
target_status, target_text = target
|
||
|
|
# assert target_status == ADDITION or source_status == PRESERVED
|
||
|
|
if target_status == ADDITION:
|
||
|
|
composed_text.append(target_text)
|
||
|
|
prev_target_text = target[1]
|
||
|
|
target = next(diff2, None) # type: ignore
|
||
|
|
|
||
|
|
return "".join(composed_text)
|