comparison mercurial/upgrade.py @ 46046:f105c49e89cd

upgrade: split actual upgrade code away from the main module The main module is getting big and hard to follow. So we are splitting all the logic to actually run an upgrade in a sub module. It nicely highlight that there are very few actual call point to the code we just moved. Differential Revision: https://phab.mercurial-scm.org/D9476
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Tue, 01 Dec 2020 09:13:08 +0100
parents 6c960b708ac4
children 4b89cf08d8dc
comparison
equal deleted inserted replaced
46045:7905899c4f8f 46046:f105c49e89cd
5 # This software may be used and distributed according to the terms of the 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version. 6 # GNU General Public License version 2 or any later version.
7 7
8 from __future__ import absolute_import 8 from __future__ import absolute_import
9 9
10 import stat
11
12 from .i18n import _ 10 from .i18n import _
13 from .pycompat import getattr
14 from . import ( 11 from . import (
15 changelog,
16 error, 12 error,
17 filelog,
18 hg, 13 hg,
19 localrepo, 14 localrepo,
20 manifest,
21 metadata,
22 pycompat, 15 pycompat,
23 requirements, 16 requirements,
24 revlog,
25 scmutil,
26 util, 17 util,
27 vfs as vfsmod, 18 )
19
20 from .upgrade_utils import (
21 engine as upgrade_engine,
28 ) 22 )
29 23
30 from .utils import compression 24 from .utils import compression
31 25
32 # list of requirements that request a clone of all revlog if added/removed 26 # list of requirements that request a clone of all revlog if added/removed
690 # e.g. adding generaldelta could schedule parent redeltas. 684 # e.g. adding generaldelta could schedule parent redeltas.
691 685
692 return newactions 686 return newactions
693 687
694 688
695 def _revlogfrompath(repo, path):
696 """Obtain a revlog from a repo path.
697
698 An instance of the appropriate class is returned.
699 """
700 if path == b'00changelog.i':
701 return changelog.changelog(repo.svfs)
702 elif path.endswith(b'00manifest.i'):
703 mandir = path[: -len(b'00manifest.i')]
704 return manifest.manifestrevlog(repo.svfs, tree=mandir)
705 else:
706 # reverse of "/".join(("data", path + ".i"))
707 return filelog.filelog(repo.svfs, path[5:-2])
708
709
710 def _copyrevlog(tr, destrepo, oldrl, unencodedname):
711 """copy all relevant files for `oldrl` into `destrepo` store
712
713 Files are copied "as is" without any transformation. The copy is performed
714 without extra checks. Callers are responsible for making sure the copied
715 content is compatible with format of the destination repository.
716 """
717 oldrl = getattr(oldrl, '_revlog', oldrl)
718 newrl = _revlogfrompath(destrepo, unencodedname)
719 newrl = getattr(newrl, '_revlog', newrl)
720
721 oldvfs = oldrl.opener
722 newvfs = newrl.opener
723 oldindex = oldvfs.join(oldrl.indexfile)
724 newindex = newvfs.join(newrl.indexfile)
725 olddata = oldvfs.join(oldrl.datafile)
726 newdata = newvfs.join(newrl.datafile)
727
728 with newvfs(newrl.indexfile, b'w'):
729 pass # create all the directories
730
731 util.copyfile(oldindex, newindex)
732 copydata = oldrl.opener.exists(oldrl.datafile)
733 if copydata:
734 util.copyfile(olddata, newdata)
735
736 if not (
737 unencodedname.endswith(b'00changelog.i')
738 or unencodedname.endswith(b'00manifest.i')
739 ):
740 destrepo.svfs.fncache.add(unencodedname)
741 if copydata:
742 destrepo.svfs.fncache.add(unencodedname[:-2] + b'.d')
743
744
745 UPGRADE_CHANGELOG = b"changelog"
746 UPGRADE_MANIFEST = b"manifest"
747 UPGRADE_FILELOGS = b"all-filelogs"
748
749 UPGRADE_ALL_REVLOGS = frozenset(
750 [UPGRADE_CHANGELOG, UPGRADE_MANIFEST, UPGRADE_FILELOGS]
751 )
752
753
754 def getsidedatacompanion(srcrepo, dstrepo):
755 sidedatacompanion = None
756 removedreqs = srcrepo.requirements - dstrepo.requirements
757 addedreqs = dstrepo.requirements - srcrepo.requirements
758 if requirements.SIDEDATA_REQUIREMENT in removedreqs:
759
760 def sidedatacompanion(rl, rev):
761 rl = getattr(rl, '_revlog', rl)
762 if rl.flags(rev) & revlog.REVIDX_SIDEDATA:
763 return True, (), {}, 0, 0
764 return False, (), {}, 0, 0
765
766 elif requirements.COPIESSDC_REQUIREMENT in addedreqs:
767 sidedatacompanion = metadata.getsidedataadder(srcrepo, dstrepo)
768 elif requirements.COPIESSDC_REQUIREMENT in removedreqs:
769 sidedatacompanion = metadata.getsidedataremover(srcrepo, dstrepo)
770 return sidedatacompanion
771
772
773 def matchrevlog(revlogfilter, entry):
774 """check if a revlog is selected for cloning.
775
776 In other words, are there any updates which need to be done on revlog
777 or it can be blindly copied.
778
779 The store entry is checked against the passed filter"""
780 if entry.endswith(b'00changelog.i'):
781 return UPGRADE_CHANGELOG in revlogfilter
782 elif entry.endswith(b'00manifest.i'):
783 return UPGRADE_MANIFEST in revlogfilter
784 return UPGRADE_FILELOGS in revlogfilter
785
786
787 def _clonerevlogs(
788 ui,
789 srcrepo,
790 dstrepo,
791 tr,
792 deltareuse,
793 forcedeltabothparents,
794 revlogs=UPGRADE_ALL_REVLOGS,
795 ):
796 """Copy revlogs between 2 repos."""
797 revcount = 0
798 srcsize = 0
799 srcrawsize = 0
800 dstsize = 0
801 fcount = 0
802 frevcount = 0
803 fsrcsize = 0
804 frawsize = 0
805 fdstsize = 0
806 mcount = 0
807 mrevcount = 0
808 msrcsize = 0
809 mrawsize = 0
810 mdstsize = 0
811 crevcount = 0
812 csrcsize = 0
813 crawsize = 0
814 cdstsize = 0
815
816 alldatafiles = list(srcrepo.store.walk())
817
818 # Perform a pass to collect metadata. This validates we can open all
819 # source files and allows a unified progress bar to be displayed.
820 for unencoded, encoded, size in alldatafiles:
821 if unencoded.endswith(b'.d'):
822 continue
823
824 rl = _revlogfrompath(srcrepo, unencoded)
825
826 info = rl.storageinfo(
827 exclusivefiles=True,
828 revisionscount=True,
829 trackedsize=True,
830 storedsize=True,
831 )
832
833 revcount += info[b'revisionscount'] or 0
834 datasize = info[b'storedsize'] or 0
835 rawsize = info[b'trackedsize'] or 0
836
837 srcsize += datasize
838 srcrawsize += rawsize
839
840 # This is for the separate progress bars.
841 if isinstance(rl, changelog.changelog):
842 crevcount += len(rl)
843 csrcsize += datasize
844 crawsize += rawsize
845 elif isinstance(rl, manifest.manifestrevlog):
846 mcount += 1
847 mrevcount += len(rl)
848 msrcsize += datasize
849 mrawsize += rawsize
850 elif isinstance(rl, filelog.filelog):
851 fcount += 1
852 frevcount += len(rl)
853 fsrcsize += datasize
854 frawsize += rawsize
855 else:
856 error.ProgrammingError(b'unknown revlog type')
857
858 if not revcount:
859 return
860
861 ui.status(
862 _(
863 b'migrating %d total revisions (%d in filelogs, %d in manifests, '
864 b'%d in changelog)\n'
865 )
866 % (revcount, frevcount, mrevcount, crevcount)
867 )
868 ui.status(
869 _(b'migrating %s in store; %s tracked data\n')
870 % ((util.bytecount(srcsize), util.bytecount(srcrawsize)))
871 )
872
873 # Used to keep track of progress.
874 progress = None
875
876 def oncopiedrevision(rl, rev, node):
877 progress.increment()
878
879 sidedatacompanion = getsidedatacompanion(srcrepo, dstrepo)
880
881 # Do the actual copying.
882 # FUTURE this operation can be farmed off to worker processes.
883 seen = set()
884 for unencoded, encoded, size in alldatafiles:
885 if unencoded.endswith(b'.d'):
886 continue
887
888 oldrl = _revlogfrompath(srcrepo, unencoded)
889
890 if isinstance(oldrl, changelog.changelog) and b'c' not in seen:
891 ui.status(
892 _(
893 b'finished migrating %d manifest revisions across %d '
894 b'manifests; change in size: %s\n'
895 )
896 % (mrevcount, mcount, util.bytecount(mdstsize - msrcsize))
897 )
898
899 ui.status(
900 _(
901 b'migrating changelog containing %d revisions '
902 b'(%s in store; %s tracked data)\n'
903 )
904 % (
905 crevcount,
906 util.bytecount(csrcsize),
907 util.bytecount(crawsize),
908 )
909 )
910 seen.add(b'c')
911 progress = srcrepo.ui.makeprogress(
912 _(b'changelog revisions'), total=crevcount
913 )
914 elif isinstance(oldrl, manifest.manifestrevlog) and b'm' not in seen:
915 ui.status(
916 _(
917 b'finished migrating %d filelog revisions across %d '
918 b'filelogs; change in size: %s\n'
919 )
920 % (frevcount, fcount, util.bytecount(fdstsize - fsrcsize))
921 )
922
923 ui.status(
924 _(
925 b'migrating %d manifests containing %d revisions '
926 b'(%s in store; %s tracked data)\n'
927 )
928 % (
929 mcount,
930 mrevcount,
931 util.bytecount(msrcsize),
932 util.bytecount(mrawsize),
933 )
934 )
935 seen.add(b'm')
936 if progress:
937 progress.complete()
938 progress = srcrepo.ui.makeprogress(
939 _(b'manifest revisions'), total=mrevcount
940 )
941 elif b'f' not in seen:
942 ui.status(
943 _(
944 b'migrating %d filelogs containing %d revisions '
945 b'(%s in store; %s tracked data)\n'
946 )
947 % (
948 fcount,
949 frevcount,
950 util.bytecount(fsrcsize),
951 util.bytecount(frawsize),
952 )
953 )
954 seen.add(b'f')
955 if progress:
956 progress.complete()
957 progress = srcrepo.ui.makeprogress(
958 _(b'file revisions'), total=frevcount
959 )
960
961 if matchrevlog(revlogs, unencoded):
962 ui.note(
963 _(b'cloning %d revisions from %s\n') % (len(oldrl), unencoded)
964 )
965 newrl = _revlogfrompath(dstrepo, unencoded)
966 oldrl.clone(
967 tr,
968 newrl,
969 addrevisioncb=oncopiedrevision,
970 deltareuse=deltareuse,
971 forcedeltabothparents=forcedeltabothparents,
972 sidedatacompanion=sidedatacompanion,
973 )
974 else:
975 msg = _(b'blindly copying %s containing %i revisions\n')
976 ui.note(msg % (unencoded, len(oldrl)))
977 _copyrevlog(tr, dstrepo, oldrl, unencoded)
978
979 newrl = _revlogfrompath(dstrepo, unencoded)
980
981 info = newrl.storageinfo(storedsize=True)
982 datasize = info[b'storedsize'] or 0
983
984 dstsize += datasize
985
986 if isinstance(newrl, changelog.changelog):
987 cdstsize += datasize
988 elif isinstance(newrl, manifest.manifestrevlog):
989 mdstsize += datasize
990 else:
991 fdstsize += datasize
992
993 progress.complete()
994
995 ui.status(
996 _(
997 b'finished migrating %d changelog revisions; change in size: '
998 b'%s\n'
999 )
1000 % (crevcount, util.bytecount(cdstsize - csrcsize))
1001 )
1002
1003 ui.status(
1004 _(
1005 b'finished migrating %d total revisions; total change in store '
1006 b'size: %s\n'
1007 )
1008 % (revcount, util.bytecount(dstsize - srcsize))
1009 )
1010
1011
1012 def _filterstorefile(srcrepo, dstrepo, requirements, path, mode, st):
1013 """Determine whether to copy a store file during upgrade.
1014
1015 This function is called when migrating store files from ``srcrepo`` to
1016 ``dstrepo`` as part of upgrading a repository.
1017
1018 Args:
1019 srcrepo: repo we are copying from
1020 dstrepo: repo we are copying to
1021 requirements: set of requirements for ``dstrepo``
1022 path: store file being examined
1023 mode: the ``ST_MODE`` file type of ``path``
1024 st: ``stat`` data structure for ``path``
1025
1026 Function should return ``True`` if the file is to be copied.
1027 """
1028 # Skip revlogs.
1029 if path.endswith((b'.i', b'.d', b'.n', b'.nd')):
1030 return False
1031 # Skip transaction related files.
1032 if path.startswith(b'undo'):
1033 return False
1034 # Only copy regular files.
1035 if mode != stat.S_IFREG:
1036 return False
1037 # Skip other skipped files.
1038 if path in (b'lock', b'fncache'):
1039 return False
1040
1041 return True
1042
1043
1044 def _finishdatamigration(ui, srcrepo, dstrepo, requirements):
1045 """Hook point for extensions to perform additional actions during upgrade.
1046
1047 This function is called after revlogs and store files have been copied but
1048 before the new store is swapped into the original location.
1049 """
1050
1051
1052 def _upgraderepo(
1053 ui, srcrepo, dstrepo, requirements, actions, revlogs=UPGRADE_ALL_REVLOGS
1054 ):
1055 """Do the low-level work of upgrading a repository.
1056
1057 The upgrade is effectively performed as a copy between a source
1058 repository and a temporary destination repository.
1059
1060 The source repository is unmodified for as long as possible so the
1061 upgrade can abort at any time without causing loss of service for
1062 readers and without corrupting the source repository.
1063 """
1064 assert srcrepo.currentwlock()
1065 assert dstrepo.currentwlock()
1066
1067 ui.status(
1068 _(
1069 b'(it is safe to interrupt this process any time before '
1070 b'data migration completes)\n'
1071 )
1072 )
1073
1074 if b're-delta-all' in actions:
1075 deltareuse = revlog.revlog.DELTAREUSENEVER
1076 elif b're-delta-parent' in actions:
1077 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
1078 elif b're-delta-multibase' in actions:
1079 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
1080 elif b're-delta-fulladd' in actions:
1081 deltareuse = revlog.revlog.DELTAREUSEFULLADD
1082 else:
1083 deltareuse = revlog.revlog.DELTAREUSEALWAYS
1084
1085 with dstrepo.transaction(b'upgrade') as tr:
1086 _clonerevlogs(
1087 ui,
1088 srcrepo,
1089 dstrepo,
1090 tr,
1091 deltareuse,
1092 b're-delta-multibase' in actions,
1093 revlogs=revlogs,
1094 )
1095
1096 # Now copy other files in the store directory.
1097 # The sorted() makes execution deterministic.
1098 for p, kind, st in sorted(srcrepo.store.vfs.readdir(b'', stat=True)):
1099 if not _filterstorefile(srcrepo, dstrepo, requirements, p, kind, st):
1100 continue
1101
1102 srcrepo.ui.status(_(b'copying %s\n') % p)
1103 src = srcrepo.store.rawvfs.join(p)
1104 dst = dstrepo.store.rawvfs.join(p)
1105 util.copyfile(src, dst, copystat=True)
1106
1107 _finishdatamigration(ui, srcrepo, dstrepo, requirements)
1108
1109 ui.status(_(b'data fully migrated to temporary repository\n'))
1110
1111 backuppath = pycompat.mkdtemp(prefix=b'upgradebackup.', dir=srcrepo.path)
1112 backupvfs = vfsmod.vfs(backuppath)
1113
1114 # Make a backup of requires file first, as it is the first to be modified.
1115 util.copyfile(srcrepo.vfs.join(b'requires'), backupvfs.join(b'requires'))
1116
1117 # We install an arbitrary requirement that clients must not support
1118 # as a mechanism to lock out new clients during the data swap. This is
1119 # better than allowing a client to continue while the repository is in
1120 # an inconsistent state.
1121 ui.status(
1122 _(
1123 b'marking source repository as being upgraded; clients will be '
1124 b'unable to read from repository\n'
1125 )
1126 )
1127 scmutil.writereporequirements(
1128 srcrepo, srcrepo.requirements | {b'upgradeinprogress'}
1129 )
1130
1131 ui.status(_(b'starting in-place swap of repository data\n'))
1132 ui.status(_(b'replaced files will be backed up at %s\n') % backuppath)
1133
1134 # Now swap in the new store directory. Doing it as a rename should make
1135 # the operation nearly instantaneous and atomic (at least in well-behaved
1136 # environments).
1137 ui.status(_(b'replacing store...\n'))
1138 tstart = util.timer()
1139 util.rename(srcrepo.spath, backupvfs.join(b'store'))
1140 util.rename(dstrepo.spath, srcrepo.spath)
1141 elapsed = util.timer() - tstart
1142 ui.status(
1143 _(
1144 b'store replacement complete; repository was inconsistent for '
1145 b'%0.1fs\n'
1146 )
1147 % elapsed
1148 )
1149
1150 # We first write the requirements file. Any new requirements will lock
1151 # out legacy clients.
1152 ui.status(
1153 _(
1154 b'finalizing requirements file and making repository readable '
1155 b'again\n'
1156 )
1157 )
1158 scmutil.writereporequirements(srcrepo, requirements)
1159
1160 # The lock file from the old store won't be removed because nothing has a
1161 # reference to its new location. So clean it up manually. Alternatively, we
1162 # could update srcrepo.svfs and other variables to point to the new
1163 # location. This is simpler.
1164 backupvfs.unlink(b'store/lock')
1165
1166 return backuppath
1167
1168
1169 def upgraderepo( 689 def upgraderepo(
1170 ui, 690 ui,
1171 repo, 691 repo,
1172 run=False, 692 run=False,
1173 optimize=None, 693 optimize=None,
1180 if optimize is None: 700 if optimize is None:
1181 optimize = [] 701 optimize = []
1182 optimize = {legacy_opts_map.get(o, o) for o in optimize} 702 optimize = {legacy_opts_map.get(o, o) for o in optimize}
1183 repo = repo.unfiltered() 703 repo = repo.unfiltered()
1184 704
1185 revlogs = set(UPGRADE_ALL_REVLOGS) 705 revlogs = set(upgrade_engine.UPGRADE_ALL_REVLOGS)
1186 specentries = ( 706 specentries = (
1187 (UPGRADE_CHANGELOG, changelog), 707 (upgrade_engine.UPGRADE_CHANGELOG, changelog),
1188 (UPGRADE_MANIFEST, manifest), 708 (upgrade_engine.UPGRADE_MANIFEST, manifest),
1189 (UPGRADE_FILELOGS, filelogs), 709 (upgrade_engine.UPGRADE_FILELOGS, filelogs),
1190 ) 710 )
1191 specified = [(y, x) for (y, x) in specentries if x is not None] 711 specified = [(y, x) for (y, x) in specentries if x is not None]
1192 if specified: 712 if specified:
1193 # we have some limitation on revlogs to be recloned 713 # we have some limitation on revlogs to be recloned
1194 if any(x for y, x in specified): 714 if any(x for y, x in specified):
1285 ) 805 )
1286 806
1287 removedreqs = repo.requirements - newreqs 807 removedreqs = repo.requirements - newreqs
1288 addedreqs = newreqs - repo.requirements 808 addedreqs = newreqs - repo.requirements
1289 809
1290 if revlogs != UPGRADE_ALL_REVLOGS: 810 if revlogs != upgrade_engine.UPGRADE_ALL_REVLOGS:
1291 incompatible = RECLONES_REQUIREMENTS & (removedreqs | addedreqs) 811 incompatible = RECLONES_REQUIREMENTS & (removedreqs | addedreqs)
1292 if incompatible: 812 if incompatible:
1293 msg = _( 813 msg = _(
1294 b'ignoring revlogs selection flags, format requirements ' 814 b'ignoring revlogs selection flags, format requirements '
1295 b'change: %s\n' 815 b'change: %s\n'
1296 ) 816 )
1297 ui.warn(msg % b', '.join(sorted(incompatible))) 817 ui.warn(msg % b', '.join(sorted(incompatible)))
1298 revlogs = UPGRADE_ALL_REVLOGS 818 revlogs = upgrade_engine.UPGRADE_ALL_REVLOGS
1299 819
1300 def write_labeled(l, label): 820 def write_labeled(l, label):
1301 first = True 821 first = True
1302 for r in sorted(l): 822 for r in sorted(l):
1303 if not first: 823 if not first:
1445 # clone ui without using ui.copy because repo.ui is protected 965 # clone ui without using ui.copy because repo.ui is protected
1446 repoui = repo.ui.__class__(repo.ui) 966 repoui = repo.ui.__class__(repo.ui)
1447 dstrepo = hg.repository(repoui, path=tmppath, create=True) 967 dstrepo = hg.repository(repoui, path=tmppath, create=True)
1448 968
1449 with dstrepo.wlock(), dstrepo.lock(): 969 with dstrepo.wlock(), dstrepo.lock():
1450 backuppath = _upgraderepo( 970 backuppath = upgrade_engine.upgrade(
1451 ui, repo, dstrepo, newreqs, upgradeactions, revlogs=revlogs 971 ui, repo, dstrepo, newreqs, upgradeactions, revlogs=revlogs
1452 ) 972 )
1453 if not (backup or backuppath is None): 973 if not (backup or backuppath is None):
1454 ui.status( 974 ui.status(
1455 _(b'removing old repository content%s\n') % backuppath 975 _(b'removing old repository content%s\n') % backuppath