Mercurial > public > mercurial-scm > hg-stable
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 |