mercurial/patch.py
changeset 37621 5537d8f5e989
parent 37574 a1bcc7ff0eac
child 37730 8d730f96e792
equal deleted inserted replaced
37620:fd1dd79cff20 37621:5537d8f5e989
     7 # GNU General Public License version 2 or any later version.
     7 # GNU General Public License version 2 or any later version.
     8 
     8 
     9 from __future__ import absolute_import, print_function
     9 from __future__ import absolute_import, print_function
    10 
    10 
    11 import collections
    11 import collections
       
    12 import contextlib
    12 import copy
    13 import copy
    13 import difflib
    14 import difflib
    14 import email
    15 import email
    15 import errno
    16 import errno
    16 import hashlib
    17 import hashlib
   190 patchheadermap = [('Date', 'date'),
   191 patchheadermap = [('Date', 'date'),
   191                   ('Branch', 'branch'),
   192                   ('Branch', 'branch'),
   192                   ('Node ID', 'nodeid'),
   193                   ('Node ID', 'nodeid'),
   193                  ]
   194                  ]
   194 
   195 
       
   196 @contextlib.contextmanager
   195 def extract(ui, fileobj):
   197 def extract(ui, fileobj):
   196     '''extract patch from data read from fileobj.
   198     '''extract patch from data read from fileobj.
   197 
   199 
   198     patch can be a normal patch or contained in an email message.
   200     patch can be a normal patch or contained in an email message.
   199 
   201 
   207       - p1,
   209       - p1,
   208       - p2.
   210       - p2.
   209     Any item can be missing from the dictionary. If filename is missing,
   211     Any item can be missing from the dictionary. If filename is missing,
   210     fileobj did not contain a patch. Caller must unlink filename when done.'''
   212     fileobj did not contain a patch. Caller must unlink filename when done.'''
   211 
   213 
       
   214     fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
       
   215     tmpfp = os.fdopen(fd, r'wb')
       
   216     try:
       
   217         yield _extract(ui, fileobj, tmpname, tmpfp)
       
   218     finally:
       
   219         tmpfp.close()
       
   220         os.unlink(tmpname)
       
   221 
       
   222 def _extract(ui, fileobj, tmpname, tmpfp):
       
   223 
   212     # attempt to detect the start of a patch
   224     # attempt to detect the start of a patch
   213     # (this heuristic is borrowed from quilt)
   225     # (this heuristic is borrowed from quilt)
   214     diffre = re.compile(br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
   226     diffre = re.compile(br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
   215                         br'retrieving revision [0-9]+(\.[0-9]+)*$|'
   227                         br'retrieving revision [0-9]+(\.[0-9]+)*$|'
   216                         br'---[ \t].*?^\+\+\+[ \t]|'
   228                         br'---[ \t].*?^\+\+\+[ \t]|'
   217                         br'\*\*\*[ \t].*?^---[ \t])',
   229                         br'\*\*\*[ \t].*?^---[ \t])',
   218                         re.MULTILINE | re.DOTALL)
   230                         re.MULTILINE | re.DOTALL)
   219 
   231 
   220     data = {}
   232     data = {}
   221     fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
   233 
   222     tmpfp = os.fdopen(fd, r'wb')
   234     msg = pycompat.emailparser().parse(fileobj)
   223     try:
   235 
   224         msg = pycompat.emailparser().parse(fileobj)
   236     subject = msg[r'Subject'] and mail.headdecode(msg[r'Subject'])
   225 
   237     data['user'] = msg[r'From'] and mail.headdecode(msg[r'From'])
   226         subject = msg[r'Subject'] and mail.headdecode(msg[r'Subject'])
   238     if not subject and not data['user']:
   227         data['user'] = msg[r'From'] and mail.headdecode(msg[r'From'])
   239         # Not an email, restore parsed headers if any
   228         if not subject and not data['user']:
   240         subject = '\n'.join(': '.join(map(encoding.strtolocal, h))
   229             # Not an email, restore parsed headers if any
   241                             for h in msg.items()) + '\n'
   230             subject = '\n'.join(': '.join(map(encoding.strtolocal, h))
   242 
   231                                 for h in msg.items()) + '\n'
   243     # should try to parse msg['Date']
   232 
   244     parents = []
   233         # should try to parse msg['Date']
   245 
   234         parents = []
   246     if subject:
   235 
   247         if subject.startswith('[PATCH'):
   236         if subject:
   248             pend = subject.find(']')
   237             if subject.startswith('[PATCH'):
   249             if pend >= 0:
   238                 pend = subject.find(']')
   250                 subject = subject[pend + 1:].lstrip()
   239                 if pend >= 0:
   251         subject = re.sub(br'\n[ \t]+', ' ', subject)
   240                     subject = subject[pend + 1:].lstrip()
   252         ui.debug('Subject: %s\n' % subject)
   241             subject = re.sub(br'\n[ \t]+', ' ', subject)
   253     if data['user']:
   242             ui.debug('Subject: %s\n' % subject)
   254         ui.debug('From: %s\n' % data['user'])
   243         if data['user']:
   255     diffs_seen = 0
   244             ui.debug('From: %s\n' % data['user'])
   256     ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
   245         diffs_seen = 0
   257     message = ''
   246         ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
   258     for part in msg.walk():
   247         message = ''
   259         content_type = pycompat.bytestr(part.get_content_type())
   248         for part in msg.walk():
   260         ui.debug('Content-Type: %s\n' % content_type)
   249             content_type = pycompat.bytestr(part.get_content_type())
   261         if content_type not in ok_types:
   250             ui.debug('Content-Type: %s\n' % content_type)
   262             continue
   251             if content_type not in ok_types:
   263         payload = part.get_payload(decode=True)
   252                 continue
   264         m = diffre.search(payload)
   253             payload = part.get_payload(decode=True)
   265         if m:
   254             m = diffre.search(payload)
   266             hgpatch = False
   255             if m:
   267             hgpatchheader = False
   256                 hgpatch = False
   268             ignoretext = False
   257                 hgpatchheader = False
   269 
   258                 ignoretext = False
   270             ui.debug('found patch at byte %d\n' % m.start(0))
   259 
   271             diffs_seen += 1
   260                 ui.debug('found patch at byte %d\n' % m.start(0))
   272             cfp = stringio()
   261                 diffs_seen += 1
   273             for line in payload[:m.start(0)].splitlines():
   262                 cfp = stringio()
   274                 if line.startswith('# HG changeset patch') and not hgpatch:
   263                 for line in payload[:m.start(0)].splitlines():
   275                     ui.debug('patch generated by hg export\n')
   264                     if line.startswith('# HG changeset patch') and not hgpatch:
   276                     hgpatch = True
   265                         ui.debug('patch generated by hg export\n')
   277                     hgpatchheader = True
   266                         hgpatch = True
   278                     # drop earlier commit message content
   267                         hgpatchheader = True
   279                     cfp.seek(0)
   268                         # drop earlier commit message content
   280                     cfp.truncate()
   269                         cfp.seek(0)
   281                     subject = None
   270                         cfp.truncate()
   282                 elif hgpatchheader:
   271                         subject = None
   283                     if line.startswith('# User '):
   272                     elif hgpatchheader:
   284                         data['user'] = line[7:]
   273                         if line.startswith('# User '):
   285                         ui.debug('From: %s\n' % data['user'])
   274                             data['user'] = line[7:]
   286                     elif line.startswith("# Parent "):
   275                             ui.debug('From: %s\n' % data['user'])
   287                         parents.append(line[9:].lstrip())
   276                         elif line.startswith("# Parent "):
   288                     elif line.startswith("# "):
   277                             parents.append(line[9:].lstrip())
   289                         for header, key in patchheadermap:
   278                         elif line.startswith("# "):
   290                             prefix = '# %s ' % header
   279                             for header, key in patchheadermap:
   291                             if line.startswith(prefix):
   280                                 prefix = '# %s ' % header
   292                                 data[key] = line[len(prefix):]
   281                                 if line.startswith(prefix):
   293                     else:
   282                                     data[key] = line[len(prefix):]
   294                         hgpatchheader = False
   283                         else:
   295                 elif line == '---':
   284                             hgpatchheader = False
   296                     ignoretext = True
   285                     elif line == '---':
   297                 if not hgpatchheader and not ignoretext:
   286                         ignoretext = True
   298                     cfp.write(line)
   287                     if not hgpatchheader and not ignoretext:
   299                     cfp.write('\n')
   288                         cfp.write(line)
   300             message = cfp.getvalue()
   289                         cfp.write('\n')
   301             if tmpfp:
   290                 message = cfp.getvalue()
   302                 tmpfp.write(payload)
   291                 if tmpfp:
   303                 if not payload.endswith('\n'):
   292                     tmpfp.write(payload)
   304                     tmpfp.write('\n')
   293                     if not payload.endswith('\n'):
   305         elif not diffs_seen and message and content_type == 'text/plain':
   294                         tmpfp.write('\n')
   306             message += '\n' + payload
   295             elif not diffs_seen and message and content_type == 'text/plain':
       
   296                 message += '\n' + payload
       
   297     except: # re-raises
       
   298         tmpfp.close()
       
   299         os.unlink(tmpname)
       
   300         raise
       
   301 
   307 
   302     if subject and not message.startswith(subject):
   308     if subject and not message.startswith(subject):
   303         message = '%s\n%s' % (subject, message)
   309         message = '%s\n%s' % (subject, message)
   304     data['message'] = message
   310     data['message'] = message
   305     tmpfp.close()
   311     tmpfp.close()
   308         if parents:
   314         if parents:
   309             data['p2'] = parents.pop(0)
   315             data['p2'] = parents.pop(0)
   310 
   316 
   311     if diffs_seen:
   317     if diffs_seen:
   312         data['filename'] = tmpname
   318         data['filename'] = tmpname
   313     else:
   319 
   314         os.unlink(tmpname)
       
   315     return data
   320     return data
   316 
   321 
   317 class patchmeta(object):
   322 class patchmeta(object):
   318     """Patched file metadata
   323     """Patched file metadata
   319 
   324