mercurial/patch.py
author Vadim Gelfer <vadim.gelfer@gmail.com>
Sat, 12 Aug 2006 12:47:18 -0700
changeset 2865 71e78f2ca5ae
parent 2863 f3c68e0ca37d
child 2866 2893e51407a4
permissions -rw-r--r--
merge git patch code.

# patch.py - patch file parsing routines
#
# Copyright 2006 Brendan Cully <brendan@kublai.com>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.

from demandload import demandload
demandload(globals(), "util")
demandload(globals(), "os re shutil tempfile")

def readgitpatch(patchname):
    """extract git-style metadata about patches from <patchname>"""
    class gitpatch:
        "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
        def __init__(self, path):
            self.path = path
            self.oldpath = None
            self.mode = None
            self.op = 'MODIFY'
            self.copymod = False
            self.lineno = 0
    
    # Filter patch for git information
    gitre = re.compile('diff --git a/(.*) b/(.*)')
    pf = file(patchname)
    gp = None
    gitpatches = []
    # Can have a git patch with only metadata, causing patch to complain
    dopatch = False

    lineno = 0
    for line in pf:
        lineno += 1
        if line.startswith('diff --git'):
            m = gitre.match(line)
            if m:
                if gp:
                    gitpatches.append(gp)
                src, dst = m.group(1,2)
                gp = gitpatch(dst)
                gp.lineno = lineno
        elif gp:
            if line.startswith('--- '):
                if gp.op in ('COPY', 'RENAME'):
                    gp.copymod = True
                    dopatch = 'filter'
                gitpatches.append(gp)
                gp = None
                if not dopatch:
                    dopatch = True
                continue
            if line.startswith('rename from '):
                gp.op = 'RENAME'
                gp.oldpath = line[12:].rstrip()
            elif line.startswith('rename to '):
                gp.path = line[10:].rstrip()
            elif line.startswith('copy from '):
                gp.op = 'COPY'
                gp.oldpath = line[10:].rstrip()
            elif line.startswith('copy to '):
                gp.path = line[8:].rstrip()
            elif line.startswith('deleted file'):
                gp.op = 'DELETE'
            elif line.startswith('new file mode '):
                gp.op = 'ADD'
                gp.mode = int(line.rstrip()[-3:], 8)
            elif line.startswith('new mode '):
                gp.mode = int(line.rstrip()[-3:], 8)
    if gp:
        gitpatches.append(gp)

    if not gitpatches:
        dopatch = True

    return (dopatch, gitpatches)

def dogitpatch(patchname, gitpatches):
    """Preprocess git patch so that vanilla patch can handle it"""
    pf = file(patchname)
    pfline = 1

    fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
    tmpfp = os.fdopen(fd, 'w')

    try:
        for i in range(len(gitpatches)):
            p = gitpatches[i]
            if not p.copymod:
                continue

            if os.path.exists(p.path):
                raise util.Abort(_("cannot create %s: destination already exists") %
                            p.path)

            (src, dst) = [os.path.join(os.getcwd(), n)
                          for n in (p.oldpath, p.path)]

            targetdir = os.path.dirname(dst)
            if not os.path.isdir(targetdir):
                os.makedirs(targetdir)
            try:
                shutil.copyfile(src, dst)
                shutil.copymode(src, dst)
            except shutil.Error, inst:
                raise util.Abort(str(inst))

            # rewrite patch hunk
            while pfline < p.lineno:
                tmpfp.write(pf.readline())
                pfline += 1
            tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
            line = pf.readline()
            pfline += 1
            while not line.startswith('--- a/'):
                tmpfp.write(line)
                line = pf.readline()
                pfline += 1
            tmpfp.write('--- a/%s\n' % p.path)

        line = pf.readline()
        while line:
            tmpfp.write(line)
            line = pf.readline()
    except:
        tmpfp.close()
        os.unlink(patchname)
        raise

    tmpfp.close()
    return patchname

def patch(strip, patchname, ui, cwd=None):
    """apply the patch <patchname> to the working directory.
    a list of patched files is returned"""

    (dopatch, gitpatches) = readgitpatch(patchname)

    files = {}
    if dopatch:
        if dopatch == 'filter':
            patchname = dogitpatch(patchname, gitpatches)
        patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
        args = []
        if cwd:
            args.append('-d %s' % util.shellquote(cwd))
        fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
                                           util.shellquote(patchname)))

        if dopatch == 'filter':
            False and os.unlink(patchname)

        for line in fp:
            line = line.rstrip()
            ui.status("%s\n" % line)
            if line.startswith('patching file '):
                pf = util.parse_patch_output(line)
                files.setdefault(pf, (None, None))
        code = fp.close()
        if code:
            raise util.Abort(_("patch command failed: %s") % explain_exit(code)[0])

    for gp in gitpatches:
        files[gp.path] = (gp.op, gp)

    return files