mercurial/mdiff.py
author Martin Geisler <mg@lazybytes.net>
Sun, 26 Apr 2009 01:08:54 +0200
changeset 8225 46293a0c7e9f
parent 7436 07faba78cf5a
child 8312 b87a50b7125c
permissions -rw-r--r--
updated license to be explicit about GPL version 2

# mdiff.py - diff and patch routines for mercurial
#
# Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

from i18n import _
import bdiff, mpatch, re, struct, util

def splitnewlines(text):
    '''like str.splitlines, but only split on newlines.'''
    lines = [l + '\n' for l in text.split('\n')]
    if lines:
        if lines[-1] == '\n':
            lines.pop()
        else:
            lines[-1] = lines[-1][:-1]
    return lines

class diffopts(object):
    '''context is the number of context lines
    text treats all files as text
    showfunc enables diff -p output
    git enables the git extended patch format
    nodates removes dates from diff headers
    ignorews ignores all whitespace changes in the diff
    ignorewsamount ignores changes in the amount of whitespace
    ignoreblanklines ignores changes whose lines are all blank'''

    defaults = {
        'context': 3,
        'text': False,
        'showfunc': False,
        'git': False,
        'nodates': False,
        'ignorews': False,
        'ignorewsamount': False,
        'ignoreblanklines': False,
        }

    __slots__ = defaults.keys()

    def __init__(self, **opts):
        for k in self.__slots__:
            v = opts.get(k)
            if v is None:
                v = self.defaults[k]
            setattr(self, k, v)

        try:
            self.context = int(self.context)
        except ValueError:
            raise util.Abort(_('diff context lines count must be '
                               'an integer, not %r') % self.context)

defaultopts = diffopts()

def wsclean(opts, text):
    if opts.ignorews:
        text = re.sub('[ \t]+', '', text)
    elif opts.ignorewsamount:
        text = re.sub('[ \t]+', ' ', text)
        text = re.sub('[ \t]+\n', '\n', text)
    if opts.ignoreblanklines:
        text = re.sub('\n+', '', text)
    return text

def diffline(revs, a, b, opts):
    parts = ['diff']
    if opts.git:
        parts.append('--git')
    if revs and not opts.git:
        parts.append(' '.join(["-r %s" % rev for rev in revs]))
    if opts.git:
        parts.append('a/%s' % a)
        parts.append('b/%s' % b)
    else:
        parts.append(a)
    return ' '.join(parts) + '\n'

def unidiff(a, ad, b, bd, fn1, fn2, r=None, opts=defaultopts):
    def datetag(date, addtab=True):
        if not opts.git and not opts.nodates:
            return '\t%s\n' % date
        if addtab and ' ' in fn1:
            return '\t\n'
        return '\n'

    if not a and not b: return ""
    epoch = util.datestr((0, 0))

    if not opts.text and (util.binary(a) or util.binary(b)):
        if a and b and len(a) == len(b) and a == b:
            return ""
        l = ['Binary file %s has changed\n' % fn1]
    elif not a:
        b = splitnewlines(b)
        if a is None:
            l1 = '--- /dev/null%s' % datetag(epoch, False)
        else:
            l1 = "--- %s%s" % ("a/" + fn1, datetag(ad))
        l2 = "+++ %s%s" % ("b/" + fn2, datetag(bd))
        l3 = "@@ -0,0 +1,%d @@\n" % len(b)
        l = [l1, l2, l3] + ["+" + e for e in b]
    elif not b:
        a = splitnewlines(a)
        l1 = "--- %s%s" % ("a/" + fn1, datetag(ad))
        if b is None:
            l2 = '+++ /dev/null%s' % datetag(epoch, False)
        else:
            l2 = "+++ %s%s" % ("b/" + fn2, datetag(bd))
        l3 = "@@ -1,%d +0,0 @@\n" % len(a)
        l = [l1, l2, l3] + ["-" + e for e in a]
    else:
        al = splitnewlines(a)
        bl = splitnewlines(b)
        l = list(bunidiff(a, b, al, bl, "a/" + fn1, "b/" + fn2, opts=opts))
        if not l: return ""
        # difflib uses a space, rather than a tab
        l[0] = "%s%s" % (l[0][:-2], datetag(ad))
        l[1] = "%s%s" % (l[1][:-2], datetag(bd))

    for ln in xrange(len(l)):
        if l[ln][-1] != '\n':
            l[ln] += "\n\ No newline at end of file\n"

    if r:
        l.insert(0, diffline(r, fn1, fn2, opts))

    return "".join(l)

