chainsaw.py
228 lines
| 7.4 KiB
| text/x-python
|
PythonLexer
Raphaël Gomès
|
r52396 | # chainsaw.py | ||
# | ||||
# Copyright 2022 Georges Racinet <georges.racinet@octobus.net> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
"""chainsaw is a collection of single-minded and dangerous tools. (EXPERIMENTAL) | ||||
"Don't use a chainsaw to cut your food!" | ||||
The chainsaw is a collection of commands that are so much geared towards a | ||||
specific use case in a specific context or environment that they are totally | ||||
inappropriate and **really dangerous** in other contexts. | ||||
The help text of each command explicitly summarizes its context of application | ||||
and the wanted end result. | ||||
It is recommended to run these commands with the ``HGPLAIN`` environment | ||||
variable (see :hg:`help scripting`). | ||||
""" | ||||
Matt Harbison
|
r52757 | from __future__ import annotations | ||
Raphaël Gomès
|
r52396 | import shutil | ||
from ..i18n import _ | ||||
from .. import ( | ||||
cmdutil, | ||||
commands, | ||||
error, | ||||
localrepo, | ||||
registrar, | ||||
) | ||||
from ..utils import ( | ||||
urlutil, | ||||
) | ||||
cmdtable = {} | ||||
command = registrar.command(cmdtable) | ||||
@command( | ||||
b'admin::chainsaw-update', | ||||
[ | ||||
( | ||||
b'', | ||||
b'purge-unknown', | ||||
True, | ||||
_( | ||||
b'Remove unversioned files before update. Disabling this can ' | ||||
b'in some cases interfere with the update.' | ||||
b'See also :hg:`purge`.' | ||||
), | ||||
), | ||||
( | ||||
b'', | ||||
b'purge-ignored', | ||||
True, | ||||
_( | ||||
b'Remove ignored files before update. Disable this for ' | ||||
b'instance to reuse previous compiler object files. ' | ||||
b'See also :hg:`purge`.' | ||||
), | ||||
), | ||||
( | ||||
b'', | ||||
b'rev', | ||||
b'', | ||||
_(b'revision to update to'), | ||||
), | ||||
( | ||||
b'', | ||||
b'source', | ||||
b'', | ||||
_(b'repository to clone from'), | ||||
), | ||||
( | ||||
b'', | ||||
b'dest', | ||||
b'', | ||||
_(b'repository to update to REV (possibly cloning)'), | ||||
), | ||||
( | ||||
b'', | ||||
b'initial-clone-minimal', | ||||
False, | ||||
_( | ||||
b'Pull only the prescribed revision upon initial cloning. ' | ||||
b'This has the side effect of ignoring clone-bundles, ' | ||||
b'which if often slower on the client side and stressful ' | ||||
b'to the server than applying available clone bundles.' | ||||
), | ||||
), | ||||
], | ||||
_( | ||||
b'hg admin::chainsaw-update [OPTION] --rev REV --source SOURCE --dest DEST' | ||||
), | ||||
helpbasic=True, | ||||
norepo=True, | ||||
) | ||||
def update(ui, **opts): | ||||
"""pull and update to a given revision, no matter what, (EXPERIMENTAL) | ||||
Context of application: *some* Continuous Integration (CI) systems, | ||||
packaging or deployment tools. | ||||
Wanted end result: local repository at the given REPO_PATH, having the | ||||
latest changes to the given revision and with a clean working directory | ||||
updated at the given revision. | ||||
chainsaw-update pulls from one source, then updates the working directory | ||||
to the given revision, overcoming anything that would stand in the way. | ||||
By default, it will: | ||||
- clone if the local repo does not exist yet, **removing any directory | ||||
at the given path** that would not be a Mercurial repository. | ||||
The initial clone is full by default, so that clonebundles can be | ||||
applied. Use the --initial-clone-minimal flag to avoid this. | ||||
- break locks if needed, leading to possible corruption if there | ||||
is a concurrent write access. | ||||
- perform recovery actions if needed | ||||
- revert any local modification. | ||||
- purge unknown and ignored files. | ||||
- go as far as to reclone if everything else failed (not implemented yet). | ||||
DO NOT use it for anything else than performing a series | ||||
of unattended updates, with full exclusive repository access each time | ||||
and without any other local work than running build scripts. | ||||
In case the local repository is a share (see :hg:`help share`), exclusive | ||||
write access to the share source is also mandatory. | ||||
It is recommended to run these commands with the ``HGPLAIN`` environment | ||||
variable (see :hg:`scripting`). | ||||
Motivation: in Continuous Integration and Delivery systems (CI/CD), the | ||||
occasional remnant or bogus lock are common sources of waste of time (both | ||||
working time and calendar time). CI/CD scripts tend to grow with counter- | ||||
measures, often done in urgency. Also, whilst it is neat to keep | ||||
repositories from one job to the next (especially with large | ||||
repositories), an exceptional recloning is better than missing a release | ||||
deadline. | ||||
""" | ||||
rev = opts['rev'] | ||||
source = opts['source'] | ||||
repo_path = opts['dest'] | ||||
if not rev: | ||||
raise error.InputError(_(b'specify a target revision with --rev')) | ||||
if not source: | ||||
raise error.InputError(_(b'specify a pull path with --source')) | ||||
if not repo_path: | ||||
raise error.InputError(_(b'specify a repo path with --dest')) | ||||
repo_path = urlutil.urllocalpath(repo_path) | ||||
try: | ||||
repo = localrepo.instance(ui, repo_path, create=False) | ||||
repo_created = False | ||||
ui.status(_(b'loaded repository at "%s"\n' % repo_path)) | ||||
except error.RepoError: | ||||
try: | ||||
shutil.rmtree(repo_path) | ||||
except FileNotFoundError: | ||||
ui.status(_(b'no such directory: "%s"\n' % repo_path)) | ||||
else: | ||||
ui.status( | ||||
_( | ||||
b'removed non-repository file or directory ' | ||||
b'at "%s"' % repo_path | ||||
) | ||||
) | ||||
ui.status(_(b'creating repository at "%s"\n' % repo_path)) | ||||
repo = localrepo.instance(ui, repo_path, create=True) | ||||
repo_created = True | ||||
if repo.svfs.tryunlink(b'lock'): | ||||
ui.status(_(b'had to break store lock\n')) | ||||
if repo.vfs.tryunlink(b'wlock'): | ||||
ui.status(_(b'had to break working copy lock\n')) | ||||
# If another process relock after the breacking above, the next locking | ||||
# will have to wait. | ||||
with repo.wlock(), repo.lock(): | ||||
ui.status(_(b'recovering after interrupted transaction, if any\n')) | ||||
repo.recover() | ||||
ui.status(_(b'pulling from %s\n') % source) | ||||
if repo_created and not opts.get('initial_clone_minimal'): | ||||
pull_revs = [] | ||||
else: | ||||
pull_revs = [rev] | ||||
overrides = {(b'ui', b'quiet'): True} | ||||
with repo.ui.configoverride(overrides, b'chainsaw-update'): | ||||
pull = cmdutil.findcmd(b'pull', commands.table)[1][0] | ||||
ret = pull( | ||||
repo.ui, | ||||
repo, | ||||
source, | ||||
rev=pull_revs, | ||||
remote_hidden=False, | ||||
) | ||||
if ret: | ||||
return ret | ||||
purge = cmdutil.findcmd(b'purge', commands.table)[1][0] | ||||
ret = purge( | ||||
ui, | ||||
repo, | ||||
dirs=True, | ||||
all=opts.get('purge_ignored'), | ||||
files=opts.get('purge_unknown'), | ||||
confirm=False, | ||||
) | ||||
if ret: | ||||
return ret | ||||
ui.status(_(b'updating to revision \'%s\'\n') % rev) | ||||
update = cmdutil.findcmd(b'update', commands.table)[1][0] | ||||
ret = update(ui, repo, rev=rev, clean=True) | ||||
if ret: | ||||
return ret | ||||
ui.status( | ||||
_( | ||||
b'chainsaw-update to revision \'%s\' ' | ||||
b'for repository at \'%s\' done\n' | ||||
) | ||||
% (rev, repo.root) | ||||
) | ||||