Mercurial > hg
changeset 6002:abd66eb0889e
merge: move the bulk of simplemerge into core
- keep existing simplemerge command in contrib
- clean up test interface
author | Matt Mackall <mpm@selenic.com> |
---|---|
date | Sun, 03 Feb 2008 19:29:05 -0600 |
parents | 30d2fecaab76 |
children | 7855b88ba838 |
files | contrib/simplemerge mercurial/simplemerge.py tests/test-simplemerge.py |
diffstat | 3 files changed, 497 insertions(+), 529 deletions(-) [+] |
line wrap: on
line diff
--- a/contrib/simplemerge Sun Feb 03 21:47:07 2008 -0200 +++ b/contrib/simplemerge Sun Feb 03 19:29:05 2008 -0600 @@ -1,503 +1,11 @@ #!/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 +import os, sys from mercurial.i18n import _ - - -class CantReprocessAndShowBase(Exception): - pass - - -def warn(message): - sys.stdout.flush() - sys.stderr.write(message) - sys.stderr.flush() - - -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'): - warn(_('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'): - warn(_("warning: conflicts during merge.\n")) - return 1 +from mercurial import simplemerge, fancyopts, util options = [('L', 'label', [], _('labels to use on conflict markers')), ('a', 'text', None, _('treat all files as text')), @@ -517,6 +25,9 @@ By default, LOCAL is overwritten with the results of this operation. ''') +class ParseError(Exception): + """Exception raised on errors in parsing the command line.""" + def showhelp(): sys.stdout.write(usage) sys.stdout.write('\noptions:\n') @@ -530,33 +41,24 @@ 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: - 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)) + args = fancyopts.fancyopts(sys.argv[1:], options, opts) + except fancyopts.getopt.GetoptError, e: + raise ParseError(e) + if opts['help']: 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)) + sys.exit(0) + if len(args) != 3: + raise ParseError(_('wrong number of arguments')) + sys.exit(simplemerge.simplemerge(*args, **opts)) +except ParseError, e: + sys.stdout.write("%s: %s\n" % (sys.argv[0], e)) + showhelp() + sys.exit(1) +except util.Abort, e: + sys.stderr.write("abort: %s\n" % e) + sys.exit(255) +except KeyboardInterrupt: + sys.exit(255)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/simplemerge.py Sun Feb 03 19:29:05 2008 -0600 @@ -0,0 +1,456 @@ +#!/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 i18n import _ +import util, mdiff, fancyopts, sys, os + +class CantReprocessAndShowBase(Exception): + pass + +def warn(message): + sys.stdout.flush() + sys.stderr.write(message) + sys.stderr.flush() + +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 + +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'): + warn(_('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'): + warn(_("warning: conflicts during merge.\n")) + return 1
--- a/tests/test-simplemerge.py Sun Feb 03 21:47:07 2008 -0200 +++ b/tests/test-simplemerge.py Sun Feb 03 19:29:05 2008 -0600 @@ -19,14 +19,24 @@ from unittest import TestCase import imp import shutil -from mercurial import util +from mercurial import util, simplemerge + +# bzr compatible interface, for the tests +class Merge3(simplemerge.Merge3Text): + """3-way merge of texts. -# 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 + 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") + simplemerge.Merge3Text.__init__(self, basetext, atext, btext, + base, a, b) + CantReprocessAndShowBase = simplemerge.CantReprocessAndShowBase def split_lines(t):