|
|
# 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
|
|
|
from i18n import gettext as _
|
|
|
demandload(globals(), "util")
|
|
|
demandload(globals(), "cStringIO email.Parser os re shutil tempfile")
|
|
|
|
|
|
def extract(ui, fileobj):
|
|
|
'''extract patch from data read from fileobj.
|
|
|
|
|
|
patch can be normal patch or contained in email message.
|
|
|
|
|
|
return tuple (filename, message, user, date). any item in returned
|
|
|
tuple can be None. if filename is None, fileobj did not contain
|
|
|
patch. caller must unlink filename when done.'''
|
|
|
|
|
|
# attempt to detect the start of a patch
|
|
|
# (this heuristic is borrowed from quilt)
|
|
|
diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
|
|
|
'retrieving revision [0-9]+(\.[0-9]+)*$|' +
|
|
|
'(---|\*\*\*)[ \t])', re.MULTILINE)
|
|
|
|
|
|
fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
|
|
|
tmpfp = os.fdopen(fd, 'w')
|
|
|
try:
|
|
|
hgpatch = False
|
|
|
|
|
|
msg = email.Parser.Parser().parse(fileobj)
|
|
|
|
|
|
message = msg['Subject']
|
|
|
user = msg['From']
|
|
|
# should try to parse msg['Date']
|
|
|
date = None
|
|
|
|
|
|
if message:
|
|
|
message = message.replace('\n\t', ' ')
|
|
|
ui.debug('Subject: %s\n' % message)
|
|
|
if user:
|
|
|
ui.debug('From: %s\n' % user)
|
|
|
diffs_seen = 0
|
|
|
ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
|
|
|
|
|
|
for part in msg.walk():
|
|
|
content_type = part.get_content_type()
|
|
|
ui.debug('Content-Type: %s\n' % content_type)
|
|
|
if content_type not in ok_types:
|
|
|
continue
|
|
|
payload = part.get_payload(decode=True)
|
|
|
m = diffre.search(payload)
|
|
|
if m:
|
|
|
ui.debug(_('found patch at byte %d\n') % m.start(0))
|
|
|
diffs_seen += 1
|
|
|
cfp = cStringIO.StringIO()
|
|
|
if message:
|
|
|
cfp.write(message)
|
|
|
cfp.write('\n')
|
|
|
for line in payload[:m.start(0)].splitlines():
|
|
|
if line.startswith('# HG changeset patch'):
|
|
|
ui.debug(_('patch generated by hg export\n'))
|
|
|
hgpatch = True
|
|
|
# drop earlier commit message content
|
|
|
cfp.seek(0)
|
|
|
cfp.truncate()
|
|
|
elif hgpatch:
|
|
|
if line.startswith('# User '):
|
|
|
user = line[7:]
|
|
|
ui.debug('From: %s\n' % user)
|
|
|
elif line.startswith("# Date "):
|
|
|
date = line[7:]
|
|
|
if not line.startswith('# '):
|
|
|
cfp.write(line)
|
|
|
cfp.write('\n')
|
|
|
message = cfp.getvalue()
|
|
|
if tmpfp:
|
|
|
tmpfp.write(payload)
|
|
|
if not payload.endswith('\n'):
|
|
|
tmpfp.write('\n')
|
|
|
elif not diffs_seen and message and content_type == 'text/plain':
|
|
|
message += '\n' + payload
|
|
|
except:
|
|
|
tmpfp.close()
|
|
|
os.unlink(tmpname)
|
|
|
raise
|
|
|
|
|
|
tmpfp.close()
|
|
|
if not diffs_seen:
|
|
|
os.unlink(tmpname)
|
|
|
return None, message, user, date
|
|
|
return tmpname, message, user, date
|
|
|
|
|
|
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
|
|
|
|