# 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 annotations

from .i18n import _
from . import (
    error,
    hg,
    localrepo,
    lock as lockmod,
    pycompat,
    requirements as requirementsmod,
    scmutil,
)

from .upgrade_utils import (
    actions as upgrade_actions,
    auto_upgrade,
    engine as upgrade_engine,
)

from .utils import (
    stringutil,
)

may_auto_upgrade = auto_upgrade.may_auto_upgrade
allformatvariant = upgrade_actions.allformatvariant


def upgraderepo(
    ui,
    repo,
    run=False,
    optimize=None,
    backup=True,
    manifest=None,
    changelog=None,
    filelogs=None,
):
    """Upgrade a repository in place."""
    if optimize is None:
        optimize = set()
    repo = repo.unfiltered()

    specified_revlogs = {}
    if changelog is not None:
        specified_revlogs[upgrade_engine.UPGRADE_CHANGELOG] = changelog
    if manifest is not None:
        specified_revlogs[upgrade_engine.UPGRADE_MANIFEST] = manifest
    if filelogs is not None:
        specified_revlogs[upgrade_engine.UPGRADE_FILELOGS] = filelogs

    # Ensure the repository can be upgraded.
    upgrade_actions.check_source_requirements(repo)

    default_options = localrepo.defaultcreateopts(repo.ui)
    newreqs = localrepo.newreporequirements(repo.ui, default_options)
    newreqs.update(upgrade_actions.preservedrequirements(repo))

    upgrade_actions.check_requirements_changes(repo, newreqs)

    # Find and validate all improvements that can be made.
    alloptimizations = upgrade_actions.findoptimizations(repo)

    # Apply and Validate arguments.
    optimizations = []
    for o in alloptimizations:
        if o.name in optimize:
            optimizations.append(o)
            optimize.discard(o.name)

    if optimize:  # anything left is unknown
        raise error.Abort(
            _(b'unknown optimization action requested: %s')
            % b', '.join(sorted(optimize)),
            hint=_(b'run without arguments to see valid optimizations'),
        )

    format_upgrades = upgrade_actions.find_format_upgrades(repo)
    up_actions = upgrade_actions.determine_upgrade_actions(
        repo, format_upgrades, optimizations, repo.requirements, newreqs
    )
    removed_actions = upgrade_actions.find_format_downgrades(repo)

    # check if we need to touch revlog and if so, which ones

    touched_revlogs = set()
    overwrite_msg = _(b'warning: ignoring %14s, as upgrade is changing: %s\n')
    select_msg = _(b'note:    selecting %s for processing to change: %s\n')
    msg_issued = 0

    FL = upgrade_engine.UPGRADE_FILELOGS
    MN = upgrade_engine.UPGRADE_MANIFEST
    CL = upgrade_engine.UPGRADE_CHANGELOG

    if optimizations:
        if any(specified_revlogs.values()):
            # we have some limitation on revlogs to be recloned
            for rl, enabled in specified_revlogs.items():
                if enabled:
                    touched_revlogs.add(rl)
        else:
            touched_revlogs = set(upgrade_engine.UPGRADE_ALL_REVLOGS)
            for rl, enabled in specified_revlogs.items():
                if not enabled:
                    touched_revlogs.discard(rl)

    if repo.shared():
        unsafe_actions = set()
        unsafe_actions.update(up_actions)
        unsafe_actions.update(removed_actions)
        unsafe_actions.update(optimizations)
        unsafe_actions = [
            a for a in unsafe_actions if not a.compatible_with_share
        ]
        unsafe_actions.sort(key=lambda a: a.name)
        if unsafe_actions:
            m = _(b'cannot use these actions on a share repository: %s')
            h = _(b'upgrade the main repository directly')
            actions = b', '.join(a.name for a in unsafe_actions)
            m %= actions
            raise error.Abort(m, hint=h)

    for action in sorted(up_actions + removed_actions, key=lambda a: a.name):
        # optimisation does not "requires anything, they just needs it.
        if action.type != upgrade_actions.FORMAT_VARIANT:
            continue

        if action.touches_filelogs and FL not in touched_revlogs:
            if FL in specified_revlogs:
                if not specified_revlogs[FL]:
                    msg = overwrite_msg % (b'--no-filelogs', action.name)
                    ui.warn(msg)
                    msg_issued = 2
            else:
                msg = select_msg % (b'all-filelogs', action.name)
                ui.status(msg)
                if not ui.quiet:
                    msg_issued = 1
            touched_revlogs.add(FL)

        if action.touches_manifests and MN not in touched_revlogs:
            if MN in specified_revlogs:
                if not specified_revlogs[MN]:
                    msg = overwrite_msg % (b'--no-manifest', action.name)
                    ui.warn(msg)
                    msg_issued = 2
            else:
                msg = select_msg % (b'all-manifestlogs', action.name)
                ui.status(msg)
                if not ui.quiet:
                    msg_issued = 1
            touched_revlogs.add(MN)

        if action.touches_changelog and CL not in touched_revlogs:
            if CL in specified_revlogs:
                if not specified_revlogs[CL]:
                    msg = overwrite_msg % (b'--no-changelog', action.name)
                    ui.warn(msg)
                    msg_issued = True
            else:
                msg = select_msg % (b'changelog', action.name)
                ui.status(msg)
                if not ui.quiet:
                    msg_issued = 1
            touched_revlogs.add(CL)
    if msg_issued >= 2:
        ui.warn((b"\n"))
    elif msg_issued >= 1:
        ui.status((b"\n"))

    upgrade_op = upgrade_actions.UpgradeOperation(
        ui,
        newreqs,
        repo.requirements,
        up_actions,
        removed_actions,
        touched_revlogs,
        backup,
    )

    if not run:
        fromconfig = []
        onlydefault = []

        for d in format_upgrades:
            if d.fromconfig(repo):
                fromconfig.append(d)
            elif d.default:
                onlydefault.append(d)

        if fromconfig or onlydefault:
            if fromconfig:
                ui.status(
                    _(
                        b'repository lacks features recommended by '
                        b'current config options:\n\n'
                    )
                )
                for i in fromconfig:
                    ui.status(b'%s\n   %s\n\n' % (i.name, i.description))

            if onlydefault:
                ui.status(
                    _(
                        b'repository lacks features used by the default '
                        b'config options:\n\n'
                    )
                )
                for i in onlydefault:
                    ui.status(b'%s\n   %s\n\n' % (i.name, i.description))

            ui.status(b'\n')
        else:
            ui.status(_(b'(no format upgrades found in existing repository)\n'))

        ui.status(
            _(
                b'performing an upgrade with "--run" will make the following '
                b'changes:\n\n'
            )
        )

        upgrade_op.print_requirements()
        upgrade_op.print_optimisations()
        upgrade_op.print_upgrade_actions()
        upgrade_op.print_affected_revlogs()

        if upgrade_op.unused_optimizations:
            ui.status(
                _(
                    b'additional optimizations are available by specifying '
                    b'"--optimize <name>":\n\n'
                )
            )
            upgrade_op.print_unused_optimizations()
        return

    if not (upgrade_op.upgrade_actions or upgrade_op.removed_actions):
        ui.status(_(b'nothing to do\n'))
        return
    # Else we're in the run=true case.
    ui.write(_(b'upgrade will perform the following actions:\n\n'))
    upgrade_op.print_requirements()
    upgrade_op.print_optimisations()
    upgrade_op.print_upgrade_actions()
    upgrade_op.print_affected_revlogs()

    ui.status(_(b'beginning upgrade...\n'))
    with repo.wlock(), repo.lock():
        ui.status(_(b'repository locked and read-only\n'))
        # Our strategy for upgrading the repository is to create a new,
        # temporary repository, write data to it, then do a swap of the
        # data. There are less heavyweight ways to do this, but it is easier
        # to create a new repo object than to instantiate all the components
        # (like the store) separately.
        tmppath = pycompat.mkdtemp(prefix=b'upgrade.', dir=repo.path)
        backuppath = None
        try:
            ui.status(
                _(
                    b'creating temporary repository to stage upgraded '
                    b'data: %s\n'
                )
                % tmppath
            )

            # clone ui without using ui.copy because repo.ui is protected
            repoui = repo.ui.__class__(repo.ui)
            dstrepo = hg.repository(repoui, path=tmppath, create=True)

            with dstrepo.wlock(), dstrepo.lock():
                backuppath = upgrade_engine.upgrade(
                    ui, repo, dstrepo, upgrade_op
                )

        finally:
            ui.status(_(b'removing temporary repository %s\n') % tmppath)
            repo.vfs.rmtree(tmppath, forcibly=True)

            if backuppath and not ui.quiet:
                ui.warn(
                    _(b'copy of old repository backed up at %s\n') % backuppath
                )
                ui.warn(
                    _(
                        b'the old repository will not be deleted; remove '
                        b'it to free up disk space once the upgraded '
                        b'repository is verified\n'
                    )
                )

            upgrade_op.print_post_op_messages()


