mercurial/patch.py
changeset 5124 06154aff2b1a
parent 5117 d4fa6bafc43a
child 5260 0fc16031bb45
equal deleted inserted replaced
5123:f94dbc6c7eaf 5124:06154aff2b1a
     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
       
    17 
       
    18 class NoHunks(PatchError):
       
    19     pass
    13 
    20 
    14 # helper functions
    21 # helper functions
    15 
    22 
    16 def copyfile(src, dst, basedir=None):
    23 def copyfile(src, dst, basedir=None):
    17     if not basedir:
    24     if not basedir:
    48     fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
    55     fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
    49     tmpfp = os.fdopen(fd, 'w')
    56     tmpfp = os.fdopen(fd, 'w')
    50     try:
    57     try:
    51         msg = email.Parser.Parser().parse(fileobj)
    58         msg = email.Parser.Parser().parse(fileobj)
    52 
    59 
    53         message = msg['Subject']
    60         subject = msg['Subject']
    54         user = msg['From']
    61         user = msg['From']
    55         # should try to parse msg['Date']
    62         # should try to parse msg['Date']
    56         date = None
    63         date = None
    57         nodeid = None
    64         nodeid = None
    58         branch = None
    65         branch = None
    59         parents = []
    66         parents = []
    60 
    67 
    61         if message:
    68         if subject:
    62             if message.startswith('[PATCH'):
    69             if subject.startswith('[PATCH'):
    63                 pend = message.find(']')
    70                 pend = subject.find(']')
    64                 if pend >= 0:
    71                 if pend >= 0:
    65                     message = message[pend+1:].lstrip()
    72                     subject = subject[pend+1:].lstrip()
    66             message = message.replace('\n\t', ' ')
    73             subject = subject.replace('\n\t', ' ')
    67             ui.debug('Subject: %s\n' % message)
    74             ui.debug('Subject: %s\n' % subject)
    68         if user:
    75         if user:
    69             ui.debug('From: %s\n' % user)
    76             ui.debug('From: %s\n' % user)
    70         diffs_seen = 0
    77         diffs_seen = 0
    71         ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
    78         ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
    72 
    79         message = ''
    73         for part in msg.walk():
    80         for part in msg.walk():
    74             content_type = part.get_content_type()
    81             content_type = part.get_content_type()
    75             ui.debug('Content-Type: %s\n' % content_type)
    82             ui.debug('Content-Type: %s\n' % content_type)
    76             if content_type not in ok_types:
    83             if content_type not in ok_types:
    77                 continue
    84                 continue
    82                 ignoretext = False
    89                 ignoretext = False
    83 
    90 
    84                 ui.debug(_('found patch at byte %d\n') % m.start(0))
    91                 ui.debug(_('found patch at byte %d\n') % m.start(0))
    85                 diffs_seen += 1
    92                 diffs_seen += 1
    86                 cfp = cStringIO.StringIO()
    93                 cfp = cStringIO.StringIO()
    87                 if message:
       
    88                     cfp.write(message)
       
    89                     cfp.write('\n')
       
    90                 for line in payload[:m.start(0)].splitlines():
    94                 for line in payload[:m.start(0)].splitlines():
    91                     if line.startswith('# HG changeset patch'):
    95                     if line.startswith('# HG changeset patch'):
    92                         ui.debug(_('patch generated by hg export\n'))
    96                         ui.debug(_('patch generated by hg export\n'))
    93                         hgpatch = True
    97                         hgpatch = True
    94                         # drop earlier commit message content
    98                         # drop earlier commit message content
    95                         cfp.seek(0)
    99                         cfp.seek(0)
    96                         cfp.truncate()
   100                         cfp.truncate()
       
   101                         subject = None
    97                     elif hgpatch:
   102                     elif hgpatch:
    98                         if line.startswith('# User '):
   103                         if line.startswith('# User '):
    99                             user = line[7:]
   104                             user = line[7:]
   100                             ui.debug('From: %s\n' % user)
   105                             ui.debug('From: %s\n' % user)
   101                         elif line.startswith("# Date "):
   106                         elif line.startswith("# Date "):
   121     except:
   126     except:
   122         tmpfp.close()
   127         tmpfp.close()
   123         os.unlink(tmpname)
   128         os.unlink(tmpname)
   124         raise
   129         raise
   125 
   130 
       
   131     if subject and not message.startswith(subject):
       
   132         message = '%s\n%s' % (subject, message)
   126     tmpfp.close()
   133     tmpfp.close()
   127     if not diffs_seen:
   134     if not diffs_seen:
   128         os.unlink(tmpname)
   135         os.unlink(tmpname)
   129         return None, message, user, date, branch, None, None, None
   136         return None, message, user, date, branch, None, None, None
   130     p1 = parents and parents.pop(0) or None
   137     p1 = parents and parents.pop(0) or None
   133 
   140 
   134 GP_PATCH  = 1 << 0  # we have to run patch
   141 GP_PATCH  = 1 << 0  # we have to run patch
   135 GP_FILTER = 1 << 1  # there's some copy/rename operation
   142 GP_FILTER = 1 << 1  # there's some copy/rename operation
   136 GP_BINARY = 1 << 2  # there's a binary patch
   143 GP_BINARY = 1 << 2  # there's a binary patch
   137 
   144 
   138 def readgitpatch(patchname):
   145 def readgitpatch(fp, firstline=None):
   139     """extract git-style metadata about patches from <patchname>"""
   146     """extract git-style metadata about patches from <patchname>"""
   140     class gitpatch:
   147     class gitpatch:
   141         "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
   148         "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
   142         def __init__(self, path):
   149         def __init__(self, path):
   143             self.path = path
   150             self.path = path
   146             self.op = 'MODIFY'
   153             self.op = 'MODIFY'
   147             self.copymod = False
   154             self.copymod = False
   148             self.lineno = 0
   155             self.lineno = 0
   149             self.binary = False
   156             self.binary = False
   150 
   157 
       
   158     def reader(fp, firstline):
       
   159         if firstline is not None:
       
   160             yield firstline
       
   161         for line in fp:
       
   162             yield line
       
   163 
   151     # Filter patch for git information
   164     # Filter patch for git information
   152     gitre = re.compile('diff --git a/(.*) b/(.*)')
   165     gitre = re.compile('diff --git a/(.*) b/(.*)')
   153     pf = file(patchname)
       
   154     gp = None
   166     gp = None
   155     gitpatches = []
   167     gitpatches = []
   156     # Can have a git patch with only metadata, causing patch to complain
   168     # Can have a git patch with only metadata, causing patch to complain
   157     dopatch = 0
   169     dopatch = 0
   158 
   170 
   159     lineno = 0
   171     lineno = 0
   160     for line in pf:
   172     for line in reader(fp, firstline):
   161         lineno += 1
   173         lineno += 1
   162         if line.startswith('diff --git'):
   174         if line.startswith('diff --git'):
   163             m = gitre.match(line)
   175             m = gitre.match(line)
   164             if m:
   176             if m:
   165                 if gp:
   177                 if gp:
   188                 gp.path = line[8:].rstrip()
   200                 gp.path = line[8:].rstrip()
   189             elif line.startswith('deleted file'):
   201             elif line.startswith('deleted file'):
   190                 gp.op = 'DELETE'
   202                 gp.op = 'DELETE'
   191             elif line.startswith('new file mode '):
   203             elif line.startswith('new file mode '):
   192                 gp.op = 'ADD'
   204                 gp.op = 'ADD'
   193                 gp.mode = int(line.rstrip()[-3:], 8)
   205                 gp.mode = int(line.rstrip()[-6:], 8)
   194             elif line.startswith('new mode '):
   206             elif line.startswith('new mode '):
   195                 gp.mode = int(line.rstrip()[-3:], 8)
   207                 gp.mode = int(line.rstrip()[-6:], 8)
   196             elif line.startswith('GIT binary patch'):
   208             elif line.startswith('GIT binary patch'):
   197                 dopatch |= GP_BINARY
   209                 dopatch |= GP_BINARY
   198                 gp.binary = True
   210                 gp.binary = True
   199     if gp:
   211     if gp:
   200         gitpatches.append(gp)
   212         gitpatches.append(gp)
   202     if not gitpatches:
   214     if not gitpatches:
   203         dopatch = GP_PATCH
   215         dopatch = GP_PATCH
   204 
   216 
   205     return (dopatch, gitpatches)
   217     return (dopatch, gitpatches)
   206 
   218 
   207 def dogitpatch(patchname, gitpatches, cwd=None):
   219 def patch(patchname, ui, strip=1, cwd=None, files={}):
   208     """Preprocess git patch so that vanilla patch can handle it"""
   220     """apply <patchname> to the working directory.
   209     def extractbin(fp):
   221     returns whether patch was applied with fuzz factor."""
   210         i = [0] # yuck
   222     patcher = ui.config('ui', 'patch')
   211         def readline():
   223     args = []
   212             i[0] += 1
   224     try:
   213             return fp.readline().rstrip()
   225         if patcher:
   214         line = readline()
   226             return externalpatch(patcher, args, patchname, ui, strip, cwd,
       
   227                                  files)
       
   228         else:
       
   229             try:
       
   230                 return internalpatch(patchname, ui, strip, cwd, files)
       
   231             except NoHunks:
       
   232                 patcher = util.find_exe('gpatch') or util.find_exe('patch')
       
   233                 ui.debug('no valid hunks found; trying with %r instead\n' %
       
   234                          patcher)
       
   235                 if util.needbinarypatch():
       
   236                     args.append('--binary')
       
   237                 return externalpatch(patcher, args, patchname, ui, strip, cwd,
       
   238                                      files)
       
   239     except PatchError, err:
       
   240         s = str(err)
       
   241         if s:
       
   242             raise util.Abort(s)
       
   243         else:
       
   244             raise util.Abort(_('patch failed to apply'))
       
   245 
       
   246 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
       
   247     """use <patcher> to apply <patchname> to the working directory.
       
   248     returns whether patch was applied with fuzz factor."""
       
   249 
       
   250     fuzz = False
       
   251     if cwd:
       
   252         args.append('-d %s' % util.shellquote(cwd))
       
   253     fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
       
   254                                        util.shellquote(patchname)))
       
   255 
       
   256     for line in fp:
       
   257         line = line.rstrip()
       
   258         ui.note(line + '\n')
       
   259         if line.startswith('patching file '):
       
   260             pf = util.parse_patch_output(line)
       
   261             printed_file = False
       
   262             files.setdefault(pf, (None, None))
       
   263         elif line.find('with fuzz') >= 0:
       
   264             fuzz = True
       
   265             if not printed_file:
       
   266                 ui.warn(pf + '\n')
       
   267                 printed_file = True
       
   268             ui.warn(line + '\n')
       
   269         elif line.find('saving rejects to file') >= 0:
       
   270             ui.warn(line + '\n')
       
   271         elif line.find('FAILED') >= 0:
       
   272             if not printed_file:
       
   273                 ui.warn(pf + '\n')
       
   274                 printed_file = True
       
   275             ui.warn(line + '\n')
       
   276     code = fp.close()
       
   277     if code:
       
   278         raise PatchError(_("patch command failed: %s") %
       
   279                          util.explain_exit(code)[0])
       
   280     return fuzz
       
   281 
       
   282 def internalpatch(patchobj, ui, strip, cwd, files={}):
       
   283     """use builtin patch to apply <patchobj> to the working directory.
       
   284     returns whether patch was applied with fuzz factor."""
       
   285     try:
       
   286         fp = file(patchobj, 'rb')
       
   287     except TypeError:
       
   288         fp = patchobj
       
   289     if cwd:
       
   290         curdir = os.getcwd()
       
   291         os.chdir(cwd)
       
   292     try:
       
   293         ret = applydiff(ui, fp, files, strip=strip)
       
   294     finally:
       
   295         if cwd:
       
   296             os.chdir(curdir)
       
   297     if ret < 0:
       
   298         raise PatchError
       
   299     return ret > 0
       
   300 
       
   301 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
       
   302 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
       
   303 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
       
   304 
       
   305 class patchfile:
       
   306     def __init__(self, ui, fname):
       
   307         self.fname = fname
       
   308         self.ui = ui
       
   309         try:
       
   310             fp = file(fname, 'rb')
       
   311             self.lines = fp.readlines()
       
   312             self.exists = True
       
   313         except IOError:
       
   314             dirname = os.path.dirname(fname)
       
   315             if dirname and not os.path.isdir(dirname):
       
   316                 dirs = dirname.split(os.path.sep)
       
   317                 d = ""
       
   318                 for x in dirs:
       
   319                     d = os.path.join(d, x)
       
   320                     if not os.path.isdir(d):
       
   321                         os.mkdir(d)
       
   322             self.lines = []
       
   323             self.exists = False
       
   324 
       
   325         self.hash = {}
       
   326         self.dirty = 0
       
   327         self.offset = 0
       
   328         self.rej = []
       
   329         self.fileprinted = False
       
   330         self.printfile(False)
       
   331         self.hunks = 0
       
   332 
       
   333     def printfile(self, warn):
       
   334         if self.fileprinted:
       
   335             return
       
   336         if warn or self.ui.verbose:
       
   337             self.fileprinted = True
       
   338         s = _("patching file %s\n") % self.fname
       
   339         if warn:
       
   340             self.ui.warn(s)
       
   341         else:
       
   342             self.ui.note(s)
       
   343 
       
   344 
       
   345     def findlines(self, l, linenum):
       
   346         # looks through the hash and finds candidate lines.  The
       
   347         # result is a list of line numbers sorted based on distance
       
   348         # from linenum
       
   349         def sorter(a, b):
       
   350             vala = abs(a - linenum)
       
   351             valb = abs(b - linenum)
       
   352             return cmp(vala, valb)
       
   353 
       
   354         try:
       
   355             cand = self.hash[l]
       
   356         except:
       
   357             return []
       
   358 
       
   359         if len(cand) > 1:
       
   360             # resort our list of potentials forward then back.
       
   361             cand.sort(cmp=sorter)
       
   362         return cand
       
   363 
       
   364     def hashlines(self):
       
   365         self.hash = {}
       
   366         for x in xrange(len(self.lines)):
       
   367             s = self.lines[x]
       
   368             self.hash.setdefault(s, []).append(x)
       
   369 
       
   370     def write_rej(self):
       
   371         # our rejects are a little different from patch(1).  This always
       
   372         # creates rejects in the same form as the original patch.  A file
       
   373         # header is inserted so that you can run the reject through patch again
       
   374         # without having to type the filename.
       
   375 
       
   376         if not self.rej:
       
   377             return
       
   378         if self.hunks != 1:
       
   379             hunkstr = "s"
       
   380         else:
       
   381             hunkstr = ""
       
   382 
       
   383         fname = self.fname + ".rej"
       
   384         self.ui.warn(
       
   385             _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") %
       
   386             (len(self.rej), self.hunks, hunkstr, fname))
       
   387         try: os.unlink(fname)
       
   388         except:
       
   389             pass
       
   390         fp = file(fname, 'wb')
       
   391         base = os.path.basename(self.fname)
       
   392         fp.write("--- %s\n+++ %s\n" % (base, base))
       
   393         for x in self.rej:
       
   394             for l in x.hunk:
       
   395                 fp.write(l)
       
   396                 if l[-1] != '\n':
       
   397                     fp.write("\n\ No newline at end of file\n")
       
   398 
       
   399     def write(self, dest=None):
       
   400         if self.dirty:
       
   401             if not dest:
       
   402                 dest = self.fname
       
   403             st = None
       
   404             try:
       
   405                 st = os.lstat(dest)
       
   406                 if st.st_nlink > 1:
       
   407                     os.unlink(dest)
       
   408             except: pass
       
   409             fp = file(dest, 'wb')
       
   410             if st:
       
   411                 os.chmod(dest, st.st_mode)
       
   412             fp.writelines(self.lines)
       
   413             fp.close()
       
   414 
       
   415     def close(self):
       
   416         self.write()
       
   417         self.write_rej()
       
   418 
       
   419     def apply(self, h, reverse):
       
   420         if not h.complete():
       
   421             raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
       
   422                             (h.number, h.desc, len(h.a), h.lena, len(h.b),
       
   423                             h.lenb))
       
   424 
       
   425         self.hunks += 1
       
   426         if reverse:
       
   427             h.reverse()
       
   428 
       
   429         if self.exists and h.createfile():
       
   430             self.ui.warn(_("file %s already exists\n") % self.fname)
       
   431             self.rej.append(h)
       
   432             return -1
       
   433 
       
   434         if isinstance(h, binhunk):
       
   435             if h.rmfile():
       
   436                 os.unlink(self.fname)
       
   437             else:
       
   438                 self.lines[:] = h.new()
       
   439                 self.offset += len(h.new())
       
   440                 self.dirty = 1
       
   441             return 0
       
   442 
       
   443         # fast case first, no offsets, no fuzz
       
   444         old = h.old()
       
   445         # patch starts counting at 1 unless we are adding the file
       
   446         if h.starta == 0:
       
   447             start = 0
       
   448         else:
       
   449             start = h.starta + self.offset - 1
       
   450         orig_start = start
       
   451         if diffhelpers.testhunk(old, self.lines, start) == 0:
       
   452             if h.rmfile():
       
   453                 os.unlink(self.fname)
       
   454             else:
       
   455                 self.lines[start : start + h.lena] = h.new()
       
   456                 self.offset += h.lenb - h.lena
       
   457                 self.dirty = 1
       
   458             return 0
       
   459 
       
   460         # ok, we couldn't match the hunk.  Lets look for offsets and fuzz it
       
   461         self.hashlines()
       
   462         if h.hunk[-1][0] != ' ':
       
   463             # if the hunk tried to put something at the bottom of the file
       
   464             # override the start line and use eof here
       
   465             search_start = len(self.lines)
       
   466         else:
       
   467             search_start = orig_start
       
   468 
       
   469         for fuzzlen in xrange(3):
       
   470             for toponly in [ True, False ]:
       
   471                 old = h.old(fuzzlen, toponly)
       
   472 
       
   473                 cand = self.findlines(old[0][1:], search_start)
       
   474                 for l in cand:
       
   475                     if diffhelpers.testhunk(old, self.lines, l) == 0:
       
   476                         newlines = h.new(fuzzlen, toponly)
       
   477                         self.lines[l : l + len(old)] = newlines
       
   478                         self.offset += len(newlines) - len(old)
       
   479                         self.dirty = 1
       
   480                         if fuzzlen:
       
   481                             fuzzstr = "with fuzz %d " % fuzzlen
       
   482                             f = self.ui.warn
       
   483                             self.printfile(True)
       
   484                         else:
       
   485                             fuzzstr = ""
       
   486                             f = self.ui.note
       
   487                         offset = l - orig_start - fuzzlen
       
   488                         if offset == 1:
       
   489                             linestr = "line"
       
   490                         else:
       
   491                             linestr = "lines"
       
   492                         f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
       
   493                           (h.number, l+1, fuzzstr, offset, linestr))
       
   494                         return fuzzlen
       
   495         self.printfile(True)
       
   496         self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
       
   497         self.rej.append(h)
       
   498         return -1
       
   499 
       
   500 class hunk:
       
   501     def __init__(self, desc, num, lr, context):
       
   502         self.number = num
       
   503         self.desc = desc
       
   504         self.hunk = [ desc ]
       
   505         self.a = []
       
   506         self.b = []
       
   507         if context:
       
   508             self.read_context_hunk(lr)
       
   509         else:
       
   510             self.read_unified_hunk(lr)
       
   511 
       
   512     def read_unified_hunk(self, lr):
       
   513         m = unidesc.match(self.desc)
       
   514         if not m:
       
   515             raise PatchError(_("bad hunk #%d") % self.number)
       
   516         self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
       
   517         if self.lena == None:
       
   518             self.lena = 1
       
   519         else:
       
   520             self.lena = int(self.lena)
       
   521         if self.lenb == None:
       
   522             self.lenb = 1
       
   523         else:
       
   524             self.lenb = int(self.lenb)
       
   525         self.starta = int(self.starta)
       
   526         self.startb = int(self.startb)
       
   527         diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
       
   528         # if we hit eof before finishing out the hunk, the last line will
       
   529         # be zero length.  Lets try to fix it up.
       
   530         while len(self.hunk[-1]) == 0:
       
   531                 del self.hunk[-1]
       
   532                 del self.a[-1]
       
   533                 del self.b[-1]
       
   534                 self.lena -= 1
       
   535                 self.lenb -= 1
       
   536 
       
   537     def read_context_hunk(self, lr):
       
   538         self.desc = lr.readline()
       
   539         m = contextdesc.match(self.desc)
       
   540         if not m:
       
   541             raise PatchError(_("bad hunk #%d") % self.number)
       
   542         foo, self.starta, foo2, aend, foo3 = m.groups()
       
   543         self.starta = int(self.starta)
       
   544         if aend == None:
       
   545             aend = self.starta
       
   546         self.lena = int(aend) - self.starta
       
   547         if self.starta:
       
   548             self.lena += 1
       
   549         for x in xrange(self.lena):
       
   550             l = lr.readline()
       
   551             if l.startswith('---'):
       
   552                 lr.push(l)
       
   553                 break
       
   554             s = l[2:]
       
   555             if l.startswith('- ') or l.startswith('! '):
       
   556                 u = '-' + s
       
   557             elif l.startswith('  '):
       
   558                 u = ' ' + s
       
   559             else:
       
   560                 raise PatchError(_("bad hunk #%d old text line %d") %
       
   561                                  (self.number, x))
       
   562             self.a.append(u)
       
   563             self.hunk.append(u)
       
   564 
       
   565         l = lr.readline()
       
   566         if l.startswith('\ '):
       
   567             s = self.a[-1][:-1]
       
   568             self.a[-1] = s
       
   569             self.hunk[-1] = s
       
   570             l = lr.readline()
       
   571         m = contextdesc.match(l)
       
   572         if not m:
       
   573             raise PatchError(_("bad hunk #%d") % self.number)
       
   574         foo, self.startb, foo2, bend, foo3 = m.groups()
       
   575         self.startb = int(self.startb)
       
   576         if bend == None:
       
   577             bend = self.startb
       
   578         self.lenb = int(bend) - self.startb
       
   579         if self.startb:
       
   580             self.lenb += 1
       
   581         hunki = 1
       
   582         for x in xrange(self.lenb):
       
   583             l = lr.readline()
       
   584             if l.startswith('\ '):
       
   585                 s = self.b[-1][:-1]
       
   586                 self.b[-1] = s
       
   587                 self.hunk[hunki-1] = s
       
   588                 continue
       
   589             if not l:
       
   590                 lr.push(l)
       
   591                 break
       
   592             s = l[2:]
       
   593             if l.startswith('+ ') or l.startswith('! '):
       
   594                 u = '+' + s
       
   595             elif l.startswith('  '):
       
   596                 u = ' ' + s
       
   597             elif len(self.b) == 0:
       
   598                 # this can happen when the hunk does not add any lines
       
   599                 lr.push(l)
       
   600                 break
       
   601             else:
       
   602                 raise PatchError(_("bad hunk #%d old text line %d") %
       
   603                                  (self.number, x))
       
   604             self.b.append(s)
       
   605             while True:
       
   606                 if hunki >= len(self.hunk):
       
   607                     h = ""
       
   608                 else:
       
   609                     h = self.hunk[hunki]
       
   610                 hunki += 1
       
   611                 if h == u:
       
   612                     break
       
   613                 elif h.startswith('-'):
       
   614                     continue
       
   615                 else:
       
   616                     self.hunk.insert(hunki-1, u)
       
   617                     break
       
   618 
       
   619         if not self.a:
       
   620             # this happens when lines were only added to the hunk
       
   621             for x in self.hunk:
       
   622                 if x.startswith('-') or x.startswith(' '):
       
   623                     self.a.append(x)
       
   624         if not self.b:
       
   625             # this happens when lines were only deleted from the hunk
       
   626             for x in self.hunk:
       
   627                 if x.startswith('+') or x.startswith(' '):
       
   628                     self.b.append(x[1:])
       
   629         # @@ -start,len +start,len @@
       
   630         self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
       
   631                                              self.startb, self.lenb)
       
   632         self.hunk[0] = self.desc
       
   633 
       
   634     def reverse(self):
       
   635         origlena = self.lena
       
   636         origstarta = self.starta
       
   637         self.lena = self.lenb
       
   638         self.starta = self.startb
       
   639         self.lenb = origlena
       
   640         self.startb = origstarta
       
   641         self.a = []
       
   642         self.b = []
       
   643         # self.hunk[0] is the @@ description
       
   644         for x in xrange(1, len(self.hunk)):
       
   645             o = self.hunk[x]
       
   646             if o.startswith('-'):
       
   647                 n = '+' + o[1:]
       
   648                 self.b.append(o[1:])
       
   649             elif o.startswith('+'):
       
   650                 n = '-' + o[1:]
       
   651                 self.a.append(n)
       
   652             else:
       
   653                 n = o
       
   654                 self.b.append(o[1:])
       
   655                 self.a.append(o)
       
   656             self.hunk[x] = o
       
   657 
       
   658     def fix_newline(self):
       
   659         diffhelpers.fix_newline(self.hunk, self.a, self.b)
       
   660 
       
   661     def complete(self):
       
   662         return len(self.a) == self.lena and len(self.b) == self.lenb
       
   663 
       
   664     def createfile(self):
       
   665         return self.starta == 0 and self.lena == 0
       
   666 
       
   667     def rmfile(self):
       
   668         return self.startb == 0 and self.lenb == 0
       
   669 
       
   670     def fuzzit(self, l, fuzz, toponly):
       
   671         # this removes context lines from the top and bottom of list 'l'.  It
       
   672         # checks the hunk to make sure only context lines are removed, and then
       
   673         # returns a new shortened list of lines.
       
   674         fuzz = min(fuzz, len(l)-1)
       
   675         if fuzz:
       
   676             top = 0
       
   677             bot = 0
       
   678             hlen = len(self.hunk)
       
   679             for x in xrange(hlen-1):
       
   680                 # the hunk starts with the @@ line, so use x+1
       
   681                 if self.hunk[x+1][0] == ' ':
       
   682                     top += 1
       
   683                 else:
       
   684                     break
       
   685             if not toponly:
       
   686                 for x in xrange(hlen-1):
       
   687                     if self.hunk[hlen-bot-1][0] == ' ':
       
   688                         bot += 1
       
   689                     else:
       
   690                         break
       
   691 
       
   692             # top and bot now count context in the hunk
       
   693             # adjust them if either one is short
       
   694             context = max(top, bot, 3)
       
   695             if bot < context:
       
   696                 bot = max(0, fuzz - (context - bot))
       
   697             else:
       
   698                 bot = min(fuzz, bot)
       
   699             if top < context:
       
   700                 top = max(0, fuzz - (context - top))
       
   701             else:
       
   702                 top = min(fuzz, top)
       
   703 
       
   704             return l[top:len(l)-bot]
       
   705         return l
       
   706 
       
   707     def old(self, fuzz=0, toponly=False):
       
   708         return self.fuzzit(self.a, fuzz, toponly)
       
   709 
       
   710     def newctrl(self):
       
   711         res = []
       
   712         for x in self.hunk:
       
   713             c = x[0]
       
   714             if c == ' ' or c == '+':
       
   715                 res.append(x)
       
   716         return res
       
   717 
       
   718     def new(self, fuzz=0, toponly=False):
       
   719         return self.fuzzit(self.b, fuzz, toponly)
       
   720 
       
   721 class binhunk:
       
   722     'A binary patch file. Only understands literals so far.'
       
   723     def __init__(self, gitpatch):
       
   724         self.gitpatch = gitpatch
       
   725         self.text = None
       
   726         self.hunk = ['GIT binary patch\n']
       
   727 
       
   728     def createfile(self):
       
   729         return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
       
   730 
       
   731     def rmfile(self):
       
   732         return self.gitpatch.op == 'DELETE'
       
   733 
       
   734     def complete(self):
       
   735         return self.text is not None
       
   736 
       
   737     def new(self):
       
   738         return [self.text]
       
   739 
       
   740     def extract(self, fp):
       
   741         line = fp.readline()
       
   742         self.hunk.append(line)
   215         while line and not line.startswith('literal '):
   743         while line and not line.startswith('literal '):
   216             line = readline()
   744             line = fp.readline()
       
   745             self.hunk.append(line)
   217         if not line:
   746         if not line:
   218             return None, i[0]
   747             raise PatchError(_('could not extract binary patch'))
   219         size = int(line[8:])
   748         size = int(line[8:].rstrip())
   220         dec = []
   749         dec = []
   221         line = readline()
   750         line = fp.readline()
   222         while line:
   751         self.hunk.append(line)
       
   752         while len(line) > 1:
   223             l = line[0]
   753             l = line[0]
   224             if l <= 'Z' and l >= 'A':
   754             if l <= 'Z' and l >= 'A':
   225                 l = ord(l) - ord('A') + 1
   755                 l = ord(l) - ord('A') + 1
   226             else:
   756             else:
   227                 l = ord(l) - ord('a') + 27
   757                 l = ord(l) - ord('a') + 27
   228             dec.append(base85.b85decode(line[1:])[:l])
   758             dec.append(base85.b85decode(line[1:-1])[:l])
   229             line = readline()
   759             line = fp.readline()
       
   760             self.hunk.append(line)
   230         text = zlib.decompress(''.join(dec))
   761         text = zlib.decompress(''.join(dec))
   231         if len(text) != size:
   762         if len(text) != size:
   232             raise util.Abort(_('binary patch is %d bytes, not %d') %
   763             raise PatchError(_('binary patch is %d bytes, not %d') %
   233                              (len(text), size))
   764                              len(text), size)
   234         return text, i[0]
   765         self.text = text
   235 
   766 
   236     pf = file(patchname)
   767 def parsefilename(str):
   237     pfline = 1
   768     # --- filename \t|space stuff
   238 
   769     s = str[4:]
   239     fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
   770     i = s.find('\t')
   240     tmpfp = os.fdopen(fd, 'w')
   771     if i < 0:
   241 
   772         i = s.find(' ')
   242     try:
   773         if i < 0:
   243         for i in xrange(len(gitpatches)):
   774             return s
   244             p = gitpatches[i]
   775     return s[:i]
   245             if not p.copymod and not p.binary:
   776 
       
   777 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
       
   778     def pathstrip(path, count=1):
       
   779         pathlen = len(path)
       
   780         i = 0
       
   781         if count == 0:
       
   782             return path.rstrip()
       
   783         while count > 0:
       
   784             i = path.find('/', i)
       
   785             if i == -1:
       
   786                 raise PatchError(_("unable to strip away %d dirs from %s") %
       
   787                                  (count, path))
       
   788             i += 1
       
   789             # consume '//' in the path
       
   790             while i < pathlen - 1 and path[i] == '/':
       
   791                 i += 1
       
   792             count -= 1
       
   793         return path[i:].rstrip()
       
   794 
       
   795     nulla = afile_orig == "/dev/null"
       
   796     nullb = bfile_orig == "/dev/null"
       
   797     afile = pathstrip(afile_orig, strip)
       
   798     gooda = os.path.exists(afile) and not nulla
       
   799     bfile = pathstrip(bfile_orig, strip)
       
   800     if afile == bfile:
       
   801         goodb = gooda
       
   802     else:
       
   803         goodb = os.path.exists(bfile) and not nullb
       
   804     createfunc = hunk.createfile
       
   805     if reverse:
       
   806         createfunc = hunk.rmfile
       
   807     if not goodb and not gooda and not createfunc():
       
   808         raise PatchError(_("unable to find %s or %s for patching") %
       
   809                          (afile, bfile))
       
   810     if gooda and goodb:
       
   811         fname = bfile
       
   812         if afile in bfile:
       
   813             fname = afile
       
   814     elif gooda:
       
   815         fname = afile
       
   816     elif not nullb:
       
   817         fname = bfile
       
   818         if afile in bfile:
       
   819             fname = afile
       
   820     elif not nulla:
       
   821         fname = afile
       
   822     return fname
       
   823 
       
   824 class linereader:
       
   825     # simple class to allow pushing lines back into the input stream
       
   826     def __init__(self, fp):
       
   827         self.fp = fp
       
   828         self.buf = []
       
   829 
       
   830     def push(self, line):
       
   831         self.buf.append(line)
       
   832 
       
   833     def readline(self):
       
   834         if self.buf:
       
   835             l = self.buf[0]
       
   836             del self.buf[0]
       
   837             return l
       
   838         return self.fp.readline()
       
   839 
       
   840 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
       
   841               rejmerge=None, updatedir=None):
       
   842     """reads a patch from fp and tries to apply it.  The dict 'changed' is
       
   843        filled in with all of the filenames changed by the patch.  Returns 0
       
   844        for a clean patch, -1 if any rejects were found and 1 if there was
       
   845        any fuzz."""
       
   846 
       
   847     def scangitpatch(fp, firstline, cwd=None):
       
   848         '''git patches can modify a file, then copy that file to
       
   849         a new file, but expect the source to be the unmodified form.
       
   850         So we scan the patch looking for that case so we can do
       
   851         the copies ahead of time.'''
       
   852 
       
   853         pos = 0
       
   854         try:
       
   855             pos = fp.tell()
       
   856         except IOError:
       
   857             fp = cStringIO.StringIO(fp.read())
       
   858 
       
   859         (dopatch, gitpatches) = readgitpatch(fp, firstline)
       
   860         for gp in gitpatches:
       
   861             if gp.copymod:
       
   862                 copyfile(gp.oldpath, gp.path, basedir=cwd)
       
   863 
       
   864         fp.seek(pos)
       
   865 
       
   866         return fp, dopatch, gitpatches
       
   867 
       
   868     current_hunk = None
       
   869     current_file = None
       
   870     afile = ""
       
   871     bfile = ""
       
   872     state = None
       
   873     hunknum = 0
       
   874     rejects = 0
       
   875 
       
   876     git = False
       
   877     gitre = re.compile('diff --git (a/.*) (b/.*)')
       
   878 
       
   879     # our states
       
   880     BFILE = 1
       
   881     err = 0
       
   882     context = None
       
   883     lr = linereader(fp)
       
   884     dopatch = True
       
   885     gitworkdone = False
       
   886 
       
   887     while True:
       
   888         newfile = False
       
   889         x = lr.readline()
       
   890         if not x:
       
   891             break
       
   892         if current_hunk:
       
   893             if x.startswith('\ '):
       
   894                 current_hunk.fix_newline()
       
   895             ret = current_file.apply(current_hunk, reverse)
       
   896             if ret >= 0:
       
   897                 changed.setdefault(current_file.fname, (None, None))
       
   898                 if ret > 0:
       
   899                     err = 1
       
   900             current_hunk = None
       
   901             gitworkdone = False
       
   902         if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
       
   903             ((context or context == None) and x.startswith('***************')))):
       
   904             try:
       
   905                 if context == None and x.startswith('***************'):
       
   906                     context = True
       
   907                 current_hunk = hunk(x, hunknum + 1, lr, context)
       
   908             except PatchError, err:
       
   909                 ui.debug(err)
       
   910                 current_hunk = None
   246                 continue
   911                 continue
   247 
   912             hunknum += 1
   248             # rewrite patch hunk
   913             if not current_file:
   249             while pfline < p.lineno:
   914                 if sourcefile:
   250                 tmpfp.write(pf.readline())
   915                     current_file = patchfile(ui, sourcefile)
   251                 pfline += 1
   916                 else:
   252 
   917                     current_file = selectfile(afile, bfile, current_hunk,
   253             if p.binary:
   918                                               strip, reverse)
   254                 text, delta = extractbin(pf)
   919                     current_file = patchfile(ui, current_file)
   255                 if not text:
   920         elif state == BFILE and x.startswith('GIT binary patch'):
   256                     raise util.Abort(_('binary patch extraction failed'))
   921             current_hunk = binhunk(changed[bfile[2:]][1])
   257                 pfline += delta
   922             if not current_file:
   258                 if not cwd:
   923                 if sourcefile:
   259                     cwd = os.getcwd()
   924                     current_file = patchfile(ui, sourcefile)
   260                 absdst = os.path.join(cwd, p.path)
   925                 else:
   261                 basedir = os.path.dirname(absdst)
   926                     current_file = selectfile(afile, bfile, current_hunk,
   262                 if not os.path.isdir(basedir):
   927                                               strip, reverse)
   263                     os.makedirs(basedir)
   928                     current_file = patchfile(ui, current_file)
   264                 out = file(absdst, 'wb')
   929             hunknum += 1
   265                 out.write(text)
   930             current_hunk.extract(fp)
   266                 out.close()
   931         elif x.startswith('diff --git'):
   267             elif p.copymod:
   932             # check for git diff, scanning the whole patch file if needed
   268                 copyfile(p.oldpath, p.path, basedir=cwd)
   933             m = gitre.match(x)
   269                 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
   934             if m:
   270                 line = pf.readline()
   935                 afile, bfile = m.group(1, 2)
   271                 pfline += 1
   936                 if not git:
   272                 while not line.startswith('--- a/'):
   937                     git = True
   273                     tmpfp.write(line)
   938                     fp, dopatch, gitpatches = scangitpatch(fp, x)
   274                     line = pf.readline()
   939                     for gp in gitpatches:
   275                     pfline += 1
   940                         changed[gp.path] = (gp.op, gp)
   276                 tmpfp.write('--- a/%s\n' % p.path)
   941                 # else error?
   277 
   942                 # copy/rename + modify should modify target, not source
   278         line = pf.readline()
   943                 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
   279         while line:
   944                                                                'RENAME'):
   280             tmpfp.write(line)
   945                     afile = bfile
   281             line = pf.readline()
   946                     gitworkdone = True
   282     except:
   947             newfile = True
   283         tmpfp.close()
   948         elif x.startswith('---'):
   284         os.unlink(patchname)
   949             # check for a unified diff
   285         raise
   950             l2 = lr.readline()
   286 
   951             if not l2.startswith('+++'):
   287     tmpfp.close()
   952                 lr.push(l2)
   288     return patchname
   953                 continue
   289 
   954             newfile = True
   290 def patch(patchname, ui, strip=1, cwd=None, files={}):
   955             context = False
   291     """apply the patch <patchname> to the working directory.
   956             afile = parsefilename(x)
   292     a list of patched files is returned"""
   957             bfile = parsefilename(l2)
   293 
   958         elif x.startswith('***'):
   294     # helper function
   959             # check for a context diff
   295     def __patch(patchname):
   960             l2 = lr.readline()
   296         """patch and updates the files and fuzz variables"""
   961             if not l2.startswith('---'):
   297         fuzz = False
   962                 lr.push(l2)
   298 
   963                 continue
   299         args = []
   964             l3 = lr.readline()
   300         patcher = ui.config('ui', 'patch')
   965             lr.push(l3)
   301         if not patcher:
   966             if not l3.startswith("***************"):
   302             patcher = util.find_exe('gpatch') or util.find_exe('patch')
   967                 lr.push(l2)
   303             # Try to be smart only if patch call was not supplied
   968                 continue
   304             if util.needbinarypatch():
   969             newfile = True
   305                 args.append('--binary')
   970             context = True
   306 
   971             afile = parsefilename(x)
   307         if not patcher:
   972             bfile = parsefilename(l2)
   308             raise util.Abort(_('no patch command found in hgrc or PATH'))
   973 
   309 
   974         if newfile:
   310         if cwd:
   975             if current_file:
   311             args.append('-d %s' % util.shellquote(cwd))
   976                 current_file.close()
   312         fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
   977                 if rejmerge:
   313                                            util.shellquote(patchname)))
   978                     rejmerge(current_file)
   314 
   979                 rejects += len(current_file.rej)
   315         for line in fp:
   980             state = BFILE
   316             line = line.rstrip()
   981             current_file = None
   317             ui.note(line + '\n')
   982             hunknum = 0
   318             if line.startswith('patching file '):
   983     if current_hunk:
   319                 pf = util.parse_patch_output(line)
   984         if current_hunk.complete():
   320                 printed_file = False
   985             ret = current_file.apply(current_hunk, reverse)
   321                 files.setdefault(pf, (None, None))
   986             if ret >= 0:
   322             elif line.find('with fuzz') >= 0:
   987                 changed.setdefault(current_file.fname, (None, None))
   323                 fuzz = True
   988                 if ret > 0:
   324                 if not printed_file:
   989                     err = 1
   325                     ui.warn(pf + '\n')
   990         else:
   326                     printed_file = True
   991             fname = current_file and current_file.fname or None
   327                 ui.warn(line + '\n')
   992             raise PatchError(_("malformed patch %s %s") % (fname,
   328             elif line.find('saving rejects to file') >= 0:
   993                              current_hunk.desc))
   329                 ui.warn(line + '\n')
   994     if current_file:
   330             elif line.find('FAILED') >= 0:
   995         current_file.close()
   331                 if not printed_file:
   996         if rejmerge:
   332                     ui.warn(pf + '\n')
   997             rejmerge(current_file)
   333                     printed_file = True
   998         rejects += len(current_file.rej)
   334                 ui.warn(line + '\n')
   999     if updatedir and git:
   335         code = fp.close()
  1000         updatedir(gitpatches)
   336         if code:
  1001     if rejects:
   337             raise util.Abort(_("patch command failed: %s") %
  1002         return -1
   338                              util.explain_exit(code)[0])
  1003     if hunknum == 0 and dopatch and not gitworkdone:
   339         return fuzz
  1004         raise NoHunks
   340 
  1005     return err
   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 
  1006 
   359 def diffopts(ui, opts={}, untrusted=False):
  1007 def diffopts(ui, opts={}, untrusted=False):
   360     def get(key, name=None):
  1008     def get(key, name=None):
   361         return (opts.get(key) or
  1009         return (opts.get(key) or
   362                 ui.configbool('diff', name or key, None, untrusted=untrusted))
  1010                 ui.configbool('diff', name or key, None, untrusted=untrusted))
   367         showfunc=get('show_function', 'showfunc'),
  1015         showfunc=get('show_function', 'showfunc'),
   368         ignorews=get('ignore_all_space', 'ignorews'),
  1016         ignorews=get('ignore_all_space', 'ignorews'),
   369         ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
  1017         ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
   370         ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
  1018         ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
   371 
  1019 
   372 def updatedir(ui, repo, patches, wlock=None):
  1020 def updatedir(ui, repo, patches):
   373     '''Update dirstate after patch application according to metadata'''
  1021     '''Update dirstate after patch application according to metadata'''
   374     if not patches:
  1022     if not patches:
   375         return
  1023         return
   376     copies = []
  1024     copies = []
   377     removes = {}
  1025     removes = {}
   389         elif ctype == 'DELETE':
  1037         elif ctype == 'DELETE':
   390             removes[gp.path] = 1
  1038             removes[gp.path] = 1
   391     for src, dst, after in copies:
  1039     for src, dst, after in copies:
   392         if not after:
  1040         if not after:
   393             copyfile(src, dst, repo.root)
  1041             copyfile(src, dst, repo.root)
   394         repo.copy(src, dst, wlock=wlock)
  1042         repo.copy(src, dst)
   395     removes = removes.keys()
  1043     removes = removes.keys()
   396     if removes:
  1044     if removes:
   397         removes.sort()
  1045         removes.sort()
   398         repo.remove(removes, True, wlock=wlock)
  1046         repo.remove(removes, True)
   399     for f in patches:
  1047     for f in patches:
   400         ctype, gp = patches[f]
  1048         ctype, gp = patches[f]
   401         if gp and gp.mode:
  1049         if gp and gp.mode:
   402             x = gp.mode & 0100 != 0
  1050             x = gp.mode & 0100 != 0
       
  1051             l = gp.mode & 020000 != 0
   403             dst = os.path.join(repo.root, gp.path)
  1052             dst = os.path.join(repo.root, gp.path)
   404             # patch won't create empty files
  1053             # patch won't create empty files
   405             if ctype == 'ADD' and not os.path.exists(dst):
  1054             if ctype == 'ADD' and not os.path.exists(dst):
   406                 repo.wwrite(gp.path, '', x and 'x' or '')
  1055                 repo.wwrite(gp.path, '', x and 'x' or '')
   407             else:
  1056             else:
   408                 util.set_exec(dst, x)
  1057                 util.set_link(dst, l)
   409     cmdutil.addremove(repo, cfiles, wlock=wlock)
  1058                 if not l:
       
  1059                     util.set_exec(dst, x)
       
  1060     cmdutil.addremove(repo, cfiles)
   410     files = patches.keys()
  1061     files = patches.keys()
   411     files.extend([r for r in removes if r not in files])
  1062     files.extend([r for r in removes if r not in files])
   412     files.sort()
  1063     files.sort()
   413 
  1064 
   414     return files
  1065     return files
   415 
  1066 
   416 def b85diff(fp, to, tn):
  1067 def b85diff(to, tn):
   417     '''print base85-encoded binary diff'''
  1068     '''print base85-encoded binary diff'''
   418     def gitindex(text):
  1069     def gitindex(text):
   419         if not text:
  1070         if not text:
   420             return '0' * 40
  1071             return '0' * 40
   421         l = len(text)
  1072         l = len(text)
   495         return
  1146         return
   496 
  1147 
   497     if node2:
  1148     if node2:
   498         ctx2 = context.changectx(repo, node2)
  1149         ctx2 = context.changectx(repo, node2)
   499         execf2 = ctx2.manifest().execf
  1150         execf2 = ctx2.manifest().execf
       
  1151         linkf2 = ctx2.manifest().linkf
   500     else:
  1152     else:
   501         ctx2 = context.workingctx(repo)
  1153         ctx2 = context.workingctx(repo)
   502         execf2 = util.execfunc(repo.root, None)
  1154         execf2 = util.execfunc(repo.root, None)
       
  1155         linkf2 = util.linkfunc(repo.root, None)
   503         if execf2 is None:
  1156         if execf2 is None:
   504             execf2 = ctx2.parents()[0].manifest().copy().execf
  1157             mc = ctx2.parents()[0].manifest().copy()
       
  1158             execf2 = mc.execf
       
  1159             linkf2 = mc.linkf
   505 
  1160 
   506     # returns False if there was no rename between ctx1 and ctx2
  1161     # returns False if there was no rename between ctx1 and ctx2
   507     # returns None if the file was created between ctx1 and ctx2
  1162     # returns None if the file was created between ctx1 and ctx2
   508     # returns the (file, node) present in ctx1 that was renamed to f in ctx2
  1163     # returns the (file, node) present in ctx1 that was renamed to f in ctx2
   509     def renamed(f):
  1164     def renamed(f):
   556         if f in man1:
  1211         if f in man1:
   557             to = getfilectx(f, ctx1).data()
  1212             to = getfilectx(f, ctx1).data()
   558         if f not in removed:
  1213         if f not in removed:
   559             tn = getfilectx(f, ctx2).data()
  1214             tn = getfilectx(f, ctx2).data()
   560         if opts.git:
  1215         if opts.git:
   561             def gitmode(x):
  1216             def gitmode(x, l):
   562                 return x and '100755' or '100644'
  1217                 return l and '120000' or (x and '100755' or '100644')
   563             def addmodehdr(header, omode, nmode):
  1218             def addmodehdr(header, omode, nmode):
   564                 if omode != nmode:
  1219                 if omode != nmode:
   565                     header.append('old mode %s\n' % omode)
  1220                     header.append('old mode %s\n' % omode)
   566                     header.append('new mode %s\n' % nmode)
  1221                     header.append('new mode %s\n' % nmode)
   567 
  1222 
   568             a, b = f, f
  1223             a, b = f, f
   569             if f in added:
  1224             if f in added:
   570                 mode = gitmode(execf2(f))
  1225                 mode = gitmode(execf2(f), linkf2(f))
   571                 if f in copied:
  1226                 if f in copied:
   572                     a = copied[f]
  1227                     a = copied[f]
   573                     omode = gitmode(man1.execf(a))
  1228                     omode = gitmode(man1.execf(a), man1.linkf(a))
   574                     addmodehdr(header, omode, mode)
  1229                     addmodehdr(header, omode, mode)
   575                     if a in removed and a not in gone:
  1230                     if a in removed and a not in gone:
   576                         op = 'rename'
  1231                         op = 'rename'
   577                         gone[a] = 1
  1232                         gone[a] = 1
   578                     else:
  1233                     else:
   586                     dodiff = 'binary'
  1241                     dodiff = 'binary'
   587             elif f in removed:
  1242             elif f in removed:
   588                 if f in srcs:
  1243                 if f in srcs:
   589                     dodiff = False
  1244                     dodiff = False
   590                 else:
  1245                 else:
   591                     mode = gitmode(man1.execf(f))
  1246                     mode = gitmode(man1.execf(f), man1.linkf(f))
   592                     header.append('deleted file mode %s\n' % mode)
  1247                     header.append('deleted file mode %s\n' % mode)
   593             else:
  1248             else:
   594                 omode = gitmode(man1.execf(f))
  1249                 omode = gitmode(man1.execf(f), man1.linkf(f))
   595                 nmode = gitmode(execf2(f))
  1250                 nmode = gitmode(execf2(f), linkf2(f))
   596                 addmodehdr(header, omode, nmode)
  1251                 addmodehdr(header, omode, nmode)
   597                 if util.binary(to) or util.binary(tn):
  1252                 if util.binary(to) or util.binary(tn):
   598                     dodiff = 'binary'
  1253                     dodiff = 'binary'
   599             r = None
  1254             r = None
   600             header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
  1255             header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
   601         if dodiff:
  1256         if dodiff:
   602             if dodiff == 'binary':
  1257             if dodiff == 'binary':
   603                 text = b85diff(fp, to, tn)
  1258                 text = b85diff(to, tn)
   604             else:
  1259             else:
   605                 text = mdiff.unidiff(to, date1,
  1260                 text = mdiff.unidiff(to, date1,
   606                                     # ctx2 date may be dynamic
  1261                                     # ctx2 date may be dynamic
   607                                     tn, util.datestr(ctx2.date()),
  1262                                     tn, util.datestr(ctx2.date()),
   608                                     f, r, opts=opts)
  1263                                     f, r, opts=opts)