# HG changeset patch
# User David Douard <david.douard@logilab.fr>
# Date 1415009565 -3600
# Node ID 80f78674c56ef4e2114e78e24e85d306de11636f
# Parent  e9ce904b62a998185441462978d32c059d054995
Add support for phases

This adds a "publish" permission level (between "init" and "write") required to
be able to change the phase of a changeset from "draft" to "public".

Update documentation accordingly.

This is meant to be used for using the changeset evolution feature of mercurial,
see http://evolution.experimentalworks.net/doc/

diff -r e9ce904b62a9 -r 80f78674c56e doc/manual.docbook
--- a/doc/manual.docbook	Mon Nov 03 11:12:35 2014 +0100
+++ b/doc/manual.docbook	Mon Nov 03 11:12:45 2014 +0100
@@ -16,6 +16,8 @@
 centralized <link xlink:href="http://hg-scm.org/">Mercurial</link>
 repositories using SSH public key authentication; it provides convenient
 and fine-grained key management and access control.
+It provides access control for <link xlink:href="http://mercurial.selenic.com/wiki/Phases">phases</link>
+move (from "draft" to "public").
 </para>
 <para>
 Though mercurial-server is currently targeted at Debian-based systems such
@@ -179,10 +181,14 @@
 </para>
 <itemizedlist>
 <listitem>
-<literal>init</literal>: allow reads, writes, and the creation of new repositories
+<literal>init</literal>: allow reads, writes, publish (move the phase
+of a chageset to "public"), and the creation of new repositories
 </listitem>
 <listitem>
-<literal>write</literal>: allow reads and writes
+<literal>publish</literal>: allow reads, writes and publish changesets
+</listitem>
+<listitem>
+<literal>write</literal>: allow reads and writes (ie. create draft changesets)
 </listitem>
 <listitem>
 <literal>read</literal>: allow only read operations
@@ -192,6 +198,13 @@
 </listitem>
 </itemizedlist>
 <para>
+  The distinction between "write" and "publish" access levels is only
+  meaningful for non-publishing repositories, in which case, the
+  "write" access level allows a user to push "draft" changesets, but
+  not to change their phase to "public". This latter operation require
+  the "publish" access level.
+</para>
+<para>
 A condition is a globpattern matched against a relative path. The two most
 important conditions are
 </para>
diff -r e9ce904b62a9 -r 80f78674c56e src/init/conf/remote-hgrc.d/access.rc
--- a/src/init/conf/remote-hgrc.d/access.rc	Mon Nov 03 11:12:35 2014 +0100
+++ b/src/init/conf/remote-hgrc.d/access.rc	Mon Nov 03 11:12:45 2014 +0100
@@ -2,3 +2,4 @@
 
 [hooks]
 pretxnchangegroup.access = python:mercurialserver.access.hook
+prepushkey.access = python:mercurialserver.access.phasehook
diff -r e9ce904b62a9 -r 80f78674c56e src/mercurialserver/access.py
--- a/src/mercurialserver/access.py	Mon Nov 03 11:12:35 2014 +0100
+++ b/src/mercurialserver/access.py	Mon Nov 03 11:12:45 2014 +0100
@@ -25,3 +25,12 @@
         if not allow(ctx):
             raise mercurial.util.Abort(_('%s: access denied for changeset %s') %
                 (__name__, mercurial.node.short(ctx.node())))
+
+def phasehook(ui, repo, hooktype, **kwargs):
+    if hooktype != 'prepushkey':
+        raise mercurial.util.Abort(_('config error - hook type "%s" cannot stop '
+                                     'incoming phase changes') % hooktype)
+    if kwargs.get('namespace') == 'phases':
+        if not ruleset.rules.allow("publish"):
+            raise mercurial.util.Abort(_('%s: access denied for making %s public') %
+                                       (__name__, kwargs['key'][:12]))
diff -r e9ce904b62a9 -r 80f78674c56e src/mercurialserver/ruleset.py
--- a/src/mercurialserver/ruleset.py	Mon Nov 03 11:12:35 2014 +0100
+++ b/src/mercurialserver/ruleset.py	Mon Nov 03 11:12:45 2014 +0100
@@ -37,7 +37,7 @@
 class Ruleset(object):
     '''Class representing the rules in a rule file'''
 
-    levels = ["init", "write", "read", "deny"]
+    levels = ["init", "publish", "write", "read", "deny"]
 
     def __init__(self):
         self.rules = []
diff -r e9ce904b62a9 -r 80f78674c56e test/unittest_ruleset.py
--- a/test/unittest_ruleset.py	Mon Nov 03 11:12:35 2014 +0100
+++ b/test/unittest_ruleset.py	Mon Nov 03 11:12:45 2014 +0100
@@ -4,7 +4,7 @@
 from mercurialserver import ruleset
 
 class _RuleSetBaseTC(TestCase):
-    alllevels = ["init", "write", "read", "deny", "none"]
+    alllevels = ["init", "publish", "write", "read", "deny", "none"]
     levels = alllevels[:-1]
     def setUp(self):
         self.rs = ruleset.Ruleset()
@@ -161,6 +161,27 @@
         self.check_level('deny', repo='no')
         self.check_level('write', repo='other')
 
+class RuleSetPublishTC(_RuleSetBaseTC):
+    accessrules = '''
+init user=root/**
+deny repo=hgadmin
+init user=users/toto/* repo=toto
+publish user=users/toto/* repo=pub/**
+publish repo=allpub/**
+write user=users/w/*
+read user=users/**
+'''
+    def test_publish(self):
+        self.rs.set(user='users/w/key')
+        self.check_level('publish', repo='allpub/stuff')
+        self.check_level('write', repo='toto')
+        self.check_level('write', repo='other/stuff')
+
+        self.rs.set(repo='pub/stuff')
+        self.check_level('write', user='users/w/key')
+        self.check_level('publish', user='users/toto/key')
+
+
 if __name__ == '__main__':
     from unittest import main
     main()