actions.py
1054 lines
| 33.0 KiB
| text/x-python
|
PythonLexer
r46662 | # upgrade.py - functions for in place upgrade of Mercurial repository | |||
# | ||||
# Copyright (c) 2016-present, Gregory Szorc | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
from __future__ import absolute_import | ||||
from ..i18n import _ | ||||
from .. import ( | ||||
r46663 | error, | |||
r46662 | localrepo, | |||
|
r47769 | pycompat, | ||
r46662 | requirements, | |||
|
r46832 | revlog, | ||
r46662 | util, | |||
) | ||||
from ..utils import compression | ||||
|
r47769 | if pycompat.TYPE_CHECKING: | ||
from typing import ( | ||||
List, | ||||
Type, | ||||
) | ||||
r46662 | # list of requirements that request a clone of all revlog if added/removed | |||
RECLONES_REQUIREMENTS = { | ||||
|
r47372 | requirements.GENERALDELTA_REQUIREMENT, | ||
r46662 | requirements.SPARSEREVLOG_REQUIREMENT, | |||
r48006 | requirements.REVLOGV2_REQUIREMENT, | |||
r48038 | requirements.CHANGELOGV2_REQUIREMENT, | |||
r46662 | } | |||
def preservedrequirements(repo): | ||||
return set() | ||||
|
r46823 | FORMAT_VARIANT = b'deficiency' | ||
r46662 | OPTIMISATION = b'optimization' | |||
class improvement(object): | ||||
|
r48110 | """Represents an improvement that can be made as part of an upgrade.""" | ||
r46662 | ||||
|
r48110 | ### The following attributes should be defined for each subclass: | ||
|
r47093 | |||
|
r48110 | # Either ``FORMAT_VARIANT`` or ``OPTIMISATION``. | ||
# A format variant is where we change the storage format. Not all format | ||||
# variant changes are an obvious problem. | ||||
# An optimization is an action (sometimes optional) that | ||||
# can be taken to further improve the state of the repository. | ||||
type = None | ||||
r46662 | ||||
# machine-readable string uniquely identifying this improvement. it will be | ||||
# mapped to an action later in the upgrade process. | ||||
name = None | ||||
# message intended for humans explaining the improvement in more detail, | ||||
|
r46823 | # including the implications of it ``FORMAT_VARIANT`` types, should be | ||
# worded | ||||
r46662 | # in the present tense. | |||
description = None | ||||
# message intended for humans explaining what an upgrade addressing this | ||||
# issue will do. should be worded in the future tense. | ||||
upgrademessage = None | ||||
# value of current Mercurial default for new repository | ||||
default = None | ||||
|
r46830 | # Message intended for humans which will be shown post an upgrade | ||
# operation when the improvement will be added | ||||
postupgrademessage = None | ||||
# Message intended for humans which will be shown post an upgrade | ||||
# operation in which this improvement was removed | ||||
postdowngrademessage = None | ||||
|
r48111 | # By default we assume that every improvement touches requirements and all revlogs | ||
|
r48110 | |||
# Whether this improvement touches filelogs | ||||
|
r47093 | touches_filelogs = True | ||
|
r48110 | |||
# Whether this improvement touches manifests | ||||
|
r47093 | touches_manifests = True | ||
|
r48110 | |||
# Whether this improvement touches changelog | ||||
|
r47093 | touches_changelog = True | ||
|
r48110 | |||
# Whether this improvement changes repository requirements | ||||
|
r47093 | touches_requirements = True | ||
|
r48111 | # Whether this improvement touches the dirstate | ||
touches_dirstate = False | ||||
|
r48110 | |||
allformatvariant = [] # type: List[Type['formatvariant']] | ||||
def registerformatvariant(cls): | ||||
allformatvariant.append(cls) | ||||
return cls | ||||
class formatvariant(improvement): | ||||
"""an improvement subclass dedicated to repository format""" | ||||
type = FORMAT_VARIANT | ||||
r46662 | ||||
@staticmethod | ||||
def fromrepo(repo): | ||||
"""current value of the variant in the repository""" | ||||
raise NotImplementedError() | ||||
@staticmethod | ||||
def fromconfig(repo): | ||||
"""current value of the variant in the configuration""" | ||||
raise NotImplementedError() | ||||
class requirementformatvariant(formatvariant): | ||||
"""formatvariant based on a 'requirement' name. | ||||
Many format variant are controlled by a 'requirement'. We define a small | ||||
subclass to factor the code. | ||||
""" | ||||
# the requirement that control this format variant | ||||
_requirement = None | ||||
@staticmethod | ||||
def _newreporequirements(ui): | ||||
return localrepo.newreporequirements( | ||||
ui, localrepo.defaultcreateopts(ui) | ||||
) | ||||
@classmethod | ||||
def fromrepo(cls, repo): | ||||
assert cls._requirement is not None | ||||
return cls._requirement in repo.requirements | ||||
@classmethod | ||||
def fromconfig(cls, repo): | ||||
assert cls._requirement is not None | ||||
return cls._requirement in cls._newreporequirements(repo.ui) | ||||
@registerformatvariant | ||||
class fncache(requirementformatvariant): | ||||
name = b'fncache' | ||||
|
r47383 | _requirement = requirements.FNCACHE_REQUIREMENT | ||
r46662 | ||||
default = True | ||||
description = _( | ||||
b'long and reserved filenames may not work correctly; ' | ||||
b'repository performance is sub-optimal' | ||||
) | ||||
upgrademessage = _( | ||||
b'repository will be more resilient to storing ' | ||||
b'certain paths and performance of certain ' | ||||
b'operations should be improved' | ||||
) | ||||
@registerformatvariant | ||||
|
r48111 | class dirstatev2(requirementformatvariant): | ||
name = b'dirstate-v2' | ||||
_requirement = requirements.DIRSTATE_V2_REQUIREMENT | ||||
default = False | ||||
description = _( | ||||
b'version 1 of the dirstate file format requires ' | ||||
r49116 | b'reading and parsing it all at once.\n' | |||
b'Version 2 has a better structure,' | ||||
b'better information and lighter update mechanism' | ||||
|
r48111 | ) | ||
upgrademessage = _(b'"hg status" will be faster') | ||||
touches_filelogs = False | ||||
touches_manifests = False | ||||
touches_changelog = False | ||||
touches_requirements = True | ||||
touches_dirstate = True | ||||
@registerformatvariant | ||||
r46662 | class dotencode(requirementformatvariant): | |||
name = b'dotencode' | ||||
|
r47381 | _requirement = requirements.DOTENCODE_REQUIREMENT | ||
r46662 | ||||
default = True | ||||
description = _( | ||||
b'storage of filenames beginning with a period or ' | ||||
b'space may not work correctly' | ||||
) | ||||
upgrademessage = _( | ||||
b'repository will be better able to store files ' | ||||
b'beginning with a space or period' | ||||
) | ||||
@registerformatvariant | ||||
class generaldelta(requirementformatvariant): | ||||
name = b'generaldelta' | ||||
|
r47372 | _requirement = requirements.GENERALDELTA_REQUIREMENT | ||
r46662 | ||||
default = True | ||||
description = _( | ||||
b'deltas within internal storage are unable to ' | ||||
b'choose optimal revisions; repository is larger and ' | ||||
b'slower than it could be; interaction with other ' | ||||
b'repositories may require extra network and CPU ' | ||||
b'resources, making "hg push" and "hg pull" slower' | ||||
) | ||||
upgrademessage = _( | ||||
b'repository storage will be able to create ' | ||||
b'optimal deltas; new repository data will be ' | ||||
b'smaller and read times should decrease; ' | ||||
b'interacting with other repositories using this ' | ||||
b'storage model should require less network and ' | ||||
b'CPU resources, making "hg push" and "hg pull" ' | ||||
b'faster' | ||||
) | ||||
@registerformatvariant | ||||
class sharesafe(requirementformatvariant): | ||||
|
r47052 | name = b'share-safe' | ||
r46662 | _requirement = requirements.SHARESAFE_REQUIREMENT | |||
default = False | ||||
description = _( | ||||
b'old shared repositories do not share source repository ' | ||||
b'requirements and config. This leads to various problems ' | ||||
b'when the source repository format is upgraded or some new ' | ||||
b'extensions are enabled.' | ||||
) | ||||
upgrademessage = _( | ||||
b'Upgrades a repository to share-safe format so that future ' | ||||
b'shares of this repository share its requirements and configs.' | ||||
) | ||||
|
r46830 | postdowngrademessage = _( | ||
b'repository downgraded to not use share safe mode, ' | ||||
b'existing shares will not work and needs to' | ||||
b' be reshared.' | ||||
) | ||||
postupgrademessage = _( | ||||
b'repository upgraded to share safe mode, existing' | ||||
b' shares will still work in old non-safe mode. ' | ||||
b'Re-share existing shares to use them in safe mode' | ||||
b' New shares will be created in safe mode.' | ||||
) | ||||
|
r47094 | # upgrade only needs to change the requirements | ||
touches_filelogs = False | ||||
touches_manifests = False | ||||
touches_changelog = False | ||||
touches_requirements = True | ||||
r46662 | ||||
@registerformatvariant | ||||
class sparserevlog(requirementformatvariant): | ||||
name = b'sparserevlog' | ||||
_requirement = requirements.SPARSEREVLOG_REQUIREMENT | ||||
default = True | ||||
description = _( | ||||
b'in order to limit disk reading and memory usage on older ' | ||||
b'version, the span of a delta chain from its root to its ' | ||||
b'end is limited, whatever the relevant data in this span. ' | ||||
b'This can severly limit Mercurial ability to build good ' | ||||
b'chain of delta resulting is much more storage space being ' | ||||
b'taken and limit reusability of on disk delta during ' | ||||
b'exchange.' | ||||
) | ||||
upgrademessage = _( | ||||
b'Revlog supports delta chain with more unused data ' | ||||
b'between payload. These gaps will be skipped at read ' | ||||
b'time. This allows for better delta chains, making a ' | ||||
b'better compression and faster exchange with server.' | ||||
) | ||||
@registerformatvariant | ||||
class persistentnodemap(requirementformatvariant): | ||||
name = b'persistent-nodemap' | ||||
_requirement = requirements.NODEMAP_REQUIREMENT | ||||
default = False | ||||
description = _( | ||||
b'persist the node -> rev mapping on disk to speedup lookup' | ||||
) | ||||
upgrademessage = _(b'Speedup revision lookup by node id.') | ||||
@registerformatvariant | ||||
class copiessdc(requirementformatvariant): | ||||
name = b'copies-sdc' | ||||
_requirement = requirements.COPIESSDC_REQUIREMENT | ||||
default = False | ||||
description = _(b'Stores copies information alongside changesets.') | ||||
upgrademessage = _( | ||||
b'Allows to use more efficient algorithm to deal with ' b'copy tracing.' | ||||
) | ||||
@registerformatvariant | ||||
|
r47439 | class revlogv2(requirementformatvariant): | ||
name = b'revlog-v2' | ||||
_requirement = requirements.REVLOGV2_REQUIREMENT | ||||
default = False | ||||
description = _(b'Version 2 of the revlog.') | ||||
upgrademessage = _(b'very experimental') | ||||
@registerformatvariant | ||||
r48037 | class changelogv2(requirementformatvariant): | |||
name = b'changelog-v2' | ||||
_requirement = requirements.CHANGELOGV2_REQUIREMENT | ||||
default = False | ||||
description = _(b'An iteration of the revlog focussed on changelog needs.') | ||||
upgrademessage = _(b'quite experimental') | ||||
@registerformatvariant | ||||
r46662 | class removecldeltachain(formatvariant): | |||
name = b'plain-cl-delta' | ||||
default = True | ||||
description = _( | ||||
b'changelog storage is using deltas instead of ' | ||||
b'raw entries; changelog reading and any ' | ||||
b'operation relying on changelog data are slower ' | ||||
b'than they could be' | ||||
) | ||||
upgrademessage = _( | ||||
b'changelog storage will be reformated to ' | ||||
b'store raw entries; changelog reading will be ' | ||||
b'faster; changelog size may be reduced' | ||||
) | ||||
@staticmethod | ||||
def fromrepo(repo): | ||||
# Mercurial 4.0 changed changelogs to not use delta chains. Search for | ||||
# changelogs with deltas. | ||||
cl = repo.changelog | ||||
chainbase = cl.chainbase | ||||
return all(rev == chainbase(rev) for rev in cl) | ||||
@staticmethod | ||||
def fromconfig(repo): | ||||
return True | ||||
r47636 | _has_zstd = ( | |||
b'zstd' in util.compengines | ||||
and util.compengines[b'zstd'].available() | ||||
and util.compengines[b'zstd'].revlogheader() | ||||
) | ||||
r46662 | @registerformatvariant | |||
class compressionengine(formatvariant): | ||||
name = b'compression' | ||||
r47636 | ||||
if _has_zstd: | ||||
default = b'zstd' | ||||
else: | ||||
default = b'zlib' | ||||
r46662 | ||||
description = _( | ||||
b'Compresion algorithm used to compress data. ' | ||||
b'Some engine are faster than other' | ||||
) | ||||
upgrademessage = _( | ||||
b'revlog content will be recompressed with the new algorithm.' | ||||
) | ||||
@classmethod | ||||
def fromrepo(cls, repo): | ||||
# we allow multiple compression engine requirement to co-exist because | ||||
# strickly speaking, revlog seems to support mixed compression style. | ||||
# | ||||
# The compression used for new entries will be "the last one" | ||||
compression = b'zlib' | ||||
for req in repo.requirements: | ||||
prefix = req.startswith | ||||
if prefix(b'revlog-compression-') or prefix(b'exp-compression-'): | ||||
compression = req.split(b'-', 2)[2] | ||||
return compression | ||||
@classmethod | ||||
def fromconfig(cls, repo): | ||||
compengines = repo.ui.configlist(b'format', b'revlog-compression') | ||||
# return the first valid value as the selection code would do | ||||
for comp in compengines: | ||||
if comp in util.compengines: | ||||
r47610 | e = util.compengines[comp] | |||
if e.available() and e.revlogheader(): | ||||
return comp | ||||
r46662 | ||||
# no valide compression found lets display it all for clarity | ||||
return b','.join(compengines) | ||||
@registerformatvariant | ||||
class compressionlevel(formatvariant): | ||||
name = b'compression-level' | ||||
default = b'default' | ||||
description = _(b'compression level') | ||||
upgrademessage = _(b'revlog content will be recompressed') | ||||
@classmethod | ||||
def fromrepo(cls, repo): | ||||
comp = compressionengine.fromrepo(repo) | ||||
level = None | ||||
if comp == b'zlib': | ||||
level = repo.ui.configint(b'storage', b'revlog.zlib.level') | ||||
elif comp == b'zstd': | ||||
level = repo.ui.configint(b'storage', b'revlog.zstd.level') | ||||
if level is None: | ||||
return b'default' | ||||
return bytes(level) | ||||
@classmethod | ||||
def fromconfig(cls, repo): | ||||
comp = compressionengine.fromconfig(repo) | ||||
level = None | ||||
if comp == b'zlib': | ||||
level = repo.ui.configint(b'storage', b'revlog.zlib.level') | ||||
elif comp == b'zstd': | ||||
level = repo.ui.configint(b'storage', b'revlog.zstd.level') | ||||
if level is None: | ||||
return b'default' | ||||
return bytes(level) | ||||
|
r46822 | def find_format_upgrades(repo): | ||
"""returns a list of format upgrades which can be perform on the repo""" | ||||
upgrades = [] | ||||
r46662 | ||||
# We could detect lack of revlogv1 and store here, but they were added | ||||
# in 0.9.2 and we don't support upgrading repos without these | ||||
# requirements, so let's not bother. | ||||
for fv in allformatvariant: | ||||
if not fv.fromrepo(repo): | ||||
|
r46822 | upgrades.append(fv) | ||
r46662 | ||||
|
r46822 | return upgrades | ||
r46662 | ||||
|
r46829 | def find_format_downgrades(repo): | ||
"""returns a list of format downgrades which will be performed on the repo | ||||
because of disabled config option for them""" | ||||
downgrades = [] | ||||
for fv in allformatvariant: | ||||
|
r46847 | if fv.name == b'compression': | ||
# If there is a compression change between repository | ||||
# and config, destination repository compression will change | ||||
# and current compression will be removed. | ||||
if fv.fromrepo(repo) != fv.fromconfig(repo): | ||||
downgrades.append(fv) | ||||
continue | ||||
|
r46829 | # format variant exist in repo but does not exist in new repository | ||
# config | ||||
if fv.fromrepo(repo) and not fv.fromconfig(repo): | ||||
downgrades.append(fv) | ||||
return downgrades | ||||
r46662 | ALL_OPTIMISATIONS = [] | |||
def register_optimization(obj): | ||||
ALL_OPTIMISATIONS.append(obj) | ||||
return obj | ||||
|
r48110 | class optimization(improvement): | ||
"""an improvement subclass dedicated to optimizations""" | ||||
type = OPTIMISATION | ||||
@register_optimization | ||||
class redeltaparents(optimization): | ||||
name = b're-delta-parent' | ||||
type = OPTIMISATION | ||||
description = _( | ||||
b'deltas within internal storage will be recalculated to ' | ||||
b'choose an optimal base revision where this was not ' | ||||
b'already done; the size of the repository may shrink and ' | ||||
b'various operations may become faster; the first time ' | ||||
b'this optimization is performed could slow down upgrade ' | ||||
b'execution considerably; subsequent invocations should ' | ||||
b'not run noticeably slower' | ||||
r46662 | ) | |||
|
r48110 | |||
upgrademessage = _( | ||||
b'deltas within internal storage will choose a new ' | ||||
b'base revision if needed' | ||||
) | ||||
@register_optimization | ||||
class redeltamultibase(optimization): | ||||
name = b're-delta-multibase' | ||||
type = OPTIMISATION | ||||
description = _( | ||||
b'deltas within internal storage will be recalculated ' | ||||
b'against multiple base revision and the smallest ' | ||||
b'difference will be used; the size of the repository may ' | ||||
b'shrink significantly when there are many merges; this ' | ||||
b'optimization will slow down execution in proportion to ' | ||||
b'the number of merges in the repository and the amount ' | ||||
b'of files in the repository; this slow down should not ' | ||||
b'be significant unless there are tens of thousands of ' | ||||
b'files and thousands of merges' | ||||
) | ||||
r46662 | ||||
|
r48110 | upgrademessage = _( | ||
b'deltas within internal storage will choose an ' | ||||
b'optimal delta by computing deltas against multiple ' | ||||
b'parents; may slow down execution time ' | ||||
b'significantly' | ||||
r46662 | ) | |||
|
r48110 | |||
@register_optimization | ||||
class redeltaall(optimization): | ||||
name = b're-delta-all' | ||||
type = OPTIMISATION | ||||
description = _( | ||||
b'deltas within internal storage will always be ' | ||||
b'recalculated without reusing prior deltas; this will ' | ||||
b'likely make execution run several times slower; this ' | ||||
b'optimization is typically not needed' | ||||
) | ||||
r46662 | ||||
|
r48110 | upgrademessage = _( | ||
b'deltas within internal storage will be fully ' | ||||
b'recomputed; this will likely drastically slow down ' | ||||
b'execution time' | ||||
r46662 | ) | |||
|
r48110 | |||
@register_optimization | ||||
class redeltafulladd(optimization): | ||||
name = b're-delta-fulladd' | ||||
type = OPTIMISATION | ||||
r46662 | ||||
|
r48110 | description = _( | ||
b'every revision will be re-added as if it was new ' | ||||
b'content. It will go through the full storage ' | ||||
b'mechanism giving extensions a chance to process it ' | ||||
b'(eg. lfs). This is similar to "re-delta-all" but even ' | ||||
b'slower since more logic is involved.' | ||||
r46662 | ) | |||
|
r48110 | |||
upgrademessage = _( | ||||
b'each revision will be added as new content to the ' | ||||
b'internal storage; this will likely drastically slow ' | ||||
b'down execution time, but some extensions might need ' | ||||
b'it' | ||||
) | ||||
r46662 | ||||
def findoptimizations(repo): | ||||
"""Determine optimisation that could be used during upgrade""" | ||||
# These are unconditionally added. There is logic later that figures out | ||||
# which ones to apply. | ||||
return list(ALL_OPTIMISATIONS) | ||||
|
r46827 | def determine_upgrade_actions( | ||
|
r46826 | repo, format_upgrades, optimizations, sourcereqs, destreqs | ||
): | ||||
r46662 | """Determine upgrade actions that will be performed. | |||
|
r46822 | Given a list of improvements as returned by ``find_format_upgrades`` and | ||
r46662 | ``findoptimizations``, determine the list of upgrade actions that | |||
will be performed. | ||||
The role of this function is to filter improvements if needed, apply | ||||
recommended optimizations from the improvements list that make sense, | ||||
etc. | ||||
Returns a list of action names. | ||||
""" | ||||
newactions = [] | ||||
|
r46822 | for d in format_upgrades: | ||
|
r48446 | if util.safehasattr(d, '_requirement'): | ||
name = d._requirement | ||||
else: | ||||
name = None | ||||
r46662 | ||||
# If the action is a requirement that doesn't show up in the | ||||
# destination requirements, prune the action. | ||||
if name is not None and name not in destreqs: | ||||
continue | ||||
newactions.append(d) | ||||
|
r46826 | newactions.extend(o for o in sorted(optimizations) if o not in newactions) | ||
r46662 | # FUTURE consider adding some optimizations here for certain transitions. | |||
# e.g. adding generaldelta could schedule parent redeltas. | ||||
return newactions | ||||
r46663 | ||||
r46673 | class UpgradeOperation(object): | |||
"""represent the work to be done during an upgrade""" | ||||
|
r46807 | def __init__( | ||
self, | ||||
ui, | ||||
new_requirements, | ||||
current_requirements, | ||||
|
r46827 | upgrade_actions, | ||
|
r46830 | removed_actions, | ||
|
r46807 | revlogs_to_process, | ||
|
r47092 | backup_store, | ||
|
r46807 | ): | ||
|
r46804 | self.ui = ui | ||
|
r46807 | self.new_requirements = new_requirements | ||
self.current_requirements = current_requirements | ||||
|
r46827 | # list of upgrade actions the operation will perform | ||
self.upgrade_actions = upgrade_actions | ||||
|
r46830 | self.removed_actions = removed_actions | ||
r46673 | self.revlogs_to_process = revlogs_to_process | |||
|
r46807 | # requirements which will be added by the operation | ||
self._added_requirements = ( | ||||
self.new_requirements - self.current_requirements | ||||
) | ||||
# requirements which will be removed by the operation | ||||
self._removed_requirements = ( | ||||
self.current_requirements - self.new_requirements | ||||
) | ||||
# requirements which will be preserved by the operation | ||||
self._preserved_requirements = ( | ||||
self.current_requirements & self.new_requirements | ||||
) | ||||
|
r46808 | # optimizations which are not used and it's recommended that they | ||
# should use them | ||||
all_optimizations = findoptimizations(None) | ||||
self.unused_optimizations = [ | ||||
|
r46827 | i for i in all_optimizations if i not in self.upgrade_actions | ||
|
r46808 | ] | ||
r46673 | ||||
|
r46832 | # delta reuse mode of this upgrade operation | ||
|
r48111 | upgrade_actions_names = self.upgrade_actions_names | ||
|
r46832 | self.delta_reuse_mode = revlog.revlog.DELTAREUSEALWAYS | ||
|
r48111 | if b're-delta-all' in upgrade_actions_names: | ||
|
r46832 | self.delta_reuse_mode = revlog.revlog.DELTAREUSENEVER | ||
|
r48111 | elif b're-delta-parent' in upgrade_actions_names: | ||
|
r46832 | self.delta_reuse_mode = revlog.revlog.DELTAREUSESAMEREVS | ||
|
r48111 | elif b're-delta-multibase' in upgrade_actions_names: | ||
|
r46832 | self.delta_reuse_mode = revlog.revlog.DELTAREUSESAMEREVS | ||
|
r48111 | elif b're-delta-fulladd' in upgrade_actions_names: | ||
|
r46832 | self.delta_reuse_mode = revlog.revlog.DELTAREUSEFULLADD | ||
|
r46835 | # should this operation force re-delta of both parents | ||
self.force_re_delta_both_parents = ( | ||||
|
r48111 | b're-delta-multibase' in upgrade_actions_names | ||
|
r46835 | ) | ||
|
r47092 | # should this operation create a backup of the store | ||
self.backup_store = backup_store | ||||
|
r48111 | @property | ||
def upgrade_actions_names(self): | ||||
return set([a.name for a in self.upgrade_actions]) | ||||
@property | ||||
def requirements_only(self): | ||||
|
r47093 | # does the operation only touches repository requirement | ||
|
r48111 | return ( | ||
self.touches_requirements | ||||
and not self.touches_filelogs | ||||
and not self.touches_manifests | ||||
and not self.touches_changelog | ||||
and not self.touches_dirstate | ||||
|
r47093 | ) | ||
|
r48111 | @property | ||
def touches_filelogs(self): | ||||
|
r47093 | for a in self.upgrade_actions: | ||
# in optimisations, we re-process the revlogs again | ||||
if a.type == OPTIMISATION: | ||||
return True | ||||
elif a.touches_filelogs: | ||||
return True | ||||
for a in self.removed_actions: | ||||
if a.touches_filelogs: | ||||
return True | ||||
return False | ||||
|
r48111 | @property | ||
def touches_manifests(self): | ||||
|
r47093 | for a in self.upgrade_actions: | ||
# in optimisations, we re-process the revlogs again | ||||
if a.type == OPTIMISATION: | ||||
return True | ||||
elif a.touches_manifests: | ||||
return True | ||||
for a in self.removed_actions: | ||||
if a.touches_manifests: | ||||
return True | ||||
return False | ||||
|
r48111 | @property | ||
def touches_changelog(self): | ||||
|
r47093 | for a in self.upgrade_actions: | ||
# in optimisations, we re-process the revlogs again | ||||
if a.type == OPTIMISATION: | ||||
return True | ||||
elif a.touches_changelog: | ||||
return True | ||||
for a in self.removed_actions: | ||||
if a.touches_changelog: | ||||
return True | ||||
return False | ||||
|
r48111 | @property | ||
def touches_requirements(self): | ||||
|
r47093 | for a in self.upgrade_actions: | ||
# optimisations are used to re-process revlogs and does not result | ||||
# in a requirement being added or removed | ||||
if a.type == OPTIMISATION: | ||||
pass | ||||
elif a.touches_requirements: | ||||
return True | ||||
for a in self.removed_actions: | ||||
if a.touches_requirements: | ||||
return True | ||||
|
r48111 | @property | ||
def touches_dirstate(self): | ||||
for a in self.upgrade_actions: | ||||
# revlog optimisations do not affect the dirstate | ||||
if a.type == OPTIMISATION: | ||||
pass | ||||
elif a.touches_dirstate: | ||||
return True | ||||
for a in self.removed_actions: | ||||
if a.touches_dirstate: | ||||
return True | ||||
|
r47093 | return False | ||
|
r46806 | def _write_labeled(self, l, label): | ||
""" | ||||
Utility function to aid writing of a list under one label | ||||
""" | ||||
first = True | ||||
for r in sorted(l): | ||||
if not first: | ||||
self.ui.write(b', ') | ||||
self.ui.write(r, label=label) | ||||
first = False | ||||
|
r46807 | def print_requirements(self): | ||
self.ui.write(_(b'requirements\n')) | ||||
self.ui.write(_(b' preserved: ')) | ||||
self._write_labeled( | ||||
self._preserved_requirements, "upgrade-repo.requirement.preserved" | ||||
) | ||||
self.ui.write((b'\n')) | ||||
if self._removed_requirements: | ||||
self.ui.write(_(b' removed: ')) | ||||
self._write_labeled( | ||||
self._removed_requirements, "upgrade-repo.requirement.removed" | ||||
) | ||||
self.ui.write((b'\n')) | ||||
if self._added_requirements: | ||||
self.ui.write(_(b' added: ')) | ||||
self._write_labeled( | ||||
self._added_requirements, "upgrade-repo.requirement.added" | ||||
) | ||||
self.ui.write((b'\n')) | ||||
self.ui.write(b'\n') | ||||
|
r46806 | def print_optimisations(self): | ||
|
r46827 | optimisations = [ | ||
a for a in self.upgrade_actions if a.type == OPTIMISATION | ||||
] | ||||
|
r46806 | optimisations.sort(key=lambda a: a.name) | ||
if optimisations: | ||||
self.ui.write(_(b'optimisations: ')) | ||||
self._write_labeled( | ||||
[a.name for a in optimisations], | ||||
"upgrade-repo.optimisation.performed", | ||||
) | ||||
self.ui.write(b'\n\n') | ||||
|
r46805 | def print_upgrade_actions(self): | ||
|
r46827 | for a in self.upgrade_actions: | ||
|
r46805 | self.ui.status(b'%s\n %s\n\n' % (a.name, a.upgrademessage)) | ||
|
r46804 | def print_affected_revlogs(self): | ||
if not self.revlogs_to_process: | ||||
self.ui.write((b'no revlogs to process\n')) | ||||
else: | ||||
self.ui.write((b'processed revlogs:\n')) | ||||
for r in sorted(self.revlogs_to_process): | ||||
self.ui.write((b' - %s\n' % r)) | ||||
self.ui.write((b'\n')) | ||||
|
r46808 | def print_unused_optimizations(self): | ||
for i in self.unused_optimizations: | ||||
self.ui.status(_(b'%s\n %s\n\n') % (i.name, i.description)) | ||||
|
r46827 | def has_upgrade_action(self, name): | ||
|
r47856 | """Check whether the upgrade operation will perform this action""" | ||
|
r46827 | return name in self._upgrade_actions_names | ||
|
r46805 | |||
|
r46830 | def print_post_op_messages(self): | ||
|
r47856 | """print post upgrade operation warning messages""" | ||
|
r46830 | for a in self.upgrade_actions: | ||
if a.postupgrademessage is not None: | ||||
self.ui.warn(b'%s\n' % a.postupgrademessage) | ||||
for a in self.removed_actions: | ||||
if a.postdowngrademessage is not None: | ||||
self.ui.warn(b'%s\n' % a.postdowngrademessage) | ||||
r46673 | ||||
r46664 | ### Code checking if a repository can got through the upgrade process at all. # | |||
def requiredsourcerequirements(repo): | ||||
"""Obtain requirements required to be present to upgrade a repo. | ||||
An upgrade will not be allowed if the repository doesn't have the | ||||
requirements returned by this function. | ||||
""" | ||||
return { | ||||
# Introduced in Mercurial 0.9.2. | ||||
|
r47382 | requirements.STORE_REQUIREMENT, | ||
r46664 | } | |||
def blocksourcerequirements(repo): | ||||
"""Obtain requirements that will prevent an upgrade from occurring. | ||||
An upgrade cannot be performed if the source repository contains a | ||||
requirements in the returned set. | ||||
""" | ||||
return { | ||||
# The upgrade code does not yet support these experimental features. | ||||
# This is an artificial limitation. | ||||
requirements.TREEMANIFEST_REQUIREMENT, | ||||
# This was a precursor to generaldelta and was never enabled by default. | ||||
# It should (hopefully) not exist in the wild. | ||||
b'parentdelta', | ||||
# Upgrade should operate on the actual store, not the shared link. | ||||
requirements.SHARED_REQUIREMENT, | ||||
} | ||||
|
r47439 | def check_revlog_version(reqs): | ||
"""Check that the requirements contain at least one Revlog version""" | ||||
all_revlogs = { | ||||
requirements.REVLOGV1_REQUIREMENT, | ||||
requirements.REVLOGV2_REQUIREMENT, | ||||
} | ||||
if not all_revlogs.intersection(reqs): | ||||
msg = _(b'cannot upgrade repository; missing a revlog version') | ||||
raise error.Abort(msg) | ||||
r46663 | def check_source_requirements(repo): | |||
"""Ensure that no existing requirements prevent the repository upgrade""" | ||||
|
r47439 | check_revlog_version(repo.requirements) | ||
r46663 | required = requiredsourcerequirements(repo) | |||
missingreqs = required - repo.requirements | ||||
if missingreqs: | ||||
msg = _(b'cannot upgrade repository; requirement missing: %s') | ||||
missingreqs = b', '.join(sorted(missingreqs)) | ||||
raise error.Abort(msg % missingreqs) | ||||
blocking = blocksourcerequirements(repo) | ||||
blockingreqs = blocking & repo.requirements | ||||
if blockingreqs: | ||||
m = _(b'cannot upgrade repository; unsupported source requirement: %s') | ||||
blockingreqs = b', '.join(sorted(blockingreqs)) | ||||
raise error.Abort(m % blockingreqs) | ||||
r46666 | ||||
### Verify the validity of the planned requirement changes #################### | ||||
r46667 | def supportremovedrequirements(repo): | |||
"""Obtain requirements that can be removed during an upgrade. | ||||
If an upgrade were to create a repository that dropped a requirement, | ||||
the dropped requirement must appear in the returned set for the upgrade | ||||
to be allowed. | ||||
""" | ||||
supported = { | ||||
requirements.SPARSEREVLOG_REQUIREMENT, | ||||
requirements.COPIESSDC_REQUIREMENT, | ||||
requirements.NODEMAP_REQUIREMENT, | ||||
requirements.SHARESAFE_REQUIREMENT, | ||||
|
r47439 | requirements.REVLOGV2_REQUIREMENT, | ||
r48038 | requirements.CHANGELOGV2_REQUIREMENT, | |||
|
r47439 | requirements.REVLOGV1_REQUIREMENT, | ||
|
r48111 | requirements.DIRSTATE_V2_REQUIREMENT, | ||
r46667 | } | |||
for name in compression.compengines: | ||||
engine = compression.compengines[name] | ||||
if engine.available() and engine.revlogheader(): | ||||
supported.add(b'exp-compression-%s' % name) | ||||
if engine.name() == b'zstd': | ||||
supported.add(b'revlog-compression-zstd') | ||||
return supported | ||||
def supporteddestrequirements(repo): | ||||
"""Obtain requirements that upgrade supports in the destination. | ||||
If the result of the upgrade would create requirements not in this set, | ||||
the upgrade is disallowed. | ||||
Extensions should monkeypatch this to add their custom requirements. | ||||
""" | ||||
supported = { | ||||
|
r47381 | requirements.DOTENCODE_REQUIREMENT, | ||
|
r47383 | requirements.FNCACHE_REQUIREMENT, | ||
|
r47372 | requirements.GENERALDELTA_REQUIREMENT, | ||
|
r47439 | requirements.REVLOGV1_REQUIREMENT, # allowed in case of downgrade | ||
|
r47382 | requirements.STORE_REQUIREMENT, | ||
r46667 | requirements.SPARSEREVLOG_REQUIREMENT, | |||
requirements.COPIESSDC_REQUIREMENT, | ||||
requirements.NODEMAP_REQUIREMENT, | ||||
requirements.SHARESAFE_REQUIREMENT, | ||||
|
r47439 | requirements.REVLOGV2_REQUIREMENT, | ||
r48038 | requirements.CHANGELOGV2_REQUIREMENT, | |||
|
r48052 | requirements.DIRSTATE_V2_REQUIREMENT, | ||
r46667 | } | |||
for name in compression.compengines: | ||||
engine = compression.compengines[name] | ||||
if engine.available() and engine.revlogheader(): | ||||
supported.add(b'exp-compression-%s' % name) | ||||
if engine.name() == b'zstd': | ||||
supported.add(b'revlog-compression-zstd') | ||||
return supported | ||||
def allowednewrequirements(repo): | ||||
"""Obtain requirements that can be added to a repository during upgrade. | ||||
This is used to disallow proposed requirements from being added when | ||||
they weren't present before. | ||||
We use a list of allowed requirement additions instead of a list of known | ||||
bad additions because the whitelist approach is safer and will prevent | ||||
future, unknown requirements from accidentally being added. | ||||
""" | ||||
supported = { | ||||
|
r47381 | requirements.DOTENCODE_REQUIREMENT, | ||
|
r47383 | requirements.FNCACHE_REQUIREMENT, | ||
|
r47372 | requirements.GENERALDELTA_REQUIREMENT, | ||
r46667 | requirements.SPARSEREVLOG_REQUIREMENT, | |||
requirements.COPIESSDC_REQUIREMENT, | ||||
requirements.NODEMAP_REQUIREMENT, | ||||
requirements.SHARESAFE_REQUIREMENT, | ||||
|
r47439 | requirements.REVLOGV1_REQUIREMENT, | ||
requirements.REVLOGV2_REQUIREMENT, | ||||
r48038 | requirements.CHANGELOGV2_REQUIREMENT, | |||
|
r48111 | requirements.DIRSTATE_V2_REQUIREMENT, | ||
r46667 | } | |||
for name in compression.compengines: | ||||
engine = compression.compengines[name] | ||||
if engine.available() and engine.revlogheader(): | ||||
supported.add(b'exp-compression-%s' % name) | ||||
if engine.name() == b'zstd': | ||||
supported.add(b'revlog-compression-zstd') | ||||
return supported | ||||
r46666 | def check_requirements_changes(repo, new_reqs): | |||
old_reqs = repo.requirements | ||||
|
r47439 | check_revlog_version(repo.requirements) | ||
r46666 | support_removal = supportremovedrequirements(repo) | |||
no_remove_reqs = old_reqs - new_reqs - support_removal | ||||
if no_remove_reqs: | ||||
msg = _(b'cannot upgrade repository; requirement would be removed: %s') | ||||
no_remove_reqs = b', '.join(sorted(no_remove_reqs)) | ||||
raise error.Abort(msg % no_remove_reqs) | ||||
support_addition = allowednewrequirements(repo) | ||||
no_add_reqs = new_reqs - old_reqs - support_addition | ||||
if no_add_reqs: | ||||
m = _(b'cannot upgrade repository; do not support adding requirement: ') | ||||
no_add_reqs = b', '.join(sorted(no_add_reqs)) | ||||
raise error.Abort(m + no_add_reqs) | ||||
supported = supporteddestrequirements(repo) | ||||
unsupported_reqs = new_reqs - supported | ||||
if unsupported_reqs: | ||||
msg = _( | ||||
b'cannot upgrade repository; do not support destination ' | ||||
b'requirement: %s' | ||||
) | ||||
unsupported_reqs = b', '.join(sorted(unsupported_reqs)) | ||||
raise error.Abort(msg % unsupported_reqs) | ||||