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.""" |
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 |
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) |
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 |