mercurial/patch.py
changeset 14452 ee574cfd0c32
parent 14451 c78d41db6f88
child 14453 ea3d548132cc
equal deleted inserted replaced
14451:c78d41db6f88 14452:ee574cfd0c32
     5 #
     5 #
     6 # This software may be used and distributed according to the terms of the
     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.
     7 # GNU General Public License version 2 or any later version.
     8 
     8 
     9 import cStringIO, email.Parser, os, errno, re
     9 import cStringIO, email.Parser, os, errno, re
    10 import tempfile, zlib
    10 import tempfile, zlib, shutil
    11 
    11 
    12 from i18n import _
    12 from i18n import _
    13 from node import hex, nullid, short
    13 from node import hex, nullid, short
    14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
    14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
    15 
    15 
   360         """Return target file data and flags as a (data, (islink,
   360         """Return target file data and flags as a (data, (islink,
   361         isexec)) tuple.
   361         isexec)) tuple.
   362         """
   362         """
   363         raise NotImplementedError
   363         raise NotImplementedError
   364 
   364 
   365     def setfile(self, fname, data, mode):
   365     def setfile(self, fname, data, mode, copysource):
   366         """Write data to target file fname and set its mode. mode is a
   366         """Write data to target file fname and set its mode. mode is a
   367         (islink, isexec) tuple. If data is None, the file content should
   367         (islink, isexec) tuple. If data is None, the file content should
   368         be left unchanged.
   368         be left unchanged. If the file is modified after being copied,
       
   369         copysource is set to the original file name.
   369         """
   370         """
   370         raise NotImplementedError
   371         raise NotImplementedError
   371 
   372 
   372     def unlink(self, fname):
   373     def unlink(self, fname):
   373         """Unlink target file."""
   374         """Unlink target file."""
   377         """Write rejected lines for fname. total is the number of hunks
   378         """Write rejected lines for fname. total is the number of hunks
   378         which failed to apply and total the total number of hunks for this
   379         which failed to apply and total the total number of hunks for this
   379         files.
   380         files.
   380         """
   381         """
   381         pass
   382         pass
   382 
       
   383     def copy(self, src, dst):
       
   384         """Copy src file into dst file. Create intermediate directories if
       
   385         necessary. Files are specified relatively to the patching base
       
   386         directory.
       
   387         """
       
   388         raise NotImplementedError
       
   389 
   383 
   390     def exists(self, fname):
   384     def exists(self, fname):
   391         raise NotImplementedError
   385         raise NotImplementedError
   392 
   386 
   393 class fsbackend(abstractbackend):
   387 class fsbackend(abstractbackend):
   409         except OSError, e:
   403         except OSError, e:
   410             if e.errno != errno.ENOENT:
   404             if e.errno != errno.ENOENT:
   411                 raise
   405                 raise
   412         return (self.opener.read(fname), (islink, isexec))
   406         return (self.opener.read(fname), (islink, isexec))
   413 
   407 
   414     def setfile(self, fname, data, mode):
   408     def setfile(self, fname, data, mode, copysource):
   415         islink, isexec = mode
   409         islink, isexec = mode
   416         if data is None:
   410         if data is None:
   417             util.setflags(self._join(fname), islink, isexec)
   411             util.setflags(self._join(fname), islink, isexec)
   418             return
   412             return
   419         if islink:
   413         if islink:
   437             (failed, total, fname))
   431             (failed, total, fname))
   438         fp = self.opener(fname, 'w')
   432         fp = self.opener(fname, 'w')
   439         fp.writelines(lines)
   433         fp.writelines(lines)
   440         fp.close()
   434         fp.close()
   441 
   435 
   442     def copy(self, src, dst):
       
   443         basedir = self.opener.base
       
   444         abssrc, absdst = [scmutil.canonpath(basedir, basedir, x)
       
   445                           for x in [src, dst]]
       
   446         if os.path.lexists(absdst):
       
   447             raise util.Abort(_("cannot create %s: destination already exists")
       
   448                              % dst)
       
   449         dstdir = os.path.dirname(absdst)
       
   450         if dstdir and not os.path.isdir(dstdir):
       
   451             try:
       
   452                 os.makedirs(dstdir)
       
   453             except IOError:
       
   454                 raise util.Abort(
       
   455                     _("cannot create %s: unable to create destination directory")
       
   456                     % dst)
       
   457         util.copyfile(abssrc, absdst)
       
   458 
       
   459     def exists(self, fname):
   436     def exists(self, fname):
   460         return os.path.lexists(self._join(fname))
   437         return os.path.lexists(self._join(fname))
   461 
   438 
   462 class workingbackend(fsbackend):
   439 class workingbackend(fsbackend):
   463     def __init__(self, ui, repo, similarity):
   440     def __init__(self, ui, repo, similarity):
   466         self.similarity = similarity
   443         self.similarity = similarity
   467         self.removed = set()
   444         self.removed = set()
   468         self.changed = set()
   445         self.changed = set()
   469         self.copied = []
   446         self.copied = []
   470 
   447 
   471     def setfile(self, fname, data, mode):
   448     def setfile(self, fname, data, mode, copysource):
   472         super(workingbackend, self).setfile(fname, data, mode)
   449         super(workingbackend, self).setfile(fname, data, mode, copysource)
       
   450         if copysource is not None:
       
   451             self.copied.append((copysource, fname))
   473         self.changed.add(fname)
   452         self.changed.add(fname)
   474 
   453 
   475     def unlink(self, fname):
   454     def unlink(self, fname):
   476         super(workingbackend, self).unlink(fname)
   455         super(workingbackend, self).unlink(fname)
   477         self.removed.add(fname)
   456         self.removed.add(fname)
   478         self.changed.add(fname)
   457         self.changed.add(fname)
   479 
       
   480     def copy(self, src, dst):
       
   481         super(workingbackend, self).copy(src, dst)
       
   482         self.copied.append((src, dst))
       
   483         self.changed.add(dst)
       
   484 
   458 
   485     def close(self):
   459     def close(self):
   486         wctx = self.repo[None]
   460         wctx = self.repo[None]
   487         addremoved = set(self.changed)
   461         addremoved = set(self.changed)
   488         for src, dst in self.copied:
   462         for src, dst in self.copied:
   496                 addremoved = [util.pathto(self.repo.root, cwd, f)
   470                 addremoved = [util.pathto(self.repo.root, cwd, f)
   497                               for f in addremoved]
   471                               for f in addremoved]
   498             scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
   472             scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
   499         return sorted(self.changed)
   473         return sorted(self.changed)
   500 
   474 
       
   475 class filestore(object):
       
   476     def __init__(self):
       
   477         self.opener = None
       
   478         self.files = {}
       
   479         self.created = 0
       
   480 
       
   481     def setfile(self, fname, data, mode):
       
   482         if self.opener is None:
       
   483             root = tempfile.mkdtemp(prefix='hg-patch-')
       
   484             self.opener = scmutil.opener(root)
       
   485         # Avoid filename issues with these simple names
       
   486         fn = str(self.created)
       
   487         self.opener.write(fn, data)
       
   488         self.created += 1
       
   489         self.files[fname] = (fn, mode)
       
   490 
       
   491     def getfile(self, fname):
       
   492         if fname not in self.files:
       
   493             raise IOError()
       
   494         fn, mode = self.files[fname]
       
   495         return self.opener.read(fn), mode
       
   496 
       
   497     def close(self):
       
   498         if self.opener:
       
   499             shutil.rmtree(self.opener.base)
       
   500 
   501 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
   501 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
   502 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
   502 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
   503 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
   503 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
   504 eolmodes = ['strict', 'crlf', 'lf', 'auto']
   504 eolmodes = ['strict', 'crlf', 'lf', 'auto']
   505 
   505 
   506 class patchfile(object):
   506 class patchfile(object):
   507     def __init__(self, ui, fname, backend, mode, create, remove, missing=False,
   507     def __init__(self, ui, fname, backend, store, mode, create, remove,
   508                  eolmode='strict'):
   508                  eolmode='strict', copysource=None):
   509         self.fname = fname
   509         self.fname = fname
   510         self.eolmode = eolmode
   510         self.eolmode = eolmode
   511         self.eol = None
   511         self.eol = None
   512         self.backend = backend
   512         self.backend = backend
   513         self.ui = ui
   513         self.ui = ui
   514         self.lines = []
   514         self.lines = []
   515         self.exists = False
   515         self.exists = False
   516         self.missing = missing
   516         self.missing = True
   517         self.mode = mode
   517         self.mode = mode
       
   518         self.copysource = copysource
   518         self.create = create
   519         self.create = create
   519         self.remove = remove
   520         self.remove = remove
   520         if not missing:
   521         try:
   521             try:
   522             if copysource is None:
   522                 data, mode = self.backend.getfile(fname)
   523                 data, mode = backend.getfile(fname)
   523                 if data:
       
   524                     self.lines = data.splitlines(True)
       
   525                 if self.mode is None:
       
   526                     self.mode = mode
       
   527                 if self.lines:
       
   528                     # Normalize line endings
       
   529                     if self.lines[0].endswith('\r\n'):
       
   530                         self.eol = '\r\n'
       
   531                     elif self.lines[0].endswith('\n'):
       
   532                         self.eol = '\n'
       
   533                     if eolmode != 'strict':
       
   534                         nlines = []
       
   535                         for l in self.lines:
       
   536                             if l.endswith('\r\n'):
       
   537                                 l = l[:-2] + '\n'
       
   538                             nlines.append(l)
       
   539                         self.lines = nlines
       
   540                 self.exists = True
   524                 self.exists = True
   541             except IOError:
   525             else:
   542                 if self.mode is None:
   526                 data, mode = store.getfile(copysource)
   543                     self.mode = (False, False)
   527                 self.exists = backend.exists(fname)
   544         else:
   528             self.missing = False
   545             self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
   529             if data:
       
   530                 self.lines = data.splitlines(True)
       
   531             if self.mode is None:
       
   532                 self.mode = mode
       
   533             if self.lines:
       
   534                 # Normalize line endings
       
   535                 if self.lines[0].endswith('\r\n'):
       
   536                     self.eol = '\r\n'
       
   537                 elif self.lines[0].endswith('\n'):
       
   538                     self.eol = '\n'
       
   539                 if eolmode != 'strict':
       
   540                     nlines = []
       
   541                     for l in self.lines:
       
   542                         if l.endswith('\r\n'):
       
   543                             l = l[:-2] + '\n'
       
   544                         nlines.append(l)
       
   545                     self.lines = nlines
       
   546         except IOError:
       
   547             if create:
       
   548                 self.missing = False
       
   549             if self.mode is None:
       
   550                 self.mode = (False, False)
       
   551         if self.missing:
       
   552              self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
   546 
   553 
   547         self.hash = {}
   554         self.hash = {}
   548         self.dirty = 0
   555         self.dirty = 0
   549         self.offset = 0
   556         self.offset = 0
   550         self.skew = 0
   557         self.skew = 0
   567                 if l and l[-1] == '\n':
   574                 if l and l[-1] == '\n':
   568                     l = l[:-1] + eol
   575                     l = l[:-1] + eol
   569                 rawlines.append(l)
   576                 rawlines.append(l)
   570             lines = rawlines
   577             lines = rawlines
   571 
   578 
   572         self.backend.setfile(fname, ''.join(lines), mode)
   579         self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
   573 
   580 
   574     def printfile(self, warn):
   581     def printfile(self, warn):
   575         if self.fileprinted:
   582         if self.fileprinted:
   576             return
   583             return
   577         if warn or self.ui.verbose:
   584         if warn or self.ui.verbose:
   621         if self.missing:
   628         if self.missing:
   622             self.rej.append(h)
   629             self.rej.append(h)
   623             return -1
   630             return -1
   624 
   631 
   625         if self.exists and self.create:
   632         if self.exists and self.create:
   626             self.ui.warn(_("file %s already exists\n") % self.fname)
   633             if self.copysource:
       
   634                 self.ui.warn(_("cannot create %s: destination already "
       
   635                                "exists\n" % self.fname))
       
   636             else:
       
   637                 self.ui.warn(_("file %s already exists\n") % self.fname)
   627             self.rej.append(h)
   638             self.rej.append(h)
   628             return -1
   639             return -1
   629 
   640 
   630         if isinstance(h, binhunk):
   641         if isinstance(h, binhunk):
   631             if self.remove:
   642             if self.remove:
  1003 def selectfile(backend, afile_orig, bfile_orig, hunk, strip, gp):
  1014 def selectfile(backend, afile_orig, bfile_orig, hunk, strip, gp):
  1004     if gp:
  1015     if gp:
  1005         # Git patches do not play games. Excluding copies from the
  1016         # Git patches do not play games. Excluding copies from the
  1006         # following heuristic avoids a lot of confusion
  1017         # following heuristic avoids a lot of confusion
  1007         fname = pathstrip(gp.path, strip - 1)[1]
  1018         fname = pathstrip(gp.path, strip - 1)[1]
  1008         create = gp.op == 'ADD'
  1019         create = gp.op in ('ADD', 'COPY', 'RENAME')
  1009         remove = gp.op == 'DELETE'
  1020         remove = gp.op == 'DELETE'
  1010         missing = not create and not backend.exists(fname)
  1021         missing = not create and not backend.exists(fname)
  1011         return fname, missing, create, remove
  1022         return fname, create, remove
  1012     nulla = afile_orig == "/dev/null"
  1023     nulla = afile_orig == "/dev/null"
  1013     nullb = bfile_orig == "/dev/null"
  1024     nullb = bfile_orig == "/dev/null"
  1014     create = nulla and hunk.starta == 0 and hunk.lena == 0
  1025     create = nulla and hunk.starta == 0 and hunk.lena == 0
  1015     remove = nullb and hunk.startb == 0 and hunk.lenb == 0
  1026     remove = nullb and hunk.startb == 0 and hunk.lenb == 0
  1016     abase, afile = pathstrip(afile_orig, strip)
  1027     abase, afile = pathstrip(afile_orig, strip)
  1048         elif not nulla:
  1059         elif not nulla:
  1049             fname = afile
  1060             fname = afile
  1050         else:
  1061         else:
  1051             raise PatchError(_("undefined source and destination files"))
  1062             raise PatchError(_("undefined source and destination files"))
  1052 
  1063 
  1053     return fname, missing, create, remove
  1064     return fname, create, remove
  1054 
  1065 
  1055 def scangitpatch(lr, firstline):
  1066 def scangitpatch(lr, firstline):
  1056     """
  1067     """
  1057     Git patches can emit:
  1068     Git patches can emit:
  1058     - rename a to b
  1069     - rename a to b
  1175 
  1186 
  1176     while gitpatches:
  1187     while gitpatches:
  1177         gp = gitpatches.pop()[1]
  1188         gp = gitpatches.pop()[1]
  1178         yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
  1189         yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
  1179 
  1190 
  1180 def applydiff(ui, fp, changed, backend, strip=1, eolmode='strict'):
  1191 def applydiff(ui, fp, changed, backend, store, strip=1, eolmode='strict'):
  1181     """Reads a patch from fp and tries to apply it.
  1192     """Reads a patch from fp and tries to apply it.
  1182 
  1193 
  1183     The dict 'changed' is filled in with all of the filenames changed
  1194     The dict 'changed' is filled in with all of the filenames changed
  1184     by the patch. Returns 0 for a clean patch, -1 if any rejects were
  1195     by the patch. Returns 0 for a clean patch, -1 if any rejects were
  1185     found and 1 if there was any fuzz.
  1196     found and 1 if there was any fuzz.
  1186 
  1197 
  1187     If 'eolmode' is 'strict', the patch content and patched file are
  1198     If 'eolmode' is 'strict', the patch content and patched file are
  1188     read in binary mode. Otherwise, line endings are ignored when
  1199     read in binary mode. Otherwise, line endings are ignored when
  1189     patching then normalized according to 'eolmode'.
  1200     patching then normalized according to 'eolmode'.
  1190     """
  1201     """
  1191     return _applydiff(ui, fp, patchfile, backend, changed, strip=strip,
  1202     return _applydiff(ui, fp, patchfile, backend, store, changed, strip=strip,
  1192                       eolmode=eolmode)
  1203                       eolmode=eolmode)
  1193 
  1204 
  1194 def _applydiff(ui, fp, patcher, backend, changed, strip=1, eolmode='strict'):
  1205 def _applydiff(ui, fp, patcher, backend, store, changed, strip=1,
       
  1206                eolmode='strict'):
  1195 
  1207 
  1196     def pstrip(p):
  1208     def pstrip(p):
  1197         return pathstrip(p, strip - 1)[1]
  1209         return pathstrip(p, strip - 1)[1]
  1198 
  1210 
  1199     rejects = 0
  1211     rejects = 0
  1212         elif state == 'file':
  1224         elif state == 'file':
  1213             if current_file:
  1225             if current_file:
  1214                 rejects += current_file.close()
  1226                 rejects += current_file.close()
  1215                 current_file = None
  1227                 current_file = None
  1216             afile, bfile, first_hunk, gp = values
  1228             afile, bfile, first_hunk, gp = values
       
  1229             copysource = None
  1217             if gp:
  1230             if gp:
  1218                 path = pstrip(gp.path)
  1231                 path = pstrip(gp.path)
       
  1232                 if gp.oldpath:
       
  1233                     copysource = pstrip(gp.oldpath)
  1219                 changed[path] = gp
  1234                 changed[path] = gp
  1220                 if gp.op == 'DELETE':
  1235                 if gp.op == 'DELETE':
  1221                     backend.unlink(path)
  1236                     backend.unlink(path)
  1222                     continue
  1237                     continue
  1223                 if gp.op == 'RENAME':
  1238                 if gp.op == 'RENAME':
  1224                     backend.unlink(pstrip(gp.oldpath))
  1239                     backend.unlink(copysource)
  1225                 if gp.mode and not first_hunk:
  1240                 if not first_hunk:
  1226                     data = None
  1241                     data, mode = None, None
  1227                     if gp.op == 'ADD':
  1242                     if gp.op in ('RENAME', 'COPY'):
  1228                         # Added files without content have no hunk and
  1243                         data, mode = store.getfile(copysource)
  1229                         # must be created
  1244                     if gp.mode:
  1230                         data = ''
  1245                         mode = gp.mode
  1231                     backend.setfile(path, data, gp.mode)
  1246                         if gp.op == 'ADD':
       
  1247                             # Added files without content have no hunk and
       
  1248                             # must be created
       
  1249                             data = ''
       
  1250                     if data or mode:
       
  1251                         if (gp.op in ('ADD', 'RENAME', 'COPY')
       
  1252                             and backend.exists(path)):
       
  1253                             raise PatchError(_("cannot create %s: destination "
       
  1254                                                "already exists") % path)
       
  1255                         backend.setfile(path, data, mode, copysource)
  1232             if not first_hunk:
  1256             if not first_hunk:
  1233                 continue
  1257                 continue
  1234             try:
  1258             try:
  1235                 mode = gp and gp.mode or None
  1259                 mode = gp and gp.mode or None
  1236                 current_file, missing, create, remove = selectfile(
  1260                 current_file, create, remove = selectfile(
  1237                     backend, afile, bfile, first_hunk, strip, gp)
  1261                     backend, afile, bfile, first_hunk, strip, gp)
  1238                 current_file = patcher(ui, current_file, backend, mode,
  1262                 current_file = patcher(ui, current_file, backend, store, mode,
  1239                                        create, remove, missing=missing,
  1263                                        create, remove, eolmode=eolmode,
  1240                                        eolmode=eolmode)
  1264                                        copysource=copysource)
  1241             except PatchError, inst:
  1265             except PatchError, inst:
  1242                 ui.warn(str(inst) + '\n')
  1266                 ui.warn(str(inst) + '\n')
  1243                 current_file = None
  1267                 current_file = None
  1244                 rejects += 1
  1268                 rejects += 1
  1245                 continue
  1269                 continue
  1246         elif state == 'git':
  1270         elif state == 'git':
  1247             for gp in values:
  1271             for gp in values:
  1248                 backend.copy(pstrip(gp.oldpath), pstrip(gp.path))
  1272                 path = pstrip(gp.oldpath)
       
  1273                 data, mode = backend.getfile(path)
       
  1274                 store.setfile(path, data, mode)
  1249         else:
  1275         else:
  1250             raise util.Abort(_('unsupported parser state: %s') % state)
  1276             raise util.Abort(_('unsupported parser state: %s') % state)
  1251 
  1277 
  1252     if current_file:
  1278     if current_file:
  1253         rejects += current_file.close()
  1279         rejects += current_file.close()
  1314         eolmode = ui.config('patch', 'eol', 'strict')
  1340         eolmode = ui.config('patch', 'eol', 'strict')
  1315     if eolmode.lower() not in eolmodes:
  1341     if eolmode.lower() not in eolmodes:
  1316         raise util.Abort(_('unsupported line endings type: %s') % eolmode)
  1342         raise util.Abort(_('unsupported line endings type: %s') % eolmode)
  1317     eolmode = eolmode.lower()
  1343     eolmode = eolmode.lower()
  1318 
  1344 
       
  1345     store = filestore()
  1319     backend = workingbackend(ui, repo, similarity)
  1346     backend = workingbackend(ui, repo, similarity)
  1320     try:
  1347     try:
  1321         fp = open(patchobj, 'rb')
  1348         fp = open(patchobj, 'rb')
  1322     except TypeError:
  1349     except TypeError:
  1323         fp = patchobj
  1350         fp = patchobj
  1324     try:
  1351     try:
  1325         ret = applydiff(ui, fp, files, backend, strip=strip, eolmode=eolmode)
  1352         ret = applydiff(ui, fp, files, backend, store, strip=strip,
       
  1353                         eolmode=eolmode)
  1326     finally:
  1354     finally:
  1327         if fp != patchobj:
  1355         if fp != patchobj:
  1328             fp.close()
  1356             fp.close()
  1329         files.update(dict.fromkeys(backend.close()))
  1357         files.update(dict.fromkeys(backend.close()))
       
  1358         store.close()
  1330     if ret < 0:
  1359     if ret < 0:
  1331         raise PatchError(_('patch failed to apply'))
  1360         raise PatchError(_('patch failed to apply'))
  1332     return ret > 0
  1361     return ret > 0
  1333 
  1362 
  1334 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
  1363 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
  1368                     changed.add(pathstrip(gp.path, strip - 1)[1])
  1397                     changed.add(pathstrip(gp.path, strip - 1)[1])
  1369                     if gp.op == 'RENAME':
  1398                     if gp.op == 'RENAME':
  1370                         changed.add(pathstrip(gp.oldpath, strip - 1)[1])
  1399                         changed.add(pathstrip(gp.oldpath, strip - 1)[1])
  1371                 if not first_hunk:
  1400                 if not first_hunk:
  1372                     continue
  1401                     continue
  1373                 current_file, missing, create, remove = selectfile(
  1402                 current_file, create, remove = selectfile(
  1374                     backend, afile, bfile, first_hunk, strip, gp)
  1403                     backend, afile, bfile, first_hunk, strip, gp)
  1375                 changed.add(current_file)
  1404                 changed.add(current_file)
  1376             elif state not in ('hunk', 'git'):
  1405             elif state not in ('hunk', 'git'):
  1377                 raise util.Abort(_('unsupported parser state: %s') % state)
  1406                 raise util.Abort(_('unsupported parser state: %s') % state)
  1378         return changed
  1407         return changed