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() |