eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/patch.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 # patch.py - patch file parsing routines
       
     2 #
       
     3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
       
     4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
       
     5 #
       
     6 # This software may be used and distributed according to the terms of the
       
     7 # GNU General Public License version 2 or any later version.
       
     8 
       
     9 import cStringIO, email.Parser, os, re
       
    10 import tempfile, zlib
       
    11 
       
    12 from i18n import _
       
    13 from node import hex, nullid, short
       
    14 import base85, mdiff, util, diffhelpers, copies, encoding
       
    15 
       
    16 gitre = re.compile('diff --git a/(.*) b/(.*)')
       
    17 
       
    18 class PatchError(Exception):
       
    19     pass
       
    20 
       
    21 # helper functions
       
    22 
       
    23 def copyfile(src, dst, basedir):
       
    24     abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
       
    25     if os.path.lexists(absdst):
       
    26         raise util.Abort(_("cannot create %s: destination already exists") %
       
    27                          dst)
       
    28 
       
    29     dstdir = os.path.dirname(absdst)
       
    30     if dstdir and not os.path.isdir(dstdir):
       
    31         try:
       
    32             os.makedirs(dstdir)
       
    33         except IOError:
       
    34             raise util.Abort(
       
    35                 _("cannot create %s: unable to create destination directory")
       
    36                 % dst)
       
    37 
       
    38     util.copyfile(abssrc, absdst)
       
    39 
       
    40 # public functions
       
    41 
       
    42 def split(stream):
       
    43     '''return an iterator of individual patches from a stream'''
       
    44     def isheader(line, inheader):
       
    45         if inheader and line[0] in (' ', '\t'):
       
    46             # continuation
       
    47             return True
       
    48         if line[0] in (' ', '-', '+'):
       
    49             # diff line - don't check for header pattern in there
       
    50             return False
       
    51         l = line.split(': ', 1)
       
    52         return len(l) == 2 and ' ' not in l[0]
       
    53 
       
    54     def chunk(lines):
       
    55         return cStringIO.StringIO(''.join(lines))
       
    56 
       
    57     def hgsplit(stream, cur):
       
    58         inheader = True
       
    59 
       
    60         for line in stream:
       
    61             if not line.strip():
       
    62                 inheader = False
       
    63             if not inheader and line.startswith('# HG changeset patch'):
       
    64                 yield chunk(cur)
       
    65                 cur = []
       
    66                 inheader = True
       
    67 
       
    68             cur.append(line)
       
    69 
       
    70         if cur:
       
    71             yield chunk(cur)
       
    72 
       
    73     def mboxsplit(stream, cur):
       
    74         for line in stream:
       
    75             if line.startswith('From '):
       
    76                 for c in split(chunk(cur[1:])):
       
    77                     yield c
       
    78                 cur = []
       
    79 
       
    80             cur.append(line)
       
    81 
       
    82         if cur:
       
    83             for c in split(chunk(cur[1:])):
       
    84                 yield c
       
    85 
       
    86     def mimesplit(stream, cur):
       
    87         def msgfp(m):
       
    88             fp = cStringIO.StringIO()
       
    89             g = email.Generator.Generator(fp, mangle_from_=False)
       
    90             g.flatten(m)
       
    91             fp.seek(0)
       
    92             return fp
       
    93 
       
    94         for line in stream:
       
    95             cur.append(line)
       
    96         c = chunk(cur)
       
    97 
       
    98         m = email.Parser.Parser().parse(c)
       
    99         if not m.is_multipart():
       
   100             yield msgfp(m)
       
   101         else:
       
   102             ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
       
   103             for part in m.walk():
       
   104                 ct = part.get_content_type()
       
   105                 if ct not in ok_types:
       
   106                     continue
       
   107                 yield msgfp(part)
       
   108 
       
   109     def headersplit(stream, cur):
       
   110         inheader = False
       
   111 
       
   112         for line in stream:
       
   113             if not inheader and isheader(line, inheader):
       
   114                 yield chunk(cur)
       
   115                 cur = []
       
   116                 inheader = True
       
   117             if inheader and not isheader(line, inheader):
       
   118                 inheader = False
       
   119 
       
   120             cur.append(line)
       
   121 
       
   122         if cur:
       
   123             yield chunk(cur)
       
   124 
       
   125     def remainder(cur):
       
   126         yield chunk(cur)
       
   127 
       
   128     class fiter(object):
       
   129         def __init__(self, fp):
       
   130             self.fp = fp
       
   131 
       
   132         def __iter__(self):
       
   133             return self
       
   134 
       
   135         def next(self):
       
   136             l = self.fp.readline()
       
   137             if not l:
       
   138                 raise StopIteration
       
   139             return l
       
   140 
       
   141     inheader = False
       
   142     cur = []
       
   143 
       
   144     mimeheaders = ['content-type']
       
   145 
       
   146     if not hasattr(stream, 'next'):
       
   147         # http responses, for example, have readline but not next
       
   148         stream = fiter(stream)
       
   149 
       
   150     for line in stream:
       
   151         cur.append(line)
       
   152         if line.startswith('# HG changeset patch'):
       
   153             return hgsplit(stream, cur)
       
   154         elif line.startswith('From '):
       
   155             return mboxsplit(stream, cur)
       
   156         elif isheader(line, inheader):
       
   157             inheader = True
       
   158             if line.split(':', 1)[0].lower() in mimeheaders:
       
   159                 # let email parser handle this
       
   160                 return mimesplit(stream, cur)
       
   161         elif line.startswith('--- ') and inheader:
       
   162             # No evil headers seen by diff start, split by hand
       
   163             return headersplit(stream, cur)
       
   164         # Not enough info, keep reading
       
   165 
       
   166     # if we are here, we have a very plain patch
       
   167     return remainder(cur)
       
   168 
       
   169 def extract(ui, fileobj):
       
   170     '''extract patch from data read from fileobj.
       
   171 
       
   172     patch can be a normal patch or contained in an email message.
       
   173 
       
   174     return tuple (filename, message, user, date, branch, node, p1, p2).
       
   175     Any item in the returned tuple can be None. If filename is None,
       
   176     fileobj did not contain a patch. Caller must unlink filename when done.'''
       
   177 
       
   178     # attempt to detect the start of a patch
       
   179     # (this heuristic is borrowed from quilt)
       
   180     diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
       
   181                         r'retrieving revision [0-9]+(\.[0-9]+)*$|'
       
   182                         r'---[ \t].*?^\+\+\+[ \t]|'
       
   183                         r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
       
   184 
       
   185     fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
       
   186     tmpfp = os.fdopen(fd, 'w')
       
   187     try:
       
   188         msg = email.Parser.Parser().parse(fileobj)
       
   189 
       
   190         subject = msg['Subject']
       
   191         user = msg['From']
       
   192         if not subject and not user:
       
   193             # Not an email, restore parsed headers if any
       
   194             subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
       
   195 
       
   196         gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
       
   197         # should try to parse msg['Date']
       
   198         date = None
       
   199         nodeid = None
       
   200         branch = None
       
   201         parents = []
       
   202 
       
   203         if subject:
       
   204             if subject.startswith('[PATCH'):
       
   205                 pend = subject.find(']')
       
   206                 if pend >= 0:
       
   207                     subject = subject[pend + 1:].lstrip()
       
   208             subject = subject.replace('\n\t', ' ')
       
   209             ui.debug('Subject: %s\n' % subject)
       
   210         if user:
       
   211             ui.debug('From: %s\n' % user)
       
   212         diffs_seen = 0
       
   213         ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
       
   214         message = ''
       
   215         for part in msg.walk():
       
   216             content_type = part.get_content_type()
       
   217             ui.debug('Content-Type: %s\n' % content_type)
       
   218             if content_type not in ok_types:
       
   219                 continue
       
   220             payload = part.get_payload(decode=True)
       
   221             m = diffre.search(payload)
       
   222             if m:
       
   223                 hgpatch = False
       
   224                 hgpatchheader = False
       
   225                 ignoretext = False
       
   226 
       
   227                 ui.debug('found patch at byte %d\n' % m.start(0))
       
   228                 diffs_seen += 1
       
   229                 cfp = cStringIO.StringIO()
       
   230                 for line in payload[:m.start(0)].splitlines():
       
   231                     if line.startswith('# HG changeset patch') and not hgpatch:
       
   232                         ui.debug('patch generated by hg export\n')
       
   233                         hgpatch = True
       
   234                         hgpatchheader = True
       
   235                         # drop earlier commit message content
       
   236                         cfp.seek(0)
       
   237                         cfp.truncate()
       
   238                         subject = None
       
   239                     elif hgpatchheader:
       
   240                         if line.startswith('# User '):
       
   241                             user = line[7:]
       
   242                             ui.debug('From: %s\n' % user)
       
   243                         elif line.startswith("# Date "):
       
   244                             date = line[7:]
       
   245                         elif line.startswith("# Branch "):
       
   246                             branch = line[9:]
       
   247                         elif line.startswith("# Node ID "):
       
   248                             nodeid = line[10:]
       
   249                         elif line.startswith("# Parent "):
       
   250                             parents.append(line[10:])
       
   251                         elif not line.startswith("# "):
       
   252                             hgpatchheader = False
       
   253                     elif line == '---' and gitsendmail:
       
   254                         ignoretext = True
       
   255                     if not hgpatchheader and not ignoretext:
       
   256                         cfp.write(line)
       
   257                         cfp.write('\n')
       
   258                 message = cfp.getvalue()
       
   259                 if tmpfp:
       
   260                     tmpfp.write(payload)
       
   261                     if not payload.endswith('\n'):
       
   262                         tmpfp.write('\n')
       
   263             elif not diffs_seen and message and content_type == 'text/plain':
       
   264                 message += '\n' + payload
       
   265     except:
       
   266         tmpfp.close()
       
   267         os.unlink(tmpname)
       
   268         raise
       
   269 
       
   270     if subject and not message.startswith(subject):
       
   271         message = '%s\n%s' % (subject, message)
       
   272     tmpfp.close()
       
   273     if not diffs_seen:
       
   274         os.unlink(tmpname)
       
   275         return None, message, user, date, branch, None, None, None
       
   276     p1 = parents and parents.pop(0) or None
       
   277     p2 = parents and parents.pop(0) or None
       
   278     return tmpname, message, user, date, branch, nodeid, p1, p2
       
   279 
       
   280 class patchmeta(object):
       
   281     """Patched file metadata
       
   282 
       
   283     'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
       
   284     or COPY.  'path' is patched file path. 'oldpath' is set to the
       
   285     origin file when 'op' is either COPY or RENAME, None otherwise. If
       
   286     file mode is changed, 'mode' is a tuple (islink, isexec) where
       
   287     'islink' is True if the file is a symlink and 'isexec' is True if
       
   288     the file is executable. Otherwise, 'mode' is None.
       
   289     """
       
   290     def __init__(self, path):
       
   291         self.path = path
       
   292         self.oldpath = None
       
   293         self.mode = None
       
   294         self.op = 'MODIFY'
       
   295         self.binary = False
       
   296 
       
   297     def setmode(self, mode):
       
   298         islink = mode & 020000
       
   299         isexec = mode & 0100
       
   300         self.mode = (islink, isexec)
       
   301 
       
   302     def __repr__(self):
       
   303         return "<patchmeta %s %r>" % (self.op, self.path)
       
   304 
       
   305 def readgitpatch(lr):
       
   306     """extract git-style metadata about patches from <patchname>"""
       
   307 
       
   308     # Filter patch for git information
       
   309     gp = None
       
   310     gitpatches = []
       
   311     for line in lr:
       
   312         line = line.rstrip(' \r\n')
       
   313         if line.startswith('diff --git'):
       
   314             m = gitre.match(line)
       
   315             if m:
       
   316                 if gp:
       
   317                     gitpatches.append(gp)
       
   318                 dst = m.group(2)
       
   319                 gp = patchmeta(dst)
       
   320         elif gp:
       
   321             if line.startswith('--- '):
       
   322                 gitpatches.append(gp)
       
   323                 gp = None
       
   324                 continue
       
   325             if line.startswith('rename from '):
       
   326                 gp.op = 'RENAME'
       
   327                 gp.oldpath = line[12:]
       
   328             elif line.startswith('rename to '):
       
   329                 gp.path = line[10:]
       
   330             elif line.startswith('copy from '):
       
   331                 gp.op = 'COPY'
       
   332                 gp.oldpath = line[10:]
       
   333             elif line.startswith('copy to '):
       
   334                 gp.path = line[8:]
       
   335             elif line.startswith('deleted file'):
       
   336                 gp.op = 'DELETE'
       
   337             elif line.startswith('new file mode '):
       
   338                 gp.op = 'ADD'
       
   339                 gp.setmode(int(line[-6:], 8))
       
   340             elif line.startswith('new mode '):
       
   341                 gp.setmode(int(line[-6:], 8))
       
   342             elif line.startswith('GIT binary patch'):
       
   343                 gp.binary = True
       
   344     if gp:
       
   345         gitpatches.append(gp)
       
   346 
       
   347     return gitpatches
       
   348 
       
   349 class linereader(object):
       
   350     # simple class to allow pushing lines back into the input stream
       
   351     def __init__(self, fp, textmode=False):
       
   352         self.fp = fp
       
   353         self.buf = []
       
   354         self.textmode = textmode
       
   355         self.eol = None
       
   356 
       
   357     def push(self, line):
       
   358         if line is not None:
       
   359             self.buf.append(line)
       
   360 
       
   361     def readline(self):
       
   362         if self.buf:
       
   363             l = self.buf[0]
       
   364             del self.buf[0]
       
   365             return l
       
   366         l = self.fp.readline()
       
   367         if not self.eol:
       
   368             if l.endswith('\r\n'):
       
   369                 self.eol = '\r\n'
       
   370             elif l.endswith('\n'):
       
   371                 self.eol = '\n'
       
   372         if self.textmode and l.endswith('\r\n'):
       
   373             l = l[:-2] + '\n'
       
   374         return l
       
   375 
       
   376     def __iter__(self):
       
   377         while 1:
       
   378             l = self.readline()
       
   379             if not l:
       
   380                 break
       
   381             yield l
       
   382 
       
   383 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
       
   384 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
       
   385 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
       
   386 eolmodes = ['strict', 'crlf', 'lf', 'auto']
       
   387 
       
   388 class patchfile(object):
       
   389     def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
       
   390         self.fname = fname
       
   391         self.eolmode = eolmode
       
   392         self.eol = None
       
   393         self.opener = opener
       
   394         self.ui = ui
       
   395         self.lines = []
       
   396         self.exists = False
       
   397         self.missing = missing
       
   398         if not missing:
       
   399             try:
       
   400                 self.lines = self.readlines(fname)
       
   401                 self.exists = True
       
   402             except IOError:
       
   403                 pass
       
   404         else:
       
   405             self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
       
   406 
       
   407         self.hash = {}
       
   408         self.dirty = 0
       
   409         self.offset = 0
       
   410         self.skew = 0
       
   411         self.rej = []
       
   412         self.fileprinted = False
       
   413         self.printfile(False)
       
   414         self.hunks = 0
       
   415 
       
   416     def readlines(self, fname):
       
   417         if os.path.islink(fname):
       
   418             return [os.readlink(fname)]
       
   419         fp = self.opener(fname, 'r')
       
   420         try:
       
   421             lr = linereader(fp, self.eolmode != 'strict')
       
   422             lines = list(lr)
       
   423             self.eol = lr.eol
       
   424             return lines
       
   425         finally:
       
   426             fp.close()
       
   427 
       
   428     def writelines(self, fname, lines):
       
   429         # Ensure supplied data ends in fname, being a regular file or
       
   430         # a symlink. cmdutil.updatedir will -too magically- take care
       
   431         # of setting it to the proper type afterwards.
       
   432         islink = os.path.islink(fname)
       
   433         if islink:
       
   434             fp = cStringIO.StringIO()
       
   435         else:
       
   436             fp = self.opener(fname, 'w')
       
   437         try:
       
   438             if self.eolmode == 'auto':
       
   439                 eol = self.eol
       
   440             elif self.eolmode == 'crlf':
       
   441                 eol = '\r\n'
       
   442             else:
       
   443                 eol = '\n'
       
   444 
       
   445             if self.eolmode != 'strict' and eol and eol != '\n':
       
   446                 for l in lines:
       
   447                     if l and l[-1] == '\n':
       
   448                         l = l[:-1] + eol
       
   449                     fp.write(l)
       
   450             else:
       
   451                 fp.writelines(lines)
       
   452             if islink:
       
   453                 self.opener.symlink(fp.getvalue(), fname)
       
   454         finally:
       
   455             fp.close()
       
   456 
       
   457     def unlink(self, fname):
       
   458         os.unlink(fname)
       
   459 
       
   460     def printfile(self, warn):
       
   461         if self.fileprinted:
       
   462             return
       
   463         if warn or self.ui.verbose:
       
   464             self.fileprinted = True
       
   465         s = _("patching file %s\n") % self.fname
       
   466         if warn:
       
   467             self.ui.warn(s)
       
   468         else:
       
   469             self.ui.note(s)
       
   470 
       
   471 
       
   472     def findlines(self, l, linenum):
       
   473         # looks through the hash and finds candidate lines.  The
       
   474         # result is a list of line numbers sorted based on distance
       
   475         # from linenum
       
   476 
       
   477         cand = self.hash.get(l, [])
       
   478         if len(cand) > 1:
       
   479             # resort our list of potentials forward then back.
       
   480             cand.sort(key=lambda x: abs(x - linenum))
       
   481         return cand
       
   482 
       
   483     def hashlines(self):
       
   484         self.hash = {}
       
   485         for x, s in enumerate(self.lines):
       
   486             self.hash.setdefault(s, []).append(x)
       
   487 
       
   488     def makerejlines(self, fname):
       
   489         base = os.path.basename(fname)
       
   490         yield "--- %s\n+++ %s\n" % (base, base)
       
   491         for x in self.rej:
       
   492             for l in x.hunk:
       
   493                 yield l
       
   494                 if l[-1] != '\n':
       
   495                     yield "\n\ No newline at end of file\n"
       
   496 
       
   497     def write_rej(self):
       
   498         # our rejects are a little different from patch(1).  This always
       
   499         # creates rejects in the same form as the original patch.  A file
       
   500         # header is inserted so that you can run the reject through patch again
       
   501         # without having to type the filename.
       
   502 
       
   503         if not self.rej:
       
   504             return
       
   505 
       
   506         fname = self.fname + ".rej"
       
   507         self.ui.warn(
       
   508             _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
       
   509             (len(self.rej), self.hunks, fname))
       
   510 
       
   511         fp = self.opener(fname, 'w')
       
   512         fp.writelines(self.makerejlines(self.fname))
       
   513         fp.close()
       
   514 
       
   515     def apply(self, h):
       
   516         if not h.complete():
       
   517             raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
       
   518                             (h.number, h.desc, len(h.a), h.lena, len(h.b),
       
   519                             h.lenb))
       
   520 
       
   521         self.hunks += 1
       
   522 
       
   523         if self.missing:
       
   524             self.rej.append(h)
       
   525             return -1
       
   526 
       
   527         if self.exists and h.createfile():
       
   528             self.ui.warn(_("file %s already exists\n") % self.fname)
       
   529             self.rej.append(h)
       
   530             return -1
       
   531 
       
   532         if isinstance(h, binhunk):
       
   533             if h.rmfile():
       
   534                 self.unlink(self.fname)
       
   535             else:
       
   536                 self.lines[:] = h.new()
       
   537                 self.offset += len(h.new())
       
   538                 self.dirty = 1
       
   539             return 0
       
   540 
       
   541         horig = h
       
   542         if (self.eolmode in ('crlf', 'lf')
       
   543             or self.eolmode == 'auto' and self.eol):
       
   544             # If new eols are going to be normalized, then normalize
       
   545             # hunk data before patching. Otherwise, preserve input
       
   546             # line-endings.
       
   547             h = h.getnormalized()
       
   548 
       
   549         # fast case first, no offsets, no fuzz
       
   550         old = h.old()
       
   551         # patch starts counting at 1 unless we are adding the file
       
   552         if h.starta == 0:
       
   553             start = 0
       
   554         else:
       
   555             start = h.starta + self.offset - 1
       
   556         orig_start = start
       
   557         # if there's skew we want to emit the "(offset %d lines)" even
       
   558         # when the hunk cleanly applies at start + skew, so skip the
       
   559         # fast case code
       
   560         if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
       
   561             if h.rmfile():
       
   562                 self.unlink(self.fname)
       
   563             else:
       
   564                 self.lines[start : start + h.lena] = h.new()
       
   565                 self.offset += h.lenb - h.lena
       
   566                 self.dirty = 1
       
   567             return 0
       
   568 
       
   569         # ok, we couldn't match the hunk.  Lets look for offsets and fuzz it
       
   570         self.hashlines()
       
   571         if h.hunk[-1][0] != ' ':
       
   572             # if the hunk tried to put something at the bottom of the file
       
   573             # override the start line and use eof here
       
   574             search_start = len(self.lines)
       
   575         else:
       
   576             search_start = orig_start + self.skew
       
   577 
       
   578         for fuzzlen in xrange(3):
       
   579             for toponly in [True, False]:
       
   580                 old = h.old(fuzzlen, toponly)
       
   581 
       
   582                 cand = self.findlines(old[0][1:], search_start)
       
   583                 for l in cand:
       
   584                     if diffhelpers.testhunk(old, self.lines, l) == 0:
       
   585                         newlines = h.new(fuzzlen, toponly)
       
   586                         self.lines[l : l + len(old)] = newlines
       
   587                         self.offset += len(newlines) - len(old)
       
   588                         self.skew = l - orig_start
       
   589                         self.dirty = 1
       
   590                         offset = l - orig_start - fuzzlen
       
   591                         if fuzzlen:
       
   592                             msg = _("Hunk #%d succeeded at %d "
       
   593                                     "with fuzz %d "
       
   594                                     "(offset %d lines).\n")
       
   595                             self.printfile(True)
       
   596                             self.ui.warn(msg %
       
   597                                 (h.number, l + 1, fuzzlen, offset))
       
   598                         else:
       
   599                             msg = _("Hunk #%d succeeded at %d "
       
   600                                     "(offset %d lines).\n")
       
   601                             self.ui.note(msg % (h.number, l + 1, offset))
       
   602                         return fuzzlen
       
   603         self.printfile(True)
       
   604         self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
       
   605         self.rej.append(horig)
       
   606         return -1
       
   607 
       
   608 class hunk(object):
       
   609     def __init__(self, desc, num, lr, context, create=False, remove=False):
       
   610         self.number = num
       
   611         self.desc = desc
       
   612         self.hunk = [desc]
       
   613         self.a = []
       
   614         self.b = []
       
   615         self.starta = self.lena = None
       
   616         self.startb = self.lenb = None
       
   617         if lr is not None:
       
   618             if context:
       
   619                 self.read_context_hunk(lr)
       
   620             else:
       
   621                 self.read_unified_hunk(lr)
       
   622         self.create = create
       
   623         self.remove = remove and not create
       
   624 
       
   625     def getnormalized(self):
       
   626         """Return a copy with line endings normalized to LF."""
       
   627 
       
   628         def normalize(lines):
       
   629             nlines = []
       
   630             for line in lines:
       
   631                 if line.endswith('\r\n'):
       
   632                     line = line[:-2] + '\n'
       
   633                 nlines.append(line)
       
   634             return nlines
       
   635 
       
   636         # Dummy object, it is rebuilt manually
       
   637         nh = hunk(self.desc, self.number, None, None, False, False)
       
   638         nh.number = self.number
       
   639         nh.desc = self.desc
       
   640         nh.hunk = self.hunk
       
   641         nh.a = normalize(self.a)
       
   642         nh.b = normalize(self.b)
       
   643         nh.starta = self.starta
       
   644         nh.startb = self.startb
       
   645         nh.lena = self.lena
       
   646         nh.lenb = self.lenb
       
   647         nh.create = self.create
       
   648         nh.remove = self.remove
       
   649         return nh
       
   650 
       
   651     def read_unified_hunk(self, lr):
       
   652         m = unidesc.match(self.desc)
       
   653         if not m:
       
   654             raise PatchError(_("bad hunk #%d") % self.number)
       
   655         self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
       
   656         if self.lena is None:
       
   657             self.lena = 1
       
   658         else:
       
   659             self.lena = int(self.lena)
       
   660         if self.lenb is None:
       
   661             self.lenb = 1
       
   662         else:
       
   663             self.lenb = int(self.lenb)
       
   664         self.starta = int(self.starta)
       
   665         self.startb = int(self.startb)
       
   666         diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
       
   667         # if we hit eof before finishing out the hunk, the last line will
       
   668         # be zero length.  Lets try to fix it up.
       
   669         while len(self.hunk[-1]) == 0:
       
   670             del self.hunk[-1]
       
   671             del self.a[-1]
       
   672             del self.b[-1]
       
   673             self.lena -= 1
       
   674             self.lenb -= 1
       
   675 
       
   676     def read_context_hunk(self, lr):
       
   677         self.desc = lr.readline()
       
   678         m = contextdesc.match(self.desc)
       
   679         if not m:
       
   680             raise PatchError(_("bad hunk #%d") % self.number)
       
   681         foo, self.starta, foo2, aend, foo3 = m.groups()
       
   682         self.starta = int(self.starta)
       
   683         if aend is None:
       
   684             aend = self.starta
       
   685         self.lena = int(aend) - self.starta
       
   686         if self.starta:
       
   687             self.lena += 1
       
   688         for x in xrange(self.lena):
       
   689             l = lr.readline()
       
   690             if l.startswith('---'):
       
   691                 # lines addition, old block is empty
       
   692                 lr.push(l)
       
   693                 break
       
   694             s = l[2:]
       
   695             if l.startswith('- ') or l.startswith('! '):
       
   696                 u = '-' + s
       
   697             elif l.startswith('  '):
       
   698                 u = ' ' + s
       
   699             else:
       
   700                 raise PatchError(_("bad hunk #%d old text line %d") %
       
   701                                  (self.number, x))
       
   702             self.a.append(u)
       
   703             self.hunk.append(u)
       
   704 
       
   705         l = lr.readline()
       
   706         if l.startswith('\ '):
       
   707             s = self.a[-1][:-1]
       
   708             self.a[-1] = s
       
   709             self.hunk[-1] = s
       
   710             l = lr.readline()
       
   711         m = contextdesc.match(l)
       
   712         if not m:
       
   713             raise PatchError(_("bad hunk #%d") % self.number)
       
   714         foo, self.startb, foo2, bend, foo3 = m.groups()
       
   715         self.startb = int(self.startb)
       
   716         if bend is None:
       
   717             bend = self.startb
       
   718         self.lenb = int(bend) - self.startb
       
   719         if self.startb:
       
   720             self.lenb += 1
       
   721         hunki = 1
       
   722         for x in xrange(self.lenb):
       
   723             l = lr.readline()
       
   724             if l.startswith('\ '):
       
   725                 # XXX: the only way to hit this is with an invalid line range.
       
   726                 # The no-eol marker is not counted in the line range, but I
       
   727                 # guess there are diff(1) out there which behave differently.
       
   728                 s = self.b[-1][:-1]
       
   729                 self.b[-1] = s
       
   730                 self.hunk[hunki - 1] = s
       
   731                 continue
       
   732             if not l:
       
   733                 # line deletions, new block is empty and we hit EOF
       
   734                 lr.push(l)
       
   735                 break
       
   736             s = l[2:]
       
   737             if l.startswith('+ ') or l.startswith('! '):
       
   738                 u = '+' + s
       
   739             elif l.startswith('  '):
       
   740                 u = ' ' + s
       
   741             elif len(self.b) == 0:
       
   742                 # line deletions, new block is empty
       
   743                 lr.push(l)
       
   744                 break
       
   745             else:
       
   746                 raise PatchError(_("bad hunk #%d old text line %d") %
       
   747                                  (self.number, x))
       
   748             self.b.append(s)
       
   749             while True:
       
   750                 if hunki >= len(self.hunk):
       
   751                     h = ""
       
   752                 else:
       
   753                     h = self.hunk[hunki]
       
   754                 hunki += 1
       
   755                 if h == u:
       
   756                     break
       
   757                 elif h.startswith('-'):
       
   758                     continue
       
   759                 else:
       
   760                     self.hunk.insert(hunki - 1, u)
       
   761                     break
       
   762 
       
   763         if not self.a:
       
   764             # this happens when lines were only added to the hunk
       
   765             for x in self.hunk:
       
   766                 if x.startswith('-') or x.startswith(' '):
       
   767                     self.a.append(x)
       
   768         if not self.b:
       
   769             # this happens when lines were only deleted from the hunk
       
   770             for x in self.hunk:
       
   771                 if x.startswith('+') or x.startswith(' '):
       
   772                     self.b.append(x[1:])
       
   773         # @@ -start,len +start,len @@
       
   774         self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
       
   775                                              self.startb, self.lenb)
       
   776         self.hunk[0] = self.desc
       
   777 
       
   778     def fix_newline(self):
       
   779         diffhelpers.fix_newline(self.hunk, self.a, self.b)
       
   780 
       
   781     def complete(self):
       
   782         return len(self.a) == self.lena and len(self.b) == self.lenb
       
   783 
       
   784     def createfile(self):
       
   785         return self.starta == 0 and self.lena == 0 and self.create
       
   786 
       
   787     def rmfile(self):
       
   788         return self.startb == 0 and self.lenb == 0 and self.remove
       
   789 
       
   790     def fuzzit(self, l, fuzz, toponly):
       
   791         # this removes context lines from the top and bottom of list 'l'.  It
       
   792         # checks the hunk to make sure only context lines are removed, and then
       
   793         # returns a new shortened list of lines.
       
   794         fuzz = min(fuzz, len(l)-1)
       
   795         if fuzz:
       
   796             top = 0
       
   797             bot = 0
       
   798             hlen = len(self.hunk)
       
   799             for x in xrange(hlen - 1):
       
   800                 # the hunk starts with the @@ line, so use x+1
       
   801                 if self.hunk[x + 1][0] == ' ':
       
   802                     top += 1
       
   803                 else:
       
   804                     break
       
   805             if not toponly:
       
   806                 for x in xrange(hlen - 1):
       
   807                     if self.hunk[hlen - bot - 1][0] == ' ':
       
   808                         bot += 1
       
   809                     else:
       
   810                         break
       
   811 
       
   812             # top and bot now count context in the hunk
       
   813             # adjust them if either one is short
       
   814             context = max(top, bot, 3)
       
   815             if bot < context:
       
   816                 bot = max(0, fuzz - (context - bot))
       
   817             else:
       
   818                 bot = min(fuzz, bot)
       
   819             if top < context:
       
   820                 top = max(0, fuzz - (context - top))
       
   821             else:
       
   822                 top = min(fuzz, top)
       
   823 
       
   824             return l[top:len(l)-bot]
       
   825         return l
       
   826 
       
   827     def old(self, fuzz=0, toponly=False):
       
   828         return self.fuzzit(self.a, fuzz, toponly)
       
   829 
       
   830     def new(self, fuzz=0, toponly=False):
       
   831         return self.fuzzit(self.b, fuzz, toponly)
       
   832 
       
   833 class binhunk:
       
   834     'A binary patch file. Only understands literals so far.'
       
   835     def __init__(self, gitpatch):
       
   836         self.gitpatch = gitpatch
       
   837         self.text = None
       
   838         self.hunk = ['GIT binary patch\n']
       
   839 
       
   840     def createfile(self):
       
   841         return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
       
   842 
       
   843     def rmfile(self):
       
   844         return self.gitpatch.op == 'DELETE'
       
   845 
       
   846     def complete(self):
       
   847         return self.text is not None
       
   848 
       
   849     def new(self):
       
   850         return [self.text]
       
   851 
       
   852     def extract(self, lr):
       
   853         line = lr.readline()
       
   854         self.hunk.append(line)
       
   855         while line and not line.startswith('literal '):
       
   856             line = lr.readline()
       
   857             self.hunk.append(line)
       
   858         if not line:
       
   859             raise PatchError(_('could not extract binary patch'))
       
   860         size = int(line[8:].rstrip())
       
   861         dec = []
       
   862         line = lr.readline()
       
   863         self.hunk.append(line)
       
   864         while len(line) > 1:
       
   865             l = line[0]
       
   866             if l <= 'Z' and l >= 'A':
       
   867                 l = ord(l) - ord('A') + 1
       
   868             else:
       
   869                 l = ord(l) - ord('a') + 27
       
   870             dec.append(base85.b85decode(line[1:-1])[:l])
       
   871             line = lr.readline()
       
   872             self.hunk.append(line)
       
   873         text = zlib.decompress(''.join(dec))
       
   874         if len(text) != size:
       
   875             raise PatchError(_('binary patch is %d bytes, not %d') %
       
   876                              len(text), size)
       
   877         self.text = text
       
   878 
       
   879 def parsefilename(str):
       
   880     # --- filename \t|space stuff
       
   881     s = str[4:].rstrip('\r\n')
       
   882     i = s.find('\t')
       
   883     if i < 0:
       
   884         i = s.find(' ')
       
   885         if i < 0:
       
   886             return s
       
   887     return s[:i]
       
   888 
       
   889 def pathstrip(path, strip):
       
   890     pathlen = len(path)
       
   891     i = 0
       
   892     if strip == 0:
       
   893         return '', path.rstrip()
       
   894     count = strip
       
   895     while count > 0:
       
   896         i = path.find('/', i)
       
   897         if i == -1:
       
   898             raise PatchError(_("unable to strip away %d of %d dirs from %s") %
       
   899                              (count, strip, path))
       
   900         i += 1
       
   901         # consume '//' in the path
       
   902         while i < pathlen - 1 and path[i] == '/':
       
   903             i += 1
       
   904         count -= 1
       
   905     return path[:i].lstrip(), path[i:].rstrip()
       
   906 
       
   907 def selectfile(afile_orig, bfile_orig, hunk, strip):
       
   908     nulla = afile_orig == "/dev/null"
       
   909     nullb = bfile_orig == "/dev/null"
       
   910     abase, afile = pathstrip(afile_orig, strip)
       
   911     gooda = not nulla and os.path.lexists(afile)
       
   912     bbase, bfile = pathstrip(bfile_orig, strip)
       
   913     if afile == bfile:
       
   914         goodb = gooda
       
   915     else:
       
   916         goodb = not nullb and os.path.lexists(bfile)
       
   917     createfunc = hunk.createfile
       
   918     missing = not goodb and not gooda and not createfunc()
       
   919 
       
   920     # some diff programs apparently produce patches where the afile is
       
   921     # not /dev/null, but afile starts with bfile
       
   922     abasedir = afile[:afile.rfind('/') + 1]
       
   923     bbasedir = bfile[:bfile.rfind('/') + 1]
       
   924     if missing and abasedir == bbasedir and afile.startswith(bfile):
       
   925         # this isn't very pretty
       
   926         hunk.create = True
       
   927         if createfunc():
       
   928             missing = False
       
   929         else:
       
   930             hunk.create = False
       
   931 
       
   932     # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
       
   933     # diff is between a file and its backup. In this case, the original
       
   934     # file should be patched (see original mpatch code).
       
   935     isbackup = (abase == bbase and bfile.startswith(afile))
       
   936     fname = None
       
   937     if not missing:
       
   938         if gooda and goodb:
       
   939             fname = isbackup and afile or bfile
       
   940         elif gooda:
       
   941             fname = afile
       
   942 
       
   943     if not fname:
       
   944         if not nullb:
       
   945             fname = isbackup and afile or bfile
       
   946         elif not nulla:
       
   947             fname = afile
       
   948         else:
       
   949             raise PatchError(_("undefined source and destination files"))
       
   950 
       
   951     return fname, missing
       
   952 
       
   953 def scangitpatch(lr, firstline):
       
   954     """
       
   955     Git patches can emit:
       
   956     - rename a to b
       
   957     - change b
       
   958     - copy a to c
       
   959     - change c
       
   960 
       
   961     We cannot apply this sequence as-is, the renamed 'a' could not be
       
   962     found for it would have been renamed already. And we cannot copy
       
   963     from 'b' instead because 'b' would have been changed already. So
       
   964     we scan the git patch for copy and rename commands so we can
       
   965     perform the copies ahead of time.
       
   966     """
       
   967     pos = 0
       
   968     try:
       
   969         pos = lr.fp.tell()
       
   970         fp = lr.fp
       
   971     except IOError:
       
   972         fp = cStringIO.StringIO(lr.fp.read())
       
   973     gitlr = linereader(fp, lr.textmode)
       
   974     gitlr.push(firstline)
       
   975     gitpatches = readgitpatch(gitlr)
       
   976     fp.seek(pos)
       
   977     return gitpatches
       
   978 
       
   979 def iterhunks(ui, fp, sourcefile=None):
       
   980     """Read a patch and yield the following events:
       
   981     - ("file", afile, bfile, firsthunk): select a new target file.
       
   982     - ("hunk", hunk): a new hunk is ready to be applied, follows a
       
   983     "file" event.
       
   984     - ("git", gitchanges): current diff is in git format, gitchanges
       
   985     maps filenames to gitpatch records. Unique event.
       
   986     """
       
   987     changed = {}
       
   988     current_hunk = None
       
   989     afile = ""
       
   990     bfile = ""
       
   991     state = None
       
   992     hunknum = 0
       
   993     emitfile = False
       
   994     git = False
       
   995 
       
   996     # our states
       
   997     BFILE = 1
       
   998     context = None
       
   999     lr = linereader(fp)
       
  1000     # gitworkdone is True if a git operation (copy, rename, ...) was
       
  1001     # performed already for the current file. Useful when the file
       
  1002     # section may have no hunk.
       
  1003     gitworkdone = False
       
  1004 
       
  1005     while True:
       
  1006         newfile = newgitfile = False
       
  1007         x = lr.readline()
       
  1008         if not x:
       
  1009             break
       
  1010         if current_hunk:
       
  1011             if x.startswith('\ '):
       
  1012                 current_hunk.fix_newline()
       
  1013             yield 'hunk', current_hunk
       
  1014             current_hunk = None
       
  1015         if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
       
  1016             ((context is not False) and x.startswith('***************')))):
       
  1017             if context is None and x.startswith('***************'):
       
  1018                 context = True
       
  1019             gpatch = changed.get(bfile)
       
  1020             create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
       
  1021             remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
       
  1022             current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
       
  1023             hunknum += 1
       
  1024             if emitfile:
       
  1025                 emitfile = False
       
  1026                 yield 'file', (afile, bfile, current_hunk)
       
  1027         elif state == BFILE and x.startswith('GIT binary patch'):
       
  1028             current_hunk = binhunk(changed[bfile])
       
  1029             hunknum += 1
       
  1030             if emitfile:
       
  1031                 emitfile = False
       
  1032                 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
       
  1033             current_hunk.extract(lr)
       
  1034         elif x.startswith('diff --git'):
       
  1035             # check for git diff, scanning the whole patch file if needed
       
  1036             m = gitre.match(x)
       
  1037             gitworkdone = False
       
  1038             if m:
       
  1039                 afile, bfile = m.group(1, 2)
       
  1040                 if not git:
       
  1041                     git = True
       
  1042                     gitpatches = scangitpatch(lr, x)
       
  1043                     yield 'git', gitpatches
       
  1044                     for gp in gitpatches:
       
  1045                         changed[gp.path] = gp
       
  1046                 # else error?
       
  1047                 # copy/rename + modify should modify target, not source
       
  1048                 gp = changed.get(bfile)
       
  1049                 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
       
  1050                            or gp.mode):
       
  1051                     afile = bfile
       
  1052                     gitworkdone = True
       
  1053                 newgitfile = True
       
  1054         elif x.startswith('---'):
       
  1055             # check for a unified diff
       
  1056             l2 = lr.readline()
       
  1057             if not l2.startswith('+++'):
       
  1058                 lr.push(l2)
       
  1059                 continue
       
  1060             newfile = True
       
  1061             context = False
       
  1062             afile = parsefilename(x)
       
  1063             bfile = parsefilename(l2)
       
  1064         elif x.startswith('***'):
       
  1065             # check for a context diff
       
  1066             l2 = lr.readline()
       
  1067             if not l2.startswith('---'):
       
  1068                 lr.push(l2)
       
  1069                 continue
       
  1070             l3 = lr.readline()
       
  1071             lr.push(l3)
       
  1072             if not l3.startswith("***************"):
       
  1073                 lr.push(l2)
       
  1074                 continue
       
  1075             newfile = True
       
  1076             context = True
       
  1077             afile = parsefilename(x)
       
  1078             bfile = parsefilename(l2)
       
  1079 
       
  1080         if newfile:
       
  1081             gitworkdone = False
       
  1082 
       
  1083         if newgitfile or newfile:
       
  1084             emitfile = True
       
  1085             state = BFILE
       
  1086             hunknum = 0
       
  1087     if current_hunk:
       
  1088         if current_hunk.complete():
       
  1089             yield 'hunk', current_hunk
       
  1090         else:
       
  1091             raise PatchError(_("malformed patch %s %s") % (afile,
       
  1092                              current_hunk.desc))
       
  1093 
       
  1094 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
       
  1095     """Reads a patch from fp and tries to apply it.
       
  1096 
       
  1097     The dict 'changed' is filled in with all of the filenames changed
       
  1098     by the patch. Returns 0 for a clean patch, -1 if any rejects were
       
  1099     found and 1 if there was any fuzz.
       
  1100 
       
  1101     If 'eolmode' is 'strict', the patch content and patched file are
       
  1102     read in binary mode. Otherwise, line endings are ignored when
       
  1103     patching then normalized according to 'eolmode'.
       
  1104 
       
  1105     Callers probably want to call 'cmdutil.updatedir' after this to
       
  1106     apply certain categories of changes not done by this function.
       
  1107     """
       
  1108     return _applydiff(
       
  1109         ui, fp, patchfile, copyfile,
       
  1110         changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
       
  1111 
       
  1112 
       
  1113 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
       
  1114                sourcefile=None, eolmode='strict'):
       
  1115     rejects = 0
       
  1116     err = 0
       
  1117     current_file = None
       
  1118     cwd = os.getcwd()
       
  1119     opener = util.opener(cwd)
       
  1120 
       
  1121     def closefile():
       
  1122         if not current_file:
       
  1123             return 0
       
  1124         if current_file.dirty:
       
  1125             current_file.writelines(current_file.fname, current_file.lines)
       
  1126         current_file.write_rej()
       
  1127         return len(current_file.rej)
       
  1128 
       
  1129     for state, values in iterhunks(ui, fp, sourcefile):
       
  1130         if state == 'hunk':
       
  1131             if not current_file:
       
  1132                 continue
       
  1133             ret = current_file.apply(values)
       
  1134             if ret >= 0:
       
  1135                 changed.setdefault(current_file.fname, None)
       
  1136                 if ret > 0:
       
  1137                     err = 1
       
  1138         elif state == 'file':
       
  1139             rejects += closefile()
       
  1140             afile, bfile, first_hunk = values
       
  1141             try:
       
  1142                 if sourcefile:
       
  1143                     current_file = patcher(ui, sourcefile, opener,
       
  1144                                            eolmode=eolmode)
       
  1145                 else:
       
  1146                     current_file, missing = selectfile(afile, bfile,
       
  1147                                                        first_hunk, strip)
       
  1148                     current_file = patcher(ui, current_file, opener,
       
  1149                                            missing=missing, eolmode=eolmode)
       
  1150             except PatchError, err:
       
  1151                 ui.warn(str(err) + '\n')
       
  1152                 current_file = None
       
  1153                 rejects += 1
       
  1154                 continue
       
  1155         elif state == 'git':
       
  1156             for gp in values:
       
  1157                 gp.path = pathstrip(gp.path, strip - 1)[1]
       
  1158                 if gp.oldpath:
       
  1159                     gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
       
  1160                 # Binary patches really overwrite target files, copying them
       
  1161                 # will just make it fails with "target file exists"
       
  1162                 if gp.op in ('COPY', 'RENAME') and not gp.binary:
       
  1163                     copyfn(gp.oldpath, gp.path, cwd)
       
  1164                 changed[gp.path] = gp
       
  1165         else:
       
  1166             raise util.Abort(_('unsupported parser state: %s') % state)
       
  1167 
       
  1168     rejects += closefile()
       
  1169 
       
  1170     if rejects:
       
  1171         return -1
       
  1172     return err
       
  1173 
       
  1174 def externalpatch(patcher, patchname, ui, strip, cwd, files):
       
  1175     """use <patcher> to apply <patchname> to the working directory.
       
  1176     returns whether patch was applied with fuzz factor."""
       
  1177 
       
  1178     fuzz = False
       
  1179     args = []
       
  1180     if cwd:
       
  1181         args.append('-d %s' % util.shellquote(cwd))
       
  1182     fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
       
  1183                                        util.shellquote(patchname)))
       
  1184 
       
  1185     for line in fp:
       
  1186         line = line.rstrip()
       
  1187         ui.note(line + '\n')
       
  1188         if line.startswith('patching file '):
       
  1189             pf = util.parse_patch_output(line)
       
  1190             printed_file = False
       
  1191             files.setdefault(pf, None)
       
  1192         elif line.find('with fuzz') >= 0:
       
  1193             fuzz = True
       
  1194             if not printed_file:
       
  1195                 ui.warn(pf + '\n')
       
  1196                 printed_file = True
       
  1197             ui.warn(line + '\n')
       
  1198         elif line.find('saving rejects to file') >= 0:
       
  1199             ui.warn(line + '\n')
       
  1200         elif line.find('FAILED') >= 0:
       
  1201             if not printed_file:
       
  1202                 ui.warn(pf + '\n')
       
  1203                 printed_file = True
       
  1204             ui.warn(line + '\n')
       
  1205     code = fp.close()
       
  1206     if code:
       
  1207         raise PatchError(_("patch command failed: %s") %
       
  1208                          util.explain_exit(code)[0])
       
  1209     return fuzz
       
  1210 
       
  1211 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
       
  1212     """use builtin patch to apply <patchobj> to the working directory.
       
  1213     returns whether patch was applied with fuzz factor."""
       
  1214 
       
  1215     if files is None:
       
  1216         files = {}
       
  1217     if eolmode is None:
       
  1218         eolmode = ui.config('patch', 'eol', 'strict')
       
  1219     if eolmode.lower() not in eolmodes:
       
  1220         raise util.Abort(_('unsupported line endings type: %s') % eolmode)
       
  1221     eolmode = eolmode.lower()
       
  1222 
       
  1223     try:
       
  1224         fp = open(patchobj, 'rb')
       
  1225     except TypeError:
       
  1226         fp = patchobj
       
  1227     if cwd:
       
  1228         curdir = os.getcwd()
       
  1229         os.chdir(cwd)
       
  1230     try:
       
  1231         ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
       
  1232     finally:
       
  1233         if cwd:
       
  1234             os.chdir(curdir)
       
  1235         if fp != patchobj:
       
  1236             fp.close()
       
  1237     if ret < 0:
       
  1238         raise PatchError(_('patch failed to apply'))
       
  1239     return ret > 0
       
  1240 
       
  1241 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
       
  1242     """Apply <patchname> to the working directory.
       
  1243 
       
  1244     'eolmode' specifies how end of lines should be handled. It can be:
       
  1245     - 'strict': inputs are read in binary mode, EOLs are preserved
       
  1246     - 'crlf': EOLs are ignored when patching and reset to CRLF
       
  1247     - 'lf': EOLs are ignored when patching and reset to LF
       
  1248     - None: get it from user settings, default to 'strict'
       
  1249     'eolmode' is ignored when using an external patcher program.
       
  1250 
       
  1251     Returns whether patch was applied with fuzz factor.
       
  1252     """
       
  1253     patcher = ui.config('ui', 'patch')
       
  1254     if files is None:
       
  1255         files = {}
       
  1256     try:
       
  1257         if patcher:
       
  1258             return externalpatch(patcher, patchname, ui, strip, cwd, files)
       
  1259         return internalpatch(patchname, ui, strip, cwd, files, eolmode)
       
  1260     except PatchError, err:
       
  1261         raise util.Abort(str(err))
       
  1262 
       
  1263 def b85diff(to, tn):
       
  1264     '''print base85-encoded binary diff'''
       
  1265     def gitindex(text):
       
  1266         if not text:
       
  1267             return hex(nullid)
       
  1268         l = len(text)
       
  1269         s = util.sha1('blob %d\0' % l)
       
  1270         s.update(text)
       
  1271         return s.hexdigest()
       
  1272 
       
  1273     def fmtline(line):
       
  1274         l = len(line)
       
  1275         if l <= 26:
       
  1276             l = chr(ord('A') + l - 1)
       
  1277         else:
       
  1278             l = chr(l - 26 + ord('a') - 1)
       
  1279         return '%c%s\n' % (l, base85.b85encode(line, True))
       
  1280 
       
  1281     def chunk(text, csize=52):
       
  1282         l = len(text)
       
  1283         i = 0
       
  1284         while i < l:
       
  1285             yield text[i:i + csize]
       
  1286             i += csize
       
  1287 
       
  1288     tohash = gitindex(to)
       
  1289     tnhash = gitindex(tn)
       
  1290     if tohash == tnhash:
       
  1291         return ""
       
  1292 
       
  1293     # TODO: deltas
       
  1294     ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
       
  1295            (tohash, tnhash, len(tn))]
       
  1296     for l in chunk(zlib.compress(tn)):
       
  1297         ret.append(fmtline(l))
       
  1298     ret.append('\n')
       
  1299     return ''.join(ret)
       
  1300 
       
  1301 class GitDiffRequired(Exception):
       
  1302     pass
       
  1303 
       
  1304 def diffopts(ui, opts=None, untrusted=False):
       
  1305     def get(key, name=None, getter=ui.configbool):
       
  1306         return ((opts and opts.get(key)) or
       
  1307                 getter('diff', name or key, None, untrusted=untrusted))
       
  1308     return mdiff.diffopts(
       
  1309         text=opts and opts.get('text'),
       
  1310         git=get('git'),
       
  1311         nodates=get('nodates'),
       
  1312         showfunc=get('show_function', 'showfunc'),
       
  1313         ignorews=get('ignore_all_space', 'ignorews'),
       
  1314         ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
       
  1315         ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
       
  1316         context=get('unified', getter=ui.config))
       
  1317 
       
  1318 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
       
  1319          losedatafn=None, prefix=''):
       
  1320     '''yields diff of changes to files between two nodes, or node and
       
  1321     working directory.
       
  1322 
       
  1323     if node1 is None, use first dirstate parent instead.
       
  1324     if node2 is None, compare node1 with working directory.
       
  1325 
       
  1326     losedatafn(**kwarg) is a callable run when opts.upgrade=True and
       
  1327     every time some change cannot be represented with the current
       
  1328     patch format. Return False to upgrade to git patch format, True to
       
  1329     accept the loss or raise an exception to abort the diff. It is
       
  1330     called with the name of current file being diffed as 'fn'. If set
       
  1331     to None, patches will always be upgraded to git format when
       
  1332     necessary.
       
  1333 
       
  1334     prefix is a filename prefix that is prepended to all filenames on
       
  1335     display (used for subrepos).
       
  1336     '''
       
  1337 
       
  1338     if opts is None:
       
  1339         opts = mdiff.defaultopts
       
  1340 
       
  1341     if not node1 and not node2:
       
  1342         node1 = repo.dirstate.parents()[0]
       
  1343 
       
  1344     def lrugetfilectx():
       
  1345         cache = {}
       
  1346         order = []
       
  1347         def getfilectx(f, ctx):
       
  1348             fctx = ctx.filectx(f, filelog=cache.get(f))
       
  1349             if f not in cache:
       
  1350                 if len(cache) > 20:
       
  1351                     del cache[order.pop(0)]
       
  1352                 cache[f] = fctx.filelog()
       
  1353             else:
       
  1354                 order.remove(f)
       
  1355             order.append(f)
       
  1356             return fctx
       
  1357         return getfilectx
       
  1358     getfilectx = lrugetfilectx()
       
  1359 
       
  1360     ctx1 = repo[node1]
       
  1361     ctx2 = repo[node2]
       
  1362 
       
  1363     if not changes:
       
  1364         changes = repo.status(ctx1, ctx2, match=match)
       
  1365     modified, added, removed = changes[:3]
       
  1366 
       
  1367     if not modified and not added and not removed:
       
  1368         return []
       
  1369 
       
  1370     revs = None
       
  1371     if not repo.ui.quiet:
       
  1372         hexfunc = repo.ui.debugflag and hex or short
       
  1373         revs = [hexfunc(node) for node in [node1, node2] if node]
       
  1374 
       
  1375     copy = {}
       
  1376     if opts.git or opts.upgrade:
       
  1377         copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
       
  1378 
       
  1379     difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
       
  1380                  modified, added, removed, copy, getfilectx, opts, losedata, prefix)
       
  1381     if opts.upgrade and not opts.git:
       
  1382         try:
       
  1383             def losedata(fn):
       
  1384                 if not losedatafn or not losedatafn(fn=fn):
       
  1385                     raise GitDiffRequired()
       
  1386             # Buffer the whole output until we are sure it can be generated
       
  1387             return list(difffn(opts.copy(git=False), losedata))
       
  1388         except GitDiffRequired:
       
  1389             return difffn(opts.copy(git=True), None)
       
  1390     else:
       
  1391         return difffn(opts, None)
       
  1392 
       
  1393 def difflabel(func, *args, **kw):
       
  1394     '''yields 2-tuples of (output, label) based on the output of func()'''
       
  1395     prefixes = [('diff', 'diff.diffline'),
       
  1396                 ('copy', 'diff.extended'),
       
  1397                 ('rename', 'diff.extended'),
       
  1398                 ('old', 'diff.extended'),
       
  1399                 ('new', 'diff.extended'),
       
  1400                 ('deleted', 'diff.extended'),
       
  1401                 ('---', 'diff.file_a'),
       
  1402                 ('+++', 'diff.file_b'),
       
  1403                 ('@@', 'diff.hunk'),
       
  1404                 ('-', 'diff.deleted'),
       
  1405                 ('+', 'diff.inserted')]
       
  1406 
       
  1407     for chunk in func(*args, **kw):
       
  1408         lines = chunk.split('\n')
       
  1409         for i, line in enumerate(lines):
       
  1410             if i != 0:
       
  1411                 yield ('\n', '')
       
  1412             stripline = line
       
  1413             if line and line[0] in '+-':
       
  1414                 # highlight trailing whitespace, but only in changed lines
       
  1415                 stripline = line.rstrip()
       
  1416             for prefix, label in prefixes:
       
  1417                 if stripline.startswith(prefix):
       
  1418                     yield (stripline, label)
       
  1419                     break
       
  1420             else:
       
  1421                 yield (line, '')
       
  1422             if line != stripline:
       
  1423                 yield (line[len(stripline):], 'diff.trailingwhitespace')
       
  1424 
       
  1425 def diffui(*args, **kw):
       
  1426     '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
       
  1427     return difflabel(diff, *args, **kw)
       
  1428 
       
  1429 
       
  1430 def _addmodehdr(header, omode, nmode):
       
  1431     if omode != nmode:
       
  1432         header.append('old mode %s\n' % omode)
       
  1433         header.append('new mode %s\n' % nmode)
       
  1434 
       
  1435 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
       
  1436             copy, getfilectx, opts, losedatafn, prefix):
       
  1437 
       
  1438     def join(f):
       
  1439         return os.path.join(prefix, f)
       
  1440 
       
  1441     date1 = util.datestr(ctx1.date())
       
  1442     man1 = ctx1.manifest()
       
  1443 
       
  1444     gone = set()
       
  1445     gitmode = {'l': '120000', 'x': '100755', '': '100644'}
       
  1446 
       
  1447     copyto = dict([(v, k) for k, v in copy.items()])
       
  1448 
       
  1449     if opts.git:
       
  1450         revs = None
       
  1451 
       
  1452     for f in sorted(modified + added + removed):
       
  1453         to = None
       
  1454         tn = None
       
  1455         dodiff = True
       
  1456         header = []
       
  1457         if f in man1:
       
  1458             to = getfilectx(f, ctx1).data()
       
  1459         if f not in removed:
       
  1460             tn = getfilectx(f, ctx2).data()
       
  1461         a, b = f, f
       
  1462         if opts.git or losedatafn:
       
  1463             if f in added:
       
  1464                 mode = gitmode[ctx2.flags(f)]
       
  1465                 if f in copy or f in copyto:
       
  1466                     if opts.git:
       
  1467                         if f in copy:
       
  1468                             a = copy[f]
       
  1469                         else:
       
  1470                             a = copyto[f]
       
  1471                         omode = gitmode[man1.flags(a)]
       
  1472                         _addmodehdr(header, omode, mode)
       
  1473                         if a in removed and a not in gone:
       
  1474                             op = 'rename'
       
  1475                             gone.add(a)
       
  1476                         else:
       
  1477                             op = 'copy'
       
  1478                         header.append('%s from %s\n' % (op, join(a)))
       
  1479                         header.append('%s to %s\n' % (op, join(f)))
       
  1480                         to = getfilectx(a, ctx1).data()
       
  1481                     else:
       
  1482                         losedatafn(f)
       
  1483                 else:
       
  1484                     if opts.git:
       
  1485                         header.append('new file mode %s\n' % mode)
       
  1486                     elif ctx2.flags(f):
       
  1487                         losedatafn(f)
       
  1488                 # In theory, if tn was copied or renamed we should check
       
  1489                 # if the source is binary too but the copy record already
       
  1490                 # forces git mode.
       
  1491                 if util.binary(tn):
       
  1492                     if opts.git:
       
  1493                         dodiff = 'binary'
       
  1494                     else:
       
  1495                         losedatafn(f)
       
  1496                 if not opts.git and not tn:
       
  1497                     # regular diffs cannot represent new empty file
       
  1498                     losedatafn(f)
       
  1499             elif f in removed:
       
  1500                 if opts.git:
       
  1501                     # have we already reported a copy above?
       
  1502                     if ((f in copy and copy[f] in added
       
  1503                          and copyto[copy[f]] == f) or
       
  1504                         (f in copyto and copyto[f] in added
       
  1505                          and copy[copyto[f]] == f)):
       
  1506                         dodiff = False
       
  1507                     else:
       
  1508                         header.append('deleted file mode %s\n' %
       
  1509                                       gitmode[man1.flags(f)])
       
  1510                 elif not to or util.binary(to):
       
  1511                     # regular diffs cannot represent empty file deletion
       
  1512                     losedatafn(f)
       
  1513             else:
       
  1514                 oflag = man1.flags(f)
       
  1515                 nflag = ctx2.flags(f)
       
  1516                 binary = util.binary(to) or util.binary(tn)
       
  1517                 if opts.git:
       
  1518                     _addmodehdr(header, gitmode[oflag], gitmode[nflag])
       
  1519                     if binary:
       
  1520                         dodiff = 'binary'
       
  1521                 elif binary or nflag != oflag:
       
  1522                     losedatafn(f)
       
  1523             if opts.git:
       
  1524                 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
       
  1525 
       
  1526         if dodiff:
       
  1527             if dodiff == 'binary':
       
  1528                 text = b85diff(to, tn)
       
  1529             else:
       
  1530                 text = mdiff.unidiff(to, date1,
       
  1531                                     # ctx2 date may be dynamic
       
  1532                                     tn, util.datestr(ctx2.date()),
       
  1533                                     join(a), join(b), revs, opts=opts)
       
  1534             if header and (text or len(header) > 1):
       
  1535                 yield ''.join(header)
       
  1536             if text:
       
  1537                 yield text
       
  1538 
       
  1539 def diffstatdata(lines):
       
  1540     filename, adds, removes = None, 0, 0
       
  1541     for line in lines:
       
  1542         if line.startswith('diff'):
       
  1543             if filename:
       
  1544                 isbinary = adds == 0 and removes == 0
       
  1545                 yield (filename, adds, removes, isbinary)
       
  1546             # set numbers to 0 anyway when starting new file
       
  1547             adds, removes = 0, 0
       
  1548             if line.startswith('diff --git'):
       
  1549                 filename = gitre.search(line).group(1)
       
  1550             else:
       
  1551                 # format: "diff -r ... -r ... filename"
       
  1552                 filename = line.split(None, 5)[-1]
       
  1553         elif line.startswith('+') and not line.startswith('+++'):
       
  1554             adds += 1
       
  1555         elif line.startswith('-') and not line.startswith('---'):
       
  1556             removes += 1
       
  1557     if filename:
       
  1558         isbinary = adds == 0 and removes == 0
       
  1559         yield (filename, adds, removes, isbinary)
       
  1560 
       
  1561 def diffstat(lines, width=80, git=False):
       
  1562     output = []
       
  1563     stats = list(diffstatdata(lines))
       
  1564 
       
  1565     maxtotal, maxname = 0, 0
       
  1566     totaladds, totalremoves = 0, 0
       
  1567     hasbinary = False
       
  1568 
       
  1569     sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
       
  1570              for filename, adds, removes, isbinary in stats]
       
  1571 
       
  1572     for filename, adds, removes, isbinary, namewidth in sized:
       
  1573         totaladds += adds
       
  1574         totalremoves += removes
       
  1575         maxname = max(maxname, namewidth)
       
  1576         maxtotal = max(maxtotal, adds + removes)
       
  1577         if isbinary:
       
  1578             hasbinary = True
       
  1579 
       
  1580     countwidth = len(str(maxtotal))
       
  1581     if hasbinary and countwidth < 3:
       
  1582         countwidth = 3
       
  1583     graphwidth = width - countwidth - maxname - 6
       
  1584     if graphwidth < 10:
       
  1585         graphwidth = 10
       
  1586 
       
  1587     def scale(i):
       
  1588         if maxtotal <= graphwidth:
       
  1589             return i
       
  1590         # If diffstat runs out of room it doesn't print anything,
       
  1591         # which isn't very useful, so always print at least one + or -
       
  1592         # if there were at least some changes.
       
  1593         return max(i * graphwidth // maxtotal, int(bool(i)))
       
  1594 
       
  1595     for filename, adds, removes, isbinary, namewidth in sized:
       
  1596         if git and isbinary:
       
  1597             count = 'Bin'
       
  1598         else:
       
  1599             count = adds + removes
       
  1600         pluses = '+' * scale(adds)
       
  1601         minuses = '-' * scale(removes)
       
  1602         output.append(' %s%s |  %*s %s%s\n' %
       
  1603                       (filename, ' ' * (maxname - namewidth),
       
  1604                        countwidth, count,
       
  1605                        pluses, minuses))
       
  1606 
       
  1607     if stats:
       
  1608         output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
       
  1609                       % (len(stats), totaladds, totalremoves))
       
  1610 
       
  1611     return ''.join(output)
       
  1612 
       
  1613 def diffstatui(*args, **kw):
       
  1614     '''like diffstat(), but yields 2-tuples of (output, label) for
       
  1615     ui.write()
       
  1616     '''
       
  1617 
       
  1618     for line in diffstat(*args, **kw).splitlines():
       
  1619         if line and line[-1] in '+-':
       
  1620             name, graph = line.rsplit(' ', 1)
       
  1621             yield (name + ' ', '')
       
  1622             m = re.search(r'\++', graph)
       
  1623             if m:
       
  1624                 yield (m.group(0), 'diffstat.inserted')
       
  1625             m = re.search(r'-+', graph)
       
  1626             if m:
       
  1627                 yield (m.group(0), 'diffstat.deleted')
       
  1628         else:
       
  1629             yield (line, '')
       
  1630         yield ('\n', '')