[PATCH RFC] readonly: experimental extension to mark repositories as read only

Gregory Szorc gregory.szorc at gmail.com
Mon Mar 21 00:43:47 UTC 2016


# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1458521001 25200
#      Sun Mar 20 17:43:21 2016 -0700
# Node ID e31e1b3ad7fcdff919d7cae2234b00de986eefed
# Parent  0e7a929754aa4510e946dec8eba1cc79f9558361
readonly: experimental extension to mark repositories as read only

(I HAVEN'T AUDITED THE CODE FOR CODE STYLE COMPLIANCE, ETC. THIS
IS AN RFC PATCH.)

This extension allows repositories to be marked as read only by
creating a file at a well-defined location. This is enforced by
hooks that run during operations that change the repo.

An individual repository can be marked read only by creating a
.hg/readonly file. Or, a global read only file can be defined so all
repos are marked read only. This is useful for servers when undergoing
maintenance, for example. If the read only file has content, it will
be printed to the client, informing them why the repository is
read only.

This extension was originally written by me for use at Mozilla. We've
had it deployed for several months and believe it is generally useful
outside of Mozilla, especially in server environments.

diff --git a/hgext/readonly.py b/hgext/readonly.py
new file mode 100644
--- /dev/null
+++ b/hgext/readonly.py
@@ -0,0 +1,78 @@
+# readonly.py - Make repositories read only
+#
+# Copyright 2016 Gregory Szorc <gregory.szorc at gmail.com>
+#
+# 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 absolute_import
+
+'''Ability to make repositories read only (EXPERIMENTAL)
+
+This extension looks for up to 2 files signaling that the repository should be
+read-only. First, it looks in ``.hg/readonlyreason``. If the file is present,
+the repository is read only. If the file has content, the content will be
+printed to the user informing them of the reason the repo is read-only.
+
+If the ``readonly.globalreasonfile`` config option is set, it defines another
+path to be checked. It operates the same as ``.hg/readonlyreason`` except it
+can be set in your global hgrc to allow a single file to mark all repositories
+as read only.
+'''
+
+import errno
+
+from mercurial.i18n import _
+from mercurial import (
+    util,
+)
+
+testedwith = 'internal'
+
+def prechangegrouphook(ui, repo, **kwargs):
+    return checkreadonly(ui, repo, 'add changesets')
+
+def prepushkeyhook(ui, repo, namespace=None, **kwargs):
+    return checkreadonly(ui, repo, 'update %s' % namespace)
+
+def checkreadonly(ui, repo, op):
+    try:
+        reporeason = repo.vfs.read('readonlyreason')
+
+        ui.warn(_('repository is read only\n'))
+        if reporeason:
+            ui.warn(reporeason.strip() + '\n')
+
+        ui.warn(_('refusing to %s\n') % op)
+        return True
+    except IOError as e:
+        if e.errno != errno.ENOENT:
+            raise
+
+    # Repo local file does not exist. Check global file.
+    rf = ui.config('readonly', 'globalreasonfile')
+    if rf:
+        try:
+            with util.posixfile(rf, 'rb') as fh:
+                globalreason = fh.read()
+
+            ui.warn(_('all repositories currently read only\n'))
+            if globalreason:
+                ui.warn(globalreason.strip() + '\n')
+
+            ui.warn(_('refusing to %s\n') % op)
+            return True
+        except IOError as e:
+            if e.errno != errno.ENOENT:
+                raise
+
+    return False
+
+def reposetup(ui, repo):
+    # Ideally we'd use pretxnopen. However
+    # https://bz.mercurial-scm.org/show_bug.cgi?id=4939 means hook output won't
+    # be displayed. So we do it the old fashioned way.
+    ui.setconfig('hooks', 'prechangegroup.readonly',
+                 prechangegrouphook, 'readonly')
+    ui.setconfig('hooks', 'prepushkey.readonly',
+                 prepushkeyhook, 'readonly')
diff --git a/tests/test-readonly.t b/tests/test-readonly.t
new file mode 100644
--- /dev/null
+++ b/tests/test-readonly.t
@@ -0,0 +1,118 @@
+Create test server
+
+  $ hg init server
+  $ cd server
+  $ cat > .hg/hgrc << EOF
+  > [extensions]
+  > readonly =
+  > 
+  > [web]
+  > push_ssl = false
+  > allow_push = *
+  > 
+  > [readonly]
+  > globalreasonfile = $TESTTMP/globalreason
+  > EOF
+
+  $ hg serve -d -p $HGPORT --pid-file hg.pid -E error.log
+  $ cat hg.pid >> $DAEMON_PIDS
+  $ cd ..
+
+  $ hg -q clone http://localhost:$HGPORT client
+  $ cd client
+
+Push to repository without any readonly reason files will work
+
+  $ touch foo
+  $ hg -q commit -A -m initial
+  $ hg push
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+
+Empty local reason file prints generic message
+
+  $ touch ../server/.hg/readonlyreason
+  $ echo readonly > foo
+  $ hg commit -m readonly
+  $ hg push
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: repository is read only
+  remote: refusing to add changesets
+  remote: prechangegroup.readonly hook failed
+  abort: push failed on remote
+  [255]
+
+Pushing a bookmark fails
+
+  $ hg bookmark -r 0 bm0
+  $ hg push -B bm0
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  remote: repository is read only
+  remote: refusing to update bookmarks
+  remote: pushkey-abort: prepushkey.readonly hook failed
+  abort: exporting bookmark bm0 failed!
+  [255]
+
+Local reason file with content prints message
+
+  $ cat > ../server/.hg/readonlyreason << EOF
+  > repository is no longer active
+  > EOF
+
+  $ hg push
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: repository is read only
+  remote: repository is no longer active
+  remote: refusing to add changesets
+  remote: prechangegroup.readonly hook failed
+  abort: push failed on remote
+  [255]
+
+Global and local reason file should print local reason
+
+  $ touch $TESTTMP/globalreason
+  $ hg push
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: repository is read only
+  remote: repository is no longer active
+  remote: refusing to add changesets
+  remote: prechangegroup.readonly hook failed
+  abort: push failed on remote
+  [255]
+
+Global reason file in isolation works
+
+  $ rm -f ../server/.hg/readonlyreason
+  $ hg push
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: all repositories currently read only
+  remote: refusing to add changesets
+  remote: prechangegroup.readonly hook failed
+  abort: push failed on remote
+  [255]
+
+Global reason file reason is printed
+
+  $ cat > $TESTTMP/globalreason << EOF
+  > this is the global reason
+  > EOF
+
+  $ hg push
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: all repositories currently read only
+  remote: this is the global reason
+  remote: refusing to add changesets
+  remote: prechangegroup.readonly hook failed
+  abort: push failed on remote
+  [255]


More information about the Mercurial-devel mailing list