mercurial/patch.py
changeset 4897 4574925db5c0
parent 4771 e321f16f4eac
child 4898 bc905a6c0e76
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))