diff --git a/hgext/chainsaw.py b/hgext/chainsaw.py new file mode 100644 --- /dev/null +++ b/hgext/chainsaw.py @@ -0,0 +1,156 @@ +# chainsaw.py +# +# Copyright 2022 Georges Racinet +# +# 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 extension provides 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`). +""" + +import shutil + +from mercurial.i18n import _ +from mercurial import ( + cmdutil, + commands, + error, + registrar, +) + +cmdtable = {} +command = registrar.command(cmdtable) +# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for +# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should +# be specifying the version(s) of Mercurial they are tested with, or +# leave the attribute unspecified. +testedwith = b'ships-with-hg-core' + + +@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'hg admin::chainsaw-update [OPTION] --rev REV --source SOURCE...'), + helpbasic=True, +) +def update(ui, repo, **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: 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: + + - 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'] + 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')) + ui.status(_(b'breaking locks, if any\n')) + repo.svfs.tryunlink(b'lock') + repo.vfs.tryunlink(b'wlock') + + ui.status(_(b'recovering after interrupted transaction, if any\n')) + repo.recover() + + ui.status(_(b'pulling from %s\n') % source) + overrides = {(b'ui', b'quiet'): True} + with ui.configoverride(overrides, b'chainsaw-update'): + pull = cmdutil.findcmd(b'pull', commands.table)[1][0] + pull(ui, repo, source, rev=[rev], remote_hidden=False) + + purge = cmdutil.findcmd(b'purge', commands.table)[1][0] + purge( + ui, + repo, + dirs=True, + all=opts.get('purge_ignored'), + files=opts.get('purge_unknown'), + confirm=False, + ) + + ui.status(_(b'updating to revision \'%s\'\n') % rev) + update = cmdutil.findcmd(b'update', commands.table)[1][0] + update(ui, repo, rev=rev, clean=True) + + ui.status( + _( + b'chainsaw-update to revision \'%s\' ' + b'for repository at \'%s\' done\n' + ) + % (rev, repo.root) + ) diff --git a/tests/test-chainsaw-update.t b/tests/test-chainsaw-update.t new file mode 100644 --- /dev/null +++ b/tests/test-chainsaw-update.t @@ -0,0 +1,105 @@ +============================================ +Tests for the admin::chainsaw-update command +============================================ + +setup +===== + + $ cat >> $HGRCPATH << EOF + > [extensions] + > chainsaw= + > EOF + + $ hg init src + $ cd src + $ echo 1 > foo + $ hg ci -Am1 + adding foo + $ cd .. + +Actual tests +============ + +Simple invocation +----------------- + + $ hg init repo + $ cd repo + $ hg admin::chainsaw-update --rev default --source ../src + breaking locks, if any + recovering after interrupted transaction, if any + no interrupted transaction available + pulling from ../src + updating to revision 'default' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + chainsaw-update to revision 'default' for repository at '$TESTTMP/repo' done + + $ cat foo + 1 + +Test lock breacking capabilities +-------------------------------- + +Demonstrate lock-breaking capabilities with locks that regular Mercurial +operation would not break, because the hostnames registered in locks differ +from the current hostname (happens a lot with succesive containers): + + $ ln -s invalid.host.test/effffffc:171814 .hg/store/lock + $ ln -s invalid.host.test/effffffc:171814 .hg/wlock + $ hg debuglock + lock: (.*?), process 171814, host invalid.host.test/effffffc \((\d+)s\) (re) + wlock: (.*?), process 171814, host invalid.host.test/effffffc \((\d+)s\) (re) + [2] + + $ hg admin::chainsaw-update --no-purge-ignored --rev default --source ../src -q + no interrupted transaction available + +Test file purging capabilities +------------------------------ + +Let's also add local modifications (tracked and untracked) to demonstrate the +purging. + + $ echo untracked > bar + $ echo modified > foo + $ hg status -A + M foo + ? bar + + $ echo 2 > ../src/foo + $ hg -R ../src commit -m2 + $ hg admin::chainsaw-update --rev default --source ../src -q + no interrupted transaction available + $ hg status -A + C foo + $ cat foo + 2 + +Now behaviour with respect to ignored files: they are not purged if +the --no-purge-ignored flag is passed, but they are purged by default + + $ echo bar > .hgignore + $ hg ci -Aqm hgignore + $ echo ignored > bar + $ hg status --all + I bar + C .hgignore + C foo + + $ hg admin::chainsaw-update --no-purge-ignored --rev default --source ../src -q + no interrupted transaction available + $ hg status --all + I bar + C .hgignore + C foo + $ cat bar + ignored + + $ hg admin::chainsaw-update --rev default --source ../src -q + no interrupted transaction available + $ hg status --all + C .hgignore + C foo + $ test -f bar + [1] +