changeset 51503:d4095f7b000a stable

admin-commands: move the chainsaw extension to the admin commands module Activating an extension is always a little bit of a chore and the long name, options and "chainsaw" bits are deterrent enough. This also allows us to help the discoverability for people looking for repo "administration" tools, with the widest semantic of "administration".
author Raphaël Gomès <rgomes@octobus.net>
date Thu, 14 Mar 2024 11:24:52 +0100
parents a5d8f261b716
children 6b2aeeec3ed0
files hgext/chainsaw.py mercurial/admin/chainsaw.py mercurial/admin_commands.py tests/test-chainsaw-update.t tests/test-completion.t tests/test-help.t tests/test-hgweb-json.t
diffstat 7 files changed, 245 insertions(+), 237 deletions(-) [+]
line wrap: on
line diff
--- a/hgext/chainsaw.py	Wed Mar 13 16:22:13 2024 -0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,231 +0,0 @@
-# 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,
-    localrepo,
-    registrar,
-)
-from mercurial.utils import (
-    urlutil,
-)
-
-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'',
-            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)
-        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/admin/chainsaw.py	Thu Mar 14 11:24:52 2024 +0100
@@ -0,0 +1,226 @@
+# 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`).
+"""
+
+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)
+        )
--- a/mercurial/admin_commands.py	Wed Mar 13 16:22:13 2024 -0300
+++ b/mercurial/admin_commands.py	Thu Mar 14 11:24:52 2024 +0100
@@ -6,11 +6,12 @@
 # GNU General Public License version 2 or any later version.
 
 from .i18n import _
-from .admin import verify
+from .admin import chainsaw, verify
 from . import error, registrar, transaction
 
 
 table = {}
+table.update(chainsaw.command._table)
 command = registrar.command(table)
 
 
--- a/tests/test-chainsaw-update.t	Wed Mar 13 16:22:13 2024 -0300
+++ b/tests/test-chainsaw-update.t	Thu Mar 14 11:24:52 2024 +0100
@@ -5,11 +5,6 @@
 setup
 =====
 
-  $ cat >> $HGRCPATH << EOF
-  > [extensions]
-  > chainsaw=
-  > EOF
-
   $ hg init src
   $ cd src
   $ echo 1 > root
--- a/tests/test-completion.t	Wed Mar 13 16:22:13 2024 -0300
+++ b/tests/test-completion.t	Thu Mar 14 11:24:52 2024 +0100
@@ -3,6 +3,7 @@
   abort
   add
   addremove
+  admin::chainsaw-update
   admin::verify
   annotate
   archive
@@ -66,6 +67,7 @@
   abort
   add
   addremove
+  admin::chainsaw-update
   admin::verify
   annotate
   archive
@@ -260,6 +262,7 @@
   abort: dry-run
   add: include, exclude, subrepos, dry-run
   addremove: similarity, subrepos, include, exclude, dry-run
+  admin::chainsaw-update: purge-unknown, purge-ignored, rev, source, dest, initial-clone-minimal
   admin::verify: check, option
   annotate: rev, follow, no-follow, text, user, file, date, number, changeset, line-number, skip, line-range, ignore-all-space, ignore-space-change, ignore-blank-lines, ignore-space-at-eol, include, exclude, template
   archive: no-decode, prefix, rev, type, subrepos, include, exclude
--- a/tests/test-help.t	Wed Mar 13 16:22:13 2024 -0300
+++ b/tests/test-help.t	Thu Mar 14 11:24:52 2024 +0100
@@ -412,6 +412,9 @@
   
    abort         abort an unfinished operation (EXPERIMENTAL)
    add           add the specified files on the next commit
+   admin::chainsaw-update, admin::chainsawupdate
+                 pull and update to a given revision, no matter what,
+                 (EXPERIMENTAL)
    annotate, blame
                  show changeset information by line for each file
    clone         make a copy of an existing repository
@@ -2535,6 +2538,13 @@
   add the specified files on the next commit
   </td></tr>
   <tr><td>
+  <a href="/help/admin::chainsaw-update">
+  admin::chainsaw-update
+  </a>
+  </td><td>
+  pull and update to a given revision, no matter what, (EXPERIMENTAL)
+  </td></tr>
+  <tr><td>
   <a href="/help/annotate">
   annotate
   </a>
--- a/tests/test-hgweb-json.t	Wed Mar 13 16:22:13 2024 -0300
+++ b/tests/test-hgweb-json.t	Thu Mar 14 11:24:52 2024 +0100
@@ -2038,6 +2038,10 @@
         "topic": "add"
       },
       {
+        "summary": "pull and update to a given revision, no matter what, (EXPERIMENTAL)",
+        "topic": "admin::chainsaw-update"
+      },
+      {
         "summary": "show changeset information by line for each file",
         "topic": "annotate"
       },