def test_append_agreement(self): m3 = merge3.Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n'], ['aaa\n', 'bbb\n', '222\n']) self.assertEqual(''.join(m3.merge_lines()), 'aaa\nbbb\n222\n')
def test_reprocess_and_base(self): """Reprocessing and showing base breaks correctly""" base_text = ("a\n" * 20).splitlines(True) this_text = ("a\n"*10+"b\n" * 10).splitlines(True) other_text = ("a\n"*10+"c\n"+"b\n" * 8 + "c\n").splitlines(True) m3 = merge3.Merge3(base_text, other_text, this_text) m_lines = m3.merge_lines('OTHER', 'THIS', reprocess=True, base_marker='|||||||') self.assertRaises(merge3.CantReprocessAndShowBase, list, m_lines)
def test_merge_poem_bytes(self): """Test case from diff3 manual""" m3 = merge3.Merge3( [line.encode() for line in TZU], [line.encode() for line in LAO], [line.encode() for line in TAO]) ml = list(m3.merge_lines('LAO', 'TAO')) self.assertEqual( ml, [line.encode() for line in MERGED_RESULT])
def test_merge3_cherrypick(self): base_text = "a\nb\n" this_text = "a\n" other_text = "a\nb\nc\n" # When cherrypicking, lines in base are not part of the conflict m3 = merge3.Merge3(base_text.splitlines(True), this_text.splitlines(True), other_text.splitlines(True), is_cherrypick=True) m_lines = m3.merge_lines() self.assertEqual('a\n<<<<<<<\n=======\nc\n>>>>>>>\n', ''.join(m_lines)) # This is not symmetric m3 = merge3.Merge3(base_text.splitlines(True), other_text.splitlines(True), this_text.splitlines(True), is_cherrypick=True) m_lines = m3.merge_lines() self.assertEqual('a\n<<<<<<<\nb\nc\n=======\n>>>>>>>\n', ''.join(m_lines))
def test_insert_agreement(self): m3 = merge3.Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEqual(''.join(ml), 'aaa\n222\nbbb\n')
def test_mac_text(self): base_text = 'a\r' this_text = 'b\r' other_text = 'c\r' m3 = merge3.Merge3(base_text.splitlines(True), other_text.splitlines(True), this_text.splitlines(True)) m_lines = m3.merge_lines('OTHER', 'THIS') self.assertEqual( '<<<<<<< OTHER\rc\r=======\rb\r' '>>>>>>> THIS\r'.splitlines(True), list(m_lines))
def test_replace_multi(self): """Replacement with regions of different size.""" m3 = merge3.Merge3([b'aaa', b'000', b'000', b'bbb'], [b'aaa', b'111', b'111', b'111', b'bbb'], [b'aaa', b'222', b'222', b'222', b'222', b'bbb']) self.assertEqual(m3.find_unconflicted(), [(0, 1), (3, 4)]) self.assertEqual(list(m3.find_sync_regions()), [(0, 1, 0, 1, 0, 1), (3, 4, 4, 5, 5, 6), (4, 4, 5, 5, 6, 6), ])
def test_replace_clash(self): """Both try to insert lines in the same place.""" m3 = merge3.Merge3(['aaa', '000', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', '222', 'bbb']) self.assertEqual(m3.find_unconflicted(), [(0, 1), (2, 3)]) self.assertEqual(list(m3.find_sync_regions()), [(0, 1, 0, 1, 0, 1), (2, 3, 2, 3, 2, 3), (3, 3, 3, 3, 3, 3), ])
def test_cherrypick(self): base_text = "ba\nb\n" this_text = "ba\n" other_text = "a\nb\nc\n" m3 = merge3.Merge3( base_text.splitlines(True), other_text.splitlines(True), this_text.splitlines(True)) self.assertEqual(m3.find_unconflicted(), []) self.assertEqual(list(m3.find_sync_regions()), [(2, 2, 3, 3, 1, 1)])
def test_null_insert(self): m3 = merge3.Merge3([], ['aaa', 'bbb'], []) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEqual(list(m3.find_sync_regions()), [(0, 0, 2, 2, 0, 0)]) self.assertEqual(list(m3.merge_regions()), [('a', 0, 2)]) self.assertEqual(list(m3.merge_lines()), ['aaa', 'bbb'])
def test_minimal_conflicts_common(self): """Reprocessing""" base_text = ("a\n" * 20).splitlines(True) this_text = ("a\n"*10 + "b\n" * 10).splitlines(True) other_text = ("a\n"*10 + "c\n" + "b\n" * 8 + "c\n").splitlines(True) m3 = merge3.Merge3(base_text, other_text, this_text) m_lines = m3.merge_lines('OTHER', 'THIS', reprocess=True) merged_text = "".join(list(m_lines)) optimal_text = ( "a\n" * 10 + "<<<<<<< OTHER\nc\n" "=======\n" + ">>>>>>> THIS\n" + 8 * "b\n" + "<<<<<<< OTHER\nc\n" + "=======\n" + 2 * "b\n" + ">>>>>>> THIS\n") self.assertEqual(optimal_text, merged_text)
def merge_text(self, params): """Perform a simple 3-way merge of a bzr NEWS file. Each section of a bzr NEWS file is essentially an ordered set of bullet points, so we can simply take a set of bullet points, determine which bullets to add and which to remove, sort, and reserialize. """ # Transform the different versions of the NEWS file into a bunch of # text lines where each line matches one part of the overall # structure, e.g. a heading or bullet. this_lines = list(simple_parse_lines(params.this_lines)) other_lines = list(simple_parse_lines(params.other_lines)) base_lines = list(simple_parse_lines(params.base_lines)) m3 = merge3.Merge3(base_lines, this_lines, other_lines, allow_objects=True) result_chunks = [] for group in m3.merge_groups(): if group[0] == 'conflict': _, base, a, b = group # Are all the conflicting lines bullets? If so, we can merge # this. for line_set in [base, a, b]: for line in line_set: if line[0] != 'bullet': # Something else :( # Maybe the default merge can cope. return 'not_applicable', None # Calculate additions and deletions. new_in_a = set(a).difference(base) new_in_b = set(b).difference(base) all_new = new_in_a.union(new_in_b) deleted_in_a = set(base).difference(a) deleted_in_b = set(base).difference(b) # Combine into the final set of bullet points. final = all_new.difference(deleted_in_a).difference( deleted_in_b) # Sort, and emit. final = sorted(final, key=sort_key) result_chunks.extend(final) else: result_chunks.extend(group[1]) # Transform the merged elements back into real blocks of lines. result_lines = '\n\n'.join(chunk[1] for chunk in result_chunks) return 'success', result_lines
def test_no_changes(self): """No conflicts because nothing changed""" m3 = merge3.Merge3(['aaa', 'bbb'], ['aaa', 'bbb'], ['aaa', 'bbb']) self.assertEqual(m3.find_unconflicted(), [(0, 2)]) self.assertEqual( list(m3.find_sync_regions()), [(0, 2, 0, 2, 0, 2), (2, 2, 2, 2, 2, 2)]) self.assertEqual(list(m3.merge_regions()), [('unchanged', 0, 2)]) self.assertEqual(list(m3.merge_groups()), [('unchanged', ['aaa', 'bbb'])])
def test_front_insert(self): m3 = merge3.Merge3([b'zz'], [b'aaa', b'bbb', b'zz'], [b'zz']) # todo: should use a sentinal at end as from get_matching_blocks # to match without zz self.assertEqual(list(m3.find_sync_regions()), [(0, 1, 2, 3, 0, 1), (1, 1, 3, 3, 1, 1), ]) self.assertEqual(list(m3.merge_regions()), [('a', 0, 2), ('unchanged', 0, 1)]) self.assertEqual(list(m3.merge_groups()), [('a', [b'aaa', b'bbb']), ('unchanged', [b'zz'])])
def test_no_conflicts(self): """No conflicts because only one side changed""" m3 = merge3.Merge3(['aaa', 'bbb'], ['aaa', '111', 'bbb'], ['aaa', 'bbb']) self.assertEqual(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEqual(list(m3.find_sync_regions()), [(0, 1, 0, 1, 0, 1), (1, 2, 2, 3, 1, 2), (2, 2, 3, 3, 2, 2), ]) self.assertEqual(list(m3.merge_regions()), [('unchanged', 0, 1), ('a', 1, 2), ('unchanged', 1, 2), ])
def test_minimal_conflicts_nonunique(self): def add_newline(s): """Add a newline to each entry in the string""" return [(x+'\n') for x in s] base_text = add_newline("abacddefgghij") this_text = add_newline("abacddefgghijkalmontfprz") other_text = add_newline("abacddefgghijknlmontfprd") m3 = merge3.Merge3(base_text, other_text, this_text) m_lines = m3.merge_lines('OTHER', 'THIS', reprocess=True) merged_text = "".join(list(m_lines)) optimal_text = ''.join( add_newline("abacddefgghijk") + ["<<<<<<< OTHER\nn\n=======\na\n>>>>>>> THIS\n"] + add_newline('lmontfpr') + ["<<<<<<< OTHER\nd\n=======\nz\n>>>>>>> THIS\n"] ) self.assertEqual(optimal_text, merged_text)
def test_minimal_conflicts_unique(self): def add_newline(s): """Add a newline to each entry in the string""" return [(x+'\n') for x in s] base_text = add_newline("abcdefghijklm") this_text = add_newline("abcdefghijklmNOPQRSTUVWXYZ") other_text = add_newline("abcdefghijklm1OPQRSTUVWXY2") m3 = merge3.Merge3(base_text, other_text, this_text) m_lines = m3.merge_lines('OTHER', 'THIS', reprocess=True) merged_text = "".join(list(m_lines)) optimal_text = ''.join( add_newline("abcdefghijklm") + ["<<<<<<< OTHER\n1\n=======\nN\n>>>>>>> THIS\n"] + add_newline('OPQRSTUVWXY') + ["<<<<<<< OTHER\n2\n=======\nZ\n>>>>>>> THIS\n"] ) self.assertEqual(optimal_text, merged_text)
def test_append_clash(self): m3 = merge3.Merge3(['aaa\n', 'bbb\n'], ['aaa\n', 'bbb\n', '222\n'], ['aaa\n', 'bbb\n', '333\n']) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEqual(''.join(ml), '''\ aaa bbb << a 222 -- 333 >> b ''')
def merge3_has_conflict(mine, base, your, reprocess=False): """ Reimplementing Merge3.merge_lines to return a has conflict """ had_conflict = False results = [] merge = merge3.Merge3(base, mine, your) # Workout the new line standard to use newline = '\n' if len(base) > 0: if base[0].endswith('\r\n'): newline = '\r\n' elif base[0].endswith('\r'): newline = '\r' start_marker = '<<<<<<< local' mid_marker = '=======' end_marker = '>>>>>>> remote' merge_regions = merge.merge_regions() if reprocess is True: merge_regions = merge.reprocess_merge_regions(merge_regions) for t in merge_regions: what = t[0] if what == 'unchanged': results.extend(base[t[1]:t[2]]) elif what == 'a' or what == 'same': results.extend(mine[t[1]:t[2]]) elif what == 'b': results.extend(your[t[1]:t[2]]) elif what == 'conflict': results.append(start_marker + newline) results.extend(mine[t[1]:t[2]]) results.append(mid_marker + newline) results.extend(your[t[1]:t[2]]) results.append(end_marker + newline) had_conflict = True else: raise ValueError(what) return had_conflict, results
def test_minimal_conflicts_common_with_patiencediff(self): """Reprocessing""" try: import patiencediff except ImportError: self.skipTest('patiencediff not available') base_text = ("a\n" * 20).splitlines(True) this_text = ("a\n"*10 + "b\n" * 10).splitlines(True) other_text = ("a\n"*10 + "c\n" + "b\n" * 8 + "c\n").splitlines(True) m3 = merge3.Merge3( base_text, other_text, this_text, sequence_matcher=patiencediff.PatienceSequenceMatcher) m_lines = m3.merge_lines( 'OTHER', 'THIS', reprocess=True) merged_text = "".join(list(m_lines)) optimal_text = ( "a\n" * 10 + "<<<<<<< OTHER\nc\n" + 8 * "b\n" + "c\n=======\n" + 10 * "b\n" + ">>>>>>> THIS\n") self.assertEqual(optimal_text, merged_text)
def test_allow_objects(self): """Objects other than strs may be used with Merge3. merge_groups and merge_regions work with non-str input. Methods that return lines like merge_lines fail. """ base = [(int2byte(x), int2byte(x)) for x in bytearray(b'abcde')] a = [(int2byte(x), int2byte(x)) for x in bytearray(b'abcdef')] b = [(int2byte(x), int2byte(x)) for x in bytearray(b'Zabcde')] m3 = merge3.Merge3(base, a, b) self.assertEqual( [('b', 0, 1), ('unchanged', 0, 5), ('a', 5, 6)], list(m3.merge_regions())) self.assertEqual( [('b', [(b'Z', b'Z')]), ('unchanged', [ (int2byte(x), int2byte(x)) for x in bytearray(b'abcde')]), ('a', [(b'f', b'f')])], list(m3.merge_groups()))
def test_merge3_cherrypick_w_mixed(self): base_text = 'a\nb\nc\nd\ne\n' this_text = 'a\nb\nq\n' other_text = 'a\nb\nc\nd\nf\ne\ng\n' # When cherrypicking, lines in base are not part of the conflict m3 = merge3.Merge3(base_text.splitlines(True), this_text.splitlines(True), other_text.splitlines(True), is_cherrypick=True) m_lines = m3.merge_lines() self.assertEqual('a\n' 'b\n' '<<<<<<<\n' 'q\n' '=======\n' 'f\n' '>>>>>>>\n' '<<<<<<<\n' '=======\n' 'g\n' '>>>>>>>\n', ''.join(m_lines))
def test_insert_clash(self): """Both try to insert lines in the same place.""" m3 = merge3.Merge3(['aaa\n', 'bbb\n'], ['aaa\n', '111\n', 'bbb\n'], ['aaa\n', '222\n', 'bbb\n']) self.assertEqual(m3.find_unconflicted(), [(0, 1), (1, 2)]) self.assertEqual(list(m3.find_sync_regions()), [(0, 1, 0, 1, 0, 1), (1, 2, 2, 3, 2, 3), (2, 2, 3, 3, 3, 3), ]) self.assertEqual(list(m3.merge_regions()), [('unchanged', 0, 1), ('conflict', 1, 1, 1, 2, 1, 2), ('unchanged', 1, 2)]) self.assertEqual(list(m3.merge_groups()), [('unchanged', ['aaa\n']), ('conflict', [], ['111\n'], ['222\n']), ('unchanged', ['bbb\n']), ]) ml = m3.merge_lines(name_a='a', name_b='b', start_marker='<<', mid_marker='--', end_marker='>>') self.assertEqual(''.join(ml), '''\ aaa << a 111 -- 222 >> b bbb ''')
def resolve(name, base_version, latest_version): new = getformslot('content') base = content.get(name, rcstore.MARKDOWN, base_version) or '' latest = content.get(name, rcstore.MARKDOWN, latest_version) or '' m = merge3.Merge3(lines(base), lines(new), lines(latest)) mg = list(m.merge_groups()) conflicts = 0 for g in mg: if g[0] == 'conflict': conflicts += 1 pass merged = ''.join( m.merge_lines(start_marker='\n!!!--Conflict--!!!\n!--Your version--', mid_marker='\n!--Other version--', end_marker='\n!--End conflict--\n')) d = { 'name': name, 'md_content': merged, 'spath': spath(), 'base_version': latest_version, 'helptext': helptext, 'msg': conflict_msg if conflicts else merge_msg } return HTMLString(template % d)
async def syncGitToPijulCommit(git, pijul, commit, branch): # Check whether Pijul repo has this commit imported already # Notice that this duplicates code from presyncGitToPijulCommit, however, # this additional check will stop the commits from being duplicated. r = await run( f"cd {pijul}; pijul log --grep 'Imported from Git commit {commit}' --hash-only --branch {branch}" ) for patch_id in r.split("\n"): patch_id = patch_id.split(":")[0] if len( patch_id ) == 88: # this is to avoid repository id to be treated as a patch desc = await run( f"cd {pijul}; pijul patch --description {patch_id}") if desc.strip() == f"Imported from Git commit {commit}": # Yay, exported to Pijul already return if (commit, branch) in handled_git_commits: # Exported to Pijul already return # Check whether we've already imported the commit as a patch, and we can # reuse it. For example, look at the following tree: # # D <- hotfix-123 # | # C # | # B <- master # | # A # # In this case, we don't want to import A and B twice. r = await run( f"cd {pijul}; pijul log --grep 'Imported from Git commit {commit}' --hash-only" ) for patch_id in r.split("\n"): patch_id = patch_id.split(":")[0] if len( patch_id ) == 88: # this is to avoid repository id to be treated as a patch desc = await run( f"cd {pijul}; pijul patch --description {patch_id}") if desc.strip() == f"Imported from Git commit {commit}": # Okay, the patch is on another branch. So we apply it print(f" Syncing commit {commit}: {message}...") await run( f"cd {pijul}; pijul apply {patch_id} --branch {branch}") print(chalk.green(f" Done. Reapplied patch {patch_id}")) return # Sync the commit itself now author = ( await run(f"cd {git}; git --no-pager show -s --format='%an <%ae>' {commit}") ).strip() date = (await run(f"cd {git}; git log -1 -s --format=%ci {commit}")).strip() date = "T".join(date.split(" ", 1)) desc = f"Imported from Git commit {commit}" message = ( await run(f"cd {git}; git log -1 --format=%B {commit}")).split("\n")[0] print(f" Syncing commit {commit}: {message}...") await run(f"cd {pijul}; pijul checkout {branch}") # For each changed file for file in (await run( f"cd {git}; git diff-tree --no-commit-id --name-only -r {commit}") ).split("\n"): if file == "": continue await run(f"cd {git}; git checkout {commit}") try: with open(f"{git}/{file}") as f: theirs = f.readlines() except IOError: theirs = None await run(f"cd {git}; git checkout {commit}^") try: with open(f"{git}/{file}") as f: base = f.readlines() except IOError: base = None try: with open(f"{pijul}/{file}") as f: ours = f.readlines() except IOError: ours = None # Perform a 3-way merge if base is None and ours is None: # Assume file creation base = [] ours = [] elif base is None and ours is not None: # Assume file recreation if ours == theirs: # No changes continue else: # Conflict with open(f"{pijul}/{file}", "w") as f: f.write("/*\n") f.write( " * Notice by GitPijul proxy: this file was recreated on Git side (commit\n" ) f.write( f" * {commit[:10]}...). The original (Pijul) version is shown below; make sure to fix\n" ) f.write( " * the conflict yourself by merging the Git changes and remove this banner.\n" ) f.write(" */\n") f.write("".join(ours)) print( chalk.yellow( f" Conflict: {file} recreated by Git with different contents" )) continue elif base is not None and theirs is None: # Assume file deletion os.unlink(f"{pijul}/{file}") continue elif base is not None and ours is None: # Deleted by us continue elif base == ours: with open(f"{pijul}/{file}", "w") as f: f.write("".join(theirs)) continue elif base == theirs: with open(f"{pijul}/{file}", "w") as f: f.write("".join(ours)) continue # Assume file modifications on Git side or both sides merge = merge3.Merge3(base, ours, theirs, is_cherrypick=True) for t in merge.merge_regions(): if t[0] == "conflict": # Aw!.. header = "" header += "/*\n" header += " * Notice by GitPijul proxy: this file was modified by both Git and Pijul. Make\n" header += " * sure to merge the conflict yourself and remove this banner.\n" header += " */\n" print( chalk.yellow( f" Conflict: {file} modified by both Git and Pijul")) break else: # Yay! No conflicts header = "" merged = merge.merge_lines(name_a="Pijul", name_b=f"Git (commit {commit})", start_marker=">" * 32, mid_marker="=" * 32, end_marker="<" * 32) os.makedirs(os.path.dirname(f"{pijul}/{file}"), exist_ok=True) with open(f"{pijul}/{file}", "w") as f: f.write(header) f.write("".join(merged)) # Check whether there are any changes if await run(f"cd {pijul}; pijul status --short") == "": print(chalk.yellow(" No changes (fast-forward)")) handled_git_commits.append((commit, branch)) return # Record changes author = shlex.quote(author) message = shlex.quote(message) r = await run( f"cd {pijul}; pijul record --add-new-files --all --author {author} --branch {branch} --date '{date}' --description '{desc}' --message {message}" ) patch = r.replace("Recorded patch ", "").strip() print(chalk.green(f" Done. Recorded patch {patch}"))
def test_merge_poem(self): """Test case from diff3 manual""" m3 = merge3.Merge3(TZU, LAO, TAO) ml = list(m3.merge_lines('LAO', 'TAO')) self.assertEqual(ml, MERGED_RESULT)