diff --git a/conf/example.conf b/conf/example.conf
index cde1130..4cf280d 100644
--- a/conf/example.conf
+++ b/conf/example.conf
@@ -72,13 +72,15 @@
# start line:
# repo [one or more repos and/or repo groups]
# followed by one or more permissions lines:
-# (C|R|RW|RW+|RWD|RW+D) [zero or more refexes] = [one or more users]
+# (C|R|RW|RW+|RWC|RW+C|RWD|RW+D|RWCD|RW+CD) [zero or more refexes] = [one or more users]
# there are 6 types of permissions: R, RW, and RW+ are simple (the "+" means
# permission to "rewind" -- force push a non-fast forward to -- a branch).
-# The C permission is described in doc/4-wildcard-repositories.mkd. The D
-# addition to RW/RW+ is described in doc/3-faq-tips-etc.mkd, in the section on
-# "separating delete and rewind rights".
+# The *standalone* C permission pertains to creating a REPO and is described
+# in doc/4-wildcard-repositories.mkd. The C and D *suffixes* to the RW/RW+
+# permissions pertain to creating or deleting a BRANCH, and are described in
+# doc/3-faq-tips-etc.mkd, in the sections on "separating push and create
+# rights" and "separating delete and rewind rights" respectively.
# how permissions are matched:
# - user, repo, and access (W or +) are known. For that combination, if
diff --git a/doc/3-faq-tips-etc.mkd b/doc/3-faq-tips-etc.mkd
index f5ac1da..935e4b3 100644
--- a/doc/3-faq-tips-etc.mkd
+++ b/doc/3-faq-tips-etc.mkd
@@ -18,6 +18,7 @@ In this document:
* better logging
* "exclude" (or "deny") rules
* separating delete and rewind rights
+ * separating create and push rights
* file/dir NAME based restrictions
* delegating parts of the config file
* convenience features
@@ -381,8 +382,29 @@ Note 2: a quick way to make this the default for *all* your repos is:
where foo can be either the administrator, or if you can ignore the warning
message when you push, a non-existant user.
+Note 3: you can combine this with the "create a branch" permissions described
+in the next section, as the example line in conf/example.conf shows.
+
[sdrr]: http://groups.google.com/group/gitolite/browse_thread/thread/9f2b4358ce406d4c#
+
+
+##### separating create and push rights
+
+This feature is similar in spirit to the previous one, so please read that
+section for a general understanding.
+
+Briefly:
+
+ * branch creation is permitted by using `RWC` or `RW+C` -- essentially the
+ current branch permissions with a `C` suffixed
+ * if a repo has a rule containing such a `C`, then the `RW` and `RW+`
+ permissions (for that repo) no longer permit creation of the ref matched;
+ they will only allow pushing to an existing ref
+
+Note: you can combine this with the "delete a branch" permissions described in
+the previous section, as the example line in conf/example.conf shows.
+
##### file/dir NAME based restrictions
diff --git a/hooks/common/update b/hooks/common/update
index b77d6ec..5999449 100755
--- a/hooks/common/update
+++ b/hooks/common/update
@@ -67,6 +67,8 @@ $att_acc = '+' if $oldsha ne $merge_base;
# were any 'D' perms specified? If they were, it means we have to separate
# deletes from rewinds, so if the new sha is all 0's, change the '+' to a 'D'
$att_acc = 'D' if ( $repos{$ENV{GL_REPO}}{DELETE_IS_D} or $repos{'@all'}{DELETE_IS_D} ) and $newsha eq '0' x 40;
+# similarly C for create a branch
+$att_acc = 'C' if ( $repos{$ENV{GL_REPO}}{CREATE_IS_C} or $repos{'@all'}{CREATE_IS_C} ) and $oldsha eq '0' x 40;
my @allowed_refs;
# @all repos: see comments in similar code in check_access
diff --git a/src/gitolite.pm b/src/gitolite.pm
index c2de81f..13d1451 100644
--- a/src/gitolite.pm
+++ b/src/gitolite.pm
@@ -291,6 +291,7 @@ sub parse_acl
for my $r ('@all', @repo_plus) {
my $dr = $repo; $dr = '@all' if $r eq '@all';
$repos{$dr}{DELETE_IS_D} = 1 if $repos{$r}{DELETE_IS_D};
+ $repos{$dr}{CREATE_IS_C} = 1 if $repos{$r}{CREATE_IS_C};
$repos{$dr}{NAME_LIMITS} = 1 if $repos{$r}{NAME_LIMITS};
for my $u ('@all', "$gl_user - wild", @user_plus) {
diff --git a/src/gl-compile-conf b/src/gl-compile-conf
index 2e75e83..f0b27fa 100755
--- a/src/gl-compile-conf
+++ b/src/gl-compile-conf
@@ -204,7 +204,7 @@ sub parse_conf_file
s/\bCREAT[EO]R\b/\$creator/g for @repos;
}
# actual permission line
- elsif (/^(-|C|R|RW\+?D?) (.* )?= (.+)/)
+ elsif (/^(-|C|R|RW\+?(?:C?D?|D?C?)) (.* )?= (.+)/)
{
my $perms = $1;
my @refs; @refs = split(' ', $2) if $2;
@@ -286,6 +286,7 @@ sub parse_conf_file
# that fact easy to find; this changes the meaning of RW+
# to no longer permit deletes (see update hook)
$repos{$repo}{DELETE_IS_D} = 1 if $perms =~ /D/;
+ $repos{$repo}{CREATE_IS_C} = 1 if $perms =~ /RW.*C/;
# for 2nd level check, store each "ref, perms" pair in order
for my $ref (@refs)
diff --git a/t/t52-deny-create-ref b/t/t52-deny-create-ref
index ff04f8c..9afa369 100644
--- a/t/t52-deny-create-ref
+++ b/t/t52-deny-create-ref
@@ -1,51 +1,105 @@
# vim: syn=sh:
-for wr in 0 1
+for bc in 0 1
do
- for bc in 0 1
- do
- cd $TESTDIR
- $TESTDIR/rollback
- editrc GL_WILDREPOS $wr
- editrc GL_BIG_CONFIG $bc
+ cd $TESTDIR
+ $TESTDIR/rollback
+ editrc GL_BIG_CONFIG $bc
- # ----------
+ # ----------
- name "INTERNAL"
- echo "
- @leads = u1 u2
- @devs = u1 u2 u3 u4
- repo foo
- RW+ = @leads
- - CREATE_REF = @devs
- RW+ = @devs
- " | ugc
+ # test "C" permissions
- cd ~/td
- runlocal git clone u1:foo
+ name "INTERNAL"
+ echo "
+ @leads = u1 u2
+ @devs = u1 u2 u3 u4
- cd foo
- mdc; mdc; mdc; mdc; mdc
+ repo foo
+ RW+C = @leads
+ RW+C personal/USER/ = @devs
+ RW = @devs
+ " | ugc
- name "u1 can push master"
- runlocal git push origin master
- expect_push_ok "master -> master"
+ cd ~/td
+ runlocal git clone u1:foo
- name "u2 can create newbr1"
- runlocal git push u2:foo master:newbr1
- expect_push_ok "master -> newbr1"
+ cd foo
+ mdc; mdc; mdc; mdc; mdc
- name "u3 can push newbr1"
- mdc; mdc; mdc; mdc; mdc
- runlocal git push u3:foo master:newbr1
- expect_push_ok "master -> newbr1"
+ name "u1 can push/rewind master on foo"
+ runlocal git push origin master
+ expect_push_ok "master -> master"
+ runlocal git push -f origin master^^:master
+ expect_push_ok "master^^ -> master"
- name "u4 canNOT push newbr3"
- mdc; mdc; mdc; mdc; mdc
- runlocal git push u3:foo master:newbr3
- expect "remote: W refs/heads/CREATE_REF u3 DENIED by refs/heads/CREATE_REF"
- expect "\[remote rejected\] master -> newbr3 (hook declined)"
- expect "failed to push"
+ name "u2 can create newbr1 on foo"
+ runlocal git push u2:foo master:newbr1
+ expect_push_ok "master -> newbr1"
- name INTERNAL
- done
+ name "u3 can push newbr1 on foo"
+ mdc; mdc; mdc; mdc; mdc
+ runlocal git push u3:foo master:newbr1
+ expect_push_ok "master -> newbr1"
+
+ name "u4 canNOT create newbr2 on foo"
+ mdc; mdc; mdc; mdc; mdc
+ runlocal git push u3:foo master:newbr2
+ expect "remote: C refs/heads/newbr2 foo u3 DENIED by fallthru"
+ expect "hook declined"
+ expect "failed to push"
+
+ name "u4 can create/rewind personal/u4/newbr3 on foo"
+ mdc; mdc; mdc; mdc; mdc
+ runlocal git push u4:foo master:personal/u4/newbr3
+ expect_push_ok "master -> personal/u4/newbr3"
+ runlocal git push -f origin master^^:personal/u4/newbr3
+ expect_push_ok "master^^ -> personal/u4/newbr3"
+
+ # bar, without "C" permissions, should behave like old
+
+ name "INTERNAL"
+ echo "
+ @leads = u1 u2
+ @devs = u1 u2 u3 u4
+
+ repo bar
+ RW+ = @leads
+ RW+ personal/USER/ = @devs
+ RW = @devs
+ " | ugc
+
+ cd ~/td
+ runlocal git clone u1:bar
+
+ cd bar
+ mdc; mdc; mdc; mdc; mdc
+
+ name "u1 can push/rewind master on bar"
+ runlocal git push origin master
+ expect_push_ok "master -> master"
+ runlocal git push -f origin master^^:master
+ expect_push_ok "master^^ -> master"
+
+ name "u2 can create newbr1 on bar"
+ runlocal git push u2:bar master:newbr1
+ expect_push_ok "master -> newbr1"
+
+ name "u3 can push newbr1 on bar"
+ mdc; mdc; mdc; mdc; mdc
+ runlocal git push u3:bar master:newbr1
+ expect_push_ok "master -> newbr1"
+
+ name "u4 can create newbr2 on bar"
+ mdc; mdc; mdc; mdc; mdc
+ runlocal git push u3:bar master:newbr2
+ expect_push_ok "master -> newbr2"
+
+ name "u4 can create/rewind personal/u4/newbr3 on bar"
+ mdc; mdc; mdc; mdc; mdc
+ runlocal git push u4:bar master:personal/u4/newbr3
+ expect_push_ok "master -> personal/u4/newbr3"
+ runlocal git push -f origin master^^:personal/u4/newbr3
+ expect_push_ok "master^^ -> personal/u4/newbr3"
+
+ name INTERNAL
done