mercurial/revlogutils/rewrite.py
branchstable
changeset 47815 b30a53ffbf9b
parent 47473 5045ba2a3afd
child 47816 32e21ac3adb1
equal deleted inserted replaced
47814:3b04e4746020 47815:b30a53ffbf9b
     5 # Copyright 2015 Google, Inc <martinvonz@google.com>
     5 # Copyright 2015 Google, Inc <martinvonz@google.com>
     6 #
     6 #
     7 # This software may be used and distributed according to the terms of the
     7 # This software may be used and distributed according to the terms of the
     8 # GNU General Public License version 2 or any later version.
     8 # GNU General Public License version 2 or any later version.
     9 
     9 
       
    10 import binascii
    10 import contextlib
    11 import contextlib
    11 import os
    12 import os
    12 
    13 
    13 from ..node import (
    14 from ..node import (
    14     nullrev,
    15     nullrev,
   470     revlog.index.append(new_entry)
   471     revlog.index.append(new_entry)
   471     entry_bin = revlog.index.entry_binary(rev)
   472     entry_bin = revlog.index.entry_binary(rev)
   472     new_index_file.write(entry_bin)
   473     new_index_file.write(entry_bin)
   473     revlog._docket.index_end = new_index_file.tell()
   474     revlog._docket.index_end = new_index_file.tell()
   474     revlog._docket.data_end = new_data_file.tell()
   475     revlog._docket.data_end = new_data_file.tell()
       
   476 
       
   477 
       
   478 def _get_filename_from_filelog_index(path):
       
   479     # Drop the extension and the `data/` prefix
       
   480     path_part = path.rsplit(b'.', 1)[0].split(b'/', 1)
       
   481     if len(path_part) < 2:
       
   482         msg = _(b"cannot recognize filelog from filename: '%s'")
       
   483         msg %= path
       
   484         raise error.Abort(msg)
       
   485 
       
   486     return path_part[1]
       
   487 
       
   488 
       
   489 def _filelog_from_filename(repo, path):
       
   490     """Returns the filelog for the given `path`. Stolen from `engine.py`"""
       
   491 
       
   492     from .. import filelog  # avoid cycle
       
   493 
       
   494     fl = filelog.filelog(repo.svfs, path)
       
   495     return fl
       
   496 
       
   497 
       
   498 def _write_swapped_parents(repo, rl, rev, offset, fp):
       
   499     """Swaps p1 and p2 and overwrites the revlog entry for `rev` in `fp`"""
       
   500     from ..pure import parsers  # avoid cycle
       
   501 
       
   502     if repo._currentlock(repo._lockref) is None:
       
   503         # Let's be paranoid about it
       
   504         msg = "repo needs to be locked to rewrite parents"
       
   505         raise error.ProgrammingError(msg)
       
   506 
       
   507     index_format = parsers.IndexObject.index_format
       
   508     entry = rl.index[rev]
       
   509     new_entry = list(entry)
       
   510     new_entry[5], new_entry[6] = entry[6], entry[5]
       
   511     packed = index_format.pack(*new_entry[:8])
       
   512     fp.seek(offset)
       
   513     fp.write(packed)
       
   514 
       
   515 
       
   516 def _reorder_filelog_parents(repo, fl, to_fix):
       
   517     """
       
   518     Swaps p1 and p2 for all `to_fix` revisions of filelog `fl` and writes the
       
   519     new version to disk, overwriting the old one with a rename.
       
   520     """
       
   521     from ..pure import parsers  # avoid cycle
       
   522 
       
   523     ui = repo.ui
       
   524     assert len(to_fix) > 0
       
   525     rl = fl._revlog
       
   526     if rl._format_version != constants.REVLOGV1:
       
   527         msg = "expected version 1 revlog, got version '%d'" % rl._format_version
       
   528         raise error.ProgrammingError(msg)
       
   529 
       
   530     index_file = rl._indexfile
       
   531     new_file_path = index_file + b'.tmp-parents-fix'
       
   532     repaired_msg = _(b"repaired revision %d of 'filelog %s'\n")
       
   533 
       
   534     with ui.uninterruptible():
       
   535         try:
       
   536             util.copyfile(
       
   537                 rl.opener.join(index_file),
       
   538                 rl.opener.join(new_file_path),
       
   539                 checkambig=rl._checkambig,
       
   540             )
       
   541 
       
   542             with rl.opener(new_file_path, mode=b"r+") as fp:
       
   543                 if rl._inline:
       
   544                     index = parsers.InlinedIndexObject(fp.read())
       
   545                     for rev in fl.revs():
       
   546                         if rev in to_fix:
       
   547                             offset = index._calculate_index(rev)
       
   548                             _write_swapped_parents(repo, rl, rev, offset, fp)
       
   549                             ui.write(repaired_msg % (rev, index_file))
       
   550                 else:
       
   551                     index_format = parsers.IndexObject.index_format
       
   552                     for rev in to_fix:
       
   553                         offset = rev * index_format.size
       
   554                         _write_swapped_parents(repo, rl, rev, offset, fp)
       
   555                         ui.write(repaired_msg % (rev, index_file))
       
   556 
       
   557             rl.opener.rename(new_file_path, index_file)
       
   558             rl.clearcaches()
       
   559             rl._loadindex()
       
   560         finally:
       
   561             util.tryunlink(new_file_path)
       
   562 
       
   563 
       
   564 def _is_revision_affected(ui, fl, filerev, path):
       
   565     """Mercurial currently (5.9rc0) uses `p1 == nullrev and p2 != nullrev` as a
       
   566     special meaning compared to the reverse in the context of filelog-based
       
   567     copytracing. issue6528 exists because new code assumed that parent ordering
       
   568     didn't matter, so this detects if the revision contains metadata (since
       
   569     it's only used for filelog-based copytracing) and its parents are in the
       
   570     "wrong" order."""
       
   571     try:
       
   572         raw_text = fl.rawdata(filerev)
       
   573     except error.CensoredNodeError:
       
   574         # We don't care about censored nodes as they never carry metadata
       
   575         return False
       
   576     has_meta = raw_text.startswith(b'\x01\n')
       
   577     if has_meta:
       
   578         (p1, p2) = fl.parentrevs(filerev)
       
   579         if p1 != nullrev and p2 == nullrev:
       
   580             return True
       
   581     return False
       
   582 
       
   583 
       
   584 def _from_report(ui, repo, context, from_report, dry_run):
       
   585     """
       
   586     Fix the revisions given in the `from_report` file, but still checks if the
       
   587     revisions are indeed affected to prevent an unfortunate cyclic situation
       
   588     where we'd swap well-ordered parents again.
       
   589 
       
   590     See the doc for `debug_fix_issue6528` for the format documentation.
       
   591     """
       
   592     ui.write(_(b"loading report file '%s'\n") % from_report)
       
   593 
       
   594     with context(), open(from_report, mode='rb') as f:
       
   595         for line in f.read().split(b'\n'):
       
   596             if not line:
       
   597                 continue
       
   598             filenodes, filename = line.split(b' ', 1)
       
   599             fl = _filelog_from_filename(repo, filename)
       
   600             to_fix = set(
       
   601                 fl.rev(binascii.unhexlify(n)) for n in filenodes.split(b',')
       
   602             )
       
   603             excluded = set()
       
   604 
       
   605             for filerev in to_fix:
       
   606                 if _is_revision_affected(ui, fl, filerev, filename):
       
   607                     msg = b"found affected revision %d for filelog '%s'\n"
       
   608                     ui.warn(msg % (filerev, filename))
       
   609                 else:
       
   610                     msg = _(b"revision %s of file '%s' is not affected\n")
       
   611                     msg %= (binascii.hexlify(fl.node(filerev)), filename)
       
   612                     ui.warn(msg)
       
   613                     excluded.add(filerev)
       
   614 
       
   615             to_fix = to_fix - excluded
       
   616             if not to_fix:
       
   617                 msg = _(b"no affected revisions were found for '%s'\n")
       
   618                 ui.write(msg % filename)
       
   619                 continue
       
   620             if not dry_run:
       
   621                 _reorder_filelog_parents(repo, fl, sorted(to_fix))
       
   622 
       
   623 
       
   624 def repair_issue6528(ui, repo, dry_run=False, to_report=None, from_report=None):
       
   625     from .. import store  # avoid cycle
       
   626 
       
   627     @contextlib.contextmanager
       
   628     def context():
       
   629         if dry_run or to_report:  # No need for locking
       
   630             yield
       
   631         else:
       
   632             with repo.wlock(), repo.lock():
       
   633                 yield
       
   634 
       
   635     if from_report:
       
   636         return _from_report(ui, repo, context, from_report, dry_run)
       
   637 
       
   638     report_entries = []
       
   639 
       
   640     with context():
       
   641         files = list(
       
   642             (file_type, path)
       
   643             for (file_type, path, _e, _s) in repo.store.datafiles()
       
   644             if path.endswith(b'.i') and file_type & store.FILEFLAGS_FILELOG
       
   645         )
       
   646 
       
   647         progress = ui.makeprogress(
       
   648             _(b"looking for affected revisions"),
       
   649             unit=_(b"filelogs"),
       
   650             total=len(files),
       
   651         )
       
   652         found_nothing = True
       
   653 
       
   654         for file_type, path in files:
       
   655             if (
       
   656                 not path.endswith(b'.i')
       
   657                 or not file_type & store.FILEFLAGS_FILELOG
       
   658             ):
       
   659                 continue
       
   660             progress.increment()
       
   661             filename = _get_filename_from_filelog_index(path)
       
   662             fl = _filelog_from_filename(repo, filename)
       
   663 
       
   664             # Set of filerevs (or hex filenodes if `to_report`) that need fixing
       
   665             to_fix = set()
       
   666             for filerev in fl.revs():
       
   667                 # TODO speed up by looking at the start of the delta
       
   668                 # If it hasn't changed, it's not worth looking at the other revs
       
   669                 # in the same chain
       
   670                 affected = _is_revision_affected(ui, fl, filerev, path)
       
   671                 if affected:
       
   672                     msg = b"found affected revision %d for filelog '%s'\n"
       
   673                     ui.warn(msg % (filerev, path))
       
   674                     found_nothing = False
       
   675                     if not dry_run:
       
   676                         if to_report:
       
   677                             to_fix.add(binascii.hexlify(fl.node(filerev)))
       
   678                         else:
       
   679                             to_fix.add(filerev)
       
   680 
       
   681             if to_fix:
       
   682                 to_fix = sorted(to_fix)
       
   683                 if to_report:
       
   684                     report_entries.append((filename, to_fix))
       
   685                 else:
       
   686                     _reorder_filelog_parents(repo, fl, to_fix)
       
   687 
       
   688         if found_nothing:
       
   689             ui.write(_(b"no affected revisions were found\n"))
       
   690 
       
   691         if to_report and report_entries:
       
   692             with open(to_report, mode="wb") as f:
       
   693                 for path, to_fix in report_entries:
       
   694                     f.write(b"%s %s\n" % (b",".join(to_fix), path))
       
   695 
       
   696         progress.complete()