diff --git a/mercurial/configitems.py b/mercurial/configitems.py --- a/mercurial/configitems.py +++ b/mercurial/configitems.py @@ -1386,6 +1386,12 @@ ) coreconfigitem( b'format', + b'use-share-safe.automatic-upgrade-of-mismatching-repositories', + default=False, + experimental=True, +) +coreconfigitem( + b'format', b'internal-phase', default=False, experimental=True, diff --git a/mercurial/helptext/config.txt b/mercurial/helptext/config.txt --- a/mercurial/helptext/config.txt +++ b/mercurial/helptext/config.txt @@ -1032,6 +1032,24 @@ Enabled by default in Mercurial 6.1. +``use-share-safe.automatic-upgrade-of-mismatching-repositories`` + When enable, automatic upgrade will be triggered when a repository format + mismatch its `use-share-safe` config. + + This is an advanced behavior that most user will not needs. We recommand you + don't use this unless you are a seasoned administrator of a Mercurial install + base. + + Automatic upgrade mean that any process accessing the repository will + upgrade the repository format to use `share-safe`. This only triggers if a + change is needed. This also apply to operation that would have been + read-only (like hg status). + + This configuration will apply for move in any direction, either adding the + `share-safe` format if `format.use-share-safe=yes` or removing the + `share-safe` requirement if `format.use-share-safe=no`. So we recommand + setting both this value and `format.use-share-safe` at the same time. + ``usestore`` Enable or disable the "store" repository format which improves compatibility with systems that fold case or otherwise mangle diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -3516,11 +3516,20 @@ def instance(ui, path, create, intents=None, createopts=None): + + # prevent cyclic import localrepo -> upgrade -> localrepo + from . import upgrade + localpath = urlutil.urllocalpath(path) if create: createrepository(ui, localpath, createopts=createopts) - return makelocalrepository(ui, localpath, intents=intents) + def repo_maker(): + return makelocalrepository(ui, localpath, intents=intents) + + repo = repo_maker() + repo = upgrade.may_auto_upgrade(repo, repo_maker) + return repo def islocal(path): diff --git a/mercurial/upgrade.py b/mercurial/upgrade.py --- a/mercurial/upgrade.py +++ b/mercurial/upgrade.py @@ -19,6 +19,7 @@ from .upgrade_utils import ( actions as upgrade_actions, + auto_upgrade, engine as upgrade_engine, ) @@ -26,6 +27,7 @@ stringutil, ) +may_auto_upgrade = auto_upgrade.may_auto_upgrade allformatvariant = upgrade_actions.allformatvariant diff --git a/mercurial/upgrade_utils/auto_upgrade.py b/mercurial/upgrade_utils/auto_upgrade.py new file mode 100644 --- /dev/null +++ b/mercurial/upgrade_utils/auto_upgrade.py @@ -0,0 +1,107 @@ +# upgrade.py - functions for automatic upgrade of Mercurial repository +# +# Copyright (c) 2022-present, Pierre-Yves David +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +from ..i18n import _ + +from .. import ( + error, + requirements as requirementsmod, + scmutil, +) + + +def get_share_safe_action(repo): + """return an automatic-upgrade action for `share-safe` if applicable + + If no action is needed, return None, otherwise return a callback to upgrade + or downgrade the repository according the configuration and repository + format. + """ + ui = repo.ui + requirements = repo.requirements + auto_upgrade_share_source = ui.configbool( + b'format', + b'use-share-safe.automatic-upgrade-of-mismatching-repositories', + ) + + action = None + + if ( + auto_upgrade_share_source + and requirementsmod.SHARED_REQUIREMENT not in requirements + ): + sf_config = ui.configbool(b'format', b'use-share-safe') + sf_local = requirementsmod.SHARESAFE_REQUIREMENT in requirements + if sf_config and not sf_local: + msg = _( + b"automatically upgrading repository to the `share-safe`" + b" feature\n" + ) + hint = b"(see `hg help config.format.use-share-safe` for details)\n" + + def action(): + if not ui.quiet: + ui.write_err(msg) + ui.write_err(hint) + requirements.add(requirementsmod.SHARESAFE_REQUIREMENT) + scmutil.writereporequirements(repo, requirements) + + elif sf_local and not sf_config: + msg = _( + b"automatically downgrading repository from the `share-safe`" + b" feature\n" + ) + hint = b"(see `hg help config.format.use-share-safe` for details)\n" + + def action(): + if not ui.quiet: + ui.write_err(msg) + ui.write_err(hint) + requirements.discard(requirementsmod.SHARESAFE_REQUIREMENT) + scmutil.writereporequirements(repo, requirements) + + return action + + +AUTO_UPGRADE_ACTIONS = [ + get_share_safe_action, +] + + +def may_auto_upgrade(repo, maker_func): + """potentially perform auto-upgrade and return the final repository to use + + Auto-upgrade are "quick" repository upgrade that might automatically be run + by "any" repository access. See `hg help config.format` for automatic + upgrade documentation. + + note: each relevant upgrades are done one after the other for simplicity. + This avoid having repository is partially inconsistent state while + upgrading. + + repo: the current repository instance + maker_func: a factory function that can recreate a repository after an upgrade + """ + clear = False + + loop = 0 + + while not clear: + loop += 1 + if loop > 100: + # XXX basic protection against infinite loop, make it better. + raise error.ProgrammingError("Too many auto upgrade loops") + clear = True + for get_action in AUTO_UPGRADE_ACTIONS: + action = get_action(repo) + if action is not None: + clear = False + with repo.wlock(wait=False), repo.lock(wait=False): + action = get_action(repo) + if action is not None: + action() + repo = maker_func() + return repo diff --git a/rust/rhg/src/main.rs b/rust/rhg/src/main.rs --- a/rust/rhg/src/main.rs +++ b/rust/rhg/src/main.rs @@ -7,10 +7,10 @@ use clap::ArgMatches; use format_bytes::{format_bytes, join}; use hg::config::{Config, ConfigSource}; -use hg::exit_codes; use hg::repo::{Repo, RepoError}; use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; use hg::utils::SliceExt; +use hg::{exit_codes, requirements}; use std::collections::HashSet; use std::ffi::OsString; use std::os::unix::prelude::CommandExt; @@ -724,6 +724,50 @@ } } +/// Array of tuples of (auto upgrade conf, feature conf, local requirement) +const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[ + ( + ("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"), + ("format", "use-share-safe"), + requirements::SHARESAFE_REQUIREMENT, + ), +]; + +/// Mercurial allows users to automatically upgrade their repository. +/// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade +/// is needed. +fn check_auto_upgrade( + config: &Config, + reqs: &HashSet, +) -> Result<(), CommandError> { + for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() { + let auto_upgrade = config + .get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?; + + if auto_upgrade { + let want_it = config.get_bool( + feature_conf.0.as_bytes(), + feature_conf.1.as_bytes(), + )?; + let have_it = reqs.contains(*local_req); + + let action = match (want_it, have_it) { + (true, false) => Some("upgrade"), + (false, true) => Some("downgrade"), + _ => None, + }; + if let Some(action) = action { + let message = format!( + "automatic {} {}.{}", + action, upgrade_conf.0, upgrade_conf.1 + ); + return Err(CommandError::unsupported(message)); + } + } + } + Ok(()) +} + fn check_unsupported( config: &Config, repo: Result<&Repo, &NoRepoInCwdError>, @@ -740,6 +784,7 @@ if repo.has_subrepos()? { Err(CommandError::unsupported("sub-repositories"))? } + check_auto_upgrade(config, repo.requirements())?; } if config.has_non_empty_section(b"encode") { diff --git a/tests/test-help.t b/tests/test-help.t --- a/tests/test-help.t +++ b/tests/test-help.t @@ -1603,6 +1603,8 @@ "use-share-safe" + "use-share-safe.automatic-upgrade-of-mismatching-repositories" + "usestore" "sparse-revlog"