# somewhat self contained replacement for difflib.unified_diff
# t1 and t2 are the text to be diffed
# l1 and l2 are the text broken up into lines
# header1 and header2 are the filenames for the diff output
def bunidiff(t1, t2, l1, l2, header1, header2, opts=defaultopts):
    def contextend(l, len):
        ret = l + opts.context
        if ret > len:
            ret = len
        return ret

    def contextstart(l):
        ret = l - opts.context
        if ret < 0:
            return 0
        return ret

    def yieldhunk(hunk, header):
        if header:
            for x in header:
                yield x
        (astart, a2, bstart, b2, delta) = hunk
        aend = contextend(a2, len(l1))
        alen = aend - astart
        blen = b2 - bstart + aend - a2

        func = ""
        if opts.showfunc:
            # walk backwards from the start of the context
            # to find a line starting with an alphanumeric char.
            for x in xrange(astart - 1, -1, -1):
                t = l1[x].rstrip()
                if funcre.match(t):
                    func = ' ' + t[:40]
                    break

        yield "@@ -%d,%d +%d,%d @@%s\n" % (astart + 1, alen,
                                           bstart + 1, blen, func)
        for x in delta:
            yield x
        for x in xrange(a2, aend):
            yield ' ' + l1[x]

    header = [ "--- %s\t\n" % header1, "+++ %s\t\n" % header2 ]

    if opts.showfunc:
        funcre = re.compile('\w')

    # bdiff.blocks gives us the matching sequences in the files.  The loop
    # below finds the spaces between those matching sequences and translates
    # them into diff output.
    #
    diff = bdiff.blocks(t1, t2)
    hunk = None
    for i in xrange(len(diff)):
        # The first match is special.
        # we've either found a match starting at line 0 or a match later
        # in the file.  If it starts later, old and new below will both be
        # empty and we'll continue to the next match.
        if i > 0:
            s = diff[i-1]
        else:
            s = [0, 0, 0, 0]
        delta = []
        s1 = diff[i]
        a1 = s[1]
        a2 = s1[0]
        b1 = s[3]
        b2 = s1[2]

        old = l1[a1:a2]
        new = l2[b1:b2]

        # bdiff sometimes gives huge matches past eof, this check eats them,
        # and deals with the special first match case described above
        if not old and not new:
            continue

        if opts.ignorews or opts.ignorewsamount or opts.ignoreblanklines:
            if wsclean(opts, "".join(old)) == wsclean(opts, "".join(new)):
                continue

        astart = contextstart(a1)
        bstart = contextstart(b1)
        prev = None
        if hunk:
            # join with the previous hunk if it falls inside the context
            if astart < hunk[1] + opts.context + 1:
                prev = hunk
                astart = hunk[1]
                bstart = hunk[3]
            else:
                for x in yieldhunk(hunk, header):
                    yield x
                # we only want to yield the header if the files differ, and
                # we only want to yield it once.
                header = None
        if prev:
            # we've joined the previous hunk, record the new ending points.
            hunk[1] = a2
            hunk[3] = b2
            delta = hunk[4]
        else:
            # create a new hunk
            hunk = [ astart, a2, bstart, b2, delta ]

        delta[len(delta):] = [ ' ' + x for x in l1[astart:a1] ]
        delta[len(delta):] = [ '-' + x for x in old ]
        delta[len(delta):] = [ '+' + x for x in new ]

    if hunk:
        for x in yieldhunk(hunk, header):
            yield x

def patchtext(bin):
    pos = 0
    t = []
    while pos < len(bin):
        p1, p2, l = struct.unpack(">lll", bin[pos:pos + 12])
        pos += 12
        t.append(bin[pos:pos + l])
        pos += l
    return "".join(t)

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)]

def trivialdiffheader(length):
    return struct.pack(">lll", 0, 0, length)

patches = mpatch.patches
patchedsize = mpatch.patchedsize
textdiff = bdiff.bdiff