mercurial/patch.py
changeset 14566 d0c2cc11e611
parent 14565 3cacc232f27f
child 14609 f53dc0787424
equal deleted inserted replaced
14565:3cacc232f27f 14566:d0c2cc11e611
   279     def setmode(self, mode):
   279     def setmode(self, mode):
   280         islink = mode & 020000
   280         islink = mode & 020000
   281         isexec = mode & 0100
   281         isexec = mode & 0100
   282         self.mode = (islink, isexec)
   282         self.mode = (islink, isexec)
   283 
   283 
       
   284     def copy(self):
       
   285         other = patchmeta(self.path)
       
   286         other.oldpath = self.oldpath
       
   287         other.mode = self.mode
       
   288         other.op = self.op
       
   289         other.binary = self.binary
       
   290         return other
       
   291 
   284     def __repr__(self):
   292     def __repr__(self):
   285         return "<patchmeta %s %r>" % (self.op, self.path)
   293         return "<patchmeta %s %r>" % (self.op, self.path)
   286 
   294 
   287 def readgitpatch(lr):
   295 def readgitpatch(lr):
   288     """extract git-style metadata about patches from <patchname>"""
   296     """extract git-style metadata about patches from <patchname>"""
   507 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
   515 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
   508 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
   516 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
   509 eolmodes = ['strict', 'crlf', 'lf', 'auto']
   517 eolmodes = ['strict', 'crlf', 'lf', 'auto']
   510 
   518 
   511 class patchfile(object):
   519 class patchfile(object):
   512     def __init__(self, ui, fname, backend, store, mode, create, remove,
   520     def __init__(self, ui, gp, backend, store, eolmode='strict'):
   513                  eolmode='strict', copysource=None):
   521         self.fname = gp.path
   514         self.fname = fname
       
   515         self.eolmode = eolmode
   522         self.eolmode = eolmode
   516         self.eol = None
   523         self.eol = None
   517         self.backend = backend
   524         self.backend = backend
   518         self.ui = ui
   525         self.ui = ui
   519         self.lines = []
   526         self.lines = []
   520         self.exists = False
   527         self.exists = False
   521         self.missing = True
   528         self.missing = True
   522         self.mode = mode
   529         self.mode = gp.mode
   523         self.copysource = copysource
   530         self.copysource = gp.oldpath
   524         self.create = create
   531         self.create = gp.op in ('ADD', 'COPY', 'RENAME')
   525         self.remove = remove
   532         self.remove = gp.op == 'DELETE'
   526         try:
   533         try:
   527             if copysource is None:
   534             if self.copysource is None:
   528                 data, mode = backend.getfile(fname)
   535                 data, mode = backend.getfile(self.fname)
   529                 self.exists = True
   536                 self.exists = True
   530             else:
   537             else:
   531                 data, mode = store.getfile(copysource)
   538                 data, mode = store.getfile(self.copysource)
   532                 self.exists = backend.exists(fname)
   539                 self.exists = backend.exists(self.fname)
   533             self.missing = False
   540             self.missing = False
   534             if data:
   541             if data:
   535                 self.lines = data.splitlines(True)
   542                 self.lines = data.splitlines(True)
   536             if self.mode is None:
   543             if self.mode is None:
   537                 self.mode = mode
   544                 self.mode = mode
   547                         if l.endswith('\r\n'):
   554                         if l.endswith('\r\n'):
   548                             l = l[:-2] + '\n'
   555                             l = l[:-2] + '\n'
   549                         nlines.append(l)
   556                         nlines.append(l)
   550                     self.lines = nlines
   557                     self.lines = nlines
   551         except IOError:
   558         except IOError:
   552             if create:
   559             if self.create:
   553                 self.missing = False
   560                 self.missing = False
   554             if self.mode is None:
   561             if self.mode is None:
   555                 self.mode = (False, False)
   562                 self.mode = (False, False)
   556         if self.missing:
   563         if self.missing:
   557              self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
   564              self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
  1014         while i < pathlen - 1 and path[i] == '/':
  1021         while i < pathlen - 1 and path[i] == '/':
  1015             i += 1
  1022             i += 1
  1016         count -= 1
  1023         count -= 1
  1017     return path[:i].lstrip(), path[i:].rstrip()
  1024     return path[:i].lstrip(), path[i:].rstrip()
  1018 
  1025 
  1019 def selectfile(backend, afile_orig, bfile_orig, hunk, strip, gp):
  1026 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
  1020     if gp:
       
  1021         # Git patches do not play games. Excluding copies from the
       
  1022         # following heuristic avoids a lot of confusion
       
  1023         fname = pathstrip(gp.path, strip - 1)[1]
       
  1024         create = gp.op in ('ADD', 'COPY', 'RENAME')
       
  1025         remove = gp.op == 'DELETE'
       
  1026         return fname, create, remove
       
  1027     nulla = afile_orig == "/dev/null"
  1027     nulla = afile_orig == "/dev/null"
  1028     nullb = bfile_orig == "/dev/null"
  1028     nullb = bfile_orig == "/dev/null"
  1029     create = nulla and hunk.starta == 0 and hunk.lena == 0
  1029     create = nulla and hunk.starta == 0 and hunk.lena == 0
  1030     remove = nullb and hunk.startb == 0 and hunk.lenb == 0
  1030     remove = nullb and hunk.startb == 0 and hunk.lenb == 0
  1031     abase, afile = pathstrip(afile_orig, strip)
  1031     abase, afile = pathstrip(afile_orig, strip)
  1063         elif not nulla:
  1063         elif not nulla:
  1064             fname = afile
  1064             fname = afile
  1065         else:
  1065         else:
  1066             raise PatchError(_("undefined source and destination files"))
  1066             raise PatchError(_("undefined source and destination files"))
  1067 
  1067 
  1068     return fname, create, remove
  1068     gp = patchmeta(fname)
       
  1069     if create:
       
  1070         gp.op = 'ADD'
       
  1071     elif remove:
       
  1072         gp.op = 'DELETE'
       
  1073     return gp
  1069 
  1074 
  1070 def scangitpatch(lr, firstline):
  1075 def scangitpatch(lr, firstline):
  1071     """
  1076     """
  1072     Git patches can emit:
  1077     Git patches can emit:
  1073     - rename a to b
  1078     - rename a to b
  1132                     context = True
  1137                     context = True
  1133                 h = hunk(x, hunknum + 1, lr, context)
  1138                 h = hunk(x, hunknum + 1, lr, context)
  1134             hunknum += 1
  1139             hunknum += 1
  1135             if emitfile:
  1140             if emitfile:
  1136                 emitfile = False
  1141                 emitfile = False
  1137                 yield 'file', (afile, bfile, h, gp)
  1142                 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
  1138             yield 'hunk', h
  1143             yield 'hunk', h
  1139         elif x.startswith('diff --git'):
  1144         elif x.startswith('diff --git'):
  1140             m = gitre.match(x)
  1145             m = gitre.match(x)
  1141             if not m:
  1146             if not m:
  1142                 continue
  1147                 continue
  1143             if gitpatches is None:
  1148             if gitpatches is None:
  1144                 # scan whole input for git metadata
  1149                 # scan whole input for git metadata
  1145                 gitpatches = [('a/' + gp.path, 'b/' + gp.path, gp) for gp
  1150                 gitpatches = [('a/' + gp.path, 'b/' + gp.path, gp) for gp
  1146                               in scangitpatch(lr, x)]
  1151                               in scangitpatch(lr, x)]
  1147                 yield 'git', [g[2] for g in gitpatches
  1152                 yield 'git', [g[2].copy() for g in gitpatches
  1148                               if g[2].op in ('COPY', 'RENAME')]
  1153                               if g[2].op in ('COPY', 'RENAME')]
  1149                 gitpatches.reverse()
  1154                 gitpatches.reverse()
  1150             afile = 'a/' + m.group(1)
  1155             afile = 'a/' + m.group(1)
  1151             bfile = 'b/' + m.group(2)
  1156             bfile = 'b/' + m.group(2)
  1152             while afile != gitpatches[-1][0] and bfile != gitpatches[-1][1]:
  1157             while afile != gitpatches[-1][0] and bfile != gitpatches[-1][1]:
  1153                 gp = gitpatches.pop()[2]
  1158                 gp = gitpatches.pop()[2]
  1154                 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
  1159                 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
  1155             gp = gitpatches[-1][2]
  1160             gp = gitpatches[-1][2]
  1156             # copy/rename + modify should modify target, not source
  1161             # copy/rename + modify should modify target, not source
  1157             if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
  1162             if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
  1158                 afile = bfile
  1163                 afile = bfile
  1159             newfile = True
  1164             newfile = True
  1189             state = BFILE
  1194             state = BFILE
  1190             hunknum = 0
  1195             hunknum = 0
  1191 
  1196 
  1192     while gitpatches:
  1197     while gitpatches:
  1193         gp = gitpatches.pop()[2]
  1198         gp = gitpatches.pop()[2]
  1194         yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
  1199         yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
  1195 
  1200 
  1196 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
  1201 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
  1197     """Reads a patch from fp and tries to apply it.
  1202     """Reads a patch from fp and tries to apply it.
  1198 
  1203 
  1199     Returns 0 for a clean patch, -1 if any rejects were found and 1 if
  1204     Returns 0 for a clean patch, -1 if any rejects were found and 1 if
  1226         elif state == 'file':
  1231         elif state == 'file':
  1227             if current_file:
  1232             if current_file:
  1228                 rejects += current_file.close()
  1233                 rejects += current_file.close()
  1229                 current_file = None
  1234                 current_file = None
  1230             afile, bfile, first_hunk, gp = values
  1235             afile, bfile, first_hunk, gp = values
  1231             copysource = None
       
  1232             if gp:
  1236             if gp:
  1233                 path = pstrip(gp.path)
  1237                 path = pstrip(gp.path)
       
  1238                 gp.path = pstrip(gp.path)
  1234                 if gp.oldpath:
  1239                 if gp.oldpath:
  1235                     copysource = pstrip(gp.oldpath)
  1240                     gp.oldpath = pstrip(gp.oldpath)
  1236                 if gp.op == 'RENAME':
  1241             else:
  1237                     backend.unlink(copysource)
  1242                 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
  1238                 if not first_hunk:
  1243             if gp.op == 'RENAME':
  1239                     if gp.op == 'DELETE':
  1244                 backend.unlink(gp.oldpath)
  1240                         backend.unlink(path)
       
  1241                         continue
       
  1242                     data, mode = None, None
       
  1243                     if gp.op in ('RENAME', 'COPY'):
       
  1244                         data, mode = store.getfile(copysource)
       
  1245                     if gp.mode:
       
  1246                         mode = gp.mode
       
  1247                         if gp.op == 'ADD':
       
  1248                             # Added files without content have no hunk and
       
  1249                             # must be created
       
  1250                             data = ''
       
  1251                     if data or mode:
       
  1252                         if (gp.op in ('ADD', 'RENAME', 'COPY')
       
  1253                             and backend.exists(path)):
       
  1254                             raise PatchError(_("cannot create %s: destination "
       
  1255                                                "already exists") % path)
       
  1256                         backend.setfile(path, data, mode, copysource)
       
  1257             if not first_hunk:
  1245             if not first_hunk:
       
  1246                 if gp.op == 'DELETE':
       
  1247                     backend.unlink(gp.path)
       
  1248                     continue
       
  1249                 data, mode = None, None
       
  1250                 if gp.op in ('RENAME', 'COPY'):
       
  1251                     data, mode = store.getfile(gp.oldpath)
       
  1252                 if gp.mode:
       
  1253                     mode = gp.mode
       
  1254                     if gp.op == 'ADD':
       
  1255                         # Added files without content have no hunk and
       
  1256                         # must be created
       
  1257                         data = ''
       
  1258                 if data or mode:
       
  1259                     if (gp.op in ('ADD', 'RENAME', 'COPY')
       
  1260                         and backend.exists(gp.path)):
       
  1261                         raise PatchError(_("cannot create %s: destination "
       
  1262                                            "already exists") % gp.path)
       
  1263                     backend.setfile(gp.path, data, mode, gp.oldpath)
  1258                 continue
  1264                 continue
  1259             try:
  1265             try:
  1260                 mode = gp and gp.mode or None
  1266                 current_file = patcher(ui, gp, backend, store,
  1261                 current_file, create, remove = selectfile(
  1267                                        eolmode=eolmode)
  1262                     backend, afile, bfile, first_hunk, strip, gp)
       
  1263                 current_file = patcher(ui, current_file, backend, store, mode,
       
  1264                                        create, remove, eolmode=eolmode,
       
  1265                                        copysource=copysource)
       
  1266             except PatchError, inst:
  1268             except PatchError, inst:
  1267                 ui.warn(str(inst) + '\n')
  1269                 ui.warn(str(inst) + '\n')
  1268                 current_file = None
  1270                 current_file = None
  1269                 rejects += 1
  1271                 rejects += 1
  1270                 continue
  1272                 continue
  1393         changed = set()
  1395         changed = set()
  1394         for state, values in iterhunks(fp):
  1396         for state, values in iterhunks(fp):
  1395             if state == 'file':
  1397             if state == 'file':
  1396                 afile, bfile, first_hunk, gp = values
  1398                 afile, bfile, first_hunk, gp = values
  1397                 if gp:
  1399                 if gp:
  1398                     changed.add(pathstrip(gp.path, strip - 1)[1])
  1400                     gp.path = pathstrip(gp.path, strip - 1)[1]
  1399                     if gp.op == 'RENAME':
  1401                     if gp.oldpath:
  1400                         changed.add(pathstrip(gp.oldpath, strip - 1)[1])
  1402                         gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
  1401                 if not first_hunk:
  1403                 else:
  1402                     continue
  1404                     gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
  1403                 current_file, create, remove = selectfile(
  1405                 changed.add(gp.path)
  1404                     backend, afile, bfile, first_hunk, strip, gp)
  1406                 if gp.op == 'RENAME':
  1405                 changed.add(current_file)
  1407                     changed.add(gp.oldpath)
  1406             elif state not in ('hunk', 'git'):
  1408             elif state not in ('hunk', 'git'):
  1407                 raise util.Abort(_('unsupported parser state: %s') % state)
  1409                 raise util.Abort(_('unsupported parser state: %s') % state)
  1408         return changed
  1410         return changed
  1409     finally:
  1411     finally:
  1410         fp.close()
  1412         fp.close()