Merge with crew.
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))
--- 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
--- 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:
--- 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)
--- a/tests/test-annotate
+++ b/tests/test-annotate
@@ -1,5 +1,7 @@
#!/bin/sh
+HGMERGE=true; export HGMERGE
+
echo % init
hg init
--- 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
--- 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
--- 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
--- 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 ..
--- a/tests/test-install
+++ b/tests/test-install
@@ -1,3 +1,3 @@
#!/bin/sh
-HGMERGE=merge hg debuginstall
+hg debuginstall
--- 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
--- 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
--- 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!
--- 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
--- 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
--- 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
--- 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% .*%%"
--- 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
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
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<OtMz#Of$!oE$ik(uyWHD(boNtT|{=(CGP`A+1u5^s{k}cZp9`uEQ|-m|_5f4Tcb@
z`EzHX^>)Z0{GM3O^4v@yu@$4H*K;Zrz<x)6^oj^gritl22KM5QEx$%5hhC7fW>pl{
z#fPvipxW~0rh!3Td3dq(7?5ZCs0kjwKcEjBBj$n3AwZsc2BCG@o&bX5<Gi49?3wPA
z*)u;cXvzdaAtU0_LKI!<D|LH*m;t1PsA$SNmCWh~o&Y^|u`4dyb7UO!0w`5*nx~g+
z!brde74h!b?|xLc;DymShU3i|$?@G+!qHnMGUHIAkDS7H<A8?mxibx|FLS#}?JBju
iNUe@|X}6wgE894nuk=L4f9JL{%p<Srx~l6Ub^Qd3d`%eu
new file mode 100644
--- /dev/null
+++ b/tests/test-simplemerge.py
@@ -0,0 +1,409 @@
+# 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
+
+import os
+import unittest
+from unittest import TestCase
+import imp
+import shutil
+from mercurial import util
+
+# copy simplemerge to the cwd to avoid creating a .pyc file in the source tree
+shutil.copyfile(os.path.join(os.environ['TESTDIR'], os.path.pardir,
+ 'contrib', 'simplemerge'),
+ 'simplemerge.py')
+simplemerge = imp.load_source('simplemerge', 'simplemerge.py')
+Merge3 = simplemerge.Merge3
+CantReprocessAndShowBase = simplemerge.CantReprocessAndShowBase
+
+def split_lines(t):
+ from cStringIO import StringIO
+ return StringIO(t).readlines()
+
+############################################################
+# test case data from the gnu diffutils manual
+# common base
+TZU = split_lines(""" The Nameless is the origin of Heaven and Earth;
+ The named is the mother of all things.
+
+ Therefore let there always be non-being,
+ so we may see their subtlety,
+ And let there always be being,
+ so we may see their outcome.
+ The two are the same,
+ But after they are produced,
+ they have different names.
+ They both may be called deep and profound.
+ Deeper and more profound,
+ The door of all subtleties!
+""")
+
+LAO = split_lines(""" The Way that can be told of is not the eternal Way;
+ The name that can be named is not the eternal name.
+ The Nameless is the origin of Heaven and Earth;
+ The Named is the mother of all things.
+ Therefore let there always be non-being,
+ so we may see their subtlety,
+ And let there always be being,
+ so we may see their outcome.
+ The two are the same,
+ But after they are produced,
+ they have different names.
+""")
+
+
+TAO = split_lines(""" The Way that can be told of is not the eternal Way;
+ The name that can be named is not the eternal name.
+ The Nameless is the origin of Heaven and Earth;
+ The named is the mother of all things.
+
+ Therefore let there always be non-being,
+ so we may see their subtlety,
+ And let there always be being,
+ so we may see their result.
+ The two are the same,
+ But after they are produced,
+ they have different names.
+
+ -- The Way of Lao-Tzu, tr. Wing-tsit Chan
+
+""")
+
+MERGED_RESULT = split_lines(""" The Way that can be told of is not the eternal Way;
+ The name that can be named is not the eternal name.
+ The Nameless is the origin of Heaven and Earth;
+ The Named is the mother of all things.
+ Therefore let there always be non-being,
+ so we may see their subtlety,
+ And let there always be being,
+ so we may see their result.
+ The two are the same,
+ But after they are produced,
+ they have different names.
+<<<<<<< LAO
+=======
+
+ -- The Way of Lao-Tzu, tr. Wing-tsit Chan
+
+>>>>>>> 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
+
new file mode 100644
--- /dev/null
+++ b/tests/test-simplemerge.py.out
@@ -0,0 +1,5 @@
+....................
+----------------------------------------------------------------------
+Ran 20 tests in 0.000s
+
+OK
--- 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