##// END OF EJS Templates
patch: use temporary files to handle intermediate copies...
Patrick Mezard -
r14452:ee574cfd default
parent child Browse files
Show More
@@ -595,11 +595,12 b' def reposetup(ui, repo):'
595 595 wlock.release()
596 596
597 597 # monkeypatches
598 def kwpatchfile_init(orig, self, ui, fname, backend, mode, create, remove,
599 missing=False, eolmode=None):
598 def kwpatchfile_init(orig, self, ui, fname, backend, store, mode, create,
599 remove, eolmode=None, copysource=None):
600 600 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
601 601 rejects or conflicts due to expanded keywords in working dir.'''
602 orig(self, ui, fname, backend, mode, create, remove, missing, eolmode)
602 orig(self, ui, fname, backend, store, mode, create, remove,
603 eolmode, copysource)
603 604 # shrink keywords read from working dir
604 605 self.lines = kwt.shrinklines(self.fname, self.lines)
605 606
@@ -7,7 +7,7 b''
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import cStringIO, email.Parser, os, errno, re
10 import tempfile, zlib
10 import tempfile, zlib, shutil
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
@@ -362,10 +362,11 b' class abstractbackend(object):'
362 362 """
363 363 raise NotImplementedError
364 364
365 def setfile(self, fname, data, mode):
365 def setfile(self, fname, data, mode, copysource):
366 366 """Write data to target file fname and set its mode. mode is a
367 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 371 raise NotImplementedError
371 372
@@ -380,13 +381,6 b' class abstractbackend(object):'
380 381 """
381 382 pass
382 383
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
390 384 def exists(self, fname):
391 385 raise NotImplementedError
392 386
@@ -411,7 +405,7 b' class fsbackend(abstractbackend):'
411 405 raise
412 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 409 islink, isexec = mode
416 410 if data is None:
417 411 util.setflags(self._join(fname), islink, isexec)
@@ -439,23 +433,6 b' class fsbackend(abstractbackend):'
439 433 fp.writelines(lines)
440 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 436 def exists(self, fname):
460 437 return os.path.lexists(self._join(fname))
461 438
@@ -468,8 +445,10 b' class workingbackend(fsbackend):'
468 445 self.changed = set()
469 446 self.copied = []
470 447
471 def setfile(self, fname, data, mode):
472 super(workingbackend, self).setfile(fname, data, mode)
448 def setfile(self, fname, data, mode, copysource):
449 super(workingbackend, self).setfile(fname, data, mode, copysource)
450 if copysource is not None:
451 self.copied.append((copysource, fname))
473 452 self.changed.add(fname)
474 453
475 454 def unlink(self, fname):
@@ -477,11 +456,6 b' class workingbackend(fsbackend):'
477 456 self.removed.add(fname)
478 457 self.changed.add(fname)
479 458
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
485 459 def close(self):
486 460 wctx = self.repo[None]
487 461 addremoved = set(self.changed)
@@ -498,14 +472,40 b' class workingbackend(fsbackend):'
498 472 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
499 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 501 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
502 502 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
503 503 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
504 504 eolmodes = ['strict', 'crlf', 'lf', 'auto']
505 505
506 506 class patchfile(object):
507 def __init__(self, ui, fname, backend, mode, create, remove, missing=False,
508 eolmode='strict'):
507 def __init__(self, ui, fname, backend, store, mode, create, remove,
508 eolmode='strict', copysource=None):
509 509 self.fname = fname
510 510 self.eolmode = eolmode
511 511 self.eol = None
@@ -513,36 +513,43 b' class patchfile(object):'
513 513 self.ui = ui
514 514 self.lines = []
515 515 self.exists = False
516 self.missing = missing
516 self.missing = True
517 517 self.mode = mode
518 self.copysource = copysource
518 519 self.create = create
519 520 self.remove = remove
520 if not missing:
521 try:
522 data, mode = self.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
521 try:
522 if copysource is None:
523 data, mode = backend.getfile(fname)
540 524 self.exists = True
541 except IOError:
542 if self.mode is None:
543 self.mode = (False, False)
544 else:
545 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
525 else:
526 data, mode = store.getfile(copysource)
527 self.exists = backend.exists(fname)
528 self.missing = False
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 554 self.hash = {}
548 555 self.dirty = 0
@@ -569,7 +576,7 b' class patchfile(object):'
569 576 rawlines.append(l)
570 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 581 def printfile(self, warn):
575 582 if self.fileprinted:
@@ -623,7 +630,11 b' class patchfile(object):'
623 630 return -1
624 631
625 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 638 self.rej.append(h)
628 639 return -1
629 640
@@ -1005,10 +1016,10 b' def selectfile(backend, afile_orig, bfil'
1005 1016 # Git patches do not play games. Excluding copies from the
1006 1017 # following heuristic avoids a lot of confusion
1007 1018 fname = pathstrip(gp.path, strip - 1)[1]
1008 create = gp.op == 'ADD'
1019 create = gp.op in ('ADD', 'COPY', 'RENAME')
1009 1020 remove = gp.op == 'DELETE'
1010 1021 missing = not create and not backend.exists(fname)
1011 return fname, missing, create, remove
1022 return fname, create, remove
1012 1023 nulla = afile_orig == "/dev/null"
1013 1024 nullb = bfile_orig == "/dev/null"
1014 1025 create = nulla and hunk.starta == 0 and hunk.lena == 0
@@ -1050,7 +1061,7 b' def selectfile(backend, afile_orig, bfil'
1050 1061 else:
1051 1062 raise PatchError(_("undefined source and destination files"))
1052 1063
1053 return fname, missing, create, remove
1064 return fname, create, remove
1054 1065
1055 1066 def scangitpatch(lr, firstline):
1056 1067 """
@@ -1177,7 +1188,7 b' def iterhunks(fp):'
1177 1188 gp = gitpatches.pop()[1]
1178 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 1192 """Reads a patch from fp and tries to apply it.
1182 1193
1183 1194 The dict 'changed' is filled in with all of the filenames changed
@@ -1188,10 +1199,11 b' def applydiff(ui, fp, changed, backend, '
1188 1199 read in binary mode. Otherwise, line endings are ignored when
1189 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 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 1208 def pstrip(p):
1197 1209 return pathstrip(p, strip - 1)[1]
@@ -1214,30 +1226,42 b' def _applydiff(ui, fp, patcher, backend,'
1214 1226 rejects += current_file.close()
1215 1227 current_file = None
1216 1228 afile, bfile, first_hunk, gp = values
1229 copysource = None
1217 1230 if gp:
1218 1231 path = pstrip(gp.path)
1232 if gp.oldpath:
1233 copysource = pstrip(gp.oldpath)
1219 1234 changed[path] = gp
1220 1235 if gp.op == 'DELETE':
1221 1236 backend.unlink(path)
1222 1237 continue
1223 1238 if gp.op == 'RENAME':
1224 backend.unlink(pstrip(gp.oldpath))
1225 if gp.mode and not first_hunk:
1226 data = None
1227 if gp.op == 'ADD':
1228 # Added files without content have no hunk and
1229 # must be created
1230 data = ''
1231 backend.setfile(path, data, gp.mode)
1239 backend.unlink(copysource)
1240 if not first_hunk:
1241 data, mode = None, None
1242 if gp.op in ('RENAME', 'COPY'):
1243 data, mode = store.getfile(copysource)
1244 if gp.mode:
1245 mode = 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 1256 if not first_hunk:
1233 1257 continue
1234 1258 try:
1235 1259 mode = gp and gp.mode or None
1236 current_file, missing, create, remove = selectfile(
1260 current_file, create, remove = selectfile(
1237 1261 backend, afile, bfile, first_hunk, strip, gp)
1238 current_file = patcher(ui, current_file, backend, mode,
1239 create, remove, missing=missing,
1240 eolmode=eolmode)
1262 current_file = patcher(ui, current_file, backend, store, mode,
1263 create, remove, eolmode=eolmode,
1264 copysource=copysource)
1241 1265 except PatchError, inst:
1242 1266 ui.warn(str(inst) + '\n')
1243 1267 current_file = None
@@ -1245,7 +1269,9 b' def _applydiff(ui, fp, patcher, backend,'
1245 1269 continue
1246 1270 elif state == 'git':
1247 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 1275 else:
1250 1276 raise util.Abort(_('unsupported parser state: %s') % state)
1251 1277
@@ -1316,17 +1342,20 b' def internalpatch(ui, repo, patchobj, st'
1316 1342 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1317 1343 eolmode = eolmode.lower()
1318 1344
1345 store = filestore()
1319 1346 backend = workingbackend(ui, repo, similarity)
1320 1347 try:
1321 1348 fp = open(patchobj, 'rb')
1322 1349 except TypeError:
1323 1350 fp = patchobj
1324 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 1354 finally:
1327 1355 if fp != patchobj:
1328 1356 fp.close()
1329 1357 files.update(dict.fromkeys(backend.close()))
1358 store.close()
1330 1359 if ret < 0:
1331 1360 raise PatchError(_('patch failed to apply'))
1332 1361 return ret > 0
@@ -1370,7 +1399,7 b' def changedfiles(ui, repo, patchpath, st'
1370 1399 changed.add(pathstrip(gp.oldpath, strip - 1)[1])
1371 1400 if not first_hunk:
1372 1401 continue
1373 current_file, missing, create, remove = selectfile(
1402 current_file, create, remove = selectfile(
1374 1403 backend, afile, bfile, first_hunk, strip, gp)
1375 1404 changed.add(current_file)
1376 1405 elif state not in ('hunk', 'git'):
@@ -436,7 +436,9 b' Copy and changes with existing destinati'
436 436 > +b
437 437 > EOF
438 438 applying patch from stdin
439 abort: cannot create b: destination already exists
439 cannot create b: destination already exists
440 1 out of 1 hunks FAILED -- saving rejects to file b.rej
441 abort: patch failed to apply
440 442 [255]
441 443 $ cat b
442 444 b
@@ -617,7 +617,7 b' test paths outside repo root'
617 617 > rename to bar
618 618 > EOF
619 619 applying patch from stdin
620 abort: ../outside/foo not under root
620 abort: path contains illegal component: ../outside/foo
621 621 [255]
622 622 $ cd ..
623 623
@@ -502,6 +502,7 b' but only after writing the bad name into'
502 502 refresh interrupted while patch was popped! (revert --all, qpush to recover)
503 503 abort: username 'foo\nbar' contains a newline!
504 504 [255]
505 $ rm a
505 506 $ cat .hg/patches/a
506 507 # HG changeset patch
507 508 # Parent 0000000000000000000000000000000000000000
@@ -115,6 +115,8 b' check patch does not overwrite untracked'
115 115 $ ln -s linkbb linkb
116 116 $ hg qpush
117 117 applying movelink
118 cannot create linkb: destination already exists
119 1 out of 1 hunks FAILED -- saving rejects to file linkb.rej
118 120 patch failed, unable to continue (try -v)
119 121 patch failed, rejects left in working dir
120 122 errors during apply, please fix and refresh movelink
General Comments 0
You need to be logged in to leave comments. Login now