def upgrade_share_to_safe(
    ui,
    hgvfs,
    storevfs,
    current_requirements,
    mismatch_config,
    mismatch_warn,
    mismatch_verbose_upgrade,
):
    """Upgrades a share to use share-safe mechanism"""
    wlock = None
    store_requirements = scmutil.readrequires(storevfs, False)
    original_crequirements = current_requirements.copy()
    # after upgrade, store requires will be shared, so lets find
    # the requirements which are not present in store and
    # write them to share's .hg/requires
    diffrequires = current_requirements - store_requirements
    # add share-safe requirement as it will mark the share as share-safe
    diffrequires.add(requirementsmod.SHARESAFE_REQUIREMENT)
    current_requirements.add(requirementsmod.SHARESAFE_REQUIREMENT)
    # in `allow` case, we don't try to upgrade, we just respect the source
    # state, update requirements and continue
    if mismatch_config == b'allow':
        return
    try:
        wlock = lockmod.trylock(ui, hgvfs, b'wlock', 0, 0)
        # some process might change the requirement in between, re-read
        # and update current_requirements
        locked_requirements = scmutil.readrequires(hgvfs, True)
        if locked_requirements != original_crequirements:
            removed = current_requirements - locked_requirements
            # update current_requirements in place because it's passed
            # as reference
            current_requirements -= removed
            current_requirements |= locked_requirements
            diffrequires = current_requirements - store_requirements
            # add share-safe requirement as it will mark the share as share-safe
            diffrequires.add(requirementsmod.SHARESAFE_REQUIREMENT)
            current_requirements.add(requirementsmod.SHARESAFE_REQUIREMENT)
        scmutil.writerequires(hgvfs, diffrequires)
        if mismatch_verbose_upgrade:
            ui.warn(_(b'repository upgraded to use share-safe mode\n'))
    except error.LockError as e:
        hint = _(
            b"see `hg help config.format.use-share-safe` for more information"
        )
        if mismatch_config == b'upgrade-abort':
            raise error.Abort(
                _(b'failed to upgrade share, got error: %s')
                % stringutil.forcebytestr(e.strerror),
                hint=hint,
            )
        elif mismatch_warn:
            ui.warn(
                _(b'failed to upgrade share, got error: %s\n')
                % stringutil.forcebytestr(e.strerror),
                hint=hint,
            )
    finally:
        if wlock:
            wlock.release()


