view mercurial/mdiff.py @ 72:4a6ab4d80dc4

Add an O(m + nlog n) patching extension
author mpm@selenic.com
date Mon, 16 May 2005 22:08:33 -0800
parents 47c9a869adee
children b942bbe4bb84
line wrap: on
line source

#!/usr/bin/python
import difflib, struct, mmap

devzero = file("/dev/zero")

def unidiff(a, ad, b, bd, fn):
    if not a and not b: return ""
    a = a.splitlines(1)
    b = b.splitlines(1)
    l = list(difflib.unified_diff(a, b, "a/" + fn, "b/" + fn, ad, bd))
    return "".join(l)

def textdiff(a, b):
    return diff(a.splitlines(1), b.splitlines(1))

def sortdiff(a, b):
    la = lb = 0

    while 1:
        if la >= len(a) or lb >= len(b): break
        if b[lb] < a[la]:
            si = lb
            while lb < len(b) and b[lb] < a[la] : lb += 1
            yield "insert", la, la, si, lb
        elif a[la] < b[lb]:
            si = la
            while la < len(a) and a[la] < b[lb]: la += 1
            yield "delete", si, la, lb, lb
        else:
            la += 1
            lb += 1

    if lb < len(b):
        yield "insert", la, la, lb, len(b)

    if la < len(a):
        yield "delete", la, len(a), lb, lb

def diff(a, b, sorted=0):
    bin = []
    p = [0]
    for i in a: p.append(p[-1] + len(i))

    if sorted:
        d = sortdiff(a, b)
    else:
        d = difflib.SequenceMatcher(None, a, b).get_opcodes()

    for o, m, n, s, t in d:
        if o == 'equal': continue
        s = "".join(b[s:t])
        bin.append(struct.pack(">lll", p[m], p[n], len(s)) + s)

    return "".join(bin)

# This attempts to apply a series of patches in time proportional to
# the total size of the patches, rather than patches * len(text). This
# means rather than shuffling strings around, we shuffle around
# pointers to fragments with fragment lists.
#
# When the fragment lists get too long, we collapse them. To do this
# efficiently, we do all our operations inside a buffer created by
# mmap and simply use memmove. This avoids creating a bunch of large
# temporary string buffers.

def patches(a, bins):
    if not bins: return a

    plens = [len(x) for x in bins]
    pl = sum(plens)
    bl = len(a) + pl
    tl = bl + bl + pl # enough for the patches and two working texts
    b1, b2 = 0, bl
    m = mmap.mmap(devzero.fileno(), tl, mmap.MAP_PRIVATE)

    # load our original text
    m.write(a)
    frags = [(len(a), b1)]

    # copy all the patches into our segment so we can memmove from them
    pos = b2 + bl
    m.seek(pos)
    for p in bins: m.write(p)

    def pull(dst, src, l): # pull l bytes from src
        while l:
            f = src.pop(0)
            if f[0] > l: # do we need to split?
                src.insert(0, (f[0] - l, f[1] + l))
                dst.append((l, f[1]))
                return
            dst.append(f)
            l -= f[0]

    def collect(buf, list):
        start = buf
        for l, p in list:
            m.move(buf, p, l)
            buf += l
        return (buf - start, start)

    for plen in plens:
        # if our list gets too long, execute it
        if len(frags) > 128:
            b2, b1 = b1, b2
            frags = [collect(b1, frags)]

        new = []
        end = pos + plen
        last = 0
        while pos < end:
            p1, p2, l = struct.unpack(">lll", m[pos:pos + 12])
            pull(new, frags, p1 - last) # what didn't change
            pull([], frags, p2 - p1)    # what got deleted
            new.append((l, pos + 12))        # what got added
            pos += l + 12
            last = p2
        frags = new + frags                    # what was left at the end

    t = collect(b2, frags)

    return m[t[1]:t[1] + t[0]]

def patch(a, bin):
    return patches(a, [bin])

try:
    import mpatch
    patches = mpatch.patches
except:
    pass