--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/chainsaw.py Sat Nov 26 12:23:56 2022 +0100
@@ -0,0 +1,156 @@
+# 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 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)
+ )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-chainsaw-update.t Sat Nov 26 12:23:56 2022 +0100
@@ -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]
+