comparison mercurial/patch.py @ 14348:c1c719103392

patch: extract fs access from patchfile into fsbackend Most filesystem calls are already isolated in patchfile but this is not enough: renames are performed before patchfile is available and some chmod calls are even done outside of the applydiff call. Once all these calls are extracted into a backend class, we can provide cleaner APIs to write to a working directory context directly into the repository.
author Patrick Mezard <pmezard@gmail.com>
date Tue, 17 May 2011 23:46:15 +0200
parents e8debe1eb255
children 776ae95b8835
comparison
equal deleted inserted replaced
14347:e8debe1eb255 14348:c1c719103392
379 l = self.readline() 379 l = self.readline()
380 if not l: 380 if not l:
381 break 381 break
382 yield l 382 yield l
383 383
384 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1 384 class abstractbackend(object):
385 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@') 385 def __init__(self, ui):
386 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)') 386 self.ui = ui
387 eolmodes = ['strict', 'crlf', 'lf', 'auto'] 387
388 388 def readlines(self, fname):
389 class patchfile(object): 389 """Return target file lines, or its content as a single line
390 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'): 390 for symlinks.
391 self.fname = fname 391 """
392 self.eolmode = eolmode 392 raise NotImplementedError
393 self.eol = None 393
394 def writelines(self, fname, lines):
395 """Write lines to target file."""
396 raise NotImplementedError
397
398 def unlink(self, fname):
399 """Unlink target file."""
400 raise NotImplementedError
401
402 def writerej(self, fname, failed, total, lines):
403 """Write rejected lines for fname. total is the number of hunks
404 which failed to apply and total the total number of hunks for this
405 files.
406 """
407 pass
408
409 class fsbackend(abstractbackend):
410 def __init__(self, ui, opener):
411 super(fsbackend, self).__init__(ui)
394 self.opener = opener 412 self.opener = opener
395 self.ui = ui
396 self.lines = []
397 self.exists = False
398 self.missing = missing
399 if not missing:
400 try:
401 self.lines = self.readlines(fname)
402 self.exists = True
403 except IOError:
404 pass
405 else:
406 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
407
408 self.hash = {}
409 self.dirty = False
410 self.offset = 0
411 self.skew = 0
412 self.rej = []
413 self.fileprinted = False
414 self.printfile(False)
415 self.hunks = 0
416 413
417 def readlines(self, fname): 414 def readlines(self, fname):
418 if os.path.islink(fname): 415 if os.path.islink(fname):
419 return [os.readlink(fname)] 416 return [os.readlink(fname)]
420 fp = self.opener(fname, 'r') 417 fp = self.opener(fname, 'r')
421 try: 418 try:
422 lr = linereader(fp, self.eolmode != 'strict') 419 return list(fp)
423 lines = list(lr)
424 self.eol = lr.eol
425 return lines
426 finally: 420 finally:
427 fp.close() 421 fp.close()
428 422
429 def writelines(self, fname, lines): 423 def writelines(self, fname, lines):
430 # Ensure supplied data ends in fname, being a regular file or 424 # Ensure supplied data ends in fname, being a regular file or
440 except OSError, e: 434 except OSError, e:
441 if e.errno != errno.ENOENT: 435 if e.errno != errno.ENOENT:
442 raise 436 raise
443 fp = self.opener(fname, 'w') 437 fp = self.opener(fname, 'w')
444 try: 438 try:
445 if self.eolmode == 'auto': 439 fp.writelines(lines)
446 eol = self.eol
447 elif self.eolmode == 'crlf':
448 eol = '\r\n'
449 else:
450 eol = '\n'
451
452 if self.eolmode != 'strict' and eol and eol != '\n':
453 for l in lines:
454 if l and l[-1] == '\n':
455 l = l[:-1] + eol
456 fp.write(l)
457 else:
458 fp.writelines(lines)
459 if islink: 440 if islink:
460 self.opener.symlink(fp.getvalue(), fname) 441 self.opener.symlink(fp.getvalue(), fname)
461 if st_mode is not None: 442 if st_mode is not None:
462 os.chmod(fname, st_mode) 443 os.chmod(fname, st_mode)
463 finally: 444 finally:
464 fp.close() 445 fp.close()
465 446
466 def unlink(self, fname): 447 def unlink(self, fname):
467 os.unlink(fname) 448 os.unlink(fname)
449
450 def writerej(self, fname, failed, total, lines):
451 fname = fname + ".rej"
452 self.ui.warn(
453 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
454 (failed, total, fname))
455 fp = self.opener(fname, 'w')
456 fp.writelines(lines)
457 fp.close()
458
459 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
460 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
461 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
462 eolmodes = ['strict', 'crlf', 'lf', 'auto']
463
464 class patchfile(object):
465 def __init__(self, ui, fname, backend, missing=False, eolmode='strict'):
466 self.fname = fname
467 self.eolmode = eolmode
468 self.eol = None
469 self.backend = backend
470 self.ui = ui
471 self.lines = []
472 self.exists = False
473 self.missing = missing
474 if not missing:
475 try:
476 self.lines = self.backend.readlines(fname)
477 if self.lines:
478 # Normalize line endings
479 if self.lines[0].endswith('\r\n'):
480 self.eol = '\r\n'
481 elif self.lines[0].endswith('\n'):
482 self.eol = '\n'
483 if eolmode != 'strict':
484 nlines = []
485 for l in self.lines:
486 if l.endswith('\r\n'):
487 l = l[:-2] + '\n'
488 nlines.append(l)
489 self.lines = nlines
490 self.exists = True
491 except IOError:
492 pass
493 else:
494 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
495
496 self.hash = {}
497 self.dirty = 0
498 self.offset = 0
499 self.skew = 0
500 self.rej = []
501 self.fileprinted = False
502 self.printfile(False)
503 self.hunks = 0
504
505 def writelines(self, fname, lines):
506 if self.eolmode == 'auto':
507 eol = self.eol
508 elif self.eolmode == 'crlf':
509 eol = '\r\n'
510 else:
511 eol = '\n'
512
513 if self.eolmode != 'strict' and eol and eol != '\n':
514 rawlines = []
515 for l in lines:
516 if l and l[-1] == '\n':
517 l = l[:-1] + eol
518 rawlines.append(l)
519 lines = rawlines
520
521 self.backend.writelines(fname, lines)
468 522
469 def printfile(self, warn): 523 def printfile(self, warn):
470 if self.fileprinted: 524 if self.fileprinted:
471 return 525 return
472 if warn or self.ui.verbose: 526 if warn or self.ui.verbose:
501 def write_rej(self): 555 def write_rej(self):
502 # our rejects are a little different from patch(1). This always 556 # our rejects are a little different from patch(1). This always
503 # creates rejects in the same form as the original patch. A file 557 # creates rejects in the same form as the original patch. A file
504 # header is inserted so that you can run the reject through patch again 558 # header is inserted so that you can run the reject through patch again
505 # without having to type the filename. 559 # without having to type the filename.
506
507 if not self.rej: 560 if not self.rej:
508 return 561 return
509 562 self.backend.writerej(self.fname, len(self.rej), self.hunks,
510 fname = self.fname + ".rej" 563 self.makerejlines(self.fname))
511 self.ui.warn(
512 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
513 (len(self.rej), self.hunks, fname))
514
515 fp = self.opener(fname, 'w')
516 fp.writelines(self.makerejlines(self.fname))
517 fp.close()
518 564
519 def apply(self, h): 565 def apply(self, h):
520 if not h.complete(): 566 if not h.complete():
521 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") % 567 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
522 (h.number, h.desc, len(h.a), h.lena, len(h.b), 568 (h.number, h.desc, len(h.a), h.lena, len(h.b),
533 self.rej.append(h) 579 self.rej.append(h)
534 return -1 580 return -1
535 581
536 if isinstance(h, binhunk): 582 if isinstance(h, binhunk):
537 if h.rmfile(): 583 if h.rmfile():
538 self.unlink(self.fname) 584 self.backend.unlink(self.fname)
539 else: 585 else:
540 self.lines[:] = h.new() 586 self.lines[:] = h.new()
541 self.offset += len(h.new()) 587 self.offset += len(h.new())
542 self.dirty = True 588 self.dirty = True
543 return 0 589 return 0
561 # if there's skew we want to emit the "(offset %d lines)" even 607 # if there's skew we want to emit the "(offset %d lines)" even
562 # when the hunk cleanly applies at start + skew, so skip the 608 # when the hunk cleanly applies at start + skew, so skip the
563 # fast case code 609 # fast case code
564 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0: 610 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
565 if h.rmfile(): 611 if h.rmfile():
566 self.unlink(self.fname) 612 self.backend.unlink(self.fname)
567 else: 613 else:
568 self.lines[start : start + h.lena] = h.new() 614 self.lines[start : start + h.lena] = h.new()
569 self.offset += h.lenb - h.lena 615 self.offset += h.lenb - h.lena
570 self.dirty = True 616 self.dirty = True
571 return 0 617 return 0
1110 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1, eolmode='strict'): 1156 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1, eolmode='strict'):
1111 rejects = 0 1157 rejects = 0
1112 err = 0 1158 err = 0
1113 current_file = None 1159 current_file = None
1114 cwd = os.getcwd() 1160 cwd = os.getcwd()
1115 opener = scmutil.opener(cwd) 1161 backend = fsbackend(ui, scmutil.opener(cwd))
1116 1162
1117 for state, values in iterhunks(fp): 1163 for state, values in iterhunks(fp):
1118 if state == 'hunk': 1164 if state == 'hunk':
1119 if not current_file: 1165 if not current_file:
1120 continue 1166 continue
1128 rejects += current_file.close() 1174 rejects += current_file.close()
1129 afile, bfile, first_hunk = values 1175 afile, bfile, first_hunk = values
1130 try: 1176 try:
1131 current_file, missing = selectfile(afile, bfile, 1177 current_file, missing = selectfile(afile, bfile,
1132 first_hunk, strip) 1178 first_hunk, strip)
1133 current_file = patcher(ui, current_file, opener, 1179 current_file = patcher(ui, current_file, backend,
1134 missing=missing, eolmode=eolmode) 1180 missing=missing, eolmode=eolmode)
1135 except PatchError, inst: 1181 except PatchError, inst:
1136 ui.warn(str(inst) + '\n') 1182 ui.warn(str(inst) + '\n')
1137 current_file = None 1183 current_file = None
1138 rejects += 1 1184 rejects += 1