comparison mercurial/patch.py @ 4897:4574925db5c0

Add Chris Mason's mpatch library. The original repo is http://oss.oracle.com/mercurial/mason/mpatch
author Bryan O'Sullivan <bos@serpentine.com>
date Tue, 17 Jul 2007 09:39:30 -0700
parents e321f16f4eac
children bc905a6c0e76
comparison
equal deleted inserted replaced
4896:ee04732fe61d 4897:4574925db5c0
1 # patch.py - patch file parsing routines 1 # patch.py - patch file parsing routines
2 # 2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com> 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # 5 #
5 # This software may be used and distributed according to the terms 6 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference. 7 # of the GNU General Public License, incorporated herein by reference.
7 8
8 from i18n import _ 9 from i18n import _
9 from node import * 10 from node import *
10 import base85, cmdutil, mdiff, util, context, revlog 11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
11 import cStringIO, email.Parser, os, popen2, re, sha 12 import cStringIO, email.Parser, os, popen2, re, sha
12 import sys, tempfile, zlib 13 import sys, tempfile, zlib
14
15 class PatchError(Exception):
16 pass
13 17
14 # helper functions 18 # helper functions
15 19
16 def copyfile(src, dst, basedir=None): 20 def copyfile(src, dst, basedir=None):
17 if not basedir: 21 if not basedir:
133 137
134 GP_PATCH = 1 << 0 # we have to run patch 138 GP_PATCH = 1 << 0 # we have to run patch
135 GP_FILTER = 1 << 1 # there's some copy/rename operation 139 GP_FILTER = 1 << 1 # there's some copy/rename operation
136 GP_BINARY = 1 << 2 # there's a binary patch 140 GP_BINARY = 1 << 2 # there's a binary patch
137 141
138 def readgitpatch(patchname): 142 def readgitpatch(fp, firstline):
139 """extract git-style metadata about patches from <patchname>""" 143 """extract git-style metadata about patches from <patchname>"""
140 class gitpatch: 144 class gitpatch:
141 "op is one of ADD, DELETE, RENAME, MODIFY or COPY" 145 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
142 def __init__(self, path): 146 def __init__(self, path):
143 self.path = path 147 self.path = path
146 self.op = 'MODIFY' 150 self.op = 'MODIFY'
147 self.copymod = False 151 self.copymod = False
148 self.lineno = 0 152 self.lineno = 0
149 self.binary = False 153 self.binary = False
150 154
155 def reader(fp, firstline):
156 yield firstline
157 for line in fp:
158 yield line
159
151 # Filter patch for git information 160 # Filter patch for git information
152 gitre = re.compile('diff --git a/(.*) b/(.*)') 161 gitre = re.compile('diff --git a/(.*) b/(.*)')
153 pf = file(patchname)
154 gp = None 162 gp = None
155 gitpatches = [] 163 gitpatches = []
156 # Can have a git patch with only metadata, causing patch to complain 164 # Can have a git patch with only metadata, causing patch to complain
157 dopatch = 0 165 dopatch = 0
158 166
159 lineno = 0 167 lineno = 0
160 for line in pf: 168 for line in reader(fp, firstline):
161 lineno += 1 169 lineno += 1
162 if line.startswith('diff --git'): 170 if line.startswith('diff --git'):
163 m = gitre.match(line) 171 m = gitre.match(line)
164 if m: 172 if m:
165 if gp: 173 if gp:
202 if not gitpatches: 210 if not gitpatches:
203 dopatch = GP_PATCH 211 dopatch = GP_PATCH
204 212
205 return (dopatch, gitpatches) 213 return (dopatch, gitpatches)
206 214
207 def dogitpatch(patchname, gitpatches, cwd=None): 215 def patch(patchname, ui, strip=1, cwd=None, files={}):
208 """Preprocess git patch so that vanilla patch can handle it""" 216 """apply the patch <patchname> to the working directory.
209 def extractbin(fp): 217 a list of patched files is returned"""
210 i = [0] # yuck 218 fp = file(patchname)
211 def readline(): 219 fuzz = False
212 i[0] += 1 220 if cwd:
213 return fp.readline().rstrip() 221 curdir = os.getcwd()
214 line = readline() 222 os.chdir(cwd)
223 try:
224 ret = applydiff(ui, fp, files, strip=strip)
225 except PatchError:
226 raise util.Abort(_("patch failed to apply"))
227 if cwd:
228 os.chdir(curdir)
229 if ret < 0:
230 raise util.Abort(_("patch failed to apply"))
231 if ret > 0:
232 fuzz = True
233 return fuzz
234
235 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
236 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
237 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
238
239 class patchfile:
240 def __init__(self, ui, fname):
241 self.fname = fname
242 self.ui = ui
243 try:
244 fp = file(fname, 'r')
245 self.lines = fp.readlines()
246 self.exists = True
247 except IOError:
248 dirname = os.path.dirname(fname)
249 if dirname and not os.path.isdir(dirname):
250 dirs = dirname.split(os.path.sep)
251 d = ""
252 for x in dirs:
253 d = os.path.join(d, x)
254 if not os.path.isdir(d):
255 os.mkdir(d)
256 self.lines = []
257 self.exists = False
258
259 self.hash = {}
260 self.dirty = 0
261 self.offset = 0
262 self.rej = []
263 self.fileprinted = False
264 self.printfile(False)
265 self.hunks = 0
266
267 def printfile(self, warn):
268 if self.fileprinted:
269 return
270 if warn or self.ui.verbose:
271 self.fileprinted = True
272 s = _("patching file %s\n" % self.fname)
273 if warn:
274 self.ui.warn(s)
275 else:
276 self.ui.note(s)
277
278
279 def findlines(self, l, linenum):
280 # looks through the hash and finds candidate lines. The
281 # result is a list of line numbers sorted based on distance
282 # from linenum
283 def sorter(a, b):
284 vala = abs(a - linenum)
285 valb = abs(b - linenum)
286 return cmp(vala, valb)
287
288 try:
289 cand = self.hash[l]
290 except:
291 return []
292
293 if len(cand) > 1:
294 # resort our list of potentials forward then back.
295 cand.sort(cmp=sorter)
296 return cand
297
298 def hashlines(self):
299 self.hash = {}
300 for x in xrange(len(self.lines)):
301 s = self.lines[x]
302 self.hash.setdefault(s, []).append(x)
303
304 def write_rej(self):
305 # our rejects are a little different from patch(1). This always
306 # creates rejects in the same form as the original patch. A file
307 # header is inserted so that you can run the reject through patch again
308 # without having to type the filename.
309
310 if not self.rej:
311 return
312 if self.hunks != 1:
313 hunkstr = "s"
314 else:
315 hunkstr = ""
316
317 fname = self.fname + ".rej"
318 self.ui.warn(
319 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n" %
320 (len(self.rej), self.hunks, hunkstr, fname)))
321 try: os.unlink(fname)
322 except:
323 pass
324 fp = file(fname, 'w')
325 base = os.path.basename(self.fname)
326 fp.write("--- %s\n+++ %s\n" % (base, base))
327 for x in self.rej:
328 for l in x.hunk:
329 fp.write(l)
330 if l[-1] != '\n':
331 fp.write("\n\ No newline at end of file\n")
332
333 def write(self, dest=None):
334 if self.dirty:
335 if not dest:
336 dest = self.fname
337 st = None
338 try:
339 st = os.lstat(dest)
340 if st.st_nlink > 1:
341 os.unlink(dest)
342 except: pass
343 fp = file(dest, 'w')
344 if st:
345 os.chmod(dest, st.st_mode)
346 fp.writelines(self.lines)
347 fp.close()
348
349 def close(self):
350 self.write()
351 self.write_rej()
352
353 def apply(self, h, reverse):
354 if not h.complete():
355 raise PatchError("bad hunk #%d %s (%d %d %d %d)" %
356 (h.number, h.desc, len(h.a), h.lena, len(h.b),
357 h.lenb))
358
359 self.hunks += 1
360 if reverse:
361 h.reverse()
362
363 if self.exists and h.createfile():
364 self.ui.warn(_("file %s already exists\n" % self.fname))
365 self.rej.append(h)
366 return -1
367
368 if isinstance(h, binhunk):
369 if h.rmfile():
370 os.unlink(self.fname)
371 else:
372 self.lines[:] = h.new()
373 self.offset += len(h.new())
374 self.dirty = 1
375 return 0
376
377 # fast case first, no offsets, no fuzz
378 old = h.old()
379 # patch starts counting at 1 unless we are adding the file
380 if h.starta == 0:
381 start = 0
382 else:
383 start = h.starta + self.offset - 1
384 orig_start = start
385 if diffhelpers.testhunk(old, self.lines, start) == 0:
386 if h.rmfile():
387 os.unlink(self.fname)
388 else:
389 self.lines[start : start + h.lena] = h.new()
390 self.offset += h.lenb - h.lena
391 self.dirty = 1
392 return 0
393
394 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
395 self.hashlines()
396 if h.hunk[-1][0] != ' ':
397 # if the hunk tried to put something at the bottom of the file
398 # override the start line and use eof here
399 search_start = len(self.lines)
400 else:
401 search_start = orig_start
402
403 for fuzzlen in xrange(3):
404 for toponly in [ True, False ]:
405 old = h.old(fuzzlen, toponly)
406
407 cand = self.findlines(old[0][1:], search_start)
408 for l in cand:
409 if diffhelpers.testhunk(old, self.lines, l) == 0:
410 newlines = h.new(fuzzlen, toponly)
411 self.lines[l : l + len(old)] = newlines
412 self.offset += len(newlines) - len(old)
413 self.dirty = 1
414 if fuzzlen:
415 fuzzstr = "with fuzz %d " % fuzzlen
416 f = self.ui.warn
417 self.printfile(True)
418 else:
419 fuzzstr = ""
420 f = self.ui.note
421 offset = l - orig_start - fuzzlen
422 if offset == 1:
423 linestr = "line"
424 else:
425 linestr = "lines"
426 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n" %
427 (h.number, l+1, fuzzstr, offset, linestr)))
428 return fuzzlen
429 self.printfile(True)
430 self.ui.warn(_("Hunk #%d FAILED at %d\n" % (h.number, orig_start)))
431 self.rej.append(h)
432 return -1
433
434 class hunk:
435 def __init__(self, desc, num, lr, context):
436 self.number = num
437 self.desc = desc
438 self.hunk = [ desc ]
439 self.a = []
440 self.b = []
441 if context:
442 self.read_context_hunk(lr)
443 else:
444 self.read_unified_hunk(lr)
445
446 def read_unified_hunk(self, lr):
447 m = unidesc.match(self.desc)
448 if not m:
449 raise PatchError("bad hunk #%d" % self.number)
450 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
451 if self.lena == None:
452 self.lena = 1
453 else:
454 self.lena = int(self.lena)
455 if self.lenb == None:
456 self.lenb = 1
457 else:
458 self.lenb = int(self.lenb)
459 self.starta = int(self.starta)
460 self.startb = int(self.startb)
461 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
462 # if we hit eof before finishing out the hunk, the last line will
463 # be zero length. Lets try to fix it up.
464 while len(self.hunk[-1]) == 0:
465 del self.hunk[-1]
466 del self.a[-1]
467 del self.b[-1]
468 self.lena -= 1
469 self.lenb -= 1
470
471 def read_context_hunk(self, lr):
472 self.desc = lr.readline()
473 m = contextdesc.match(self.desc)
474 if not m:
475 raise PatchError("bad hunk #%d" % self.number)
476 foo, self.starta, foo2, aend, foo3 = m.groups()
477 self.starta = int(self.starta)
478 if aend == None:
479 aend = self.starta
480 self.lena = int(aend) - self.starta
481 if self.starta:
482 self.lena += 1
483 for x in xrange(self.lena):
484 l = lr.readline()
485 if l.startswith('---'):
486 lr.push(l)
487 break
488 s = l[2:]
489 if l.startswith('- ') or l.startswith('! '):
490 u = '-' + s
491 elif l.startswith(' '):
492 u = ' ' + s
493 else:
494 raise PatchError("bad hunk #%d old text line %d" % (self.number, x))
495 self.a.append(u)
496 self.hunk.append(u)
497
498 l = lr.readline()
499 if l.startswith('\ '):
500 s = self.a[-1][:-1]
501 self.a[-1] = s
502 self.hunk[-1] = s
503 l = lr.readline()
504 m = contextdesc.match(l)
505 if not m:
506 raise PatchError("bad hunk #%d" % self.number)
507 foo, self.startb, foo2, bend, foo3 = m.groups()
508 self.startb = int(self.startb)
509 if bend == None:
510 bend = self.startb
511 self.lenb = int(bend) - self.startb
512 if self.startb:
513 self.lenb += 1
514 hunki = 1
515 for x in xrange(self.lenb):
516 l = lr.readline()
517 if l.startswith('\ '):
518 s = self.b[-1][:-1]
519 self.b[-1] = s
520 self.hunk[hunki-1] = s
521 continue
522 if not l:
523 lr.push(l)
524 break
525 s = l[2:]
526 if l.startswith('+ ') or l.startswith('! '):
527 u = '+' + s
528 elif l.startswith(' '):
529 u = ' ' + s
530 elif len(self.b) == 0:
531 # this can happen when the hunk does not add any lines
532 lr.push(l)
533 break
534 else:
535 raise PatchError("bad hunk #%d old text line %d" % (self.number, x))
536 self.b.append(s)
537 while True:
538 if hunki >= len(self.hunk):
539 h = ""
540 else:
541 h = self.hunk[hunki]
542 hunki += 1
543 if h == u:
544 break
545 elif h.startswith('-'):
546 continue
547 else:
548 self.hunk.insert(hunki-1, u)
549 break
550
551 if not self.a:
552 # this happens when lines were only added to the hunk
553 for x in self.hunk:
554 if x.startswith('-') or x.startswith(' '):
555 self.a.append(x)
556 if not self.b:
557 # this happens when lines were only deleted from the hunk
558 for x in self.hunk:
559 if x.startswith('+') or x.startswith(' '):
560 self.b.append(x[1:])
561 # @@ -start,len +start,len @@
562 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
563 self.startb, self.lenb)
564 self.hunk[0] = self.desc
565
566 def reverse(self):
567 origlena = self.lena
568 origstarta = self.starta
569 self.lena = self.lenb
570 self.starta = self.startb
571 self.lenb = origlena
572 self.startb = origstarta
573 self.a = []
574 self.b = []
575 # self.hunk[0] is the @@ description
576 for x in xrange(1, len(self.hunk)):
577 o = self.hunk[x]
578 if o.startswith('-'):
579 n = '+' + o[1:]
580 self.b.append(o[1:])
581 elif o.startswith('+'):
582 n = '-' + o[1:]
583 self.a.append(n)
584 else:
585 n = o
586 self.b.append(o[1:])
587 self.a.append(o)
588 self.hunk[x] = o
589
590 def fix_newline(self):
591 diffhelpers.fix_newline(self.hunk, self.a, self.b)
592
593 def complete(self):
594 return len(self.a) == self.lena and len(self.b) == self.lenb
595
596 def createfile(self):
597 return self.starta == 0 and self.lena == 0
598
599 def rmfile(self):
600 return self.startb == 0 and self.lenb == 0
601
602 def fuzzit(self, l, fuzz, toponly):
603 # this removes context lines from the top and bottom of list 'l'. It
604 # checks the hunk to make sure only context lines are removed, and then
605 # returns a new shortened list of lines.
606 fuzz = min(fuzz, len(l)-1)
607 if fuzz:
608 top = 0
609 bot = 0
610 hlen = len(self.hunk)
611 for x in xrange(hlen-1):
612 # the hunk starts with the @@ line, so use x+1
613 if self.hunk[x+1][0] == ' ':
614 top += 1
615 else:
616 break
617 if not toponly:
618 for x in xrange(hlen-1):
619 if self.hunk[hlen-bot-1][0] == ' ':
620 bot += 1
621 else:
622 break
623
624 # top and bot now count context in the hunk
625 # adjust them if either one is short
626 context = max(top, bot, 3)
627 if bot < context:
628 bot = max(0, fuzz - (context - bot))
629 else:
630 bot = min(fuzz, bot)
631 if top < context:
632 top = max(0, fuzz - (context - top))
633 else:
634 top = min(fuzz, top)
635
636 return l[top:len(l)-bot]
637 return l
638
639 def old(self, fuzz=0, toponly=False):
640 return self.fuzzit(self.a, fuzz, toponly)
641
642 def newctrl(self):
643 res = []
644 for x in self.hunk:
645 c = x[0]
646 if c == ' ' or c == '+':
647 res.append(x)
648 return res
649
650 def new(self, fuzz=0, toponly=False):
651 return self.fuzzit(self.b, fuzz, toponly)
652
653 class binhunk:
654 'A binary patch file. Only understands literals so far.'
655 def __init__(self, gitpatch):
656 self.gitpatch = gitpatch
657 self.text = None
658 self.hunk = ['GIT binary patch\n']
659
660 def createfile(self):
661 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
662
663 def rmfile(self):
664 return self.gitpatch.op == 'DELETE'
665
666 def complete(self):
667 return self.text is not None
668
669 def new(self):
670 return [self.text]
671
672 def extract(self, fp):
673 line = fp.readline()
674 self.hunk.append(line)
215 while line and not line.startswith('literal '): 675 while line and not line.startswith('literal '):
216 line = readline() 676 line = fp.readline()
677 self.hunk.append(line)
217 if not line: 678 if not line:
218 return None, i[0] 679 raise PatchError('could not extract binary patch')
219 size = int(line[8:]) 680 size = int(line[8:].rstrip())
220 dec = [] 681 dec = []
221 line = readline() 682 line = fp.readline()
222 while line: 683 self.hunk.append(line)
684 while len(line) > 1:
223 l = line[0] 685 l = line[0]
224 if l <= 'Z' and l >= 'A': 686 if l <= 'Z' and l >= 'A':
225 l = ord(l) - ord('A') + 1 687 l = ord(l) - ord('A') + 1
226 else: 688 else:
227 l = ord(l) - ord('a') + 27 689 l = ord(l) - ord('a') + 27
228 dec.append(base85.b85decode(line[1:])[:l]) 690 dec.append(base85.b85decode(line[1:-1])[:l])
229 line = readline() 691 line = fp.readline()
692 self.hunk.append(line)
230 text = zlib.decompress(''.join(dec)) 693 text = zlib.decompress(''.join(dec))
231 if len(text) != size: 694 if len(text) != size:
232 raise util.Abort(_('binary patch is %d bytes, not %d') % 695 raise PatchError('binary patch is %d bytes, not %d' %
233 (len(text), size)) 696 len(text), size)
234 return text, i[0] 697 self.text = text
235 698
236 pf = file(patchname) 699 def parsefilename(str):
237 pfline = 1 700 # --- filename \t|space stuff
238 701 s = str[4:]
239 fd, patchname = tempfile.mkstemp(prefix='hg-patch-') 702 i = s.find('\t')
240 tmpfp = os.fdopen(fd, 'w') 703 if i < 0:
241 704 i = s.find(' ')
242 try: 705 if i < 0:
243 for i in xrange(len(gitpatches)): 706 return s
244 p = gitpatches[i] 707 return s[:i]
245 if not p.copymod and not p.binary: 708
709 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
710 def pathstrip(path, count=1):
711 pathlen = len(path)
712 i = 0
713 if count == 0:
714 return path.rstrip()
715 while count > 0:
716 i = path.find(os.sep, i)
717 if i == -1:
718 raise PatchError("Unable to strip away %d dirs from %s" %
719 (count, path))
720 i += 1
721 # consume '//' in the path
722 while i < pathlen - 1 and path[i] == os.sep:
723 i += 1
724 count -= 1
725 return path[i:].rstrip()
726
727 nulla = afile_orig == "/dev/null"
728 nullb = bfile_orig == "/dev/null"
729 afile = pathstrip(afile_orig, strip)
730 gooda = os.path.exists(afile) and not nulla
731 bfile = pathstrip(bfile_orig, strip)
732 if afile == bfile:
733 goodb = gooda
734 else:
735 goodb = os.path.exists(bfile) and not nullb
736 createfunc = hunk.createfile
737 if reverse:
738 createfunc = hunk.rmfile
739 if not goodb and not gooda and not createfunc():
740 raise PatchError(_("Unable to find %s or %s for patching\n" %
741 (afile, bfile)))
742 if gooda and goodb:
743 fname = bfile
744 if afile in bfile:
745 fname = afile
746 elif gooda:
747 fname = afile
748 elif not nullb:
749 fname = bfile
750 if afile in bfile:
751 fname = afile
752 elif not nulla:
753 fname = afile
754 return fname
755
756 class linereader:
757 # simple class to allow pushing lines back into the input stream
758 def __init__(self, fp):
759 self.fp = fp
760 self.buf = []
761
762 def push(self, line):
763 self.buf.append(line)
764
765 def readline(self):
766 if self.buf:
767 l = self.buf[0]
768 del self.buf[0]
769 return l
770 return self.fp.readline()
771
772 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
773 rejmerge=None, updatedir=None):
774 """reads a patch from fp and tries to apply it. The dict 'changed' is
775 filled in with all of the filenames changed by the patch. Returns 0
776 for a clean patch, -1 if any rejects were found and 1 if there was
777 any fuzz."""
778
779 def scangitpatch(fp, firstline, cwd=None):
780 '''git patches can modify a file, then copy that file to
781 a new file, but expect the source to be the unmodified form.
782 So we scan the patch looking for that case so we can do
783 the copies ahead of time.'''
784
785 pos = 0
786 try:
787 pos = fp.tell()
788 except IOError:
789 fp = cStringIO.StringIO(fp.read())
790
791 (dopatch, gitpatches) = readgitpatch(fp, firstline)
792 for gp in gitpatches:
793 if gp.copymod:
794 copyfile(gp.oldpath, gp.path, basedir=cwd)
795
796 fp.seek(pos)
797
798 return fp, dopatch, gitpatches
799
800 current_hunk = None
801 current_file = None
802 afile = ""
803 bfile = ""
804 state = None
805 hunknum = 0
806 rejects = 0
807
808 git = False
809 gitre = re.compile('diff --git (a/.*) (b/.*)')
810
811 # our states
812 BFILE = 1
813 err = 0
814 context = None
815 lr = linereader(fp)
816 dopatch = True
817 gitworkdone = False
818
819 while True:
820 newfile = False
821 x = lr.readline()
822 if not x:
823 break
824 if current_hunk:
825 if x.startswith('\ '):
826 current_hunk.fix_newline()
827 ret = current_file.apply(current_hunk, reverse)
828 if ret > 0:
829 err = 1
830 current_hunk = None
831 gitworkdone = False
832 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
833 ((context or context == None) and x.startswith('***************')))):
834 try:
835 if context == None and x.startswith('***************'):
836 context = True
837 current_hunk = hunk(x, hunknum + 1, lr, context)
838 except PatchError:
839 current_hunk = None
246 continue 840 continue
247 841 hunknum += 1
248 # rewrite patch hunk 842 if not current_file:
249 while pfline < p.lineno: 843 if sourcefile:
250 tmpfp.write(pf.readline()) 844 current_file = patchfile(ui, sourcefile)
251 pfline += 1 845 else:
252 846 current_file = selectfile(afile, bfile, current_hunk,
253 if p.binary: 847 strip, reverse)
254 text, delta = extractbin(pf) 848 current_file = patchfile(ui, current_file)
255 if not text: 849 changed.setdefault(current_file.fname, (None, None))
256 raise util.Abort(_('binary patch extraction failed')) 850 elif state == BFILE and x.startswith('GIT binary patch'):
257 pfline += delta 851 current_hunk = binhunk(changed[bfile[2:]][1])
258 if not cwd: 852 if not current_file:
259 cwd = os.getcwd() 853 if sourcefile:
260 absdst = os.path.join(cwd, p.path) 854 current_file = patchfile(ui, sourcefile)
261 basedir = os.path.dirname(absdst) 855 else:
262 if not os.path.isdir(basedir): 856 current_file = selectfile(afile, bfile, current_hunk,
263 os.makedirs(basedir) 857 strip, reverse)
264 out = file(absdst, 'wb') 858 current_file = patchfile(ui, current_file)
265 out.write(text) 859 hunknum += 1
266 out.close() 860 current_hunk.extract(fp)
267 elif p.copymod: 861 elif x.startswith('diff --git'):
268 copyfile(p.oldpath, p.path, basedir=cwd) 862 # check for git diff, scanning the whole patch file if needed
269 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path)) 863 m = gitre.match(x)
270 line = pf.readline() 864 if m:
271 pfline += 1 865 afile, bfile = m.group(1, 2)
272 while not line.startswith('--- a/'): 866 if not git:
273 tmpfp.write(line) 867 git = True
274 line = pf.readline() 868 fp, dopatch, gitpatches = scangitpatch(fp, x)
275 pfline += 1 869 for gp in gitpatches:
276 tmpfp.write('--- a/%s\n' % p.path) 870 changed[gp.path] = (gp.op, gp)
277 871 # else error?
278 line = pf.readline() 872 # copy/rename + modify should modify target, not source
279 while line: 873 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
280 tmpfp.write(line) 874 'RENAME'):
281 line = pf.readline() 875 afile = bfile
282 except: 876 gitworkdone = True
283 tmpfp.close() 877 newfile = True
284 os.unlink(patchname) 878 elif x.startswith('---'):
285 raise 879 # check for a unified diff
286 880 l2 = lr.readline()
287 tmpfp.close() 881 if not l2.startswith('+++'):
288 return patchname 882 lr.push(l2)
289 883 continue
290 def patch(patchname, ui, strip=1, cwd=None, files={}): 884 newfile = True
291 """apply the patch <patchname> to the working directory. 885 context = False
292 a list of patched files is returned""" 886 afile = parsefilename(x)
293 887 bfile = parsefilename(l2)
294 # helper function 888 elif x.startswith('***'):
295 def __patch(patchname): 889 # check for a context diff
296 """patch and updates the files and fuzz variables""" 890 l2 = lr.readline()
297 fuzz = False 891 if not l2.startswith('---'):
298 892 lr.push(l2)
299 args = [] 893 continue
300 patcher = ui.config('ui', 'patch') 894 l3 = lr.readline()
301 if not patcher: 895 lr.push(l3)
302 patcher = util.find_exe('gpatch') or util.find_exe('patch') 896 if not l3.startswith("***************"):
303 # Try to be smart only if patch call was not supplied 897 lr.push(l2)
304 if util.needbinarypatch(): 898 continue
305 args.append('--binary') 899 newfile = True
306 900 context = True
307 if not patcher: 901 afile = parsefilename(x)
308 raise util.Abort(_('no patch command found in hgrc or PATH')) 902 bfile = parsefilename(l2)
309 903
310 if cwd: 904 if newfile:
311 args.append('-d %s' % util.shellquote(cwd)) 905 if current_file:
312 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, 906 current_file.close()
313 util.shellquote(patchname))) 907 if rejmerge:
314 908 rejmerge(current_file)
315 for line in fp: 909 rejects += len(current_file.rej)
316 line = line.rstrip() 910 state = BFILE
317 ui.note(line + '\n') 911 current_file = None
318 if line.startswith('patching file '): 912 hunknum = 0
319 pf = util.parse_patch_output(line) 913 if current_hunk:
320 printed_file = False 914 if current_hunk.complete():
321 files.setdefault(pf, (None, None)) 915 ret = current_file.apply(current_hunk, reverse)
322 elif line.find('with fuzz') >= 0: 916 if ret > 0:
323 fuzz = True 917 err = 1
324 if not printed_file: 918 else:
325 ui.warn(pf + '\n') 919 fname = current_file and current_file.fname or None
326 printed_file = True 920 raise PatchError("malformed patch %s %s" % (fname,
327 ui.warn(line + '\n') 921 current_hunk.desc))
328 elif line.find('saving rejects to file') >= 0: 922 if current_file:
329 ui.warn(line + '\n') 923 current_file.close()
330 elif line.find('FAILED') >= 0: 924 if rejmerge:
331 if not printed_file: 925 rejmerge(current_file)
332 ui.warn(pf + '\n') 926 rejects += len(current_file.rej)
333 printed_file = True 927 if updatedir and git:
334 ui.warn(line + '\n') 928 updatedir(gitpatches)
335 code = fp.close() 929 if rejects:
336 if code: 930 return -1
337 raise util.Abort(_("patch command failed: %s") % 931 if hunknum == 0 and dopatch and not gitworkdone:
338 util.explain_exit(code)[0]) 932 raise PatchError("No valid hunks found")
339 return fuzz 933 return err
340
341 (dopatch, gitpatches) = readgitpatch(patchname)
342 for gp in gitpatches:
343 files[gp.path] = (gp.op, gp)
344
345 fuzz = False
346 if dopatch:
347 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
348 if filterpatch:
349 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
350 try:
351 if dopatch & GP_PATCH:
352 fuzz = __patch(patchname)
353 finally:
354 if filterpatch:
355 os.unlink(patchname)
356
357 return fuzz
358 934
359 def diffopts(ui, opts={}, untrusted=False): 935 def diffopts(ui, opts={}, untrusted=False):
360 def get(key, name=None): 936 def get(key, name=None):
361 return (opts.get(key) or 937 return (opts.get(key) or
362 ui.configbool('diff', name or key, None, untrusted=untrusted)) 938 ui.configbool('diff', name or key, None, untrusted=untrusted))