def downgrade_share_to_non_safe(
    ui,
    hgvfs,
    sharedvfs,
    current_requirements,
    mismatch_config,
    mismatch_warn,
    mismatch_verbose_upgrade,
):
    """Downgrades a share which use share-safe to not use it"""
    wlock = None
    source_requirements = scmutil.readrequires(sharedvfs, True)
    original_crequirements = current_requirements.copy()
    # we cannot be 100% sure on which requirements were present in store when
    # the source supported share-safe. However, we do know that working
    # directory requirements were not there. Hence we remove them
    source_requirements -= requirementsmod.WORKING_DIR_REQUIREMENTS
    current_requirements |= source_requirements
    current_requirements.remove(requirementsmod.SHARESAFE_REQUIREMENT)
    if mismatch_config == b'allow':
        return

    try:
        wlock = lockmod.trylock(ui, hgvfs, b'wlock', 0, 0)
        # some process might change the requirement in between, re-read
        # and update current_requirements
        locked_requirements = scmutil.readrequires(hgvfs, True)
        if locked_requirements != original_crequirements:
            removed = current_requirements - locked_requirements
            # update current_requirements in place because it's passed
            # as reference
            current_requirements -= removed
            current_requirements |= locked_requirements
            current_requirements |= source_requirements
            current_requirements -= set(requirementsmod.SHARESAFE_REQUIREMENT)
        scmutil.writerequires(hgvfs, current_requirements)
        if mismatch_verbose_upgrade:
            ui.warn(_(b'repository downgraded to not use share-safe mode\n'))
    except error.LockError as e:
        hint = _(
            b"see `hg help config.format.use-share-safe` for more information"
        )
        # If upgrade-abort is set, abort when upgrade fails, else let the
        # process continue as `upgrade-allow` is set
        if mismatch_config == b'downgrade-abort':
            raise error.Abort(
                _(b'failed to downgrade share, got error: %s')
                % stringutil.forcebytestr(e.strerror),
                hint=hint,
            )
        elif mismatch_warn:
            ui.warn(
                _(b'failed to downgrade share, got error: %s\n')
                % stringutil.forcebytestr(e.strerror),
                hint=hint,
            )
    finally:
        if wlock:
            wlock.release()
