# patch.py - patch file parsing routines # # 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 """ 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)] print "copying %s to %s" % (src, dst) 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 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