comparison mercurial/patch.py @ 37621:5537d8f5e989

patch: make extract() a context manager (API) Previously, this function was creating a temporary file and relying on callers to unlink it. Yuck. We convert the function to a context manager and tie the lifetime of the temporary file to that of the context manager. This changed indentation not only from the context manager, but also from the elination of try blocks. It was just easier to split the heart of extract() into its own function. The single consumer of this function has been refactored to use it as a context manager. Code for cleaning up the file in tryimportone() has also been removed. .. api:: ``patch.extract()`` is now a context manager. Callers no longer have to worry about deleting the temporary file it creates, as the file is tied to the lifetime of the context manager. Differential Revision: https://phab.mercurial-scm.org/D3306
author Gregory Szorc <gregory.szorc@gmail.com>
date Thu, 12 Apr 2018 23:14:38 -0700
parents a1bcc7ff0eac
children 8d730f96e792
comparison
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