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

login
register
mail settings
Submitter Gregory Szorc
Date March 21, 2016, 12:43 a.m.
Message ID <e31e1b3ad7fcdff919d7.1458521027@gps-mbp.local>
Download mbox | patch
Permalink /patch/13997/
State Changes Requested
Headers show

Comments

Gregory Szorc - March 21, 2016, 12:43 a.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@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.
timeless - March 21, 2016, 3:36 a.m.
Gregory Szorc wrote:
> 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

This doesn't match the file comment:

> +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,
timeless - March 21, 2016, 3:40 a.m.
Gregory Szorc wrote:
> +        ui.warn(_('refusing to %s\n') % op)

I /think/ I'd suggest % _(op).

Although we should probably figure out what this is supposed to look like.

In general we don't want to do: _('... %s' % foo), but, if op is a
limited set, then it might actually be better, so that localizers can
write complete strings instead of being bound to unfortunate gender
issues/etc.
Danek Duvall - March 21, 2016, 6:06 p.m.
It would be convenient -- for our uses, at least -- to have optional allow
and deny lists.  If allow is present, then all users are denied except for
those in the list, and if deny is present, then all users are allowed
except for those in the list.  (If both allow and deny are present, then it
can be closed for everyone due to bad configuration.)  We sometimes want to
close a gate off for all but a set of maintainers, or just to a couple of
people.

Thanks,
Danek
Pierre-Yves David - March 22, 2016, 1:16 a.m.
On 03/20/2016 05:43 PM, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@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 seems like a job for

   [hooks]
   pretxnopen=echo foo bar >2; false

But I assume that the value here is that you just have to create and 
delete the file and deleting line in the config is painful. (also, multi 
line echo is annoying).

Is there anything else that I missed ?

>
> 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@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]
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>
Pierre-Yves David - March 27, 2016, 6:44 p.m.
On 03/20/2016 05:43 PM, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@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@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)

According to our new shinny process, experimental extension need to 
fulfill multiple criteria and multiple information need to be updated on 
the wiki. Can you add an entry for that one before we move forward?

https://www.mercurial-scm.org/wiki/ExperimentalExtensionsPlan#Current_Candidate_extensions

Cheers,

Patch

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@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]