##// END OF EJS Templates
git-send-email compatibility: stop reading changelog after ^---$
Brendan Cully -
r4220:12537038 default
parent child Browse files
Show More
@@ -1,645 +1,648 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 i18n import _
9 9 from node import *
10 10 import base85, cmdutil, mdiff, util, context, revlog
11 11 import cStringIO, email.Parser, os, popen2, re, sha
12 12 import sys, tempfile, zlib
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
29 29 util.copyfile(abssrc, absdst)
30 30
31 31 # public functions
32 32
33 33 def extract(ui, fileobj):
34 34 '''extract patch from data read from fileobj.
35 35
36 36 patch can be normal patch or contained in email message.
37 37
38 38 return tuple (filename, message, user, date). any item in returned
39 39 tuple can be None. if filename is None, fileobj did not contain
40 40 patch. caller must unlink filename when done.'''
41 41
42 42 # attempt to detect the start of a patch
43 43 # (this heuristic is borrowed from quilt)
44 44 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
45 45 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
46 46 '(---|\*\*\*)[ \t])', re.MULTILINE)
47 47
48 48 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
49 49 tmpfp = os.fdopen(fd, 'w')
50 50 try:
51 hgpatch = False
52
53 51 msg = email.Parser.Parser().parse(fileobj)
54 52
55 53 message = msg['Subject']
56 54 user = msg['From']
57 55 # should try to parse msg['Date']
58 56 date = None
59 57
60 58 if message:
61 59 if message.startswith('[PATCH'):
62 60 pend = message.find(']')
63 61 if pend >= 0:
64 62 message = message[pend+1:].lstrip()
65 63 message = message.replace('\n\t', ' ')
66 64 ui.debug('Subject: %s\n' % message)
67 65 if user:
68 66 ui.debug('From: %s\n' % user)
69 67 diffs_seen = 0
70 68 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
71 69
72 70 for part in msg.walk():
73 71 content_type = part.get_content_type()
74 72 ui.debug('Content-Type: %s\n' % content_type)
75 73 if content_type not in ok_types:
76 74 continue
77 75 payload = part.get_payload(decode=True)
78 76 m = diffre.search(payload)
79 77 if m:
78 hgpatch = False
79 ignoretext = False
80
80 81 ui.debug(_('found patch at byte %d\n') % m.start(0))
81 82 diffs_seen += 1
82 83 cfp = cStringIO.StringIO()
83 84 if message:
84 85 cfp.write(message)
85 86 cfp.write('\n')
86 87 for line in payload[:m.start(0)].splitlines():
87 88 if line.startswith('# HG changeset patch'):
88 89 ui.debug(_('patch generated by hg export\n'))
89 90 hgpatch = True
90 91 # drop earlier commit message content
91 92 cfp.seek(0)
92 93 cfp.truncate()
93 94 elif hgpatch:
94 95 if line.startswith('# User '):
95 96 user = line[7:]
96 97 ui.debug('From: %s\n' % user)
97 98 elif line.startswith("# Date "):
98 99 date = line[7:]
99 if not line.startswith('# '):
100 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
101 ignoretext = True
102 if not line.startswith('# ') and not ignoretext:
100 103 cfp.write(line)
101 104 cfp.write('\n')
102 105 message = cfp.getvalue()
103 106 if tmpfp:
104 107 tmpfp.write(payload)
105 108 if not payload.endswith('\n'):
106 109 tmpfp.write('\n')
107 110 elif not diffs_seen and message and content_type == 'text/plain':
108 111 message += '\n' + payload
109 112 except:
110 113 tmpfp.close()
111 114 os.unlink(tmpname)
112 115 raise
113 116
114 117 tmpfp.close()
115 118 if not diffs_seen:
116 119 os.unlink(tmpname)
117 120 return None, message, user, date
118 121 return tmpname, message, user, date
119 122
120 123 GP_PATCH = 1 << 0 # we have to run patch
121 124 GP_FILTER = 1 << 1 # there's some copy/rename operation
122 125 GP_BINARY = 1 << 2 # there's a binary patch
123 126
124 127 def readgitpatch(patchname):
125 128 """extract git-style metadata about patches from <patchname>"""
126 129 class gitpatch:
127 130 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
128 131 def __init__(self, path):
129 132 self.path = path
130 133 self.oldpath = None
131 134 self.mode = None
132 135 self.op = 'MODIFY'
133 136 self.copymod = False
134 137 self.lineno = 0
135 138 self.binary = False
136 139
137 140 # Filter patch for git information
138 141 gitre = re.compile('diff --git a/(.*) b/(.*)')
139 142 pf = file(patchname)
140 143 gp = None
141 144 gitpatches = []
142 145 # Can have a git patch with only metadata, causing patch to complain
143 146 dopatch = 0
144 147
145 148 lineno = 0
146 149 for line in pf:
147 150 lineno += 1
148 151 if line.startswith('diff --git'):
149 152 m = gitre.match(line)
150 153 if m:
151 154 if gp:
152 155 gitpatches.append(gp)
153 156 src, dst = m.group(1, 2)
154 157 gp = gitpatch(dst)
155 158 gp.lineno = lineno
156 159 elif gp:
157 160 if line.startswith('--- '):
158 161 if gp.op in ('COPY', 'RENAME'):
159 162 gp.copymod = True
160 163 dopatch |= GP_FILTER
161 164 gitpatches.append(gp)
162 165 gp = None
163 166 dopatch |= GP_PATCH
164 167 continue
165 168 if line.startswith('rename from '):
166 169 gp.op = 'RENAME'
167 170 gp.oldpath = line[12:].rstrip()
168 171 elif line.startswith('rename to '):
169 172 gp.path = line[10:].rstrip()
170 173 elif line.startswith('copy from '):
171 174 gp.op = 'COPY'
172 175 gp.oldpath = line[10:].rstrip()
173 176 elif line.startswith('copy to '):
174 177 gp.path = line[8:].rstrip()
175 178 elif line.startswith('deleted file'):
176 179 gp.op = 'DELETE'
177 180 elif line.startswith('new file mode '):
178 181 gp.op = 'ADD'
179 182 gp.mode = int(line.rstrip()[-3:], 8)
180 183 elif line.startswith('new mode '):
181 184 gp.mode = int(line.rstrip()[-3:], 8)
182 185 elif line.startswith('GIT binary patch'):
183 186 dopatch |= GP_BINARY
184 187 gp.binary = True
185 188 if gp:
186 189 gitpatches.append(gp)
187 190
188 191 if not gitpatches:
189 192 dopatch = GP_PATCH
190 193
191 194 return (dopatch, gitpatches)
192 195
193 196 def dogitpatch(patchname, gitpatches, cwd=None):
194 197 """Preprocess git patch so that vanilla patch can handle it"""
195 198 def extractbin(fp):
196 199 i = [0] # yuck
197 200 def readline():
198 201 i[0] += 1
199 202 return fp.readline().rstrip()
200 203 line = readline()
201 204 while line and not line.startswith('literal '):
202 205 line = readline()
203 206 if not line:
204 207 return None, i[0]
205 208 size = int(line[8:])
206 209 dec = []
207 210 line = readline()
208 211 while line:
209 212 l = line[0]
210 213 if l <= 'Z' and l >= 'A':
211 214 l = ord(l) - ord('A') + 1
212 215 else:
213 216 l = ord(l) - ord('a') + 27
214 217 dec.append(base85.b85decode(line[1:])[:l])
215 218 line = readline()
216 219 text = zlib.decompress(''.join(dec))
217 220 if len(text) != size:
218 221 raise util.Abort(_('binary patch is %d bytes, not %d') %
219 222 (len(text), size))
220 223 return text, i[0]
221 224
222 225 pf = file(patchname)
223 226 pfline = 1
224 227
225 228 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
226 229 tmpfp = os.fdopen(fd, 'w')
227 230
228 231 try:
229 232 for i in xrange(len(gitpatches)):
230 233 p = gitpatches[i]
231 234 if not p.copymod and not p.binary:
232 235 continue
233 236
234 237 # rewrite patch hunk
235 238 while pfline < p.lineno:
236 239 tmpfp.write(pf.readline())
237 240 pfline += 1
238 241
239 242 if p.binary:
240 243 text, delta = extractbin(pf)
241 244 if not text:
242 245 raise util.Abort(_('binary patch extraction failed'))
243 246 pfline += delta
244 247 if not cwd:
245 248 cwd = os.getcwd()
246 249 absdst = os.path.join(cwd, p.path)
247 250 basedir = os.path.dirname(absdst)
248 251 if not os.path.isdir(basedir):
249 252 os.makedirs(basedir)
250 253 out = file(absdst, 'wb')
251 254 out.write(text)
252 255 out.close()
253 256 elif p.copymod:
254 257 copyfile(p.oldpath, p.path, basedir=cwd)
255 258 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
256 259 line = pf.readline()
257 260 pfline += 1
258 261 while not line.startswith('--- a/'):
259 262 tmpfp.write(line)
260 263 line = pf.readline()
261 264 pfline += 1
262 265 tmpfp.write('--- a/%s\n' % p.path)
263 266
264 267 line = pf.readline()
265 268 while line:
266 269 tmpfp.write(line)
267 270 line = pf.readline()
268 271 except:
269 272 tmpfp.close()
270 273 os.unlink(patchname)
271 274 raise
272 275
273 276 tmpfp.close()
274 277 return patchname
275 278
276 279 def patch(patchname, ui, strip=1, cwd=None, files={}):
277 280 """apply the patch <patchname> to the working directory.
278 281 a list of patched files is returned"""
279 282
280 283 # helper function
281 284 def __patch(patchname):
282 285 """patch and updates the files and fuzz variables"""
283 286 fuzz = False
284 287
285 288 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
286 289 'patch')
287 290 args = []
288 291 if cwd:
289 292 args.append('-d %s' % util.shellquote(cwd))
290 293 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
291 294 util.shellquote(patchname)))
292 295
293 296 for line in fp:
294 297 line = line.rstrip()
295 298 ui.note(line + '\n')
296 299 if line.startswith('patching file '):
297 300 pf = util.parse_patch_output(line)
298 301 printed_file = False
299 302 files.setdefault(pf, (None, None))
300 303 elif line.find('with fuzz') >= 0:
301 304 fuzz = True
302 305 if not printed_file:
303 306 ui.warn(pf + '\n')
304 307 printed_file = True
305 308 ui.warn(line + '\n')
306 309 elif line.find('saving rejects to file') >= 0:
307 310 ui.warn(line + '\n')
308 311 elif line.find('FAILED') >= 0:
309 312 if not printed_file:
310 313 ui.warn(pf + '\n')
311 314 printed_file = True
312 315 ui.warn(line + '\n')
313 316 code = fp.close()
314 317 if code:
315 318 raise util.Abort(_("patch command failed: %s") %
316 319 util.explain_exit(code)[0])
317 320 return fuzz
318 321
319 322 (dopatch, gitpatches) = readgitpatch(patchname)
320 323 for gp in gitpatches:
321 324 files[gp.path] = (gp.op, gp)
322 325
323 326 fuzz = False
324 327 if dopatch:
325 328 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
326 329 if filterpatch:
327 330 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
328 331 try:
329 332 if dopatch & GP_PATCH:
330 333 fuzz = __patch(patchname)
331 334 finally:
332 335 if filterpatch:
333 336 os.unlink(patchname)
334 337
335 338 return fuzz
336 339
337 340 def diffopts(ui, opts={}, untrusted=False):
338 341 def get(key, name=None):
339 342 return (opts.get(key) or
340 343 ui.configbool('diff', name or key, None, untrusted=untrusted))
341 344 return mdiff.diffopts(
342 345 text=opts.get('text'),
343 346 git=get('git'),
344 347 nodates=get('nodates'),
345 348 showfunc=get('show_function', 'showfunc'),
346 349 ignorews=get('ignore_all_space', 'ignorews'),
347 350 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
348 351 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
349 352
350 353 def updatedir(ui, repo, patches, wlock=None):
351 354 '''Update dirstate after patch application according to metadata'''
352 355 if not patches:
353 356 return
354 357 copies = []
355 358 removes = {}
356 359 cfiles = patches.keys()
357 360 cwd = repo.getcwd()
358 361 if cwd:
359 362 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
360 363 for f in patches:
361 364 ctype, gp = patches[f]
362 365 if ctype == 'RENAME':
363 366 copies.append((gp.oldpath, gp.path, gp.copymod))
364 367 removes[gp.oldpath] = 1
365 368 elif ctype == 'COPY':
366 369 copies.append((gp.oldpath, gp.path, gp.copymod))
367 370 elif ctype == 'DELETE':
368 371 removes[gp.path] = 1
369 372 for src, dst, after in copies:
370 373 if not after:
371 374 copyfile(src, dst, repo.root)
372 375 repo.copy(src, dst, wlock=wlock)
373 376 removes = removes.keys()
374 377 if removes:
375 378 removes.sort()
376 379 repo.remove(removes, True, wlock=wlock)
377 380 for f in patches:
378 381 ctype, gp = patches[f]
379 382 if gp and gp.mode:
380 383 x = gp.mode & 0100 != 0
381 384 dst = os.path.join(repo.root, gp.path)
382 385 # patch won't create empty files
383 386 if ctype == 'ADD' and not os.path.exists(dst):
384 387 repo.wwrite(gp.path, '', x and 'x' or '')
385 388 else:
386 389 util.set_exec(dst, x)
387 390 cmdutil.addremove(repo, cfiles, wlock=wlock)
388 391 files = patches.keys()
389 392 files.extend([r for r in removes if r not in files])
390 393 files.sort()
391 394
392 395 return files
393 396
394 397 def b85diff(fp, to, tn):
395 398 '''print base85-encoded binary diff'''
396 399 def gitindex(text):
397 400 if not text:
398 401 return '0' * 40
399 402 l = len(text)
400 403 s = sha.new('blob %d\0' % l)
401 404 s.update(text)
402 405 return s.hexdigest()
403 406
404 407 def fmtline(line):
405 408 l = len(line)
406 409 if l <= 26:
407 410 l = chr(ord('A') + l - 1)
408 411 else:
409 412 l = chr(l - 26 + ord('a') - 1)
410 413 return '%c%s\n' % (l, base85.b85encode(line, True))
411 414
412 415 def chunk(text, csize=52):
413 416 l = len(text)
414 417 i = 0
415 418 while i < l:
416 419 yield text[i:i+csize]
417 420 i += csize
418 421
419 422 tohash = gitindex(to)
420 423 tnhash = gitindex(tn)
421 424 if tohash == tnhash:
422 425 return ""
423 426
424 427 # TODO: deltas
425 428 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
426 429 (tohash, tnhash, len(tn))]
427 430 for l in chunk(zlib.compress(tn)):
428 431 ret.append(fmtline(l))
429 432 ret.append('\n')
430 433 return ''.join(ret)
431 434
432 435 def diff(repo, node1=None, node2=None, files=None, match=util.always,
433 436 fp=None, changes=None, opts=None):
434 437 '''print diff of changes to files between two nodes, or node and
435 438 working directory.
436 439
437 440 if node1 is None, use first dirstate parent instead.
438 441 if node2 is None, compare node1 with working directory.'''
439 442
440 443 if opts is None:
441 444 opts = mdiff.defaultopts
442 445 if fp is None:
443 446 fp = repo.ui
444 447
445 448 if not node1:
446 449 node1 = repo.dirstate.parents()[0]
447 450
448 451 ccache = {}
449 452 def getctx(r):
450 453 if r not in ccache:
451 454 ccache[r] = context.changectx(repo, r)
452 455 return ccache[r]
453 456
454 457 flcache = {}
455 458 def getfilectx(f, ctx):
456 459 flctx = ctx.filectx(f, filelog=flcache.get(f))
457 460 if f not in flcache:
458 461 flcache[f] = flctx._filelog
459 462 return flctx
460 463
461 464 # reading the data for node1 early allows it to play nicely
462 465 # with repo.status and the revlog cache.
463 466 ctx1 = context.changectx(repo, node1)
464 467 # force manifest reading
465 468 man1 = ctx1.manifest()
466 469 date1 = util.datestr(ctx1.date())
467 470
468 471 if not changes:
469 472 changes = repo.status(node1, node2, files, match=match)[:5]
470 473 modified, added, removed, deleted, unknown = changes
471 474
472 475 if not modified and not added and not removed:
473 476 return
474 477
475 478 if node2:
476 479 ctx2 = context.changectx(repo, node2)
477 480 else:
478 481 ctx2 = context.workingctx(repo)
479 482 man2 = ctx2.manifest()
480 483
481 484 # returns False if there was no rename between ctx1 and ctx2
482 485 # returns None if the file was created between ctx1 and ctx2
483 486 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
484 487 def renamed(f):
485 488 startrev = ctx1.rev()
486 489 c = ctx2
487 490 crev = c.rev()
488 491 if crev is None:
489 492 crev = repo.changelog.count()
490 493 orig = f
491 494 while crev > startrev:
492 495 if f in c.files():
493 496 try:
494 497 src = getfilectx(f, c).renamed()
495 498 except revlog.LookupError:
496 499 return None
497 500 if src:
498 501 f = src[0]
499 502 crev = c.parents()[0].rev()
500 503 # try to reuse
501 504 c = getctx(crev)
502 505 if f not in man1:
503 506 return None
504 507 if f == orig:
505 508 return False
506 509 return f
507 510
508 511 if repo.ui.quiet:
509 512 r = None
510 513 else:
511 514 hexfunc = repo.ui.debugflag and hex or short
512 515 r = [hexfunc(node) for node in [node1, node2] if node]
513 516
514 517 if opts.git:
515 518 copied = {}
516 519 for f in added:
517 520 src = renamed(f)
518 521 if src:
519 522 copied[f] = src
520 523 srcs = [x[1] for x in copied.items()]
521 524
522 525 all = modified + added + removed
523 526 all.sort()
524 527 gone = {}
525 528
526 529 for f in all:
527 530 to = None
528 531 tn = None
529 532 dodiff = True
530 533 header = []
531 534 if f in man1:
532 535 to = getfilectx(f, ctx1).data()
533 536 if f not in removed:
534 537 tn = getfilectx(f, ctx2).data()
535 538 if opts.git:
536 539 def gitmode(x):
537 540 return x and '100755' or '100644'
538 541 def addmodehdr(header, omode, nmode):
539 542 if omode != nmode:
540 543 header.append('old mode %s\n' % omode)
541 544 header.append('new mode %s\n' % nmode)
542 545
543 546 a, b = f, f
544 547 if f in added:
545 548 mode = gitmode(man2.execf(f))
546 549 if f in copied:
547 550 a = copied[f]
548 551 omode = gitmode(man1.execf(a))
549 552 addmodehdr(header, omode, mode)
550 553 if a in removed and a not in gone:
551 554 op = 'rename'
552 555 gone[a] = 1
553 556 else:
554 557 op = 'copy'
555 558 header.append('%s from %s\n' % (op, a))
556 559 header.append('%s to %s\n' % (op, f))
557 560 to = getfilectx(a, ctx1).data()
558 561 else:
559 562 header.append('new file mode %s\n' % mode)
560 563 if util.binary(tn):
561 564 dodiff = 'binary'
562 565 elif f in removed:
563 566 if f in srcs:
564 567 dodiff = False
565 568 else:
566 569 mode = gitmode(man1.execf(f))
567 570 header.append('deleted file mode %s\n' % mode)
568 571 else:
569 572 omode = gitmode(man1.execf(f))
570 573 nmode = gitmode(man2.execf(f))
571 574 addmodehdr(header, omode, nmode)
572 575 if util.binary(to) or util.binary(tn):
573 576 dodiff = 'binary'
574 577 r = None
575 578 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
576 579 if dodiff:
577 580 if dodiff == 'binary':
578 581 text = b85diff(fp, to, tn)
579 582 else:
580 583 text = mdiff.unidiff(to, date1,
581 584 # ctx2 date may be dynamic
582 585 tn, util.datestr(ctx2.date()),
583 586 f, r, opts=opts)
584 587 if text or len(header) > 1:
585 588 fp.write(''.join(header))
586 589 fp.write(text)
587 590
588 591 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
589 592 opts=None):
590 593 '''export changesets as hg patches.'''
591 594
592 595 total = len(revs)
593 596 revwidth = max([len(str(rev)) for rev in revs])
594 597
595 598 def single(rev, seqno, fp):
596 599 ctx = repo.changectx(rev)
597 600 node = ctx.node()
598 601 parents = [p.node() for p in ctx.parents() if p]
599 602 if switch_parent:
600 603 parents.reverse()
601 604 prev = (parents and parents[0]) or nullid
602 605
603 606 if not fp:
604 607 fp = cmdutil.make_file(repo, template, node, total=total,
605 608 seqno=seqno, revwidth=revwidth)
606 609 if fp != sys.stdout and hasattr(fp, 'name'):
607 610 repo.ui.note("%s\n" % fp.name)
608 611
609 612 fp.write("# HG changeset patch\n")
610 613 fp.write("# User %s\n" % ctx.user())
611 614 fp.write("# Date %d %d\n" % ctx.date())
612 615 fp.write("# Node ID %s\n" % hex(node))
613 616 fp.write("# Parent %s\n" % hex(prev))
614 617 if len(parents) > 1:
615 618 fp.write("# Parent %s\n" % hex(parents[1]))
616 619 fp.write(ctx.description().rstrip())
617 620 fp.write("\n\n")
618 621
619 622 diff(repo, prev, node, fp=fp, opts=opts)
620 623 if fp not in (sys.stdout, repo.ui):
621 624 fp.close()
622 625
623 626 for seqno, rev in enumerate(revs):
624 627 single(rev, seqno+1, fp)
625 628
626 629 def diffstat(patchlines):
627 630 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
628 631 try:
629 632 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
630 633 try:
631 634 for line in patchlines: print >> p.tochild, line
632 635 p.tochild.close()
633 636 if p.wait(): return
634 637 fp = os.fdopen(fd, 'r')
635 638 stat = []
636 639 for line in fp: stat.append(line.lstrip())
637 640 last = stat.pop()
638 641 stat.insert(0, last)
639 642 stat = ''.join(stat)
640 643 if stat.startswith('0 files'): raise ValueError
641 644 return stat
642 645 except: raise
643 646 finally:
644 647 try: os.unlink(name)
645 648 except: pass
General Comments 0
You need to be logged in to leave comments. Login now