##// END OF EJS Templates
unlink temporary patch files even when an exception is raised
Benoit Boissinot -
r3057:d16b93f4 default
parent child Browse files
Show More
@@ -1,539 +1,549 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from demandload import demandload
9 9 from i18n import gettext as _
10 10 from node import *
11 11 demandload(globals(), "cmdutil mdiff util")
12 12 demandload(globals(), "cStringIO email.Parser errno os re shutil sys tempfile")
13 13
14 14 # helper functions
15 15
16 16 def copyfile(src, dst, basedir=None):
17 17 if not basedir:
18 18 basedir = os.getcwd()
19 19
20 20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 21 if os.path.exists(absdst):
22 22 raise util.Abort(_("cannot create %s: destination already exists") %
23 23 dst)
24 24
25 25 targetdir = os.path.dirname(absdst)
26 26 if not os.path.isdir(targetdir):
27 27 os.makedirs(targetdir)
28 28 try:
29 29 shutil.copyfile(abssrc, absdst)
30 30 shutil.copymode(abssrc, absdst)
31 31 except shutil.Error, inst:
32 32 raise util.Abort(str(inst))
33 33
34 34 # public functions
35 35
36 36 def extract(ui, fileobj):
37 37 '''extract patch from data read from fileobj.
38 38
39 39 patch can be normal patch or contained in email message.
40 40
41 41 return tuple (filename, message, user, date). any item in returned
42 42 tuple can be None. if filename is None, fileobj did not contain
43 43 patch. caller must unlink filename when done.'''
44 44
45 45 # attempt to detect the start of a patch
46 46 # (this heuristic is borrowed from quilt)
47 47 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
48 48 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
49 49 '(---|\*\*\*)[ \t])', re.MULTILINE)
50 50
51 51 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
52 52 tmpfp = os.fdopen(fd, 'w')
53 53 try:
54 54 hgpatch = False
55 55
56 56 msg = email.Parser.Parser().parse(fileobj)
57 57
58 58 message = msg['Subject']
59 59 user = msg['From']
60 60 # should try to parse msg['Date']
61 61 date = None
62 62
63 63 if message:
64 64 message = message.replace('\n\t', ' ')
65 65 ui.debug('Subject: %s\n' % message)
66 66 if user:
67 67 ui.debug('From: %s\n' % user)
68 68 diffs_seen = 0
69 69 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
70 70
71 71 for part in msg.walk():
72 72 content_type = part.get_content_type()
73 73 ui.debug('Content-Type: %s\n' % content_type)
74 74 if content_type not in ok_types:
75 75 continue
76 76 payload = part.get_payload(decode=True)
77 77 m = diffre.search(payload)
78 78 if m:
79 79 ui.debug(_('found patch at byte %d\n') % m.start(0))
80 80 diffs_seen += 1
81 81 cfp = cStringIO.StringIO()
82 82 if message:
83 83 cfp.write(message)
84 84 cfp.write('\n')
85 85 for line in payload[:m.start(0)].splitlines():
86 86 if line.startswith('# HG changeset patch'):
87 87 ui.debug(_('patch generated by hg export\n'))
88 88 hgpatch = True
89 89 # drop earlier commit message content
90 90 cfp.seek(0)
91 91 cfp.truncate()
92 92 elif hgpatch:
93 93 if line.startswith('# User '):
94 94 user = line[7:]
95 95 ui.debug('From: %s\n' % user)
96 96 elif line.startswith("# Date "):
97 97 date = line[7:]
98 98 if not line.startswith('# '):
99 99 cfp.write(line)
100 100 cfp.write('\n')
101 101 message = cfp.getvalue()
102 102 if tmpfp:
103 103 tmpfp.write(payload)
104 104 if not payload.endswith('\n'):
105 105 tmpfp.write('\n')
106 106 elif not diffs_seen and message and content_type == 'text/plain':
107 107 message += '\n' + payload
108 108 except:
109 109 tmpfp.close()
110 110 os.unlink(tmpname)
111 111 raise
112 112
113 113 tmpfp.close()
114 114 if not diffs_seen:
115 115 os.unlink(tmpname)
116 116 return None, message, user, date
117 117 return tmpname, message, user, date
118 118
119 119 def readgitpatch(patchname):
120 120 """extract git-style metadata about patches from <patchname>"""
121 121 class gitpatch:
122 122 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
123 123 def __init__(self, path):
124 124 self.path = path
125 125 self.oldpath = None
126 126 self.mode = None
127 127 self.op = 'MODIFY'
128 128 self.copymod = False
129 129 self.lineno = 0
130 130
131 131 # Filter patch for git information
132 132 gitre = re.compile('diff --git a/(.*) b/(.*)')
133 133 pf = file(patchname)
134 134 gp = None
135 135 gitpatches = []
136 136 # Can have a git patch with only metadata, causing patch to complain
137 137 dopatch = False
138 138
139 139 lineno = 0
140 140 for line in pf:
141 141 lineno += 1
142 142 if line.startswith('diff --git'):
143 143 m = gitre.match(line)
144 144 if m:
145 145 if gp:
146 146 gitpatches.append(gp)
147 147 src, dst = m.group(1,2)
148 148 gp = gitpatch(dst)
149 149 gp.lineno = lineno
150 150 elif gp:
151 151 if line.startswith('--- '):
152 152 if gp.op in ('COPY', 'RENAME'):
153 153 gp.copymod = True
154 154 dopatch = 'filter'
155 155 gitpatches.append(gp)
156 156 gp = None
157 157 if not dopatch:
158 158 dopatch = True
159 159 continue
160 160 if line.startswith('rename from '):
161 161 gp.op = 'RENAME'
162 162 gp.oldpath = line[12:].rstrip()
163 163 elif line.startswith('rename to '):
164 164 gp.path = line[10:].rstrip()
165 165 elif line.startswith('copy from '):
166 166 gp.op = 'COPY'
167 167 gp.oldpath = line[10:].rstrip()
168 168 elif line.startswith('copy to '):
169 169 gp.path = line[8:].rstrip()
170 170 elif line.startswith('deleted file'):
171 171 gp.op = 'DELETE'
172 172 elif line.startswith('new file mode '):
173 173 gp.op = 'ADD'
174 174 gp.mode = int(line.rstrip()[-3:], 8)
175 175 elif line.startswith('new mode '):
176 176 gp.mode = int(line.rstrip()[-3:], 8)
177 177 if gp:
178 178 gitpatches.append(gp)
179 179
180 180 if not gitpatches:
181 181 dopatch = True
182 182
183 183 return (dopatch, gitpatches)
184 184
185 185 def dogitpatch(patchname, gitpatches, cwd=None):
186 186 """Preprocess git patch so that vanilla patch can handle it"""
187 187 pf = file(patchname)
188 188 pfline = 1
189 189
190 190 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
191 191 tmpfp = os.fdopen(fd, 'w')
192 192
193 193 try:
194 194 for i in range(len(gitpatches)):
195 195 p = gitpatches[i]
196 196 if not p.copymod:
197 197 continue
198 198
199 199 copyfile(p.oldpath, p.path, basedir=cwd)
200 200
201 201 # rewrite patch hunk
202 202 while pfline < p.lineno:
203 203 tmpfp.write(pf.readline())
204 204 pfline += 1
205 205 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
206 206 line = pf.readline()
207 207 pfline += 1
208 208 while not line.startswith('--- a/'):
209 209 tmpfp.write(line)
210 210 line = pf.readline()
211 211 pfline += 1
212 212 tmpfp.write('--- a/%s\n' % p.path)
213 213
214 214 line = pf.readline()
215 215 while line:
216 216 tmpfp.write(line)
217 217 line = pf.readline()
218 218 except:
219 219 tmpfp.close()
220 220 os.unlink(patchname)
221 221 raise
222 222
223 223 tmpfp.close()
224 224 return patchname
225 225
226 226 def patch(patchname, ui, strip=1, cwd=None):
227 227 """apply the patch <patchname> to the working directory.
228 228 a list of patched files is returned"""
229 229
230 (dopatch, gitpatches) = readgitpatch(patchname)
230 # helper function
231 def __patch(patchname):
232 """patch and updates the files and fuzz variables"""
233 files = {}
234 fuzz = False
231 235
232 files = {}
233 fuzz = False
234 if dopatch:
235 if dopatch == 'filter':
236 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
237 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
236 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
237 'patch')
238 238 args = []
239 239 if cwd:
240 240 args.append('-d %s' % util.shellquote(cwd))
241 241 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
242 242 util.shellquote(patchname)))
243 243
244 244 for line in fp:
245 245 line = line.rstrip()
246 246 ui.note(line + '\n')
247 247 if line.startswith('patching file '):
248 248 pf = util.parse_patch_output(line)
249 249 printed_file = False
250 250 files.setdefault(pf, (None, None))
251 251 elif line.find('with fuzz') >= 0:
252 252 fuzz = True
253 253 if not printed_file:
254 254 ui.warn(pf + '\n')
255 255 printed_file = True
256 256 ui.warn(line + '\n')
257 257 elif line.find('saving rejects to file') >= 0:
258 258 ui.warn(line + '\n')
259 259 elif line.find('FAILED') >= 0:
260 260 if not printed_file:
261 261 ui.warn(pf + '\n')
262 262 printed_file = True
263 263 ui.warn(line + '\n')
264
265 if dopatch == 'filter':
266 os.unlink(patchname)
267
268 264 code = fp.close()
269 265 if code:
270 266 raise util.Abort(_("patch command failed: %s") %
271 267 util.explain_exit(code)[0])
268 return files, fuzz
269
270 (dopatch, gitpatches) = readgitpatch(patchname)
271
272 if dopatch:
273 if dopatch == 'filter':
274 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
275 try:
276 files, fuzz = __patch(patchname)
277 finally:
278 if dopatch == 'filter':
279 os.unlink(patchname)
280 else:
281 files, fuzz = {}, False
272 282
273 283 for gp in gitpatches:
274 284 files[gp.path] = (gp.op, gp)
275 285
276 286 return (files, fuzz)
277 287
278 288 def diffopts(ui, opts={}):
279 289 return mdiff.diffopts(
280 290 text=opts.get('text'),
281 291 git=(opts.get('git') or
282 292 ui.configbool('diff', 'git', None)),
283 293 showfunc=(opts.get('show_function') or
284 294 ui.configbool('diff', 'showfunc', None)),
285 295 ignorews=(opts.get('ignore_all_space') or
286 296 ui.configbool('diff', 'ignorews', None)),
287 297 ignorewsamount=(opts.get('ignore_space_change') or
288 298 ui.configbool('diff', 'ignorewsamount', None)),
289 299 ignoreblanklines=(opts.get('ignore_blank_lines') or
290 300 ui.configbool('diff', 'ignoreblanklines', None)))
291 301
292 302 def updatedir(ui, repo, patches, wlock=None):
293 303 '''Update dirstate after patch application according to metadata'''
294 304 if not patches:
295 305 return
296 306 copies = []
297 307 removes = []
298 308 cfiles = patches.keys()
299 309 copts = {'after': False, 'force': False}
300 310 cwd = repo.getcwd()
301 311 if cwd:
302 312 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
303 313 for f in patches:
304 314 ctype, gp = patches[f]
305 315 if ctype == 'RENAME':
306 316 copies.append((gp.oldpath, gp.path, gp.copymod))
307 317 removes.append(gp.oldpath)
308 318 elif ctype == 'COPY':
309 319 copies.append((gp.oldpath, gp.path, gp.copymod))
310 320 elif ctype == 'DELETE':
311 321 removes.append(gp.path)
312 322 for src, dst, after in copies:
313 323 if not after:
314 324 copyfile(src, dst, repo.root)
315 325 repo.copy(src, dst, wlock=wlock)
316 326 if removes:
317 327 repo.remove(removes, True, wlock=wlock)
318 328 for f in patches:
319 329 ctype, gp = patches[f]
320 330 if gp and gp.mode:
321 331 x = gp.mode & 0100 != 0
322 332 dst = os.path.join(repo.root, gp.path)
323 333 util.set_exec(dst, x)
324 334 cmdutil.addremove(repo, cfiles, wlock=wlock)
325 335 files = patches.keys()
326 336 files.extend([r for r in removes if r not in files])
327 337 files.sort()
328 338
329 339 return files
330 340
331 341 def diff(repo, node1=None, node2=None, files=None, match=util.always,
332 342 fp=None, changes=None, opts=None):
333 343 '''print diff of changes to files between two nodes, or node and
334 344 working directory.
335 345
336 346 if node1 is None, use first dirstate parent instead.
337 347 if node2 is None, compare node1 with working directory.'''
338 348
339 349 if opts is None:
340 350 opts = mdiff.defaultopts
341 351 if fp is None:
342 352 fp = repo.ui
343 353
344 354 if not node1:
345 355 node1 = repo.dirstate.parents()[0]
346 356
347 357 clcache = {}
348 358 def getchangelog(n):
349 359 if n not in clcache:
350 360 clcache[n] = repo.changelog.read(n)
351 361 return clcache[n]
352 362 mcache = {}
353 363 def getmanifest(n):
354 364 if n not in mcache:
355 365 mcache[n] = repo.manifest.read(n)
356 366 return mcache[n]
357 367 fcache = {}
358 368 def getfile(f):
359 369 if f not in fcache:
360 370 fcache[f] = repo.file(f)
361 371 return fcache[f]
362 372
363 373 # reading the data for node1 early allows it to play nicely
364 374 # with repo.status and the revlog cache.
365 375 change = getchangelog(node1)
366 376 mmap = getmanifest(change[0])
367 377 date1 = util.datestr(change[2])
368 378
369 379 if not changes:
370 380 changes = repo.status(node1, node2, files, match=match)[:5]
371 381 modified, added, removed, deleted, unknown = changes
372 382 if files:
373 383 def filterfiles(filters):
374 384 l = [x for x in filters if x in files]
375 385
376 386 for t in files:
377 387 if not t.endswith("/"):
378 388 t += "/"
379 389 l += [x for x in filters if x.startswith(t)]
380 390 return l
381 391
382 392 modified, added, removed = map(filterfiles, (modified, added, removed))
383 393
384 394 if not modified and not added and not removed:
385 395 return
386 396
387 397 def renamedbetween(f, n1, n2):
388 398 r1, r2 = map(repo.changelog.rev, (n1, n2))
389 399 src = None
390 400 while r2 > r1:
391 401 cl = getchangelog(n2)[0]
392 402 m = getmanifest(cl)
393 403 try:
394 404 src = getfile(f).renamed(m[f])
395 405 except KeyError:
396 406 return None
397 407 if src:
398 408 f = src[0]
399 409 n2 = repo.changelog.parents(n2)[0]
400 410 r2 = repo.changelog.rev(n2)
401 411 return src
402 412
403 413 if node2:
404 414 change = getchangelog(node2)
405 415 mmap2 = getmanifest(change[0])
406 416 _date2 = util.datestr(change[2])
407 417 def date2(f):
408 418 return _date2
409 419 def read(f):
410 420 return getfile(f).read(mmap2[f])
411 421 def renamed(f):
412 422 return renamedbetween(f, node1, node2)
413 423 else:
414 424 tz = util.makedate()[1]
415 425 _date2 = util.datestr()
416 426 def date2(f):
417 427 try:
418 428 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
419 429 except OSError, err:
420 430 if err.errno != errno.ENOENT: raise
421 431 return _date2
422 432 def read(f):
423 433 return repo.wread(f)
424 434 def renamed(f):
425 435 src = repo.dirstate.copies.get(f)
426 436 parent = repo.dirstate.parents()[0]
427 437 if src:
428 438 f = src[0]
429 439 of = renamedbetween(f, node1, parent)
430 440 if of:
431 441 return of
432 442 elif src:
433 443 cl = getchangelog(parent)[0]
434 444 return (src, getmanifest(cl)[src])
435 445 else:
436 446 return None
437 447
438 448 if repo.ui.quiet:
439 449 r = None
440 450 else:
441 451 hexfunc = repo.ui.verbose and hex or short
442 452 r = [hexfunc(node) for node in [node1, node2] if node]
443 453
444 454 if opts.git:
445 455 copied = {}
446 456 for f in added:
447 457 src = renamed(f)
448 458 if src:
449 459 copied[f] = src
450 460 srcs = [x[1][0] for x in copied.items()]
451 461
452 462 all = modified + added + removed
453 463 all.sort()
454 464 for f in all:
455 465 to = None
456 466 tn = None
457 467 dodiff = True
458 468 if f in mmap:
459 469 to = getfile(f).read(mmap[f])
460 470 if f not in removed:
461 471 tn = read(f)
462 472 if opts.git:
463 473 def gitmode(x):
464 474 return x and '100755' or '100644'
465 475 def addmodehdr(header, omode, nmode):
466 476 if omode != nmode:
467 477 header.append('old mode %s\n' % omode)
468 478 header.append('new mode %s\n' % nmode)
469 479
470 480 a, b = f, f
471 481 header = []
472 482 if f in added:
473 483 if node2:
474 484 mode = gitmode(mmap2.execf(f))
475 485 else:
476 486 mode = gitmode(util.is_exec(repo.wjoin(f), None))
477 487 if f in copied:
478 488 a, arev = copied[f]
479 489 omode = gitmode(mmap.execf(a))
480 490 addmodehdr(header, omode, mode)
481 491 op = a in removed and 'rename' or 'copy'
482 492 header.append('%s from %s\n' % (op, a))
483 493 header.append('%s to %s\n' % (op, f))
484 494 to = getfile(a).read(arev)
485 495 else:
486 496 header.append('new file mode %s\n' % mode)
487 497 elif f in removed:
488 498 if f in srcs:
489 499 dodiff = False
490 500 else:
491 501 mode = gitmode(mmap.execf(f))
492 502 header.append('deleted file mode %s\n' % mode)
493 503 else:
494 504 omode = gitmode(mmap.execf(f))
495 505 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
496 506 addmodehdr(header, omode, nmode)
497 507 r = None
498 508 if dodiff:
499 509 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
500 510 fp.write(''.join(header))
501 511 if dodiff:
502 512 fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts))
503 513
504 514 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
505 515 opts=None):
506 516 '''export changesets as hg patches.'''
507 517
508 518 total = len(revs)
509 519 revwidth = max(map(len, revs))
510 520
511 521 def single(node, seqno, fp):
512 522 parents = [p for p in repo.changelog.parents(node) if p != nullid]
513 523 if switch_parent:
514 524 parents.reverse()
515 525 prev = (parents and parents[0]) or nullid
516 526 change = repo.changelog.read(node)
517 527
518 528 if not fp:
519 529 fp = cmdutil.make_file(repo, template, node, total=total,
520 530 seqno=seqno, revwidth=revwidth)
521 531 if fp not in (sys.stdout, repo.ui):
522 532 repo.ui.note("%s\n" % fp.name)
523 533
524 534 fp.write("# HG changeset patch\n")
525 535 fp.write("# User %s\n" % change[1])
526 536 fp.write("# Date %d %d\n" % change[2])
527 537 fp.write("# Node ID %s\n" % hex(node))
528 538 fp.write("# Parent %s\n" % hex(prev))
529 539 if len(parents) > 1:
530 540 fp.write("# Parent %s\n" % hex(parents[1]))
531 541 fp.write(change[4].rstrip())
532 542 fp.write("\n\n")
533 543
534 544 diff(repo, prev, node, fp=fp, opts=opts)
535 545 if fp not in (sys.stdout, repo.ui):
536 546 fp.close()
537 547
538 548 for seqno, cset in enumerate(revs):
539 549 single(cset, seqno, fp)
General Comments 0
You need to be logged in to leave comments. Login now