# -*- coding: utf-8 -*- # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see LICENSE for details) """Main three_merge function.""" # Third-party imports from diff_match_patch import diff_match_patch # Constants DIFFER = diff_match_patch() PRESERVED = 0 DELETION = -1 ADDITION = 1 def merge(source: str, target: str, base: str) -> str: diff1_l = DIFFER.diff_main(base, source) diff2_l = DIFFER.diff_main(base, target) diff1 = iter(diff1_l) diff2 = iter(diff2_l) composed_text = [] source = next(diff1, None) target = next(diff2, None) 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) _, (_, invariant) = DIFFER.diff_main(target_text, source_text) target = next(diff2, None) 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) else: invariant = invariant[len(target_text):] target = next(diff2, None) elif target_status == ADDITION: composed_text.append(target_text) target = next(diff2, None) 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):]) composed_text.append(target_text) invariant = '' advance = False target = next(diff2, None) else: target_text = target_text[len(invariant):] composed_text.append(invariant) invariant = '' target = (target_status, target_text) if advance: source = next(diff1, None) elif len(source_text) < len(target_text): # Addition performed by source advance = True composed_text.append(source_text) _, (_, invariant) = DIFFER.diff_main(source_text, target_text) source = next(diff1, None) while invariant != '' and target 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) else: invariant = invariant[len(source_text):] source = next(diff1, None) elif source_status == ADDITION: composed_text.append(source_text) source = next(diff1, None) 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):]) composed_text.append(source_text) invariant = '' advance = False source = next(diff1, None) else: source_text = source_text[len(invariant):] composed_text.append(invariant) invariant = '' source = (source_status, source_text) if advance: target = next(diff2, None) else: # Source and target are equal composed_text.append(source_text) source = next(diff1, None) target = next(diff2, None) elif source_status == ADDITION and target_status == PRESERVED: # Source is adding text composed_text.append(source_text) source = next(diff1, None) elif source_status == PRESERVED and target_status == ADDITION: # Target is adding text composed_text.append(target_text) target = next(diff2, None) 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) source = next(diff1, None) elif len(target_text) < len(source_text): source_text = source_text[len(target_text):] source = (source_status, source_text) target = next(diff2, None) 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) target = next(diff2, None) elif len(source_text) < len(target_text): # Advance to next source target_text = target_text[len(source_text):] target = (target_status, target_text) source = next(diff1, None) elif source_status == DELETION and target_status == ADDITION: # Merge conflict composed_text.append('<<<<<<< ++ {0} '.format(target_text)) composed_text.append('======= -- {0} '.format(source_text)) composed_text.append('>>>>>>>') source = next(diff1, None) target = next(diff2, None) 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) elif source_status == ADDITION and target_status == DELETION: # Merge conflict composed_text.append('<<<<<<< ++ {0} '.format(source_text)) composed_text.append('======= -- {0} '.format(target_text)) composed_text.append('>>>>>>>') source = next(diff1, None) target = next(diff2, None) 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) 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 composed_text.append('<<<<<<< ++ {0} '.format(source_text)) composed_text.append('======= ++ {0} '.format(target_text)) composed_text.append('>>>>>>>') else: if target_text.startswith(source_text): composed_text.append(target_text) else: # Merge conflict composed_text.append('<<<<<<< ++ {0} '.format(source_text)) composed_text.append('======= ++ {0} '.format(target_text)) composed_text.append('>>>>>>>') source = next(diff1, None) target = next(diff2, None) 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) target = next(diff2, None) 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) source = next(diff1, None) else: merge_conflict = True else: if target_text == source_text: # Both source and target remove the same text source = next(diff1, None) target = next(diff2, None) else: merge_conflict = True if merge_conflict: 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) source = next(diff1, None) 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) target = next(diff2, None) return ''.join(composed_text)