# HG changeset patch # User Alexis S. L. Carvalho # Date 1176765825 10800 # Node ID 390d110a57b856834b83d4581d1eb2234d704bcb # Parent 46280c004f224017da0a45935e383a8563847496# Parent 1ccd3b9a7b1f77135e3060595159b2f36d0991ab Merge with crew. diff --git a/contrib/simplemerge b/contrib/simplemerge new file mode 100755 --- /dev/null +++ b/contrib/simplemerge @@ -0,0 +1,557 @@ +#!/usr/bin/env python +# Copyright (C) 2004, 2005 Canonical Ltd +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +# mbp: "you know that thing where cvs gives you conflict markers?" +# s: "i hate that." + +from mercurial import demandimport +demandimport.enable() + +from mercurial import util, mdiff, fancyopts +from mercurial.i18n import _ + + +class CantReprocessAndShowBase(Exception): + pass + + +def intersect(ra, rb): + """Given two ranges return the range where they intersect or None. + + >>> intersect((0, 10), (0, 6)) + (0, 6) + >>> intersect((0, 10), (5, 15)) + (5, 10) + >>> intersect((0, 10), (10, 15)) + >>> intersect((0, 9), (10, 15)) + >>> intersect((0, 9), (7, 15)) + (7, 9) + """ + assert ra[0] <= ra[1] + assert rb[0] <= rb[1] + + sa = max(ra[0], rb[0]) + sb = min(ra[1], rb[1]) + if sa < sb: + return sa, sb + else: + return None + + +def compare_range(a, astart, aend, b, bstart, bend): + """Compare a[astart:aend] == b[bstart:bend], without slicing. + """ + if (aend-astart) != (bend-bstart): + return False + for ia, ib in zip(xrange(astart, aend), xrange(bstart, bend)): + if a[ia] != b[ib]: + return False + else: + return True + + + + +class Merge3Text(object): + """3-way merge of texts. + + Given strings BASE, OTHER, THIS, tries to produce a combined text + incorporating the changes from both BASE->OTHER and BASE->THIS.""" + def __init__(self, basetext, atext, btext, base=None, a=None, b=None): + self.basetext = basetext + self.atext = atext + self.btext = btext + if base is None: + base = mdiff.splitnewlines(basetext) + if a is None: + a = mdiff.splitnewlines(atext) + if b is None: + b = mdiff.splitnewlines(btext) + self.base = base + self.a = a + self.b = b + + + + def merge_lines(self, + name_a=None, + name_b=None, + name_base=None, + start_marker='<<<<<<<', + mid_marker='=======', + end_marker='>>>>>>>', + base_marker=None, + reprocess=False): + """Return merge in cvs-like form. + """ + self.conflicts = False + newline = '\n' + if len(self.a) > 0: + if self.a[0].endswith('\r\n'): + newline = '\r\n' + elif self.a[0].endswith('\r'): + newline = '\r' + if base_marker and reprocess: + raise CantReprocessAndShowBase() + if name_a: + start_marker = start_marker + ' ' + name_a + if name_b: + end_marker = end_marker + ' ' + name_b + if name_base and base_marker: + base_marker = base_marker + ' ' + name_base + merge_regions = self.merge_regions() + if reprocess is True: + merge_regions = self.reprocess_merge_regions(merge_regions) + for t in merge_regions: + what = t[0] + if what == 'unchanged': + for i in range(t[1], t[2]): + yield self.base[i] + elif what == 'a' or what == 'same': + for i in range(t[1], t[2]): + yield self.a[i] + elif what == 'b': + for i in range(t[1], t[2]): + yield self.b[i] + elif what == 'conflict': + self.conflicts = True + yield start_marker + newline + for i in range(t[3], t[4]): + yield self.a[i] + if base_marker is not None: + yield base_marker + newline + for i in range(t[1], t[2]): + yield self.base[i] + yield mid_marker + newline + for i in range(t[5], t[6]): + yield self.b[i] + yield end_marker + newline + else: + raise ValueError(what) + + + + + + def merge_annotated(self): + """Return merge with conflicts, showing origin of lines. + + Most useful for debugging merge. + """ + for t in self.merge_regions(): + what = t[0] + if what == 'unchanged': + for i in range(t[1], t[2]): + yield 'u | ' + self.base[i] + elif what == 'a' or what == 'same': + for i in range(t[1], t[2]): + yield what[0] + ' | ' + self.a[i] + elif what == 'b': + for i in range(t[1], t[2]): + yield 'b | ' + self.b[i] + elif what == 'conflict': + yield '<<<<\n' + for i in range(t[3], t[4]): + yield 'A | ' + self.a[i] + yield '----\n' + for i in range(t[5], t[6]): + yield 'B | ' + self.b[i] + yield '>>>>\n' + else: + raise ValueError(what) + + + + + + def merge_groups(self): + """Yield sequence of line groups. Each one is a tuple: + + 'unchanged', lines + Lines unchanged from base + + 'a', lines + Lines taken from a + + 'same', lines + Lines taken from a (and equal to b) + + 'b', lines + Lines taken from b + + 'conflict', base_lines, a_lines, b_lines + Lines from base were changed to either a or b and conflict. + """ + for t in self.merge_regions(): + what = t[0] + if what == 'unchanged': + yield what, self.base[t[1]:t[2]] + elif what == 'a' or what == 'same': + yield what, self.a[t[1]:t[2]] + elif what == 'b': + yield what, self.b[t[1]:t[2]] + elif what == 'conflict': + yield (what, + self.base[t[1]:t[2]], + self.a[t[3]:t[4]], + self.b[t[5]:t[6]]) + else: + raise ValueError(what) + + + def merge_regions(self): + """Return sequences of matching and conflicting regions. + + This returns tuples, where the first value says what kind we + have: + + 'unchanged', start, end + Take a region of base[start:end] + + 'same', astart, aend + b and a are different from base but give the same result + + 'a', start, end + Non-clashing insertion from a[start:end] + + Method is as follows: + + The two sequences align only on regions which match the base + and both descendents. These are found by doing a two-way diff + of each one against the base, and then finding the + intersections between those regions. These "sync regions" + are by definition unchanged in both and easily dealt with. + + The regions in between can be in any of three cases: + conflicted, or changed on only one side. + """ + + # section a[0:ia] has been disposed of, etc + iz = ia = ib = 0 + + for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions(): + #print 'match base [%d:%d]' % (zmatch, zend) + + matchlen = zend - zmatch + assert matchlen >= 0 + assert matchlen == (aend - amatch) + assert matchlen == (bend - bmatch) + + len_a = amatch - ia + len_b = bmatch - ib + len_base = zmatch - iz + assert len_a >= 0 + assert len_b >= 0 + assert len_base >= 0 + + #print 'unmatched a=%d, b=%d' % (len_a, len_b) + + if len_a or len_b: + # try to avoid actually slicing the lists + equal_a = compare_range(self.a, ia, amatch, + self.base, iz, zmatch) + equal_b = compare_range(self.b, ib, bmatch, + self.base, iz, zmatch) + same = compare_range(self.a, ia, amatch, + self.b, ib, bmatch) + + if same: + yield 'same', ia, amatch + elif equal_a and not equal_b: + yield 'b', ib, bmatch + elif equal_b and not equal_a: + yield 'a', ia, amatch + elif not equal_a and not equal_b: + yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch + else: + raise AssertionError("can't handle a=b=base but unmatched") + + ia = amatch + ib = bmatch + iz = zmatch + + # if the same part of the base was deleted on both sides + # that's OK, we can just skip it. + + + if matchlen > 0: + assert ia == amatch + assert ib == bmatch + assert iz == zmatch + + yield 'unchanged', zmatch, zend + iz = zend + ia = aend + ib = bend + + + def reprocess_merge_regions(self, merge_regions): + """Where there are conflict regions, remove the agreed lines. + + Lines where both A and B have made the same changes are + eliminated. + """ + for region in merge_regions: + if region[0] != "conflict": + yield region + continue + type, iz, zmatch, ia, amatch, ib, bmatch = region + a_region = self.a[ia:amatch] + b_region = self.b[ib:bmatch] + matches = mdiff.get_matching_blocks(''.join(a_region), + ''.join(b_region)) + next_a = ia + next_b = ib + for region_ia, region_ib, region_len in matches[:-1]: + region_ia += ia + region_ib += ib + reg = self.mismatch_region(next_a, region_ia, next_b, + region_ib) + if reg is not None: + yield reg + yield 'same', region_ia, region_len+region_ia + next_a = region_ia + region_len + next_b = region_ib + region_len + reg = self.mismatch_region(next_a, amatch, next_b, bmatch) + if reg is not None: + yield reg + + + def mismatch_region(next_a, region_ia, next_b, region_ib): + if next_a < region_ia or next_b < region_ib: + return 'conflict', None, None, next_a, region_ia, next_b, region_ib + mismatch_region = staticmethod(mismatch_region) + + + def find_sync_regions(self): + """Return a list of sync regions, where both descendents match the base. + + Generates a list of (base1, base2, a1, a2, b1, b2). There is + always a zero-length sync region at the end of all the files. + """ + + ia = ib = 0 + amatches = mdiff.get_matching_blocks(self.basetext, self.atext) + bmatches = mdiff.get_matching_blocks(self.basetext, self.btext) + len_a = len(amatches) + len_b = len(bmatches) + + sl = [] + + while ia < len_a and ib < len_b: + abase, amatch, alen = amatches[ia] + bbase, bmatch, blen = bmatches[ib] + + # there is an unconflicted block at i; how long does it + # extend? until whichever one ends earlier. + i = intersect((abase, abase+alen), (bbase, bbase+blen)) + if i: + intbase = i[0] + intend = i[1] + intlen = intend - intbase + + # found a match of base[i[0], i[1]]; this may be less than + # the region that matches in either one + assert intlen <= alen + assert intlen <= blen + assert abase <= intbase + assert bbase <= intbase + + asub = amatch + (intbase - abase) + bsub = bmatch + (intbase - bbase) + aend = asub + intlen + bend = bsub + intlen + + assert self.base[intbase:intend] == self.a[asub:aend], \ + (self.base[intbase:intend], self.a[asub:aend]) + + assert self.base[intbase:intend] == self.b[bsub:bend] + + sl.append((intbase, intend, + asub, aend, + bsub, bend)) + + # advance whichever one ends first in the base text + if (abase + alen) < (bbase + blen): + ia += 1 + else: + ib += 1 + + intbase = len(self.base) + abase = len(self.a) + bbase = len(self.b) + sl.append((intbase, intbase, abase, abase, bbase, bbase)) + + return sl + + + + def find_unconflicted(self): + """Return a list of ranges in base that are not conflicted.""" + am = mdiff.get_matching_blocks(self.basetext, self.atext) + bm = mdiff.get_matching_blocks(self.basetext, self.btext) + + unc = [] + + while am and bm: + # there is an unconflicted block at i; how long does it + # extend? until whichever one ends earlier. + a1 = am[0][0] + a2 = a1 + am[0][2] + b1 = bm[0][0] + b2 = b1 + bm[0][2] + i = intersect((a1, a2), (b1, b2)) + if i: + unc.append(i) + + if a2 < b2: + del am[0] + else: + del bm[0] + + return unc + + +# bzr compatible interface, for the tests +class Merge3(Merge3Text): + """3-way merge of texts. + + Given BASE, OTHER, THIS, tries to produce a combined text + incorporating the changes from both BASE->OTHER and BASE->THIS. + All three will typically be sequences of lines.""" + def __init__(self, base, a, b): + basetext = '\n'.join([i.strip('\n') for i in base] + ['']) + atext = '\n'.join([i.strip('\n') for i in a] + ['']) + btext = '\n'.join([i.strip('\n') for i in b] + ['']) + if util.binary(basetext) or util.binary(atext) or util.binary(btext): + raise util.Abort(_("don't know how to merge binary files")) + Merge3Text.__init__(self, basetext, atext, btext, base, a, b) + + +def simplemerge(local, base, other, **opts): + def readfile(filename): + f = open(filename, "rb") + text = f.read() + f.close() + if util.binary(text): + msg = _("%s looks like a binary file.") % filename + if not opts.get('text'): + raise util.Abort(msg) + elif not opts.get('quiet'): + sys.stderr.write(_('warning: %s\n') % msg) + return text + + name_a = local + name_b = other + labels = opts.get('label', []) + if labels: + name_a = labels.pop(0) + if labels: + name_b = labels.pop(0) + if labels: + raise util.Abort(_("can only specify two labels.")) + + localtext = readfile(local) + basetext = readfile(base) + othertext = readfile(other) + + orig = local + local = os.path.realpath(local) + if not opts.get('print'): + opener = util.opener(os.path.dirname(local)) + out = opener(os.path.basename(local), "w", atomictemp=True) + else: + out = sys.stdout + + reprocess = not opts.get('no_minimal') + + m3 = Merge3Text(basetext, localtext, othertext) + for line in m3.merge_lines(name_a=name_a, name_b=name_b, + reprocess=reprocess): + out.write(line) + + if not opts.get('print'): + out.rename() + + if m3.conflicts: + if not opts.get('quiet'): + sys.stdout.flush() + sys.stderr.write(_("warning: conflicts during merge.\n")) + return 1 + +options = [('L', 'label', [], _('labels to use on conflict markers')), + ('a', 'text', None, _('treat all files as text')), + ('p', 'print', None, + _('print results instead of overwriting LOCAL')), + ('', 'no-minimal', None, + _('do not try to minimize conflict regions')), + ('h', 'help', None, _('display help and exit')), + ('q', 'quiet', None, _('suppress output'))] + +usage = _('''simplemerge [OPTS] LOCAL BASE OTHER + + Simple three-way file merge utility with a minimal feature set. + + Apply to LOCAL the changes necessary to go from BASE to OTHER. + + By default, LOCAL is overwritten with the results of this operation. +''') + +def showhelp(): + sys.stdout.write(usage) + sys.stdout.write('\noptions:\n') + + out_opts = [] + for shortopt, longopt, default, desc in options: + out_opts.append(('%2s%s' % (shortopt and '-%s' % shortopt, + longopt and ' --%s' % longopt), + '%s' % desc)) + opts_len = max([len(opt[0]) for opt in out_opts]) + for first, second in out_opts: + sys.stdout.write(' %-*s %s\n' % (opts_len, first, second)) + +class ParseError(Exception): + """Exception raised on errors in parsing the command line.""" + +def main(argv): + try: + opts = {} + try: + args = fancyopts.fancyopts(argv[1:], options, opts) + except fancyopts.getopt.GetoptError, e: + raise ParseError(e) + if opts['help']: + showhelp() + return 0 + if len(args) != 3: + raise ParseError(_('wrong number of arguments')) + return simplemerge(*args, **opts) + except ParseError, e: + sys.stdout.write("%s: %s\n" % (sys.argv[0], e)) + showhelp() + return 1 + except util.Abort, e: + sys.stderr.write("abort: %s\n" % e) + return 255 + except KeyboardInterrupt: + return 255 + +if __name__ == '__main__': + import sys + import os + sys.exit(main(sys.argv)) diff --git a/mercurial/mdiff.py b/mercurial/mdiff.py --- a/mercurial/mdiff.py +++ b/mercurial/mdiff.py @@ -250,6 +250,10 @@ def patchtext(bin): def patch(a, bin): return mpatch.patches(a, [bin]) +# similar to difflib.SequenceMatcher.get_matching_blocks +def get_matching_blocks(a, b): + return [(d[0], d[2], d[1] - d[0]) for d in bdiff.blocks(a, b)] + patches = mpatch.patches patchedsize = mpatch.patchedsize textdiff = bdiff.bdiff diff --git a/mercurial/merge.py b/mercurial/merge.py --- a/mercurial/merge.py +++ b/mercurial/merge.py @@ -105,10 +105,15 @@ def findcopies(repo, m1, m2, ma, limit): def findold(fctx): "find files that path was copied from, back to linkrev limit" old = {} + seen = {} orig = fctx.path() visit = [fctx] while visit: fc = visit.pop() + s = str(fc) + if s in seen: + continue + seen[s] = 1 if fc.path() != orig and fc.path() not in old: old[fc.path()] = 1 if fc.rev() < limit: diff --git a/tests/run-tests.py b/tests/run-tests.py --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -19,7 +19,7 @@ import sys import tempfile import time -required_tools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed", "merge"] +required_tools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"] parser = optparse.OptionParser("%prog [options] [tests]") parser.add_option("-v", "--verbose", action="store_true", @@ -340,17 +340,18 @@ check_required_tools() os.environ['LANG'] = os.environ['LC_ALL'] = 'C' os.environ['TZ'] = 'GMT' -os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' -os.environ["HGMERGE"] = sys.executable + ' -c "import sys; sys.exit(0)"' -os.environ["HGUSER"] = "test" -os.environ["HGENCODING"] = "ascii" -os.environ["HGENCODINGMODE"] = "strict" - TESTDIR = os.environ["TESTDIR"] = os.getcwd() HGTMP = os.environ["HGTMP"] = tempfile.mkdtemp("", "hgtests.") DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids') HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc') +os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' +os.environ["HGMERGE"] = 'python "%s"' % os.path.join(TESTDIR, os.path.pardir, + 'contrib', 'simplemerge') +os.environ["HGUSER"] = "test" +os.environ["HGENCODING"] = "ascii" +os.environ["HGENCODINGMODE"] = "strict" + vlog("# Using TESTDIR", TESTDIR) vlog("# Using HGTMP", HGTMP) diff --git a/tests/test-annotate b/tests/test-annotate --- a/tests/test-annotate +++ b/tests/test-annotate @@ -1,5 +1,7 @@ #!/bin/sh +HGMERGE=true; export HGMERGE + echo % init hg init diff --git a/tests/test-backout b/tests/test-backout --- a/tests/test-backout +++ b/tests/test-backout @@ -1,5 +1,7 @@ #!/bin/sh +HGMERGE=true; export HGMERGE + echo '# basic operation' hg init basic cd basic diff --git a/tests/test-conflict b/tests/test-conflict --- a/tests/test-conflict +++ b/tests/test-conflict @@ -9,7 +9,6 @@ hg commit -m branch1 -d "1000000 0" hg co 0 echo "something else" > a hg commit -m branch2 -d "1000000 0" -HGMERGE=merge; export HGMERGE hg merge 1 hg id egrep -v ">>>|<<<" a diff --git a/tests/test-conflict.out b/tests/test-conflict.out --- a/tests/test-conflict.out +++ b/tests/test-conflict.out @@ -1,5 +1,5 @@ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved -merge: warning: conflicts during merge +warning: conflicts during merge. merging a merging a failed! 0 files updated, 0 files merged, 0 files removed, 1 files unresolved diff --git a/tests/test-globalopts b/tests/test-globalopts --- a/tests/test-globalopts +++ b/tests/test-globalopts @@ -17,7 +17,7 @@ cd .. hg clone a c cd c hg pull -f ../b -HGMERGE=merge hg merge +hg merge cd .. diff --git a/tests/test-install b/tests/test-install --- a/tests/test-install +++ b/tests/test-install @@ -1,3 +1,3 @@ #!/bin/sh -HGMERGE=merge hg debuginstall +hg debuginstall diff --git a/tests/test-merge-commit b/tests/test-merge-commit --- a/tests/test-merge-commit +++ b/tests/test-merge-commit @@ -1,9 +1,6 @@ #!/bin/sh # check that renames are correctly saved by a commit after a merge -HGMERGE=merge -export HGMERGE - # test with the merge on 3 having the rename on the local parent hg init a cd a diff --git a/tests/test-merge-local b/tests/test-merge-local --- a/tests/test-merge-local +++ b/tests/test-merge-local @@ -38,13 +38,13 @@ hg diff --nodates | grep "^[+-][^<>]" hg st echo "# local merge with conflicts" -HGMERGE=merge hg co +hg co hg co 0 hg diff --nodates | grep "^[+-][^<>]" hg st echo "# local merge without conflicts" hg revert zzz2_merge_bad -HGMERGE=merge hg co +hg co hg diff --nodates | grep "^[+-][^<>]" hg st diff --git a/tests/test-merge-local.out b/tests/test-merge-local.out --- a/tests/test-merge-local.out +++ b/tests/test-merge-local.out @@ -36,7 +36,7 @@ 2 files updated, 0 files merged, 3 files M zzz1_merge_ok M zzz2_merge_bad # local merge with conflicts -merge: warning: conflicts during merge +warning: conflicts during merge. merging zzz1_merge_ok merging zzz2_merge_bad merging zzz2_merge_bad failed! diff --git a/tests/test-merge-revert b/tests/test-merge-revert --- a/tests/test-merge-revert +++ b/tests/test-merge-revert @@ -25,7 +25,7 @@ hg status hg id hg update -C 0 echo "changed file1" >> file1 -HGMERGE=merge hg update +hg update hg diff hg status hg id diff --git a/tests/test-merge-revert2 b/tests/test-merge-revert2 --- a/tests/test-merge-revert2 +++ b/tests/test-merge-revert2 @@ -26,7 +26,7 @@ hg status hg id hg update -C 0 echo "changed file1 different" >> file1 -HGMERGE=merge hg update +hg update hg diff --nodates | sed -e "s/\(<<<<<<<\) .*/\1/" -e "s/\(>>>>>>>\) .*/\1/" hg status hg id diff --git a/tests/test-merge-revert2.out b/tests/test-merge-revert2.out --- a/tests/test-merge-revert2.out +++ b/tests/test-merge-revert2.out @@ -9,7 +9,7 @@ 9eca13a34789 1 files updated, 0 files merged, 0 files removed, 0 files unresolved f248da0d4c3e tip 1 files updated, 0 files merged, 0 files removed, 0 files unresolved -merge: warning: conflicts during merge +warning: conflicts during merge. merging file1 merging file1 failed! 0 files updated, 0 files merged, 0 files removed, 1 files unresolved diff --git a/tests/test-merge7 b/tests/test-merge7 --- a/tests/test-merge7 +++ b/tests/test-merge7 @@ -35,7 +35,7 @@ hg commit -m "2 -> 2.5" -d "1000000 0" # now pull and merge from test-a hg pull ../test-a -HGMERGE=merge hg merge +hg merge # resolve conflict cat >test.txt <<"EOF" one @@ -57,7 +57,7 @@ hg commit -m "two -> two-point-one" -d " # pull and merge from test-a again cd ../test-b hg pull ../test-a -HGMERGE=merge hg merge --debug +hg merge --debug cat test.txt | sed "s% .*%%" diff --git a/tests/test-merge7.out b/tests/test-merge7.out --- a/tests/test-merge7.out +++ b/tests/test-merge7.out @@ -6,7 +6,7 @@ adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) -merge: warning: conflicts during merge +warning: conflicts during merge. merging test.txt merging test.txt failed! 0 files updated, 0 files merged, 0 files removed, 1 files unresolved @@ -20,7 +20,7 @@ adding manifests adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) -merge: warning: conflicts during merge +warning: conflicts during merge. resolving manifests overwrite None partial False ancestor faaea63e63a9 local 451c744aabcc+ remote a070d41e8360 diff --git a/tests/test-simplemerge-cmd b/tests/test-simplemerge-cmd new file mode 100755 --- /dev/null +++ b/tests/test-simplemerge-cmd @@ -0,0 +1,61 @@ +#!/bin/sh + +cp "$TESTDIR"/../contrib/simplemerge . + +echo base > base + +echo local > local +cat base >> local +cp local orig + +cat base > other +echo other >> other + +echo '% changing local directly' +python simplemerge local base other && echo "merge succeeded" +cat local +cp orig local + +echo '% printing to stdout' +python simplemerge -p local base other +echo ' local:' +cat local + +echo '% conflicts' +cp base conflict-local +cp other conflict-other +echo not other >> conflict-local +echo end >> conflict-local +echo end >> conflict-other +python simplemerge -p conflict-local base conflict-other || echo "merge failed" + +echo '% --no-minimal' +python simplemerge -p --no-minimal conflict-local base conflict-other + +echo '% 1 label' +python simplemerge -p -L foo conflict-local base conflict-other + +echo '% 2 labels' +python simplemerge -p -L foo -L bar conflict-local base conflict-other + +echo '% too many labels' +python simplemerge -p -L foo -L bar -L baz conflict-local base conflict-other + +echo '% binary file' +printf '\x00' > binary-local +cat orig >> binary-local +python simplemerge -p binary-local base other + +echo '% binary file --text' +python simplemerge -a -p binary-local base other + +echo '% help' +python simplemerge --help + +echo '% wrong number of arguments' +python simplemerge + +echo '% bad option' +python simplemerge --foo -p local base other + +exit 0 diff --git a/tests/test-simplemerge-cmd.out b/tests/test-simplemerge-cmd.out new file mode 100644 index 0000000000000000000000000000000000000000..8141eb588a69a7b55ec50be707781ceb376ad3dc GIT binary patch literal 2581 zc%1E(J#X7E5QaPJSKOklGSH4jTfiv-bTHte&Q^4w#FIprA{p{jNB#9Zl2YuXA906n z=3q#t`*@D`k}jd?h3S;(K-)%Wkji62dMXC=9YPqJ22r9c(&wTU0Sik#`l1Bqm7y3) z76M5-(yv}Q?fU#f35_+aRt<%0c$e3sxysJQ667TB^O{qB$m`^u&1htC6y7k<`cwhr z=p$lkXEk@#3Z=QF5~|ABYEVWE3*3t=vCLDRX!!%vi##Q~0WE5LR=u_MzqDV%+gvXE zd%ap*T!gHJL6~W&C2H%51B3x<^aR0SquL4RXlIugRVyR>1TD{IiO8lr16Gnf1kmb$ zAkLki#qv-IALzt+VUkzBQFxEq6+sP7V^+@B-RHagH`wkzZnki<+26wM?$hm;q5xj| z)Z0{GM3O^4v@yu@$4H*K;Zrzpl{ z#fPvipxW~0rh!3Td3dq(7?5ZCs0kjwKcEjBBj$n3AwZsc2BCG@o&bX5>>>>>> TAO +""") + +class TestMerge3(TestCase): + def log(self, msg): + pass + + def test_no_changes(self): + """No conflicts because nothing changed""" + m3 = Merge3(['aaa', 'bbb'], + ['aaa', 'bbb'], + ['aaa', 'bbb']) + + self.assertEquals(m3.find_unconflicted(), + [(0, 2)]) + + self.assertEquals(list(m3.find_sync_regions()), + [(0, 2, + 0, 2, + 0, 2), + (2,2, 2,2, 2,2)]) + + self.assertEquals(list(m3.merge_regions()), + [('unchanged', 0, 2)]) + + self.assertEquals(list(m3.merge_groups()), + [('unchanged', ['aaa', 'bbb'])]) + + def test_front_insert(self): + m3 = Merge3(['zz'], + ['aaa', 'bbb', 'zz'], + ['zz']) + + # todo: should use a sentinal at end as from get_matching_blocks + # to match without zz + self.assertEquals(list(m3.find_sync_regions()), + [(0,1, 2,3, 0,1), + (1,1, 3,3, 1,1),]) + + self.assertEquals(list(m3.merge_regions()), + [('a', 0, 2), + ('unchanged', 0, 1)]) + + self.assertEquals(list(m3.merge_groups()), + [('a', ['aaa', 'bbb']), + ('unchanged', ['zz'])]) + + def test_null_insert(self): + m3 = Merge3([], + ['aaa', 'bbb'], + []) + # todo: should use a sentinal at end as from get_matching_blocks + # to match without zz + self.assertEquals(list(m3.find_sync_regions()), + [(0,0, 2,2, 0,0)]) + + self.assertEquals(list(m3.merge_regions()), + [('a', 0, 2)]) + + self.assertEquals(list(m3.merge_lines()), + ['aaa', 'bbb']) + + def test_no_conflicts(self): + """No conflicts because only one side changed""" + m3 = Merge3(['aaa', 'bbb'], + ['aaa', '111', 'bbb'], + ['aaa', 'bbb']) + + self.assertEquals(m3.find_unconflicted(), + [(0, 1), (1, 2)]) + + self.assertEquals(list(m3.find_sync_regions()), + [(0,1, 0,1, 0,1), + (1,2, 2,3, 1,2), + (2,2, 3,3, 2,2),]) + + self.assertEquals(list(m3.merge_regions()), + [('unchanged', 0, 1), + ('a', 1, 2), + ('unchanged', 1, 2),]) + + def test_append_a(self): + m3 = Merge3(['aaa\n', 'bbb\n'], + ['aaa\n', 'bbb\n', '222\n'], + ['aaa\n', 'bbb\n']) + + self.assertEquals(''.join(m3.merge_lines()), + 'aaa\nbbb\n222\n') + + def test_append_b(self): + m3 = Merge3(['aaa\n', 'bbb\n'], + ['aaa\n', 'bbb\n'], + ['aaa\n', 'bbb\n', '222\n']) + + self.assertEquals(''.join(m3.merge_lines()), + 'aaa\nbbb\n222\n') + + def test_append_agreement(self): + m3 = Merge3(['aaa\n', 'bbb\n'], + ['aaa\n', 'bbb\n', '222\n'], + ['aaa\n', 'bbb\n', '222\n']) + + self.assertEquals(''.join(m3.merge_lines()), + 'aaa\nbbb\n222\n') + + def test_append_clash(self): + m3 = 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.assertEquals(''.join(ml), +'''\ +aaa +bbb +<< a +222 +-- +333 +>> b +''') + + def test_insert_agreement(self): + m3 = 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.assertEquals(''.join(ml), 'aaa\n222\nbbb\n') + + + def test_insert_clash(self): + """Both try to insert lines in the same place.""" + m3 = Merge3(['aaa\n', 'bbb\n'], + ['aaa\n', '111\n', 'bbb\n'], + ['aaa\n', '222\n', 'bbb\n']) + + self.assertEquals(m3.find_unconflicted(), + [(0, 1), (1, 2)]) + + self.assertEquals(list(m3.find_sync_regions()), + [(0,1, 0,1, 0,1), + (1,2, 2,3, 2,3), + (2,2, 3,3, 3,3),]) + + self.assertEquals(list(m3.merge_regions()), + [('unchanged', 0,1), + ('conflict', 1,1, 1,2, 1,2), + ('unchanged', 1,2)]) + + self.assertEquals(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.assertEquals(''.join(ml), +'''aaa +<< a +111 +-- +222 +>> b +bbb +''') + + def test_replace_clash(self): + """Both try to insert lines in the same place.""" + m3 = Merge3(['aaa', '000', 'bbb'], + ['aaa', '111', 'bbb'], + ['aaa', '222', 'bbb']) + + self.assertEquals(m3.find_unconflicted(), + [(0, 1), (2, 3)]) + + self.assertEquals(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_replace_multi(self): + """Replacement with regions of different size.""" + m3 = Merge3(['aaa', '000', '000', 'bbb'], + ['aaa', '111', '111', '111', 'bbb'], + ['aaa', '222', '222', '222', '222', 'bbb']) + + self.assertEquals(m3.find_unconflicted(), + [(0, 1), (3, 4)]) + + + self.assertEquals(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_merge_poem(self): + """Test case from diff3 manual""" + m3 = Merge3(TZU, LAO, TAO) + ml = list(m3.merge_lines('LAO', 'TAO')) + self.log('merge result:') + self.log(''.join(ml)) + self.assertEquals(ml, MERGED_RESULT) + + 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(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.assertEquals(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(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.assertEquals(optimal_text, merged_text) + + 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(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.assertEquals(optimal_text, merged_text) + + 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(base_text, other_text, this_text) + m_lines = m3.merge_lines('OTHER', 'THIS', reprocess=True, + base_marker='|||||||') + self.assertRaises(CantReprocessAndShowBase, list, m_lines) + + def test_binary(self): + self.assertRaises(util.Abort, Merge3, ['\x00'], ['a'], ['b']) + + def test_dos_text(self): + base_text = 'a\r\n' + this_text = 'b\r\n' + other_text = 'c\r\n' + m3 = Merge3(base_text.splitlines(True), other_text.splitlines(True), + this_text.splitlines(True)) + m_lines = m3.merge_lines('OTHER', 'THIS') + self.assertEqual('<<<<<<< OTHER\r\nc\r\n=======\r\nb\r\n' + '>>>>>>> THIS\r\n'.splitlines(True), list(m_lines)) + + def test_mac_text(self): + base_text = 'a\r' + this_text = 'b\r' + other_text = 'c\r' + m3 = 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)) + +if __name__ == '__main__': + # hide the timer + import time + orig = time.time + try: + time.time = lambda: 0 + unittest.main() + finally: + time.time = orig + diff --git a/tests/test-simplemerge.py.out b/tests/test-simplemerge.py.out new file mode 100644 --- /dev/null +++ b/tests/test-simplemerge.py.out @@ -0,0 +1,5 @@ +.................... +---------------------------------------------------------------------- +Ran 20 tests in 0.000s + +OK diff --git a/tests/test-up-local-change b/tests/test-up-local-change --- a/tests/test-up-local-change +++ b/tests/test-up-local-change @@ -1,5 +1,7 @@ #!/bin/sh +HGMERGE=true; export HGMERGE + set -e mkdir r1 cd r1