summaryrefslogtreecommitdiffstats
path: root/t
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 14:17:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 14:17:27 +0000
commitaae1a14ea756102251351d96e2567b4986d30e2b (patch)
treea1af617672e26aee4c1031a3aa83e8ff08f6a0a5 /t
parentInitial commit. (diff)
downloadgitolite3-aae1a14ea756102251351d96e2567b4986d30e2b.tar.xz
gitolite3-aae1a14ea756102251351d96e2567b4986d30e2b.zip
Adding upstream version 3.6.12.upstream/3.6.12upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 't')
-rwxr-xr-xt/0-me-first.t94
-rw-r--r--t/C-vs-C.t43
-rw-r--r--t/README116
-rwxr-xr-xt/access.t253
-rwxr-xr-xt/all-yall.t89
-rwxr-xr-xt/basic.t284
-rwxr-xr-xt/branch-perms.t122
-rwxr-xr-xt/daemon-gitweb-via-perms.t78
-rwxr-xr-xt/deleg-1.t100
-rwxr-xr-xt/deleg-2.t122
-rwxr-xr-xt/deny-create.t137
-rwxr-xr-xt/deny-rules-2.t172
-rwxr-xr-xt/deny-rules.t67
-rwxr-xr-xt/easy.t181
-rwxr-xr-xt/fedora-root-smart-http-test-setup95
-rwxr-xr-xt/fork.t89
-rwxr-xr-xt/git-config.t191
-rwxr-xr-xt/gitolite-receive-pack12
-rwxr-xr-xt/gitolite-upload-pack12
-rwxr-xr-xt/glt43
-rwxr-xr-xt/hostname.t77
-rwxr-xr-xt/include-subconf.t120
-rwxr-xr-xt/info-json.t183
-rwxr-xr-xt/info.t105
-rwxr-xr-xt/invalid-refnames-filenames.t100
-rw-r--r--t/keys/admin27
-rw-r--r--t/keys/admin.pub1
-rw-r--r--t/keys/config20
-rw-r--r--t/keys/u127
-rw-r--r--t/keys/u1.pub1
-rw-r--r--t/keys/u227
-rw-r--r--t/keys/u2.pub1
-rw-r--r--t/keys/u327
-rw-r--r--t/keys/u3.pub1
-rw-r--r--t/keys/u427
-rw-r--r--t/keys/u4.pub1
-rw-r--r--t/keys/u527
-rw-r--r--t/keys/u5.pub1
-rw-r--r--t/keys/u627
-rw-r--r--t/keys/u6.pub1
-rwxr-xr-xt/listers.t134
-rwxr-xr-xt/manjaro-root-smart-http-test-setup114
-rwxr-xr-xt/merge-check.t95
-rwxr-xr-xt/mirror-test445
-rw-r--r--t/mirror-test-rc162
-rwxr-xr-xt/mirror-test-setup.sh196
-rw-r--r--t/mirror-test-ssh-config11
-rwxr-xr-xt/partial-copy.t181
-rwxr-xr-xt/perm-default-roles.t169
-rwxr-xr-xt/perm-roles.t218
-rwxr-xr-xt/perms-groups.t81
-rwxr-xr-xt/personal-branches.t100
-rwxr-xr-xt/reference.t48
-rwxr-xr-xt/refex-expr-test-162
-rwxr-xr-xt/refex-expr-test-260
-rwxr-xr-xt/refex-expr-test-355
-rwxr-xr-xt/refex-expr-test-9107
-rwxr-xr-xt/repo-specific-hooks.t231
-rwxr-xr-xt/reset33
-rwxr-xr-xt/rule-seq.t87
-rwxr-xr-xt/sequence.t116
-rwxr-xr-xt/smart-http90
-rwxr-xr-xt/smart-http.root-setup105
-rwxr-xr-xt/ssh-authkeys.t77
-rwxr-xr-xt/ssh-basic.t57
-rwxr-xr-xt/templates.t1322
-rwxr-xr-xt/vrefs-1.t138
-rwxr-xr-xt/vrefs-2.t109
-rwxr-xr-xt/wild-1.t126
-rwxr-xr-xt/wild-2.t128
-rwxr-xr-xt/writable.t124
-rwxr-xr-xt/z-end.t14
72 files changed, 8096 insertions, 0 deletions
diff --git a/t/0-me-first.t b/t/0-me-first.t
new file mode 100755
index 0000000..8c9d12b
--- /dev/null
+++ b/t/0-me-first.t
@@ -0,0 +1,94 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+my $rb = `gitolite query-rc -n GL_REPO_BASE`;
+
+# initial smoke tests
+# ----------------------------------------------------------------------
+
+try "plan 71";
+
+# basic push admin repo
+confreset;confadd '
+ repo aa
+ RW+ = u1
+ RW = u2 u3
+
+ repo cc/..*
+ C = u4
+ RW+ = CREATOR u5
+ R = READERS
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ /Initialized empty Git repository in .*/aa.git//
+
+ # basic clone
+ cd ..
+ glt clone u1 file:///aa u1aa; ok; /Cloning into 'u1aa'.../
+ /warning: You appear to have cloned an empty repository/
+ [ -d u1aa ]; ok
+
+ # basic clone deny
+ glt clone u4 file:///aa u4aa; !ok; /R any aa u4 DENIED by fallthru/
+ [ -d u4aa ]; !ok
+
+ # basic push
+ cd u1aa; ok
+ tc z-507; ok; /master .root-commit. 7cf7624. z-507/
+ glt push u1 origin HEAD; ok; /To file:///aa/
+ /\\[new branch\\] *HEAD -> master/
+
+ # basic rewind
+ tc o-866 o-867 o-868; ok; /master 2d066fb. o-868/
+ glt push u1 origin HEAD; ok; /7cf7624..2d066fb HEAD -> master/
+ git reset --hard HEAD^; ok; /HEAD is now at 8b1456b o-867/
+ tc x-967; ok; /master 284951d. x-967/
+ glt push u1 -f origin HEAD; ok; /\\+ 2d066fb...284951d HEAD -> master \\(forced update\\)/
+
+ # log file
+ cat \$(gitolite query-rc GL_LOGFILE);
+ ok; /\tupdate\t/
+ /aa\tu1\t\\+\trefs/heads/master/
+ /2d066fb4860c29cf321170c17695c6883f3d50e8/
+ /284951dfa11d58f99ab76b9f4e4c1ad2f2461236/
+
+ # basic rewind deny
+ cd ..
+ glt clone u2 file:///aa u2aa; ok; /Cloning into 'u2aa'.../
+ cd u2aa; ok
+ tc g-776 g-777 g-778; ok; /master 9cbc181. g-778/
+ glt push u2 origin HEAD; ok; /284951d..9cbc181 HEAD -> master/
+ git reset --hard HEAD^; ok; /HEAD is now at 2edf7fc g-777/
+ tc d-485; ok; /master 1c01d32. d-485/
+ glt push u2 -f origin HEAD; !ok; reject
+ /\\+ refs/heads/master aa u2 DENIED by fallthru/
+
+ # non-existent repos etc
+ glt ls-remote u4 file:///bb; !ok; /DENIED by fallthru/
+ glt ls-remote u4 file:///cc/1; ok; /Initialized empty/
+ glt ls-remote u5 file:///cc/1; ok; perl s/TRACE.*//g; !/\\S/
+ glt ls-remote u5 file:///cc/2; !ok; /DENIED by fallthru/
+ glt ls-remote u6 file:///cc/2; !ok; /DENIED by fallthru/
+
+ # command
+ glt perms u4 -c cc/bar/baz/frob + READERS u2;
+ ok; /Initialized empty .*cc/bar/baz/frob.git/
+
+ # path traversal
+ glt ls-remote u4 file:///cc/dd/../ee
+ !ok; /FATAL: 'cc/dd/\\.\\./ee' contains '\\.\\.'/
+ glt ls-remote u5 file:///cc/../../../../../..$rb/gitolite-admin
+ !ok; /FATAL: 'cc/../../../../../..$rb/gitolite-admin' contains '\\.\\.'/
+
+ glt perms u4 -c cc/bar/baz/../frob + READERS u2
+ !ok; /FATAL: no relative paths allowed anywhere!/
+
+";
diff --git a/t/C-vs-C.t b/t/C-vs-C.t
new file mode 100644
index 0000000..fee5cc4
--- /dev/null
+++ b/t/C-vs-C.t
@@ -0,0 +1,43 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# the commit message in which this test is introduced should have details, but
+# briefly, this test makes sure that access() does not get confused by
+# repo-create permissions being allowed, when looking for branch-create
+# permissions.
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# branch permissions test
+# ----------------------------------------------------------------------
+
+try "plan 25";
+
+confreset;confadd '
+ repo foo/..*
+ C = @all
+ RW+CD = CREATOR
+ RW = u2
+
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd ..; ok
+ glt clone u1 file:///foo/aa; ok
+ cd aa; ok
+ tc l-1; ok; /master/
+ glt push u1 origin master:m1; ok; /To file:///foo/aa/
+ /\\* \\[new branch\\] master -> m1/
+
+ tc l-2; ok; /master/
+ glt push u2 origin master:m2; !ok; /FATAL: C/
+ /DENIED by fallthru/
+ glt push u2 origin master:m1; ok; /To file:///foo/aa/
+ /8cd302a..29b8683/
+ /master -> m1/
+";
diff --git a/t/README b/t/README
new file mode 100644
index 0000000..4dc1594
--- /dev/null
+++ b/t/README
@@ -0,0 +1,116 @@
+# instructions for running the tests
+
+# Pre-requisites
+
+Install the following packages:
+
+* Manjaro (and probably Arch):
+
+ pacman -S perl-json perl-json-xs apache
+
+* Fedora (and probably CentOS):
+
+ dnf install -y perl-Test-Harness perl-JSON perl-JSON-XS httpd httpd-tools
+
+* others:
+
+ (TBD)
+
+# RUNNING THE MAIN TEST SUITE
+
+ ======================================
+ WARNING: THE TEST SUITE DELETES STUFF!
+ ======================================
+
+Please run the tests ONLY on a userid where it's ok to LOSE DATA.
+
+On such a userid, clone gitolite then run this command in the clone:
+
+ GITOLITE_TEST=y prove
+
+http://gitolite.com/gitolite/testing.html has more details. Alternatively,
+http://gitolite.com/gitolite/req.html#trying will help you try out gitolite if
+you want to play with gitolite safely.
+
+# RUNNING THE HTTP AND MIRROR TESTS
+
+ ======================================
+ WARNING: THE TEST SUITE DELETES STUFF!
+ ======================================
+
+The http and mirror tests require a lot more preparation, including commands
+and/or scripts to be run as root, so they're not invoked when you simply run
+"prove" as above.
+
+## Manjaro
+
+1. Create 3 users: sam, frodo, and gollum (`useradd -m`).
+
+2. Assuming you're running the tests using a local user called `g3`, run
+ `visudo` and add the following line:
+
+ g3 ALL = (sam,frodo,gollum) NOPASSWD: ALL
+
+ Test this by running this command from within `g3` and making sure you get
+ the correct results:
+
+ sudo -u sam -i pwd
+ # should print /home/sam
+ # similarly make sure frodo and gollum also give correct results
+
+ The mirror test will not run if this does not work. That does not mean
+ *mirroring* will not work; only the test suite depends on this feature.
+
+3. Manjaro does not, by default, add $HOME/bin to $PATH, so you will need the
+ following on at least sam, frodo, and gollum:
+
+ # copy-paste this into a root terminal
+ for u in frodo sam gollum; do
+ grep '$HOME/bin' /home/$u/.bash_profile || echo 'export PATH="$HOME/bin:$PATH"' >> /home/$u/.bash_profile
+ done
+
+ Again, test this by running:
+
+ sudo -u sam -i echo '$PATH'
+
+ and making sure the output starts with `/home/sam/bin:` (and similarly for
+ frodo and gollum).
+
+4. Take a look inside `t/manjaro-root-smart-http-test-setup` to make sure
+ everything looks sane (because you have to run it as root!!), then run it
+ as root.
+
+5. Now you are ready to run the last two tests:
+
+ GITOLITE_TEST=y prove t/smart-http
+ GITOLITE_TEST=y prove t/mirror-test
+
+## Fedora
+
+1. Create 3 users: sam, frodo, and gollum (`useradd`).
+
+2. Assuming you're running the tests using a local user called `g3`, run
+ `visudo` and add the following line:
+
+ g3 ALL = (sam,frodo,gollum) NOPASSWD: ALL
+
+ Test this by running this command from within `g3` and making sure you get
+ the correct results:
+
+ sudo -u sam -i pwd
+ # should print /home/sam
+ # similarly make sure frodo and gollum also give correct results
+
+ The mirror test will not run if this does not work. That does not mean
+ *mirroring* will not work; only the test suite depends on this feature.
+
+3. Take a look inside `t/fedora-root-smart-http-test-setup` to make sure
+ everything looks sane (because you have to run it as root!!), then run it
+ as root.
+
+4. Now you are ready to run the last two tests:
+
+ prove t/smart-http
+ prove t/mirror-test
+
+vim: ft=markdown
diff --git a/t/access.t b/t/access.t
new file mode 100755
index 0000000..c3f3341
--- /dev/null
+++ b/t/access.t
@@ -0,0 +1,253 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# test 'gitolite access'
+# ----------------------------------------------------------------------
+
+try "plan 254";
+
+confreset;confadd '
+ @admins = admin dev1
+ repo gitolite-admin
+ RW+ = admin
+
+ repo testing
+ RW+ = @all
+
+ @g1 = t1
+ repo @g1
+ R = u2
+ RW = u3
+ RW+ = u4
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+
+ gitolite access -q t1 u1; !ok; !/./
+ gitolite access -q t1 u1 R; !ok; !/./
+ gitolite access -q t1 u1 W; !ok; !/./
+ gitolite access -q t1 u1 +; !ok; !/./
+ gitolite access -q t1 u2; !ok; !/./
+ gitolite access -q t1 u2 R; ok; !/./
+ gitolite access -q t1 u2 W; !ok; !/./
+ gitolite access -q t1 u2 +; !ok; !/./
+ gitolite access -q t1 u3; !ok; !/./
+ gitolite access -q t1 u3 R; ok; !/./
+ gitolite access -q t1 u3 W; ok; !/./
+ gitolite access -q t1 u3 +; !ok; !/./
+ gitolite access -q t1 u4; ok; !/./
+ gitolite access -q t1 u4 R; ok; !/./
+ gitolite access -q t1 u4 W; ok; !/./
+ gitolite access -q t1 u4 +; ok; !/./
+
+ gitolite access t1 u1; !ok; /\\+ any t1 u1 DENIED by fallthru/
+ gitolite access t1 u1 R; !ok; /R any t1 u1 DENIED by fallthru/
+ gitolite access t1 u1 W; !ok; /W any t1 u1 DENIED by fallthru/
+ gitolite access t1 u1 +; !ok; /\\+ any t1 u1 DENIED by fallthru/
+ gitolite access t1 u2; !ok; /\\+ any t1 u2 DENIED by fallthru/
+ gitolite access t1 u2 R; ok; /refs/\.\*/
+ gitolite access t1 u2 W; !ok; /W any t1 u2 DENIED by fallthru/
+ gitolite access t1 u2 +; !ok; /\\+ any t1 u2 DENIED by fallthru/
+ gitolite access t1 u3; !ok; /\\+ any t1 u3 DENIED by fallthru/
+ gitolite access t1 u3 R; ok; /refs/\.\*/
+ gitolite access t1 u3 W; ok; /refs/\.\*/
+ gitolite access t1 u3 +; !ok; /\\+ any t1 u3 DENIED by fallthru/
+ gitolite access t1 u4; ok; /refs/\.\*/
+ gitolite access t1 u4 R; ok; /refs/\.\*/
+ gitolite access t1 u4 W; ok; /refs/\.\*/
+ gitolite access t1 u4 +; ok; /refs/\.\*/
+
+";
+
+confreset;confadd '
+ @admins = admin dev1
+ repo gitolite-admin
+ RW+ = admin
+
+ @g1 = u1
+ @g2 = u2
+ @g3 = u3
+ @gaa = aa
+ repo @gaa
+ RW+ = @g1
+ RW = @g2
+ RW+ master = @g3
+ RW master = u4
+ - master = u5
+ RW+ dev = u5
+ RW = u5
+';
+
+try "ADMIN_PUSH set2; !/FATAL/" or die text();
+
+try "
+ gitolite access \@gaa \@g1 + any ; ok; /refs/.*/; !/DENIED/
+ gitolite access aa \@g1 + refs/heads/master ; ok; /refs/.*/; !/DENIED/
+ gitolite access \@gaa \@g1 + refs/heads/next ; ok; /refs/.*/; !/DENIED/
+ gitolite access \@gaa \@g1 W refs/heads/next ; ok; /refs/.*/; !/DENIED/
+ gitolite access \@gaa u1 + refs/heads/dev ; ok; /refs/.*/; !/DENIED/
+ gitolite access \@gaa u1 + refs/heads/next ; ok; /refs/.*/; !/DENIED/
+ gitolite access aa u1 W refs/heads/next ; ok; /refs/.*/; !/DENIED/
+ gitolite access \@gaa \@g2 + refs/heads/master ; !ok; /\\+ refs/heads/master \@gaa \@g2 DENIED by fallthru/
+ gitolite access \@gaa \@g2 + refs/heads/next ; !ok; /\\+ refs/heads/next \@gaa \@g2 DENIED by fallthru/
+ gitolite access aa \@g2 W refs/heads/master ; ok; /refs/.*/; !/DENIED/
+ gitolite access aa u2 + any ; !ok; /\\+ any aa u2 DENIED by fallthru/
+ gitolite access \@gaa u2 + refs/heads/master ; !ok; /\\+ refs/heads/master \@gaa u2 DENIED by fallthru/
+ gitolite access \@gaa u2 W refs/heads/master ; ok; /refs/.*/; !/DENIED/
+ gitolite access \@gaa \@g3 + refs/heads/master ; ok; /refs/heads/master/; !/DENIED/
+ gitolite access \@gaa \@g3 W refs/heads/next ; !ok; /W refs/heads/next \@gaa \@g3 DENIED by fallthru/
+ gitolite access \@gaa \@g3 W refs/heads/dev ; !ok; /W refs/heads/dev \@gaa \@g3 DENIED by fallthru/
+ gitolite access aa u3 + refs/heads/dev ; !ok; /\\+ refs/heads/dev aa u3 DENIED by fallthru/
+ gitolite access aa u3 + refs/heads/next ; !ok; /\\+ refs/heads/next aa u3 DENIED by fallthru/
+ gitolite access \@gaa u4 + refs/heads/master ; !ok; /\\+ refs/heads/master \@gaa u4 DENIED by fallthru/
+ gitolite access \@gaa u4 W refs/heads/master ; ok; /refs/heads/master/; !/DENIED/
+ gitolite access aa u4 + refs/heads/next ; !ok; /\\+ refs/heads/next aa u4 DENIED by fallthru/
+ gitolite access \@gaa u4 W refs/heads/next ; !ok; /W refs/heads/next \@gaa u4 DENIED by fallthru/
+ gitolite access \@gaa u5 R any ; ok; /refs/heads/dev/; !/DENIED/
+ gitolite access aa u5 R any ; ok; /refs/heads/dev/; !/DENIED/
+ gitolite access \@gaa u5 + refs/heads/dev ; ok; /refs/heads/dev/; !/DENIED/
+ gitolite access \@gaa u5 + refs/heads/master ; !ok; /\\+ refs/heads/master \@gaa u5 DENIED by refs/heads/master/
+ gitolite access aa u5 + refs/heads/next ; !ok; /\\+ refs/heads/next aa u5 DENIED by fallthru/
+ gitolite access \@gaa u5 R refs/heads/dev ; ok; /refs/heads/dev/; !/DENIED/
+ gitolite access \@gaa u5 R refs/heads/master ; !ok; /R refs/heads/master \@gaa u5 DENIED by refs/heads/master/
+ gitolite access \@gaa u5 R refs/heads/next ; ok; /refs/.*/; !/DENIED/
+ gitolite access aa u5 W refs/heads/dev ; ok; /refs/heads/dev/; !/DENIED/
+ gitolite access aa u5 W refs/heads/master ; !ok; /W refs/heads/master aa u5 DENIED by refs/heads/master/
+ gitolite access \@gaa u5 W refs/heads/next ; ok; /refs/.*/; !/DENIED/
+";
+
+confreset;confadd '
+ @admins = admin dev1
+ repo gitolite-admin
+ RW+ = admin
+
+ @gr1 = r1
+ repo @gr1
+ RW refs/heads/v[0-9] = u1
+ RW refs/heads = tester
+
+ @gr2 = r2
+ repo @gr2
+ RW refs/heads/v[0-9] = u1
+ - refs/heads/v[0-9] = tester
+ RW refs/heads = tester
+';
+
+try "ADMIN_PUSH set3; !/FATAL/" or die text();
+
+try "
+ gitolite access \@gr2 tester W refs/heads/v1; !ok; /W refs/heads/v1 \@gr2 tester DENIED by refs/heads/v\\[0-9\\]/
+ gitolite access \@gr1 tester W refs/heads/v1; ok; /refs/heads/; !/DENIED/
+ gitolite access r1 tester W refs/heads/v1; ok; /refs/heads/; !/DENIED/
+ gitolite access r2 tester W refs/heads/v1; !ok; /W refs/heads/v1 r2 tester DENIED by refs/heads/v\\[0-9\\]/
+ gitolite access r2 tester W refs/heads/va; ok; /refs/heads/; !/DENIED/
+";
+
+confreset;confadd '
+ repo foo
+ RW+ = u1
+
+ @gr1 = foo bar
+
+ repo @gr1
+ RW = u2
+ R = u3
+
+ repo @all
+ R = gitweb
+
+ repo c0
+ RW+ = @all
+ repo c1
+ RWC = u1
+ RW+ = @all
+';
+
+try "ADMIN_PUSH set4; !/FATAL/" or die text();
+
+try "
+ gitolite access foo u1 +; ok
+ gitolite access foo u2 +; !ok
+ gitolite access foo u3 +; !ok
+ gitolite access foo u4 +; !ok
+ gitolite access foo gitweb +; !ok
+
+ gitolite access foo u1 W; ok
+ gitolite access foo u2 W; ok
+ gitolite access foo u3 W; !ok
+ gitolite access foo u4 W; !ok
+ gitolite access foo gitweb W; !ok
+
+ gitolite access foo u1 R; ok
+ gitolite access foo u2 R; ok
+ gitolite access foo u3 R; ok
+ gitolite access foo u4 R; !ok
+ gitolite access foo gitweb R; ok
+
+ gitolite access c0 u1 +; ok
+ gitolite access c0 u1 C; ok
+ gitolite access c0 u2 +; ok
+ gitolite access c0 u2 C; ok
+ gitolite access c1 u1 +; ok
+ gitolite access c1 u1 C; ok
+ gitolite access c1 u2 +; ok
+ gitolite access c1 u2 C; !ok
+";
+
+confreset;confadd '
+ repo foo
+ R = u1
+ RW = u2
+ RW+ = u3
+
+ repo bar
+ R = u1
+ RW = u2
+ RW+ = u3
+ RW+CDM = u6
+
+';
+
+try "ADMIN_PUSH set4; !/FATAL/" or die text();
+
+try "
+ gitolite access foo u1 +; !ok
+ gitolite access foo u2 +; !ok
+ gitolite access foo u3 +; ok
+ gitolite access foo u1 C; !ok
+ gitolite access foo u2 C; ok
+ gitolite access foo u3 C; ok
+ gitolite access foo u1 D; !ok
+ gitolite access foo u2 D; !ok
+ gitolite access foo u3 D; ok
+ gitolite access foo u1 M; !ok
+ gitolite access foo u2 M; ok
+ gitolite access foo u3 M; ok
+
+ gitolite access bar u1 +; !ok
+ gitolite access bar u2 +; !ok
+ gitolite access bar u3 +; ok
+ gitolite access bar u1 C; !ok
+ gitolite access bar u2 C; !ok
+ gitolite access bar u3 C; !ok
+ gitolite access bar u1 D; !ok
+ gitolite access bar u2 D; !ok
+ gitolite access bar u3 D; !ok
+ gitolite access bar u1 M; !ok
+ gitolite access bar u2 M; !ok
+ gitolite access bar u3 M; !ok
+
+ gitolite access bar u6 R; ok
+ gitolite access bar u6 W; ok
+ gitolite access bar u6 +; ok
+ gitolite access bar u6 C; ok
+ gitolite access bar u6 D; ok
+ gitolite access bar u6 M; ok
+";
diff --git a/t/all-yall.t b/t/all-yall.t
new file mode 100755
index 0000000..901b1c2
--- /dev/null
+++ b/t/all-yall.t
@@ -0,0 +1,89 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# could anything be clearer than "all y'all"?
+# ----------------------------------------------------------------------
+
+try "plan 26";
+
+confreset;confadd '
+ repo @all
+ R = @all
+ repo foo
+ RW+ = u1
+ repo bar
+ RW+ = u2
+ repo dev/..*
+ C = u3 u4
+ RW+ = CREATOR
+';
+
+try "
+ rm $ENV{HOME}/projects.list
+";
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ glt ls-remote u1 file:///dev/wild1
+ /FATAL: R any dev/wild1 u1 DENIED by fallthru/
+
+ glt clone u3 file:///dev/wild1
+ /Cloning into 'wild1'.../
+ /Initialized empty Git repository in .*/dev/wild1.git//
+ /warning: You appear to have cloned an empty repository./
+
+ cd wild1
+ tc n-855 n-856
+ glt push u3 origin master:wild1
+ /To file:///dev/wild1/
+ /\\* \\[new branch\\] master -> wild1/
+ glt push u1 file:///foo master:br-foo
+ /To file:///foo/
+ /\\* \\[new branch\\] master -> br-foo/
+ glt push u2 file:///bar master:br-bar
+ /To file:///bar/
+ /\\* \\[new branch\\] master -> br-bar/
+
+ glt ls-remote u6 file:///foo
+ /refs/heads/br-foo/
+
+ glt ls-remote u6 file:///bar
+ /refs/heads/br-bar/
+
+ glt ls-remote u6 file:///dev/wild1
+ /refs/heads/wild1/
+";
+
+try "
+ gitolite ../triggers/post-compile/update-git-daemon-access-list; ok
+ gitolite ../triggers/post-compile/update-gitweb-access-list; ok
+ cat $ENV{HOME}/projects.list; ok
+";
+cmp 'bar.git
+dev/wild1.git
+foo.git
+gitolite-admin.git
+testing.git
+';
+
+my $rb = `gitolite query-rc -n GL_REPO_BASE`;
+
+try "
+ cd ..
+ cd ..
+ echo $rb
+ find $rb -name git-daemon-export-ok | sort
+ perl s,$rb/,,g
+";
+
+cmp 'bar.git/git-daemon-export-ok
+dev/wild1.git/git-daemon-export-ok
+foo.git/git-daemon-export-ok
+gitolite-admin.git/git-daemon-export-ok
+testing.git/git-daemon-export-ok
+';
diff --git a/t/basic.t b/t/basic.t
new file mode 100755
index 0000000..7579935
--- /dev/null
+++ b/t/basic.t
@@ -0,0 +1,284 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# some more basic tests
+# ----------------------------------------------------------------------
+
+try "
+ plan 217
+ CHECK_SETUP
+
+ # subtest 1
+ cd ..
+ CLONE dev2 gitolite-admin ga2
+ !ok; gsh
+ /DENIED by fallthru/
+ /fatal: Could not read from remote repository/
+ glt clone admin --progress file:///gitolite-admin ga2
+ ok; gsh
+ /Counting/; /Compressing/; /Total/
+ cd gitolite-admin; ok
+ ";
+
+put "conf/gitolite.conf", "
+ \@admins = admin dev1
+ repo gitolite-admin
+ - mm = \@admins
+ RW = \@admins
+ RW+ = admin
+
+ repo testing
+ RW+ = \@all
+";
+
+try "
+ # push
+ git add conf; ok
+ git status -s; ok; /M conf/gitolite.conf/
+ git commit -m t01a; ok; /master.*t01a/
+ PUSH dev2; !ok; gsh
+ /DENIED by fallthru/
+ /fatal: Could not read from remote repository/
+ PUSH admin; ok; /master -> master/
+ empty; ok;
+ PUSH admin master:mm
+ !ok; gsh
+ /DENIED by refs/heads/mm/
+ reject
+ ";
+
+put "conf/gitolite.conf", "
+ \@admins = admin dev1
+ repo gitolite-admin
+ RW+ = admin
+
+ repo testing
+ RW+ = \@all
+
+ repo t1
+ R = u2
+ RW = u3
+ RW+ = u4
+";
+
+try "
+ # subtest 2
+ ADMIN_PUSH t01b
+
+ # clone
+ cd ..; ok;
+ CLONE u1 t1; !ok; gsh
+ /DENIED by fallthru/
+ /fatal: Could not read from remote repository/
+ CLONE u2 t1; ok; gsh
+ /warning: You appear to have cloned an empty repository./
+ [ -d t1/.git ]; ok
+ cd t1; ok;
+
+ # push
+ test-commit tc1 tc2 tc2; ok; /a530e66/
+ PUSH u2; !ok; gsh
+ /DENIED by fallthru/
+ /fatal: Could not read from remote repository/
+ PUSH u3 master; ok; gsh
+ /master -> master/
+
+ # rewind
+ reset-h HEAD^; ok; /HEAD is now at aa2b5c5 tc2/
+ test-tick; test-commit tc3; ok; /3ffced1/
+ PUSH u3; !ok; gsh
+ /rejected.*master -> master.*non-fast-forward./
+ PUSH u3 -f; !ok; gsh
+ reject
+ /DENIED by fallthru/
+ PUSH u4 +master; ok; gsh
+ / \\+ a530e66...3ffced1 master -> master.*forced update./
+";
+
+put "../gitolite-admin/conf/gitolite.conf", "
+ \@admins = admin dev1
+ repo gitolite-admin
+ RW+ = admin
+
+ include 'i1.conf'
+";
+
+put "../gitolite-admin/conf/i1.conf", "
+ \@g1 = u1
+ \@g2 = u2
+ \@g3 = u3
+ \@gaa = aa
+ repo \@gaa
+ RW+ = \@g1
+ RW = \@g2
+ RW+ master = \@g3
+ RW master = u4
+ - master = u5
+ RW+ dev = u5
+ RW = u5
+";
+
+try "
+ # subtest 3
+ ADMIN_PUSH t01c
+
+ cd ..; ok
+";
+
+try "
+ CLONE u1 aa; ok; gsh
+ cd aa; ok
+ test-commit set3 t1 t2 t3 t4 t5 t6 t7 t8 t9
+ ok
+ PUSH u1 HEAD; ok; gsh
+ /To file:///aa/
+ /\\* \\[new branch\\] HEAD -> master/
+ branch dev; ok
+ branch foo; ok
+
+ # u1 rewind master ok
+ reset-h HEAD^; ok
+ test-commit r1; ok
+ PUSH u1 +master; ok; gsh
+ /To file:///aa/
+ /\\+ 27ed463...05adfb0 master -> master .forced update./
+
+ # u2 rewind master !ok
+ reset-h HEAD^; ok
+ test-commit r2; ok
+ PUSH u2 +master; !ok; gsh
+ reject
+ /DENIED by fallthru/
+
+ # u3 rewind master ok
+ reset-h HEAD^; ok
+ test-commit r3; ok
+ PUSH u3 +master; ok; gsh
+ /To file:///aa/
+ /\\+ 05adfb0...6a532fe master -> master .forced update./
+
+ # u4 push master ok
+ test-commit u4; ok
+ PUSH u4 master; ok; gsh
+ /To file:///aa/
+ /6a532fe..f929773 +master -> master/
+
+ # u4 rewind master !ok
+ reset-h HEAD^; ok
+ PUSH u4 +master; !ok; gsh
+ reject
+ /DENIED by fallthru/
+
+ # u3,u4 push other branches !ok
+ PUSH u3 dev; !ok; gsh
+ reject
+ /DENIED by fallthru/
+ PUSH u4 dev; !ok; gsh
+ reject
+ /DENIED by fallthru/
+ PUSH u3 foo; !ok; gsh
+ reject
+ /DENIED by fallthru/
+ PUSH u4 foo; !ok; gsh
+ reject
+ /DENIED by fallthru/
+
+ # clean up for next set
+ glt push u1 -f origin master dev foo
+ ok; gsh
+ /f929773...6a532fe master -> master .forced update./
+ /new branch.*dev -> dev/
+ /new branch.*foo -> foo/
+
+ # u5 push master !ok
+ test-commit u5
+ PUSH u5 master; !ok; gsh
+ reject
+ /DENIED by refs/heads/master/
+
+ # u5 rewind dev ok
+ PUSH u5 +dev^:dev
+ ok; gsh
+ /\\+ 27ed463...1ad477a dev\\^ -> dev .forced update./
+
+
+ # u5 rewind foo !ok
+ PUSH u5 +foo^:foo
+ !ok; gsh
+ reject
+ /remote: FATAL: \\+ refs/heads/foo aa u5 DENIED by fallthru/
+
+ # u5 push foo ok
+ git checkout foo
+ /Switched to branch 'foo'/
+
+ test-commit u5
+ PUSH u5 foo; ok; gsh
+ /27ed463..83da62c *foo -> foo/
+
+ # u1 delete dev ok
+ PUSH u1 :dev; ok; gsh
+ / - \\[deleted\\] *dev/
+
+ # push it back
+ PUSH u1 dev; ok; gsh
+ /\\* \\[new branch\\] *dev -> dev/
+
+";
+
+put "| cat >> ../gitolite-admin/conf/gitolite.conf", "
+ \@gr1 = r1
+ repo \@gr1
+ RW refs/heads/v[0-9] = u1
+ RW refs/heads = tester
+";
+
+try "
+ # subtest 4
+ ADMIN_PUSH t01d
+
+ cd ..; ok
+
+ CLONE tester r1; ok; gsh
+ /Cloning into 'r1'.../
+ cd r1; ok
+ test-commit r1a r1b r1c r1d r1e r1f
+ ok
+ PUSH tester HEAD; ok; gsh
+ /\\* \\[new branch\\] *HEAD -> master/
+ git branch v1
+ PUSH tester v1; ok; gsh
+ /\\* \\[new branch\\] *v1 -> v1/
+
+";
+
+put "| cat >> ../gitolite-admin/conf/gitolite.conf", "
+ \@gr2 = r2
+ repo \@gr2
+ RW refs/heads/v[0-9] = u1
+ - refs/heads/v[0-9] = tester
+ RW refs/heads = tester
+";
+
+try "
+ # subtest 5
+ ADMIN_PUSH t01e
+
+ cd ..; ok
+
+ CLONE tester r2; ok; gsh
+ /Cloning into 'r2'.../
+ cd r2; ok
+ test-commit r2a r2b r2c r2d r2e r2f
+ ok
+ PUSH tester HEAD; ok; gsh
+ /\\* \\[new branch\\] *HEAD -> master/
+ git branch v1
+ PUSH tester v1; !ok; gsh
+ /W refs/heads/v1 r2 tester DENIED by refs/heads/v\\[0-9\\]/
+"
diff --git a/t/branch-perms.t b/t/branch-perms.t
new file mode 100755
index 0000000..e59baea
--- /dev/null
+++ b/t/branch-perms.t
@@ -0,0 +1,122 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# branch permissions test
+# ----------------------------------------------------------------------
+
+try "plan 82";
+
+confreset;confadd '
+ @g1 = u1
+ @g2 = u2
+ @g3 = u3
+ @gaa = aa
+ repo @gaa
+ RW+ = @g1
+ RW = @g2
+ RW+ master = @g3
+ RW master = u4
+ - master = u5
+ RW+ dev = u5
+ RW = u5
+
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd ..; ok
+ glt clone u1 file:///aa; ok
+ cd aa; ok
+ tc l-995 l-996 l-997 l-998 l-999 l-1000 l-1001 l-1002 l-1003;
+ ok; /master a788db9. l-1003/
+ glt push u1 origin HEAD; ok; /To file:///aa/
+ /\\* \\[new branch\\] HEAD -> master/
+
+ git branch dev; ok
+ git branch foo; ok
+
+ # u1 rewind master succeed
+ git reset --hard HEAD^; ok; /HEAD is now at 65d5f4a l-1002/
+ tc v-865; ok; /master 3053bb4. v-865/
+ glt push u1 origin +master; ok; /\\+ a788db9...3053bb4 master -> master \\(forced update\\)/
+
+ # u2 rewind master fail
+ git reset --hard HEAD^; ok; /HEAD is now at 65d5f4a l-1002/
+ tc s-361; ok; /master b331651. s-361/
+ glt push u2 file:///aa +master; !ok; reject
+ /\\+ refs/heads/master aa u2 DENIED by fallthru/
+
+ # u3 rewind master succeed
+ git reset --hard HEAD^; ok
+ tc m-508; ok
+ glt push u3 file:///aa +master; ok; /\\+ .* master -> master \\(forced update\\)/
+
+ # u4 push master succeed
+ tc f-526; ok;
+ glt push u4 file:///aa master; ok; /master -> master/
+
+ # u4 rewind master fail
+ git reset --hard HEAD^; ok;
+ glt push u4 file:///aa +master; !ok; /\\+ refs/heads/master aa u4 DENIED by fallthru/
+
+ # u3 and u4 / dev foo -- all 4 fail
+ glt push u3 file:///aa dev; !ok; /W refs/heads/dev aa u3 DENIED by fallthru/
+ glt push u4 file:///aa dev; !ok; /W refs/heads/dev aa u4 DENIED by fallthru/
+ glt push u3 file:///aa foo; !ok; /W refs/heads/foo aa u3 DENIED by fallthru/
+ glt push u4 file:///aa foo; !ok; /W refs/heads/foo aa u4 DENIED by fallthru/
+
+ # clean up for next set
+ glt push u1 -f origin master dev foo
+ ok
+
+ # u5 push master fail
+ tc l-417; ok
+ glt push u5 file:///aa master; !ok; /W refs/heads/master aa u5 DENIED by refs/heads/master/
+
+ # u5 rewind dev succeed
+ glt push u5 file:///aa +dev^:dev
+ ok; /\\+ .* dev\\^ -> dev \\(forced update\\)/
+
+ # u5 rewind foo fail
+ glt push u5 file:///aa +foo^:foo
+ !ok; /\\+ refs/heads/foo aa u5 DENIED by fallthru/
+
+ # u5 tries to push foo; succeeds
+ git checkout foo; ok; /Switched to branch 'foo'/
+
+ # u5 push foo succeed
+ tc e-530; ok;
+ glt push u5 file:///aa foo; ok; /foo -> foo/
+
+ # u1 delete branch dev succeed
+ glt push u1 origin :dev; ok; / - \\[deleted\\] *dev/
+
+ # quietly push it back again
+ glt push u1 origin dev; ok; / * \\[new branch\\] dev -> dev/
+
+ ";
+
+ confadd '
+ repo @gaa
+ RWD dev = u4
+ ';
+
+try "ADMIN_PUSH set2; !/FATAL/" or die text();
+
+try "
+ # u1 tries to delete dev on a new setup
+ cd ../aa; ok; /master -> master/
+
+ # u1 delete branch dev fail
+ glt push u1 origin :dev; !ok; /D refs/heads/dev aa u1 DENIED by fallthru/
+
+ # u4 delete branch dev succeed
+ glt push u4 file:///aa :dev; ok; / - \\[deleted\\] *dev/
+
+";
diff --git a/t/daemon-gitweb-via-perms.t b/t/daemon-gitweb-via-perms.t
new file mode 100755
index 0000000..8707e19
--- /dev/null
+++ b/t/daemon-gitweb-via-perms.t
@@ -0,0 +1,78 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# basic tests
+# ----------------------------------------------------------------------
+
+try "plan 24";
+try "DEF POK = !/DENIED/; !/failed to push/";
+
+confreset;confadd '
+
+@leads = u1 u2
+@devs = u1 u2 u3 u4
+
+@gbar = bar/CREATOR/..*
+repo @gbar
+ C = @leads
+ RW+ = @leads
+ RW = WRITERS @devs
+ R = READERS
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+my $rb = `gitolite query-rc -n GL_REPO_BASE`;
+chdir($rb);
+my $h = $ENV{HOME};
+
+try "
+ glt ls-remote u1 file:///bar/u1/try1
+ /Initialized empty Git repository in .*/bar/u1/try1.git/
+
+ find . -name git-daemon-export-ok
+ /testing.git/git-daemon-export-ok/
+
+ cat $h/projects.list
+ /testing.git/
+
+ glt ls-remote u1 file:///bar/u1/try2
+ /Initialized empty Git repository in .*/bar/u1/try2.git/
+
+ find $h/repositories -name git-daemon-export-ok
+ /testing.git/git-daemon-export-ok/
+
+ cat $h/projects.list
+ /testing.git/
+
+ glt perms u1 bar/u1/try1 + READERS daemon
+ !/./
+
+ glt perms u1 bar/u1/try1 -l
+ /READERS daemon/
+
+ find $h/repositories -name git-daemon-export-ok
+ /repositories/testing.git/git-daemon-export-ok/
+ /repositories/bar/u1/try1.git/git-daemon-export-ok/
+
+ cat $h/projects.list
+ /testing.git/
+
+ glt perms u1 bar/u1/try2 + READERS gitweb
+
+ glt perms u1 bar/u1/try2 -l
+ /READERS gitweb/
+
+ find $h/repositories -name git-daemon-export-ok
+ /testing.git/git-daemon-export-ok/
+ /bar/u1/try1.git/git-daemon-export-ok/
+
+ cat $h/projects.list
+ /bar/u1/try2.git/
+ /testing.git/
+";
diff --git a/t/deleg-1.t b/t/deleg-1.t
new file mode 100755
index 0000000..89da137
--- /dev/null
+++ b/t/deleg-1.t
@@ -0,0 +1,100 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# delegation tests -- part 1
+# ----------------------------------------------------------------------
+
+try "plan 54";
+
+try "
+ DEF SP_1 = git add conf ; ok; git commit -m %1; ok; /master.* %1/
+ DEF SUBCONF_PUSH = SP_1 %2; glt push %1 origin; gsh; /master -> master/
+";
+
+confreset;confadd '
+ @u1r = r1a r1b
+ @u2r = r2a r2b
+ @u3r = r3a r3b
+
+ # the admin repo access was probably like this to start with:
+ repo gitolite-admin
+ RW = u1 u2 u3
+ RW+ NAME/ = admin
+ RW NAME/conf/fragments/u1r = u1
+ RW NAME/conf/fragments/u2r = u2
+ RW NAME/conf/fragments/u3r = u3
+ - NAME/ = @all
+
+ subconf "fragments/*.conf"
+';
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+mkdir "conf/fragments";
+put "conf/fragments/u1r.conf", '
+ repo @u1r
+ RW+ = tester
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+try "
+ /Initialized empty Git repository in .*/r1a.git//
+ /Initialized empty Git repository in .*/r1b.git//
+";
+
+# u1 push u1r pass
+put "conf/fragments/u1r.conf", '
+ repo @u1r
+ RW+ = u5
+';
+try "SUBCONF_PUSH u1 u1; !/FATAL/" or die text();
+
+# u2 main push fail
+confadd '
+ repo @u1r
+ RW+ = u6
+';
+try "SUBCONF_PUSH u2 u2; /FATAL/;
+ /W VREF/NAME/conf/gitolite.conf gitolite-admin u2 DENIED by VREF/NAME//
+";
+
+try "git reset --hard origin/master; ok";
+
+# u2 push u1r fail
+put "conf/fragments/u1r.conf", '
+ repo @u1r
+ RW+ = u6
+';
+try "SUBCONF_PUSH u2 u2; /FATAL/
+ /W VREF/NAME/conf/fragments/u1r.conf gitolite-admin u2 DENIED by VREF/NAME//
+";
+
+try "git reset --hard origin/master; ok";
+
+# u3 set perms for r2a fail
+put "conf/fragments/u3r.conf", '
+ repo r2a
+ RW+ = u6
+';
+try "SUBCONF_PUSH u3 u3;
+ /WARNING: subconf 'u3r' attempting to set access for r2a/
+";
+
+try "git reset --hard origin/master; ok";
+
+# u3 add r2b to u3r fail
+
+put "conf/fragments/u3r.conf", '
+ @u3r = r2b
+ repo @u3r
+ RW+ = u6
+';
+
+try "SUBCONF_PUSH u3 u3
+ /WARNING: expanding '\@u3r'/
+ /WARNING: subconf 'u3r' attempting to set access for r2b/
+";
diff --git a/t/deleg-2.t b/t/deleg-2.t
new file mode 100755
index 0000000..98fb02e
--- /dev/null
+++ b/t/deleg-2.t
@@ -0,0 +1,122 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# delegation tests -- part 2
+# ----------------------------------------------------------------------
+
+try "plan 55";
+
+try "
+ DEF SP_1 = git add conf ; ok; git commit -m %1; ok; /master.* %1/
+ DEF SUBCONF_PUSH = SP_1 %2; glt push %1 origin; gsh; /master -> master/
+";
+
+confreset;confadd '
+ # group your projects/repos however you want
+ @u1r = r1[ab]
+ @u2r = r2[ab]
+ @u3r = r3[ab]
+
+ # the admin repo access was probably like this to start with:
+ repo gitolite-admin
+ RW = u1 u2 u3
+ RW+ NAME/ = admin
+ RW NAME/conf/fragments/u1r = u1
+ RW NAME/conf/fragments/u2r = u2
+ RW NAME/conf/fragments/u3r = u3
+ - NAME/ = @all
+
+ subconf "fragments/*.conf"
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "mkdir -p conf/fragments; ok";
+
+put "conf/fragments/u1r.conf", '
+ repo r1a r1b
+ C = @all
+ RW+ = CREATOR
+ repo @u1r
+ RW+ = tester
+';
+
+put "conf/fragments/u2r.conf", '
+ repo @u2r
+ C = @all
+ RW+ = CREATOR
+ repo @u2r
+ RW+ = tester
+';
+
+put "conf/fragments/u3r.conf", '
+ repo @u3r
+ C = @all
+ RW+ = CREATOR
+ repo @u3r
+ RW+ = tester
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+try "
+ /Initialized empty Git repository in .*/r1a.git//
+ /Initialized empty Git repository in .*/r1b.git//
+";
+
+# u1 push u1r pass
+put "conf/fragments/u1r.conf", '
+ repo @u1r
+ RW+ = u5
+';
+try "SUBCONF_PUSH u1 u1; !/FATAL/" or die text();
+
+# u2 main push fail
+confadd '
+ repo @u1r
+ RW+ = u6
+';
+try "SUBCONF_PUSH u2 u2; /FATAL/;
+ /W VREF/NAME/conf/gitolite.conf gitolite-admin u2 DENIED by VREF/NAME//
+";
+
+try "git reset --hard origin/master; ok";
+
+# u2 push u1r fail
+put "conf/fragments/u1r.conf", '
+ repo @u1r
+ RW+ = u6
+';
+try "SUBCONF_PUSH u2 u2; /FATAL/
+ /W VREF/NAME/conf/fragments/u1r.conf gitolite-admin u2 DENIED by VREF/NAME//
+";
+
+try "git reset --hard origin/master; ok";
+
+# u3 set perms for r2a fail
+put "conf/fragments/u3r.conf", '
+ repo r2a
+ RW+ = u6
+';
+try "SUBCONF_PUSH u3 u3;
+ /WARNING: subconf 'u3r' attempting to set access for r2a/
+";
+
+try "git reset --hard origin/master; ok";
+
+# u3 add r2b to u3r fail
+
+put "conf/fragments/u3r.conf", '
+ @u3r = r2b
+ repo @u3r
+ RW+ = u6
+';
+
+try "SUBCONF_PUSH u3 u3
+ /WARNING: expanding '\@u3r'/
+ /WARNING: subconf 'u3r' attempting to set access for r2b/
+";
diff --git a/t/deny-create.t b/t/deny-create.t
new file mode 100755
index 0000000..a4b7e4f
--- /dev/null
+++ b/t/deny-create.t
@@ -0,0 +1,137 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# deny-create, the RW.*C flag
+# ----------------------------------------------------------------------
+
+try "plan 72";
+
+try "DEF POK = !/DENIED/; !/failed to push/";
+
+# test "C" permissions
+
+confreset; confadd '
+ @leads = u1 u2
+ @devs = u1 u2 u3 u4
+
+ @gfoo = foo
+ repo @gfoo
+ RW+C = @leads
+ RW+C personal/USER/ = @devs
+ RW = @devs
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd ..
+ glt clone u1 file:///foo
+
+ cd foo
+ tc t-413 t-414 t-415 t-416 t-417
+
+ # u1 can push/rewind master on foo
+ glt push u1 origin master
+ POK; /master -> master/
+ glt push u1 -f origin master^^:master
+ POK; /master\\^\\^ -> master/
+
+ # u2 can create newbr1 on foo
+ glt push u2 file:///foo master:newbr1
+ POK; /master -> newbr1/
+
+ # u2 can create newtag on foo
+ git tag newtag
+ glt push u2 file:///foo newtag
+ POK; /newtag -> newtag/
+
+ # u3 can push newbr1 on foo
+ tc u-962 u-963 u-964 u-965 u-966
+ glt push u3 file:///foo master:newbr1
+ POK; /master -> newbr1/
+
+ # u4 canNOT create newbr2 on foo
+ tc e-615 e-616 e-617 e-618 e-619
+ glt push u3 file:///foo master:newbr2
+ /C refs/heads/newbr2 foo u3 DENIED by fallthru/
+ reject
+
+ # u4 canNOT create newtag2 on foo
+ git tag newtag2
+ glt push u3 file:///foo newtag2
+ /C refs/tags/newtag2 foo u3 DENIED by fallthru/
+ reject
+
+ # u4 can create/rewind personal/u4/newbr3 on foo
+ tc f-664 f-665 f-666 f-667 f-668
+ glt push u4 file:///foo master:personal/u4/newbr3
+ POK; /master -> personal/u4/newbr3/
+ glt push u4 -f origin master^^:personal/u4/newbr3
+ POK; /master\\^\\^ -> personal/u4/newbr3/
+";
+
+# bar, without "C" permissions, should behave like old
+
+confadd '
+ @leads = u1 u2
+ @devs = u1 u2 u3 u4
+
+ @gbar = bar
+ repo @gbar
+ RW+ = @leads
+ RW+ personal/USER/ = @devs
+ RW = @devs
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd ..
+ glt clone u1 file:///bar
+
+ cd bar
+ tc u-907 u-908 u-909 u-910 u-911
+
+ # u1 can push/rewind master on bar
+ glt push u1 origin master
+ POK; /master -> master/
+ glt push u1 -f origin master^^:master
+ POK; /master\\^\\^ -> master/
+
+ # u2 can create newbr1 on bar
+ glt push u2 file:///bar master:newbr1
+ POK; /master -> newbr1/
+
+ # u2 can create newtag on bar
+ git tag newtag
+ glt push u2 file:///bar newtag
+ POK; /newtag -> newtag/
+
+ # u3 can push newbr1 on bar
+ tc y-862 y-863 y-864 y-865 y-866
+ glt push u3 file:///bar master:newbr1
+ POK; /master -> newbr1/
+
+ # u4 can create newbr2 on bar
+ tc q-417 q-418 q-419 q-420 q-421
+ glt push u3 file:///bar master:newbr2
+ POK; /master -> newbr2/
+
+ # u4 can create newtag2 on bar
+ git tag newtag2
+ glt push u3 file:///bar newtag2
+ POK; /newtag2 -> newtag2/
+
+ # u4 can create/rewind personal/u4/newbr3 on bar
+ tc v-605 v-606 v-607 v-608 v-609
+ glt push u4 file:///bar master:personal/u4/newbr3
+ POK; /master -> personal/u4/newbr3/
+ glt push u4 -f origin master^^:personal/u4/newbr3
+ POK; /master\\^\\^ -> personal/u4/newbr3/
+
+";
diff --git a/t/deny-rules-2.t b/t/deny-rules-2.t
new file mode 100755
index 0000000..0ca15fe
--- /dev/null
+++ b/t/deny-rules-2.t
@@ -0,0 +1,172 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# more on deny-rules
+# ----------------------------------------------------------------------
+
+try "plan 126";
+
+try "
+ DEF GOOD = /refs/\\.\\*/
+ DEF BAD = /DENIED/
+
+ DEF Ryes = gitolite access %1 %2 R any; ok; GOOD
+ DEF Rno = gitolite access %1 %2 R any; !ok; BAD
+
+ DEF Wyes = gitolite access %1 %2 W any; ok; GOOD
+ DEF Wno = gitolite access %1 %2 W any; !ok; BAD
+
+ DEF GWyes = Ryes %1 gitweb
+ DEF GWno = Rno %1 gitweb
+
+ DEF GDyes = Ryes %1 daemon
+ DEF GDno = Rno %1 daemon
+";
+
+confreset;confadd '
+ repo one
+ RW+ = u1
+ R = u2
+ - = u2 u3
+ R = @all
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ Wyes one u1
+
+ Ryes one u2
+ Wno one u2
+
+ Ryes one u3
+ Wno one u3
+
+ Ryes one u6
+ Wno one u6
+
+ GDyes one
+ GWyes one
+";
+
+confadd '
+ option deny-rules = 1
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ Wyes one u1
+
+ Ryes one u2
+ Wno one u2
+
+ Rno one u3
+
+ Ryes one u6
+ Wno one u6
+
+ GDyes one
+ GWyes one
+";
+
+confadd '
+ repo two
+ RW+ = u1
+ R = u2
+ - = u2 u3 gitweb daemon
+ R = @all
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ GWyes two
+ GDyes two
+";
+
+confadd '
+ option deny-rules = 1
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ GWno two
+ GDno two
+";
+
+# set 3 -- allow gitweb to all but admin repo
+
+confadd '
+ repo gitolite-admin
+ - = gitweb daemon
+ option deny-rules = 1
+
+ repo three
+ RW+ = u3
+ R = gitweb daemon
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ GDyes three
+ GWyes three
+ GDno gitolite-admin
+ GWno gitolite-admin
+";
+
+# set 4 -- allow gitweb to all but admin repo
+
+confadd '
+ repo four
+ RW+ = u4
+ - = gitweb daemon
+
+ repo @all
+ R = @all
+';
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ GDyes four
+ GWyes four
+ GDno gitolite-admin
+ GWno gitolite-admin
+";
+
+# set 5 -- go wild
+
+confreset; confadd '
+ repo foo/..*
+ C = u1
+ RW+ = CREATOR
+ - = gitweb daemon
+ R = @all
+
+ repo bar/..*
+ C = u2
+ RW+ = CREATOR
+ - = gitweb daemon
+ R = @all
+ option deny-rules = 1
+';
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ glt ls-remote u1 file:///foo/one
+ glt ls-remote u2 file:///bar/two
+ Wyes foo/one u1
+ Wyes bar/two u2
+
+ GDyes foo/one
+ GDyes foo/one
+ GWno bar/two
+ GWno bar/two
+";
diff --git a/t/deny-rules.t b/t/deny-rules.t
new file mode 100755
index 0000000..c0e7cbb
--- /dev/null
+++ b/t/deny-rules.t
@@ -0,0 +1,67 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# deny rules
+# ----------------------------------------------------------------------
+
+try "plan 11";
+
+confreset;confadd '
+ # start with...
+
+ repo gitolite-admin
+ - = gitweb daemon
+ option deny-rules = 1
+
+ # main ruleset goes here
+
+ @ga = a
+ @gb = b
+ @gc = c
+
+ # and end with
+
+ repo @ga
+ RW = u1
+ - = @all
+ option deny-rules = 1
+
+ repo @gb
+ RW = u2
+ - = daemon
+ option deny-rules = 1
+
+ repo @gc
+ RW = u3
+
+ repo @all
+ R = @all
+
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+my $rb = `gitolite query-rc -n GL_REPO_BASE`;
+try "
+ cat $ENV{HOME}/projects.list; ok
+";
+cmp 'b.git
+c.git
+testing.git
+';
+
+try "
+ cd ..
+ cd ..
+ echo $rb
+ find $rb -name git-daemon-export-ok | sort
+ perl s,$rb/,,g
+";
+cmp 'c.git/git-daemon-export-ok
+testing.git/git-daemon-export-ok
+'
diff --git a/t/easy.t b/t/easy.t
new file mode 100755
index 0000000..dcd6c1a
--- /dev/null
+++ b/t/easy.t
@@ -0,0 +1,181 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Easy;
+use Gitolite::Test;
+# put this after ::Easy because it chdirs away from where you were and the
+# 'use lib "src"', not being absolute, fails
+
+# smoke tests for Easy.pm
+# ----------------------------------------------------------------------
+# for a change these are actual perl tests, so not much call for tsh here,
+# although I still need the basic infrastructure for setting up the repos and
+# I still can't intermix this with perl's Test.pm or Test::More etc
+sub ok { (+shift) ? print "ok\n" : print "not ok\n"; }
+sub nok { (+shift) ? print "not ok\n" : print "ok\n"; }
+sub msg { return unless $ENV{D}; print STDERR "#" . +shift . "\n"; }
+
+try "plan 98";
+
+try "
+ cat $ENV{HOME}/.gitolite.rc
+ perl s/GIT_CONFIG_KEYS.*/GIT_CONFIG_KEYS => '.*',/
+ put $ENV{HOME}/.gitolite.rc
+";
+
+# basic push admin repo
+confreset;confadd '
+ repo gitolite-admin
+ RW+ VREF/NAME/ = admin
+ RW+ VREF/NAME/u5/ = u5
+
+ repo aa
+ RW+ = u1
+ RW = u2
+ R = u4
+
+ config for.aa = 1
+
+ repo cc/..*
+ C = u4
+ RW+ = CREATOR u5
+ R = u6
+
+ config for.cc = 1
+
+ @oddguys = u1 u3 u5
+ @evensout = u2 u4 u6
+
+ repo cc/sub/..*
+ config sub.cc = 1
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+# valid_user() -- an internal function but still worth testing by itself first
+eval { Gitolite::Easy::valid_user(); };
+ok($@ =~ /FATAL.*GL_USER not set/);
+$ENV{GL_USER} = "u2";
+eval { Gitolite::Easy::valid_user(); };
+nok($@ =~ /FATAL.*GL_USER not set/);
+
+# is_admin
+msg('is_admin');
+$ENV{GL_USER} = "admin"; ok(is_admin());
+$ENV{GL_USER} = "u5"; ok(is_admin());
+$ENV{GL_USER} = "u2"; nok(is_admin());
+
+# is_super_admin -- not sure how useful it is right now
+msg('is_super_admin');
+$ENV{GL_USER} = "admin"; ok( is_super_admin() );
+$ENV{GL_USER} = "u5"; nok( is_super_admin() );
+$ENV{GL_USER} = "u2"; nok( is_super_admin() );
+
+# in_group
+msg('in_group');
+$ENV{GL_USER} = "u1"; ok( in_group('oddguys') ); nok( in_group('evensout') );
+$ENV{GL_USER} = "u3"; ok( in_group('oddguys') ); nok( in_group('evensout') );
+$ENV{GL_USER} = "u4"; nok( in_group('oddguys') ); ok( in_group('evensout') );
+$ENV{GL_USER} = "u2"; nok( in_group('oddguys') ); ok( in_group('evensout') );
+
+# owns
+msg('owns');
+try("glt ls-remote u4 cc/u4; /Initialized empty.*cc/u4/");
+$ENV{GL_USER} = "u3"; nok( owns("cc/u3") ); nok( owns("cc/u4") );
+$ENV{GL_USER} = "u4"; nok( owns("cc/u3") ); ok( owns("cc/u4") );
+$ENV{GL_USER} = "u5"; nok( owns("cc/u3") ); nok( owns("cc/u4") );
+
+# can_read
+msg('can_read');
+$ENV{GL_USER} = "u1"; ok(can_read("aa"));
+$ENV{GL_USER} = "u2"; ok(can_read("aa"));
+$ENV{GL_USER} = "u3"; nok(can_read("aa"));
+$ENV{GL_USER} = "u4"; ok(can_read("aa"));
+
+$ENV{GL_USER} = "u1"; nok(can_read("bb"));
+$ENV{GL_USER} = "u2"; nok(can_read("bb"));
+$ENV{GL_USER} = "u3"; nok(can_read("bb"));
+$ENV{GL_USER} = "u4"; nok(can_read("bb"));
+
+$ENV{GL_USER} = "u3"; nok(can_read("cc/u3"));
+$ENV{GL_USER} = "u4"; nok(can_read("cc/u3"));
+$ENV{GL_USER} = "u5"; nok(can_read("cc/u3"));
+$ENV{GL_USER} = "u6"; nok(can_read("cc/u3"));
+
+$ENV{GL_USER} = "u3"; nok(can_read("cc/u4"));
+$ENV{GL_USER} = "u4"; ok(can_read("cc/u4"));
+$ENV{GL_USER} = "u5"; ok(can_read("cc/u4"));
+$ENV{GL_USER} = "u6"; ok(can_read("cc/u4"));
+
+# can_write
+msg('can_write');
+$ENV{GL_USER} = "u1"; ok(can_write("aa"));
+$ENV{GL_USER} = "u2"; ok(can_write("aa"));
+$ENV{GL_USER} = "u3"; nok(can_write("aa"));
+$ENV{GL_USER} = "u4"; nok(can_write("aa"));
+
+$ENV{GL_USER} = "u1"; ok(can_write("aa", "+"));
+$ENV{GL_USER} = "u2"; nok(can_write("aa", "+"));
+$ENV{GL_USER} = "u3"; nok(can_write("aa", "+"));
+$ENV{GL_USER} = "u4"; nok(can_write("aa", "+"));
+
+$ENV{GL_USER} = "u1"; nok(can_write("bb"));
+$ENV{GL_USER} = "u2"; nok(can_write("bb"));
+$ENV{GL_USER} = "u3"; nok(can_write("bb"));
+$ENV{GL_USER} = "u4"; nok(can_write("bb"));
+
+$ENV{GL_USER} = "u3"; nok(can_write("cc/u3"));
+$ENV{GL_USER} = "u4"; nok(can_write("cc/u3"));
+$ENV{GL_USER} = "u5"; nok(can_write("cc/u3"));
+$ENV{GL_USER} = "u6"; nok(can_write("cc/u3"));
+
+$ENV{GL_USER} = "u3"; nok(can_write("cc/u4"));
+$ENV{GL_USER} = "u4"; ok(can_write("cc/u4"));
+$ENV{GL_USER} = "u5"; ok(can_write("cc/u4"));
+$ENV{GL_USER} = "u6"; nok(can_write("cc/u4"));
+
+$ENV{GL_USER} = "u3"; nok(can_write("cc/u4", "+"));
+$ENV{GL_USER} = "u4"; ok(can_write("cc/u4", "+"));
+$ENV{GL_USER} = "u5"; ok(can_write("cc/u4", "+"));
+$ENV{GL_USER} = "u6"; nok(can_write("cc/u4", "+"));
+
+# config
+try("glt ls-remote u4 cc/sub/one; /Initialized empty.*cc/sub/one/");
+try("glt ls-remote u4 cc/two; /Initialized empty.*cc/two/");
+ok(1);
+my @a;
+@a = config("aa", "fo..aa"); ok($a[0] eq 'for.aa' and $a[1] eq '1');
+@a = config("aa", "for.aa"); ok($a[0] eq 'for.aa' and $a[1] eq '1');
+@a = config("aa", "fo\\..aa"); ok(scalar(@a) == 0);
+
+@a = config("aa", "fo..cc"); ok(scalar(@a) == 0);
+@a = config("aa", "for.cc"); ok(scalar(@a) == 0);
+@a = config("aa", "fo\\..cc"); ok(scalar(@a) == 0);
+
+@a = config("bb", "fo..aa"); ok(scalar(@a) == 0);
+@a = config("bb", "for.aa"); ok(scalar(@a) == 0);
+@a = config("bb", "fo\\..aa"); ok(scalar(@a) == 0);
+
+@a = config("cc/u4", "fo..aa"); ok(scalar(@a) == 0);
+@a = config("cc/u4", "for.aa"); ok(scalar(@a) == 0);
+@a = config("cc/u4", "fo\\..aa"); ok(scalar(@a) == 0);
+
+@a = config("cc/u4", "fo..cc"); ok($a[0] eq 'for.cc' and $a[1] eq '1');
+@a = config("cc/u4", "for.cc"); ok($a[0] eq 'for.cc' and $a[1] eq '1');
+@a = config("cc/u4", "fo\\..cc"); ok(scalar(@a) == 0);
+
+@a = config("cc/two", "fo..cc"); ok($a[0] eq 'for.cc' and $a[1] eq '1');
+@a = config("cc/two", "for.cc"); ok($a[0] eq 'for.cc' and $a[1] eq '1');
+@a = config("cc/two", "fo\\..cc"); ok(scalar(@a) == 0);
+
+@a = config("cc/sub/one", "fo..cc"); ok($a[0] eq 'for.cc' and $a[1] eq '1');
+@a = config("cc/sub/one", "for.cc"); ok($a[0] eq 'for.cc' and $a[1] eq '1');
+@a = config("cc/sub/one", "fo\\..cc"); ok(scalar(@a) == 0);
+
+@a = config("cc/sub/one", "su..cc"); ok($a[0] eq 'sub.cc' and $a[1] eq '1');
+@a = config("cc/sub/one", "sub.cc"); ok($a[0] eq 'sub.cc' and $a[1] eq '1');
+@a = config("cc/sub/one", "su\\..cc"); ok(scalar(@a) == 0);
+
diff --git a/t/fedora-root-smart-http-test-setup b/t/fedora-root-smart-http-test-setup
new file mode 100755
index 0000000..869d13d
--- /dev/null
+++ b/t/fedora-root-smart-http-test-setup
@@ -0,0 +1,95 @@
+#!/bin/bash
+
+# gitolite http mode TESTING setup for Fedora
+# - Probably works for CentOS also; if someone tests it let me know
+# - Use the comments to create a version for your distro if needed
+
+# CAUTION: This script needs to be run as root, so you best eyeball it at
+# least once to make sure you know what changes it is making.
+
+# WARNING: clobbers /usr/share/httpd/gitolite-home, and also creates 7 http
+# users with trivial passwords FOR TESTING.
+
+# HOWEVER: if you remove some of that, especially the part that creates test
+# users, this *should* work as a quick "setup gitolite http mode" script.
+
+# CAUTION: This script assumes the httpd.conf file is pretty much the default
+# "as shipped" version. If you fiddled with it, this script *may* break.
+# It's on you to determine if that is the case and manually simulate the
+# actions of this script. It's not that hard, and anyway it's just once (for
+# a given server) so it's not too bad.
+
+# ----------------------------------------------------------------------
+
+cd ~apache
+# should be /usr/share/httpd; you may want to check just to be safe
+export GITOLITE_HTTP_HOME=$PWD/gitolite-home
+
+[[ -d gitolite-home ]] && {
+ [[ $GITOLITE_TEST != y ]] && {
+ echo "If you're OK with clobbering $GITOLITE_HTTP_HOME, please rerun with
+environment variable GITOLITE_TEST set to 'y'."
+ exit 1;
+ }
+}
+
+rm -rf gitolite-home
+mkdir gitolite-home
+
+# setup apache conf for gitolite
+cd /etc/httpd/conf.d
+[[ -f gitolite.conf ]] || {
+ cat > gitolite.conf <<-EOF
+ SetEnv GIT_PROJECT_ROOT $GITOLITE_HTTP_HOME/repositories
+ ScriptAlias /git/ $GITOLITE_HTTP_HOME/gitolite-source/src/gitolite-shell/
+ ScriptAlias /gitmob/ $GITOLITE_HTTP_HOME/gitolite-source/src/gitolite-shell/
+ SetEnv GITOLITE_HTTP_HOME $GITOLITE_HTTP_HOME
+ SetEnv GIT_HTTP_EXPORT_ALL
+
+ <Location /git>
+ AuthType Basic
+ AuthName "Private Git Access"
+ Require valid-user
+ AuthUserFile $GITOLITE_HTTP_HOME/gitolite-http-authuserfile
+ </Location>
+ EOF
+}
+
+# get the gitolite sources
+cd $GITOLITE_HTTP_HOME
+
+if [[ -d /tmp/gitolite.git ]]; then
+ git clone /tmp/gitolite.git gitolite-source
+ # I do this because I have to test stuff *before* it gets to github, so I
+ # can't simply clone what's on github. Instead, I use a local
+ # world-readable bare repo cloned from my dev environment.
+else
+ git clone 'https://github.com/sitaramc/gitolite' gitolite-source
+fi
+
+# make the bin directory, and add it to PATH
+cd gitolite-source
+mkdir $GITOLITE_HTTP_HOME/bin
+./install -ln $GITOLITE_HTTP_HOME/bin
+export PATH=$PATH:$GITOLITE_HTTP_HOME/bin
+
+# come back to base, then run setup. Notice that you have to point HOME to
+# the right place, even if it is just for this command
+cd $GITOLITE_HTTP_HOME
+HOME=$GITOLITE_HTTP_HOME gitolite setup -a admin
+
+# insert some essential lines at the beginning of the rc file
+echo '$ENV{PATH} .= ":$ENV{GITOLITE_HTTP_HOME}/bin";' > 1
+echo >> 1
+cat .gitolite.rc >> 1
+\mv 1 .gitolite.rc
+
+# create users "admin" and "u1" thru "u6" for testing
+htpasswd -bc $GITOLITE_HTTP_HOME/gitolite-http-authuserfile admin admin
+seq 6 | xargs -I % htpasswd -b $GITOLITE_HTTP_HOME/gitolite-http-authuserfile u% u%
+
+# fix up ownership
+chown -R apache:apache $GITOLITE_HTTP_HOME
+
+# restart httpd to make it pick up all the new stuff
+systemctl restart httpd
diff --git a/t/fork.t b/t/fork.t
new file mode 100755
index 0000000..2a7a7b7
--- /dev/null
+++ b/t/fork.t
@@ -0,0 +1,89 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+my $h = $ENV{HOME};
+
+# fork command
+# ----------------------------------------------------------------------
+
+try "plan 38";
+
+my $rb = `gitolite query-rc -n GL_REPO_BASE`;
+
+confreset;confadd '
+
+ repo foo/CREATOR/..*
+ C = u1 u2
+ RW+ = CREATOR
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd ..
+
+ # make the initial repo
+ glt ls-remote u1 file:///foo/u1/u1a;ok; gsh
+ /Initialized empty Git repository in .*/foo/u1/u1a.git/
+ # vrc doesn't have the fork command
+ glt fork u1 foo/u1/u1a foo/u1/u1a2; !ok; /FATAL: unknown git/gitolite command: \\'fork/
+";
+
+# allow fork as a valid command
+$ENV{G3T_RC} = "$ENV{HOME}/g3trc";
+put "$ENV{G3T_RC}", "\$rc{COMMANDS}{fork} = 1;\n";
+
+# enable set-default-roles feature, add options, push
+try "
+ cat $h/.gitolite.rc
+ perl s/# 'set-default-roles'/'set-default-roles'/
+ put $h/.gitolite.rc
+";
+try "cd gitolite-admin";
+confadd '
+ repo foo/CREATOR/..*
+ C = u1 u2
+ RW+ = CREATOR
+ option default.roles-1 = READERS @all
+';
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+try "cd ..";
+
+try "
+ # now the fork succeeds
+ glt fork u1 foo/u1/u1a foo/u1/u1a2; ok; /Cloning into bare repository '.*/foo/u1/u1a2.git'/
+ /foo/u1/u1a forked to foo/u1/u1a2/
+
+ # now the actual testing starts
+ # read error
+ glt fork u1 foo/u1/u1c foo/u1/u1d; !ok; /'foo/u1/u1c' does not exist or you are not allowed to read it/
+ glt fork u2 foo/u1/u1a foo/u1/u1d; !ok; /'foo/u1/u1a' does not exist or you are not allowed to read it/
+
+ # write error
+ glt fork u1 foo/u1/u1a foo/u2/u1d; !ok; /'foo/u2/u1d' already exists or you are not allowed to create it/
+
+ # no error
+ glt fork u1 foo/u1/u1a foo/u1/u1e; ok; /Cloning into bare repository '.*/foo/u1/u1e.git'/
+ /warning: You appear to have cloned an empty repository/
+ /foo/u1/u1a forked to foo/u1/u1e/
+ # both exist
+ glt fork u1 foo/u1/u1a foo/u1/u1e; !ok; /'foo/u1/u1e' already exists or you are not allowed to create it/
+";
+
+# now check the various files that should have been produced
+
+my $t;
+try "cd $rb; find . -name gl-perms"; $t = md5sum(sort (lines())); cmp $t,
+'59b3a74b4d33c7631f08e75e7b60c7ce ./foo/u1/u1a2.git/gl-perms
+59b3a74b4d33c7631f08e75e7b60c7ce ./foo/u1/u1e.git/gl-perms
+';
+
+try "cd $rb; find . -name gl-creator"; $t = md5sum(sort (lines())); cmp $t,
+'e4774cdda0793f86414e8b9140bb6db4 ./foo/u1/u1a.git/gl-creator
+346955ff2eadbf76e19373f07dd370a9 ./foo/u1/u1a2.git/gl-creator
+346955ff2eadbf76e19373f07dd370a9 ./foo/u1/u1e.git/gl-creator
+';
diff --git a/t/git-config.t b/t/git-config.t
new file mode 100755
index 0000000..fb5097e
--- /dev/null
+++ b/t/git-config.t
@@ -0,0 +1,191 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# git config settings
+# ----------------------------------------------------------------------
+
+try "plan 57";
+
+try "pwd";
+my $od = text();
+chomp($od);
+
+my $t; # temp
+
+# try an invalid config key
+confreset;confadd '
+
+ repo @all
+ config foo.bar = dft
+';
+
+try "ADMIN_PUSH set1; /FATAL/" or die text();
+try "
+ /git config \\'foo.bar\\' not allowed/
+ /check GIT_CONFIG_KEYS in the rc file/
+";
+
+# make foo.bar a valid gc key
+$ENV{G3T_RC} = "$ENV{HOME}/g3trc";
+put "$ENV{G3T_RC}", "\$rc{GIT_CONFIG_KEYS} = 'foo\.bar';\n";
+
+confreset;confadd '
+
+ repo @all
+ config foo.bar = dft
+
+ repo gitolite-admin
+ RW+ = admin
+ config foo.bar =
+
+ repo testing
+ RW+ = @all
+
+ repo foo
+ RW = u1
+ config foo.bar = f1
+
+ repo frob
+ RW = u3
+
+ repo bar
+ RW = u2
+ config foo.bar = one
+
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+my $rb = `gitolite query-rc -n GL_REPO_BASE`;
+try "
+ cd $rb; ok
+ egrep foo\\|bar *.git/config
+";
+$t = join("\n", sort (lines()));
+
+cmp $t, 'bar.git/config: bar = one
+bar.git/config: bare = true
+bar.git/config:[foo]
+foo.git/config: bar = f1
+foo.git/config: bare = true
+foo.git/config:[foo]
+frob.git/config: bar = dft
+frob.git/config: bare = true
+frob.git/config:[foo]
+gitolite-admin.git/config: bare = true
+testing.git/config: bar = dft
+testing.git/config: bare = true
+testing.git/config:[foo]';
+
+try "cd $od; ok";
+
+confadd '
+
+ repo frob
+ RW = u3
+ config foo.bar = none
+
+ repo bar
+ RW = u2
+ config foo.bar = one
+
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd $rb; ok
+ egrep foo\\|bar *.git/config
+";
+$t = join("\n", sort (lines()));
+
+cmp $t, 'bar.git/config: bar = one
+bar.git/config: bare = true
+bar.git/config:[foo]
+foo.git/config: bar = f1
+foo.git/config: bare = true
+foo.git/config:[foo]
+frob.git/config: bar = none
+frob.git/config: bare = true
+frob.git/config:[foo]
+gitolite-admin.git/config: bare = true
+testing.git/config: bar = dft
+testing.git/config: bare = true
+testing.git/config:[foo]';
+
+try "cd $od; ok";
+
+confadd '
+
+ repo bar
+ RW = u2
+ config foo.bar =
+
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd $rb; ok
+ egrep foo\\|bar *.git/config
+";
+$t = join("\n", sort (lines()));
+
+cmp $t, 'bar.git/config: bare = true
+foo.git/config: bar = f1
+foo.git/config: bare = true
+foo.git/config:[foo]
+frob.git/config: bar = none
+frob.git/config: bare = true
+frob.git/config:[foo]
+gitolite-admin.git/config: bare = true
+testing.git/config: bar = dft
+testing.git/config: bare = true
+testing.git/config:[foo]';
+
+try "cd $od; ok";
+
+confreset;confadd '
+
+ repo @gr1
+ RW = u1
+ config foo.bar = f1
+
+ repo bar/CREATOR/[one].*
+ C = u2
+ RW = u2
+ config foo.bar = one
+
+ @gr1 = foo frob
+
+';
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+try "
+ glt ls-remote u2 file:///bar/u2/one; ok; /Initialized empty/
+ glt ls-remote u2 file:///bar/u2/two; !ok; /DENIED by fallthru/
+";
+
+try "
+ cd $rb; ok
+ find . -name config | xargs egrep foo\\|bar
+";
+$t = join("\n", sort (lines()));
+
+cmp $t, './bar/u2/one.git/config: bar = one
+./bar/u2/one.git/config: bare = true
+./bar/u2/one.git/config:[foo]
+./foo.git/config: bar = f1
+./foo.git/config: bare = true
+./foo.git/config:[foo]
+./frob.git/config: bar = f1
+./frob.git/config: bare = true
+./frob.git/config:[foo]
+./gitolite-admin.git/config: bare = true
+./testing.git/config: bar = dft
+./testing.git/config: bare = true
+./testing.git/config:[foo]';
diff --git a/t/gitolite-receive-pack b/t/gitolite-receive-pack
new file mode 100755
index 0000000..a4cc5be
--- /dev/null
+++ b/t/gitolite-receive-pack
@@ -0,0 +1,12 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+print STDERR "TRACE: grp(", join( ")(", @ARGV ), ")\n";
+
+my $repo = shift;
+$repo =~ s/\.git$//;
+my $user = $ENV{G3T_USER} || 'no-such-user';
+
+$ENV{SSH_ORIGINAL_COMMAND} = "git-receive-pack '$repo'";
+exec( "$ENV{GL_BINDIR}/../src/gitolite-shell", $user );
diff --git a/t/gitolite-upload-pack b/t/gitolite-upload-pack
new file mode 100755
index 0000000..5981f17
--- /dev/null
+++ b/t/gitolite-upload-pack
@@ -0,0 +1,12 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+print STDERR "TRACE: gup(", join( ")(", @ARGV ), ")\n";
+
+my $repo = shift;
+$repo =~ s/\.git$//;
+my $user = $ENV{G3T_USER} || 'no-such-user';
+
+$ENV{SSH_ORIGINAL_COMMAND} = "git-upload-pack '$repo'";
+exec( "$ENV{GL_BINDIR}/../src/gitolite-shell", $user );
diff --git a/t/glt b/t/glt
new file mode 100755
index 0000000..1bf31e8
--- /dev/null
+++ b/t/glt
@@ -0,0 +1,43 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use FindBin;
+BEGIN { $ENV{GL_BINDIR} = $FindBin::RealBin; }
+
+my $cmd = shift or die "need command";
+my $user = shift or die "need user";
+my $rc;
+
+my %extcmds = (
+ help => 1,
+ info => 1,
+ desc => 1,
+ fork => 1,
+ perms => 1,
+ writable => 1,
+);
+
+$ENV{G3T_USER} = $user;
+if ($extcmds{$cmd}) {
+ $ENV{SSH_ORIGINAL_COMMAND} = join(" ", $cmd, @ARGV);
+ exec( "$ENV{GL_BINDIR}/../src/gitolite-shell", $user );
+} elsif ( $cmd eq 'push' ) {
+ print STDERR "TRACE: glt(", join( ")(", @ARGV ), ")\n";
+ $rc = system( "git", $cmd, "--receive-pack=$ENV{GL_BINDIR}/gitolite-receive-pack", @ARGV );
+} else {
+ print STDERR "TRACE: glt(", join( ")(", @ARGV ), ")\n";
+ $rc = system( "git", $cmd, "--upload-pack=$ENV{GL_BINDIR}/gitolite-upload-pack", @ARGV );
+}
+
+if ( $? == -1 ) {
+ die "F: failed to execute: $!\n";
+} elsif ( $? & 127 ) {
+ printf STDERR "E: child died with signal %d\n", ( $? & 127 );
+ exit 1;
+} else {
+ printf STDERR "W: child exited with value %d\n", $? >> 8 if $? >> 8;
+ exit( $? >> 8 );
+}
+
+exit 0;
diff --git a/t/hostname.t b/t/hostname.t
new file mode 100755
index 0000000..dfb8885
--- /dev/null
+++ b/t/hostname.t
@@ -0,0 +1,77 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# %HOSTNAME tests
+# ----------------------------------------------------------------------
+
+try "plan 60";
+
+try "pwd";
+my $od = text();
+chomp($od);
+
+# without setting HOSTNAME in rc
+confreset;confadd '
+
+ repo foo
+ RW dev/%HOSTNAME = u1
+';
+
+try "ADMIN_PUSH set1; /FATAL/";
+try "/bad ref 'refs/heads/dev/%HOSTNAME'/";
+
+# make a hostname entry
+$ENV{G3T_RC} = "$ENV{HOME}/g3trc";
+put "$ENV{G3T_RC}", "\$rc{HOSTNAME} = 'frodo';\n";
+
+confreset;confadd '
+
+ repo bar
+ RW %HOSTNAME_baz = u1
+';
+
+try "ADMIN_PUSH set1; /FATAL/";
+try "/bad ref 'refs/heads/%HOSTNAME_baz'/";
+
+confreset;confadd '
+
+ repo bar
+ RW %HOSTNAME/ = u1
+ RW %HOSTNAME-baz = u1
+';
+
+try "ADMIN_PUSH set1; !/FATAL/";
+try "
+ gitolite access bar u2 R any; /R any bar u2 DENIED by fallthru/
+ gitolite access bar u2 W any; /W any bar u2 DENIED by fallthru/
+ gitolite access bar u1 W any; !/DENIED/; /refs/heads/frodo/; !/baz/
+ gitolite access bar u1 R any; !/DENIED/; /refs/heads/frodo/; !/baz/
+ gitolite access bar u1 R refs/heads/frodo; /R refs/heads/frodo bar u1 DENIED by fallthru/
+ gitolite access bar u1 W refs/heads/frodo; /W refs/heads/frodo bar u1 DENIED by fallthru/
+ gitolite access bar u1 R refs/heads/frodo/1; !/DENIED/; /refs/heads/frodo/; !/baz/
+ gitolite access bar u1 W refs/heads/frodo/1; !/DENIED/; /refs/heads/frodo/; !/baz/
+ gitolite access bar u1 R refs/heads/sam; /R refs/heads/sam bar u1 DENIED by fallthru/
+ gitolite access bar u1 W refs/heads/sam; /W refs/heads/sam bar u1 DENIED by fallthru/
+ gitolite access bar u1 R refs/heads/master; /R refs/heads/master bar u1 DENIED by fallthru/
+ gitolite access bar u1 W refs/heads/master; /W refs/heads/master bar u1 DENIED by fallthru/
+
+ gitolite access bar u1 R refs/heads/frodo-baz; !/DENIED/; /refs/heads/frodo-baz/
+ gitolite access bar u1 W refs/heads/frodo-baz; !/DENIED/; /refs/heads/frodo-baz/
+";
+
+confreset;confadd '
+
+ repo foo-%HOSTNAME
+ RW = u1
+';
+
+try "ADMIN_PUSH set1; !/FATAL/";
+try "
+ gitolite list-repos; /foo-frodo/
+ gitolite list-phy-repos; /foo-frodo/
+";
diff --git a/t/include-subconf.t b/t/include-subconf.t
new file mode 100755
index 0000000..48bdaee
--- /dev/null
+++ b/t/include-subconf.t
@@ -0,0 +1,120 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# include and subconf
+# ----------------------------------------------------------------------
+
+try 'plan 58';
+
+confreset; confadd '
+ include "i1.conf"
+ @i2 = b1
+ subconf "i2.conf"
+ include "i1.conf"
+';
+confadd 'i1.conf', '
+ @g1 = a1 a2
+ repo foo
+ RW = u1
+
+ include "j1.conf"
+';
+confadd 'i2.conf', '
+ @g2 = b1 b2
+ repo bar b1 b2 i1 i2 @i1 @i2 @g2
+ RW = u2
+';
+confadd 'j1.conf', '
+ @h2 = c1 c2
+ repo baz
+ RW = u3
+';
+
+try "ADMIN_PUSH set2; !/FATAL/" or die text();
+
+try "
+ /i1.conf already included/
+ /subconf 'i2' attempting to set access for \@i1, b2, bar, i1/
+ /WARNING: expanding '\@g2'/
+
+ !/attempting to set access.*i2/
+ /Initialized.*empty.*baz.git/
+ /Initialized.*empty.*foo.git/
+ /Initialized.*empty.*b1.git/
+ /Initialized.*empty.*i2.git/
+ !/Initialized.*empty.*b2.git/
+ !/Initialized.*empty.*i1.git/
+ !/Initialized.*empty.*bar.git/
+";
+
+confreset;confadd '
+ @g2 = i1 i2 i3
+ subconf "g2.conf"
+';
+confadd 'g2.conf', '
+ @g2 = g2 h2 i2
+ repo @g2
+ RW = u1
+';
+
+try "ADMIN_PUSH set3; !/FATAL/" or die text();
+try "
+ /WARNING: expanding '\@g2'/
+ /WARNING: subconf 'g2' attempting to set access for h2/
+ /Initialized.*empty.*g2.git/
+ /Initialized.*empty.*i2.git/
+";
+
+confreset;confadd '
+ @g2 = i1 i2 i3
+ subconf "g2.conf"
+';
+confadd 'g2.conf', '
+ subconf master
+ @g2 = g2 h2 i2
+ repo @g2
+ RW = u1
+';
+
+try "
+ ADMIN_PUSH set3; ok; /FATAL: subconf \\'g2\\' attempting to run 'subconf'/
+";
+
+# ----------------------------------------------------------------------
+
+confreset; confadd '
+ include "i1.conf"
+ @i2 = b1
+ subconf i2 "eye2.conf"
+';
+confadd 'eye2.conf', '
+ repo @eye2
+ RW = u2
+';
+
+try "ADMIN_PUSH set2; !/FATAL/" or die text();
+
+try "
+ /subconf 'i2' attempting to set access for \@eye2/
+";
+
+confreset; confadd '
+ include "i1.conf"
+ @i2 = b1
+ subconf i2 "eye2.conf"
+';
+confadd 'eye2.conf', '
+ repo @i2
+ RW = u2
+';
+
+try "ADMIN_PUSH set2; !/FATAL/" or die text();
+
+try "
+ !/subconf 'i2' attempting to set access for \@eye2/
+";
diff --git a/t/info-json.t b/t/info-json.t
new file mode 100755
index 0000000..74fbdf4
--- /dev/null
+++ b/t/info-json.t
@@ -0,0 +1,183 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+use JSON;
+
+# the info command
+# ----------------------------------------------------------------------
+
+try 'plan 162';
+
+try "## info";
+
+confreset;confadd '
+ @t1 = t1
+ repo @t1
+ RW = u1
+ R = u2
+ repo t2
+ RW = u2
+ R = u1
+ repo t3
+ RW = u3
+ R = u4
+
+ repo foo/..*
+ C = u1
+ RW = CREATOR u3
+';
+
+try "ADMIN_PUSH info; !/FATAL/" or die text();
+try "
+ /Initialized.*empty.*t1.git/
+ /Initialized.*empty.*t2.git/
+ /Initialized.*empty.*t3.git/
+";
+
+my $href; # semi-global (or at least file scoped lexical!)
+
+# testing for info -json is a bit unusual. The actual tests are done within
+# this test script itself, and we send Tsh just enough for it to decide if
+# it's 'ok' or 'not ok' and print that.
+
+try "glt info u1 -json; ok";
+$href = from_json(text());
+try "## u1 test_gs";
+test_gs('u1');
+try "## u1";
+perm('foo/..*', 'r w C');
+perm('testing', 'R W c');
+perm('t1', 'R W c');
+perm('t2', 'R w c');
+perm('t3', 'r w c');
+
+try "## u2";
+try "glt info u2 -json; ok";
+$href = from_json(text());
+perm('foo/..*', 'r w c');
+perm('testing', 'R W c');
+perm('t1', 'R w c');
+perm('t2', 'R W c');
+perm('t3', 'r w c');
+
+try "## u3";
+try "glt info u3 -json; ok";
+$href = from_json(text());
+perm('foo/..*', 'R W c');
+perm('testing', 'R W c');
+perm('t1', 'r w c');
+perm('t2', 'r w c');
+perm('t3', 'R W c');
+
+try "## u4";
+try "glt info u4 -json; ok";
+$href = from_json(text());
+perm('foo/..*', 'r w c');
+perm('testing', 'R W c');
+perm('t1', 'r w c');
+perm('t2', 'r w c');
+perm('t3', 'R w c');
+
+try "## u5";
+try "glt info u5 -json; ok";
+$href = from_json(text());
+perm('foo/..*', 'r w c');
+perm('testing', 'R W c');
+perm('t1', 'r w c');
+perm('t2', 'r w c');
+perm('t3', 'r w c');
+
+try "## u6";
+try "glt info u6 -json; ok";
+$href = from_json(text());
+perm('foo/..*', 'r w c');
+perm('testing', 'R W c');
+perm('t1', 'r w c');
+perm('t2', 'r w c');
+perm('t3', 'r w c');
+
+try "## ls-remote foo/one";
+try "glt ls-remote u1 file:///foo/one; ok";
+
+try "## u1";
+try "glt info u1 -json; ok; !/creator..:/";
+$href = from_json(text());
+perm('foo/..*', 'r w C');
+perm('foo/one', 'R W c');
+test_creator('foo/one', 'u1', 'undef');
+
+try "## u2";
+try "glt info u2 -json; ok; !/creator..:/";
+$href = from_json(text());
+perm('foo/..*', 'r w c');
+perm('foo/one', 'r w c');
+test_creator('foo/one', 'u1', 'undef');
+
+try "## u3";
+try "glt info u3 -json; ok; !/creator..:/";
+$href = from_json(text());
+perm('foo/..*', 'R W c');
+perm('foo/one', 'R W c');
+test_creator('foo/one', 'u1', 'undef');
+
+try("## with -lc now");
+
+try "## u1";
+try "glt info u1 -lc -json; ok";
+$href = from_json(text());
+perm('foo/..*', 'r w C');
+perm('foo/one', 'R W c');
+test_creator('foo/one', 'u1', 1);
+
+try "## u2";
+try "glt info u2 -lc -json; ok";
+$href = from_json(text());
+perm('foo/..*', 'r w c');
+perm('foo/one', 'r w c');
+test_creator('foo/one', 'u1', 'undef');
+
+try "## u3";
+try "glt info u3 -lc -json; ok";
+$href = from_json(text());
+perm('foo/..*', 'R W c');
+perm('foo/one', 'R W c');
+test_creator('foo/one', 'u1', 1);
+
+# ----------------------------------------------------------------------
+
+# test perms given repo and expected perms. (lowercase r/w/c means NOT
+# expected, uppercase means expected)
+sub perm {
+ my ($repo, $aa) = @_;
+ for my $aa1 (split ' ', $aa) {
+ my $exp = 1;
+ if ($aa1 =~ /[a-z]/) {
+ $exp = 'undef'; # we can't use 0, though I'd like to
+ $aa1 = uc($aa1);
+ }
+ my $perm = $href->{repos}{$repo}{perms}{$aa1} || 'undef';
+ try 'perl $_ = "' . $perm . '"; /' . $exp . '/';
+ }
+}
+
+# test versions in greeting string
+sub test_gs {
+ my $glu = shift;
+ my $res = ( $href->{GL_USER} eq $glu ? 1 : 'undef' );
+ try 'perl $_ = "' . $res . '"; /1/';
+ $res = ( $href->{gitolite_version} =~ /^v3.[5-9]/ ? 1 : 'undef' );
+ try 'perl $_ = "' . $res . '"; /1/';
+ $res = ( $href->{git_version} =~ /^1.[6-9]|^2./ ? 1 : 'undef' );
+ try 'perl $_ = "' . $res . '"; /1/';
+}
+
+# test creator
+sub test_creator {
+ my ($r, $c, $exp) = @_;
+ my $res = ( ($href->{repos}{$r}{creator} || '') eq $c ? 1 : 'undef' );
+ try 'perl $_ = "' . $res . '"; /' . $exp . '/';
+}
diff --git a/t/info.t b/t/info.t
new file mode 100755
index 0000000..22b5b94
--- /dev/null
+++ b/t/info.t
@@ -0,0 +1,105 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# the info command
+# ----------------------------------------------------------------------
+
+try 'plan 78';
+
+try "## info";
+
+confreset;confadd '
+ @t1 = t1
+ repo @t1
+ RW = u1
+ R = u2
+ repo t2
+ RW = u2
+ R = u1
+ repo t3
+ RW = u3
+ R = u4
+
+ repo foo/..*
+ C = u1
+ RW = CREATOR u3
+';
+
+try "ADMIN_PUSH info; !/FATAL/" or die text();
+try "
+ /Initialized.*empty.*t1.git/
+ /Initialized.*empty.*t2.git/
+ /Initialized.*empty.*t3.git/
+";
+
+# GS == greeting string
+try "DEF GS = /hello %1, this is $ENV{USER}\\@.* running gitolite/";
+
+try "
+ glt info u1; ok; GS u1
+ /C\tfoo/\\.\\.\\*/
+ /R W *\tt1/
+ /R *\tt2/
+ /R W *\ttesting/
+ !/R W *\tt3/
+ glt info u2; ok; GS u2
+ !/C\tfoo/
+ /R *\tt1/
+ /R W *\tt2/
+ /R W *\ttesting/
+ !/R W *\tt3/
+ glt info u3; ok; GS u3
+ /R W *\tt3/
+ /R W *\ttesting/
+ !/R *\tt1/
+ !/R W *\tt2/
+ glt info u4; ok; GS u4
+ /R *\tt3/
+ /R W *\ttesting/
+ !/R *\tt1/
+ !/R W *\tt2/
+ glt info u5; ok; GS u5
+ /R W *\ttesting/
+ !/R *\tt1/
+ !/R W *\tt2/
+ !/R W *\tt3/
+ glt info u6; ok; GS u6
+ /R W *\ttesting/
+ !/R *\tt1/
+ !/R W *\tt2/
+ !/R W *\tt3/
+";
+
+try "
+ glt ls-remote u1 file:///foo/one; ok
+ glt info u1; ok; GS u1
+ /C\tfoo/\\.\\.\\*/
+ /R W *\tfoo/one/
+ !/R W *\tfoo/one\tu1/
+ glt info u2; ok; GS u2
+ !/C\tfoo/
+ !/R W *\tfoo/one/
+ glt info u3; ok; GS u3
+ !/C\tfoo/
+ /R W *\tfoo/one/
+ !/R W *\tfoo/one\tu1/
+";
+
+try "
+ glt ls-remote u1 file:///foo/one; ok
+ glt info u1 -lc; ok; GS u1
+ /C\tfoo/\\.\\.\\*/
+ !/C\tfoo.*u1/
+ /R W *\tfoo/one\tu1/
+ glt info u2 -lc; ok; GS u2
+ !/C\tfoo/
+ !/R W *\tfoo/one/
+ glt info u3 -lc; ok; GS u3
+ !/C\tfoo/
+ /R W *\tfoo/one\tu1/
+";
diff --git a/t/invalid-refnames-filenames.t b/t/invalid-refnames-filenames.t
new file mode 100755
index 0000000..19267fe
--- /dev/null
+++ b/t/invalid-refnames-filenames.t
@@ -0,0 +1,100 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# invalid refnames
+# ----------------------------------------------------------------------
+
+try "plan 56";
+try "DEF POK = !/DENIED/; !/failed to push/";
+
+confreset; confadd '
+ repo aa
+ RW+ = @all
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+
+cd ..
+rm -rf aa
+glt clone u1 file:///aa
+cd aa
+tc v-869
+
+glt push u1 origin HEAD
+ /To file:///aa/
+ POK; /\\* \\[new branch\\] HEAD -> master/
+
+# push file aa,bb ok
+tc aa,bb
+glt push u1 origin HEAD
+ /To file:///aa/
+ POK; /HEAD -> master/
+
+# push file aa=bb ok
+tc aa=bb
+glt push u1 origin HEAD
+ /To file:///aa/
+ POK; /HEAD -> master/
+
+# push to branch dd,ee ok
+glt push u1 origin master:dd,ee
+ /To file:///aa/
+ POK; /\\* \\[new branch\\] master -> dd,ee/
+
+# push to branch dd=ee fail
+glt push u1 origin master:dd=ee
+ /invalid characters in ref or filename: \\'refs/heads/dd=ee/
+ reject
+";
+
+confreset; confadd '
+ repo aa
+ RW+ = @all
+ RW+ NAME/ = @all
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+
+cd ..
+rm -rf aa
+glt clone u1 file:///aa
+cd aa
+tc file-1
+
+glt push u1 origin HEAD
+ /To file:///aa/
+ POK; /\\* \\[new branch\\] HEAD -> master/
+
+# push file aa,bb ok
+tc aa,bb
+glt push u1 origin HEAD
+ /To file:///aa/
+ POK; /HEAD -> master/
+
+# push file aa=bb fail
+tc aa=bb
+glt push u1 origin HEAD
+ /To file:///aa/
+ POK; /HEAD -> master/
+
+# push to branch dd,ee ok
+git reset --hard HEAD^
+tc some-file
+glt push u1 origin master:dd,ee
+ /To file:///aa/
+ POK; /\\* \\[new branch\\] master -> dd,ee/
+
+# push to branch dd=ee fail
+glt push u1 origin master:dd=ee
+ /invalid characters in ref or filename: \\'refs/heads/dd=ee/
+ reject
+";
diff --git a/t/keys/admin b/t/keys/admin
new file mode 100644
index 0000000..676a711
--- /dev/null
+++ b/t/keys/admin
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA0/X7uwd7xOvC3UTZaAnFOR5xqhdgcyc8vk3d1bXXthiuUSmq
+5t4uhS9qj0ismcPX0YRNhSDElotG1KSWp8DceJpR2c7GYmELpqoE7ebVPnEBKY0c
+PX+G9KZNgbJyyx35QlpJmlO+LimM0oO8XarRphn3kc0jCTLKaI5ndEjGDHH4fsOe
+wlryW7a0RR/brMNiJYYzy5ADCOkAnyXQmbrQW4nROCPqFojBFTEBLpe8EuODlJXE
+mRHpa1k+k/grFgp/c1xbOrjox127LZT4vkLf5+lSwkhq8oyzWHUrQ+rUJZssi1LC
+GEZSudhX5reqksmIlTwX0GPIIwSwZFNVxhj51wIDAQABAoIBAFFLlzE0vZPZmPOk
+5H2ywaIWuyGxtZx1ACc9VkgRZprA/JrEkHfb35vVg9lQ1mJjavNA+zqERuI2qQQF
+3IKaxfS7u4j+dbhl4EIcE6frUP6R+RAmvx4XO3u6DSAhgUXGSUPZvUEjvV2XMhvL
+ywNh8Ob0LrANLdLpWBiiBavj/ZHnsVZj7y+39GKEtLm8BbsH8L7GanPl4uQDNruR
+Ef2L2pSEqml+Iva6yk5a22U293JCVZ/aqDdGvrnbjkaxqin5jBgL47D91A3xo4SW
+zFZkJR5SDYnFLdwnwTPvxSA9IXj6TZ2ZdnTNvI5Utpd0Wy5N4GlfXS3TBPBD4kP+
++pS1vgECgYEA/D7k1IyOM2hGamHc0qG+sIxMHge6rJE8pYtW2suB2KX0bJvAeBY8
+eHM+pPFMbjjZHlU7qB9qmGbZ++ZVlEv/SqqHizvwLzth0P4MJ6/UjAcmAdEUclV0
+YSzMRFk/g0J7aMlJb5NXgA5xlArHLsmV3Zc+QwFPECgzz5LHwD5nTMcCgYEA1x2Y
+qb7FXwcQPDcs4Qwux12tCUaExQMZbNoLHWrXw7ktHRfR9sI8TcEErLzc2wnZbdyZ
+av/GYSTR9UCeq12kUhmTL4SNryktHKNKEfDEvR7wK6sa9iffzZH8FDe5JktLXnu7
+Oe5SNfLDHA5X7qQefz8s2658+xVJmdp+uasNOnECgYEAlzBXVbJ9VQCuG/tWMQVz
+VzxwLxuw3tgagprWz0NlK2ak7ygXn6KsUgG5TYG3ruTx9gVeQXG7IWecRiiTqNQ4
+SxeVMHYXiyfLhEmRHYR9IAT02efomnLv04LXWCwqLlF9yJvFIVQuAPonR3WCV1/K
+LMwHLIAvVF7UVxkCEw8UOWcCgYEAwiUH/0sZvuYVFQOHEaV5Ip286b4nXdeqPr+b
+gHVJPnAF81foO5iZ7GLj4TKi8V02SxzpqdQmKs6cX4huq6LcBuzmFeDALvIusMX+
+t6phJX6irAbFUpwyNMoog+a2x4T1BNUO6P3aXK44wT2AxvSAQb+2sJ4OVl2kC6NS
+9CcYzUECgYEAzqbWlJIbjJx5620Q95bu+lpessOUtoVcYryO6dQNZOiis8aDZjJk
+RKdB9qhJlcUeFqnvJS5L5gohaJoyx0wMSYVB7MrO6kAIMHo/OriEQ6CTsQOTqa8n
+q4cV9Wuk9pRb1b/x5eXCcHe8P7wBt8KKIh/VBB4aDyeIwGbO0NODYr4=
+-----END RSA PRIVATE KEY-----
diff --git a/t/keys/admin.pub b/t/keys/admin.pub
new file mode 100644
index 0000000..b50a5b9
--- /dev/null
+++ b/t/keys/admin.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDT9fu7B3vE68LdRNloCcU5HnGqF2BzJzy+Td3Vtde2GK5RKarm3i6FL2qPSKyZw9fRhE2FIMSWi0bUpJanwNx4mlHZzsZiYQumqgTt5tU+cQEpjRw9f4b0pk2BsnLLHflCWkmaU74uKYzSg7xdqtGmGfeRzSMJMspojmd0SMYMcfh+w57CWvJbtrRFH9usw2IlhjPLkAMI6QCfJdCZutBbidE4I+oWiMEVMQEul7wS44OUlcSZEelrWT6T+CsWCn9zXFs6uOjHXbstlPi+Qt/n6VLCSGryjLNYdStD6tQlmyyLUsIYRlK52Ffmt6qSyYiVPBfQY8gjBLBkU1XGGPnX g3@sita-lt.atc.tcs.com
diff --git a/t/keys/config b/t/keys/config
new file mode 100644
index 0000000..fc75580
--- /dev/null
+++ b/t/keys/config
@@ -0,0 +1,20 @@
+host *
+ stricthostkeychecking no
+host admin
+ identityfile ~/.ssh/admin
+
+host u? admin
+ user %USER
+ hostname localhost
+host u1
+ identityfile ~/.ssh/u1
+host u2
+ identityfile ~/.ssh/u2
+host u3
+ identityfile ~/.ssh/u3
+host u4
+ identityfile ~/.ssh/u4
+host u5
+ identityfile ~/.ssh/u5
+host u6
+ identityfile ~/.ssh/u6
diff --git a/t/keys/u1 b/t/keys/u1
new file mode 100644
index 0000000..828d1c3
--- /dev/null
+++ b/t/keys/u1
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAuB8HmJ5UX30xFktmvlLgSrzsGIzSiYiAYH8eU6epJGr/xjYD
+9GE6G9EcL+/NTc0ziPhIUtVm+h+kFEHtQ1VeOFEuVuWAukPNXRFDrYOMfeR4U1h7
+olT58U3+IlctreKavXr+7bPPhbhvdB7FPM8xusaPfwT6BG4sTqHyOPL5a3eXW7Sp
+tuVD87KNHKo3skWcbb78EVQt7MfCMTam6X2O4Y0y1da51kOht6qRaeJHoaAjXB4V
+pY9lJSQA+Km5uYu+FIwn89z7P4j2aUNZFq2UOSwuViekSR6lfHvortyh8YFrSRUs
+Q4berZUICDp0Ek2BiJirWiPXAp9uxwXms9UbvQIDAQABAoIBAAUTnfMAUpU7b3IM
+7C1NPa/x25SltVxjbh67AowN8GT3qku9y4geciq4Lk3ID+IYSVZ6egwGpEs7Ohvw
+4Wjc3rcwzdVJiK4aFnx9cF9FZEdIWGT76JTGQQn9O4eY3cKQn/GfhY3qSkuGlVQf
+URLnJ5jdxrEa4wXiP8h/QJ1/XY8v9en0uWf/18fMHyzImpd8dqgtTdomSQLjM2ie
+1k9Fllh/5Q6e1Kd5mn5QP1QPC65Z78IskOot9Wrg0sk19sWiFMsopgJkQg66BlnM
+Fj8A4HPS8Yylt0oXJ5yytUYfvZNDRANZRiAz9mfgQw9oapJLnCLt3Awi5bcKJIVk
+/nTccEECgYEA8lSPFeTyzid0sIextXjjHvV4riSZKAHAa5M5DfsPkXLQfbEF4zog
+FLZ6c85jUCk7/vTzDRXr38XKaGbAg/1M3UJk7lS9shh1Zdo0+Hshq7rNICNLKz5H
+/u1/0gkxqqJcpXaoOHYN1hM7HJrP3uZFDIA5ZrB7JD20YDCQ/2PWrS0CgYEAwoHi
+qEKKXOol7FIiG1upTjPaWMTmxtmSs2/pobMVf+ZGDMPts0pbqDegXruaNFUrUQOG
+9yvrjA3a2QaKt7R5eDvHAafeMeu0WcHaqUWtIueTtEuduK4fXsCGONBPEGJu+Ct/
+BOCV/W8MAmzIOvffT7Jg1HWsZyAstoikPi1w4tECgYEAuvVmFxw1/7sNGgz2m+2S
+PJZh7uipiOYhEF3bTN//mNWd6PskcbSsf45xVttKX9QQR5mv0s6w1koA6R8tNCe+
+n43T1NRoLfkUyenZqENHLPjHvR29prU8Un/ld6REP0NYewfarQTXk+vuVRlTesLp
+TsW2g3Vw6/r3KKcPlxntzFkCgYEAqqkH1BY+DHQtPgJ6hoKQNFtuswBgdAymmOYS
+mZvlu0iyIbUvNGaDsT7NaRE1pcEstnJf0zMoAsSNRmpk//ZLteDNJXjCjg5/OVnL
+n0XROZTylfjatBWi1KIbonGzTW7warLPSdo8ABeU8/O6Y3Lk7qpWJ1PwJrOmR6nw
+YdXA/GECgYEAsSokOABGGLQ2zOmDYPDKrRenLho8cSe4g/CF5b6x5MXfJvvBfI/M
+hOS/2AFj7UkOSVM9B9QH607F67YJuBPNGvciCatqY/Bcs6ViuuyqG9U4O13OrrmA
+oG8Cho3Zy2v3udaLJ6O5JYcpX+hdvkwoG5j/8hLrzkD/3AA12z6QnV0=
+-----END RSA PRIVATE KEY-----
diff --git a/t/keys/u1.pub b/t/keys/u1.pub
new file mode 100644
index 0000000..264c1f0
--- /dev/null
+++ b/t/keys/u1.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4HweYnlRffTEWS2a+UuBKvOwYjNKJiIBgfx5Tp6kkav/GNgP0YTob0Rwv781NzTOI+EhS1Wb6H6QUQe1DVV44US5W5YC6Q81dEUOtg4x95HhTWHuiVPnxTf4iVy2t4pq9ev7ts8+FuG90HsU8zzG6xo9/BPoEbixOofI48vlrd5dbtKm25UPzso0cqjeyRZxtvvwRVC3sx8IxNqbpfY7hjTLV1rnWQ6G3qpFp4kehoCNcHhWlj2UlJAD4qbm5i74UjCfz3Ps/iPZpQ1kWrZQ5LC5WJ6RJHqV8e+iu3KHxgWtJFSxDht6tlQgIOnQSTYGImKtaI9cCn27HBeaz1Ru9 g3@sita-lt.atc.tcs.com
diff --git a/t/keys/u2 b/t/keys/u2
new file mode 100644
index 0000000..02486a6
--- /dev/null
+++ b/t/keys/u2
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpgIBAAKCAQEA4/t3WV4q98MlYV9Jbvbf7gE2Dzit6dD8xHsWBh2wTmggQM9I
+2RsPp1tTIoOpt4YdlTzV41577BKLVMvqG7AxnVljC7+m7PrT2YmxEGrrbcrHebHu
++pwh0lkN/CIz6MHjWzLbFiRoHhKh9LyfSiKxCgJ3dyjFgAnA+wrXkwIfPRG8qOLk
+sb1KUsMdfYm0OUEC/mHU2dsIS5HIY6xhy5Q5BXWgkfYUK7LL7Eyw6ffLhvq4U9tv
+Qv+u64Qowew4BY4B8rvyb73iEGcInIF2JRY0jVC8yJFejTJo2KKlXxu8MyAtAhth
+cHE6gA7xmn1d8TF7sxHDNqprvD9mLTjUVdz1owIDAQABAoIBAQDEGaWLZYioHV+l
+5gSQQiJT4w7RAPv3RyBlEUrcb+UbTE2R8brDpJdOaSuVYJM3nVEM8Ys5TChj43+d
+rNjugBvtMNoVXQEEjqxzThDUAmQHyIjUkMzzHCGrgZaZ7gGgkEY0SAZTgXVdiMFu
+dmC9sCGAbqa8BIH9pGYuiiDr/sNIDr3ekyqyuSA0Aa2JIrEx1TKFXF5JtGseU9S6
+bUfesCpoWGyVdulr3NbsLM26eDCsZo45OF5QdpTtU99xc9K4gsOre0ZtqEJMVGlR
+2nDQILCroH93GabSIW+fiUZD2lO9BxXAM0NA730ODQWyM8IgoWrqxGUuFz40l7X1
+teB7yRwBAoGBAPkkF//ZaEUUyCX6+a6TjaPQ1atemQCxHtIm1phfIM/u2uANqeOP
+Z+N3dM0TL53lRx/xbT0dO4sfF73XcF0sYkQj9rep28Q04W8Z6iC/I+jQAJJ0bYl5
+skUGTECxIPLmKOciQF7N5PUGvxhI4oq/0vgDYFc+NW18mow1Z66H1PkjAoGBAOpC
+P9EJXEFKZY1lK6ZL6wvJ6tJaPWAuiQlkOdj9wLvvzywQPlEdBJ1k0q+ndAqpR6WJ
+eTlkP/bzDpLRi+l4PEVTsFlpxjcfVn19RRabsKTNIS0usl1el/uQP7u4rRimBCz5
+MO72GD4ARM5CgMZZMGU5AXmxKEM9feTcUNss3RmBAoGBALTYsmMRmVKr5y1KpPtI
+OER1TuR6Ym3SJCE/9/3a76KAK3j/8hYw/qRrDenex23CBIL3aOg31AUEqOMxA2te
+0GXOBUUEk3Y1PH69POpQVOymMAQfZ3OnVvQrwiYjbVtkHsTIZBltM4l5QDWMkoVN
+AQLu0HwDuBylmjm0enKCPuIpAoGBAObd257bprwB4gtzhY0ijMbVfENLA+nictN6
+nzgm/Oc68+XtLD0sZ/vl/W13jnljU2TlEz9oeVGbQOWY9lZlVKDOVaIJCHwSul56
+MriRP4lrUCMDPm2eaBJYmzcaTh1YoAzimUMn7cRM54KPL/JKu9NGVxnjala6J3SB
+XH5kvJIBAoGBAITZe45tIbVkGjK3jTjgBfaqFfhyws4L3QgRbjqpy2h9nLEGHMYV
+RkIt6bCkw6cy+8TnYB0iG9lcGP1S5E4C6zpTdldi/5tGl7uu9qfEz4hxRpRbJ4Q0
+nwZzUkgBaLxAiLHfntBpwk16UvHkCpo5hiKzSLzxfReXzQPUUudGRnsG
+-----END RSA PRIVATE KEY-----
diff --git a/t/keys/u2.pub b/t/keys/u2.pub
new file mode 100644
index 0000000..916dcf5
--- /dev/null
+++ b/t/keys/u2.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDj+3dZXir3wyVhX0lu9t/uATYPOK3p0PzEexYGHbBOaCBAz0jZGw+nW1Mig6m3hh2VPNXjXnvsEotUy+obsDGdWWMLv6bs+tPZibEQauttysd5se76nCHSWQ38IjPoweNbMtsWJGgeEqH0vJ9KIrEKAnd3KMWACcD7CteTAh89Ebyo4uSxvUpSwx19ibQ5QQL+YdTZ2whLkchjrGHLlDkFdaCR9hQrssvsTLDp98uG+rhT229C/67rhCjB7DgFjgHyu/JvveIQZwicgXYlFjSNULzIkV6NMmjYoqVfG7wzIC0CG2FwcTqADvGafV3xMXuzEcM2qmu8P2YtONRV3PWj g3@sita-lt.atc.tcs.com
diff --git a/t/keys/u3 b/t/keys/u3
new file mode 100644
index 0000000..163bdef
--- /dev/null
+++ b/t/keys/u3
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEArK6uQkgoAnS+w6vs4DmvdOfcGdFUzh3ivHAXHug7sjjKGUB2
+KwcXNwcmJPtyNXM0BJ9ZoOJSqJo5cWLMl7Qn1HBubuyM02Id9EqeJu1Ytn/i1XFY
+NMp8fj4wma3LZTdeOy3ockbefkw6VQUq3cheXIeaVpT91jsuyIaE0ejWkjGMu596
+zXFQZYTSO28TIYghRvhXGq5W9hJ3V2k+ZAQ/AsWQqjK4XruICi+RGRpKj59ECI7V
+zIrq4MUEti+3LldaY6tyYsTGQLtFJYu+l/ZavkrM979cPOxLeoi8ZHPTPKh6asdr
+ZXA2J6++IyIgNy2Q4sNxTmkIlU/EDysvTTGdMwIDAQABAoIBABF+NJr0UlFFYFnU
+Hc/tKBAQuORIp22l62Upeb4gyoNYa2i5df8P3dMuPzf53Oz7OabKObspkjQQQ4dv
++cfYcTx9E0LbZby4MM6hjHnnC1iZhfIXZFccuBXV2PiIeZVMUZhvIyAIe9uRf0tD
+lb8X4C9BcWoZ98ju/+NCdUwKaUov3jXc32hmsAhK+dtqBE+lwccX9keJQcncf+6j
++2znpDAJdaF70dM0DidbkmXmOTgH6LjjqrYRTAUnVsvUyDsr5YhopwOHs0E95YB4
+RzO7V/DA8H7DQ+XE20Iqp2i6dSbRQQPxbQLvJG4BaITIqb1L8/WJ6N75O/uns4rE
+Y8WVwWECgYEA1gDDP70DqvVMu6KDvn18pvfxzxJ3jccYcBZITV/E6xgj1AbH7fdA
+0iwvG6jQ26DTUBfLBquZ2fCCyI44OUpZ8GZu/wYBn5CQjYtr1vJ7zHKM3dVf+POT
+cGgOVDIHopCCX5Dwb74OpbkTV4g/WxouClvz0Ovobq8NncSkJJkyGEMCgYEAzpIF
+oj2AVMWDd6NL+P8e+QXPYEvJ/trAHoAteI83Eof4ZOC/sBr7Gf3c6nM7ZJbCXGhJ
+NCCr4teHJMg9DThQ1KxKn9qEf9dWiAE+HCouaZU98Z0UcLQ+tgPeBh/Dw7rTF3M2
+7A/LtLYbg1MoPCZNj6V4qGjmNWCy2lku1ZGBUFECgYBD/4oKvqxjrf3rwP/Lj2QE
+SdRzz5JdYl3Jf8sJityvNsRropv0aRQXtCJjz4hNwRRj5quEOxJvxZRI1afXzGA3
+mtS6A9aQNQc5couZiQL9O4i3FA2itQKsPOQQrLTwWqqSYyOC3gkZb21N6uT2taLb
+d8xJHiyEvuq8rrbZSjQ4sQKBgEkUDZws58aVrYHYqlrnXny4mnm1tjtMBiWEMRHy
+kIgkxDJj9EyH7wdt8QacR4m5b/8jAarIWCbDGtNfZ4HSx33FigztUGytsLYiwmdS
+YOMHYkeky4NnsLvRuG0wNaB76ovkPazblbRTrH4UICrPXicQYhQqMC74C64FWPVD
+KZ1RAoGAE/vKCHCzPegTT9gr2aaIrhLPUUZboOF4gHajYr9Scr176nJhpIvP/0Y2
+yQtqfas5lID8ouqXb+oL0Q4Yi00hdu+TRYQHm3M+2UL7wgffR5H2vfhk24UUDfV5
+0qQjNKp3pNUWZdiZ2J+RGUszXt0THfWbWI/ntwnG5QchqXCqYEA=
+-----END RSA PRIVATE KEY-----
diff --git a/t/keys/u3.pub b/t/keys/u3.pub
new file mode 100644
index 0000000..e97645c
--- /dev/null
+++ b/t/keys/u3.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsrq5CSCgCdL7Dq+zgOa9059wZ0VTOHeK8cBce6DuyOMoZQHYrBxc3ByYk+3I1czQEn1mg4lKomjlxYsyXtCfUcG5u7IzTYh30Sp4m7Vi2f+LVcVg0ynx+PjCZrctlN147LehyRt5+TDpVBSrdyF5ch5pWlP3WOy7IhoTR6NaSMYy7n3rNcVBlhNI7bxMhiCFG+Fcarlb2EndXaT5kBD8CxZCqMrheu4gKL5EZGkqPn0QIjtXMiurgxQS2L7cuV1pjq3JixMZAu0Uli76X9lq+Ssz3v1w87Et6iLxkc9M8qHpqx2tlcDYnr74jIiA3LZDiw3FOaQiVT8QPKy9NMZ0z g3@sita-lt.atc.tcs.com
diff --git a/t/keys/u4 b/t/keys/u4
new file mode 100644
index 0000000..a669e34
--- /dev/null
+++ b/t/keys/u4
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA2Wg3bl7T0C8VuR8HdbAqmwvQH4/T/maaqlQeJqcATRgWQNDv
+VthEasW5Kx8DzcSVRWS0cJ5EpTLGvrs84aXgdvg6TwFZKO/ujrkFDZmw9hd4I/Dv
+0dp/Y7himS8vAvnnWfYyqJBiX4plEx9Kg8oWHwy8KK3HHKoO8jAWAHOWO3GMB8BV
+128VLovGB6Sp99GrgltzP9UhNRHt/doa3ve8+fDk782SandVTTR9MNLt7qhMSOKr
+bmoFWtL36W5hmVjFGTZC1ZNIU6PiqygUqPtyAYblwv/nZ35s72KmxP7Seag3SsuZ
+UPnRjM4PZ+7YE7tcPTOUxcct8xuTZXgjh2WiNwIDAQABAoIBADSrR8qIVJ452fRo
+LQF49UlsmjYbPQuDxfJ/wHIywSLsM+/t7h3G9QQ89Hga4mwGNPeDxycFYLH41Cc+
+6yfrbK7FwjKDrBr7zXpsHmpGEpX755Ile6QGYBhDgjeEM8pvynmD6I/nsr1cpNH2
+IbI90hAhoK/mMbejB03rEll3pyytCgEvJQsu847dTNTZN+PHsumPr9HERi/dDrbw
+JrDSH2tbOvYw4UW9HLFfZAB+IgMo34WXf6kcUY69wWQbxnv1e7KHnYkxIuLCGZ6x
+Lc9APM97f4atlGt+mAtqK97CJB1AqoIE3KpPq1x+gpFSBNEE+9U9KnItuSqF3mG4
+vq/VjIkCgYEA+ASubOgzJaAldNz3dinLYK6MLB+V1R+RpkDc3+OgfF5mgg11ZtpE
+/DO1Ndpf5dOYB4JYGfhC9+xomHGsQmbOsIUprWaAR/xaO6Bgxw7cxkOBIadsrdwu
+MWJ6h3gN72zuhKPERpUVVH7ZH5KQfZwxEMgquUkGdEwumU4rMXeCFqsCgYEA4GdX
+qI/5UNXEPnXk3dDWNzNJO7gRoonU228IM18qz5ZKMRIKp6Sq+wIuO8+aQMr2K0rO
+RMQk+lLOZ17omaWh9ysDPzTCWRrjOiDOnYWDPCclTeqSnEk8uuWyppDGNXgQ6osM
+LnwJSeP8+1EDgkKf5zoiQfcyBc0v8PSRSdTfEqUCgYEA2PqHeqHd9UHY4xdZq1e/
+JLMv0H5Ff/GhY7iFQ54JziRsO8T4e+Xiyl2WYCnPEer+qzseRoIKXInHq+5uzJzS
+oF2va5MsEU41xsp1QFDBVvbBpyapDqV9CBlmptOiJV/Af+wiD7nnskdTPqrjm/Ck
+gFEOB5Fagy4O6nIXmaw69AcCgYEAhVSBndKlZKUOa7oqmKzLipK7UXNFbxiL0zE+
+Yx+JVTvLqyo4EHFjca5TABCSayrsZr6UngEYo27t2jdm5lumRzBURoq3aq/yEIiL
+msZIOkZcANZ988QEBFwT8KmWSxCipGinfTsPXcrLdhslhZDGZ2GAF0ejfhTzBiyZ
+4o9LV00CgYBf46Z2M1fWI6Tc8HUKr9WoyUmTmUtHUFPt9IZ2CKm2czoltpku7cC7
+ztlt4LmVPKv1UUavbC/nCz6s1ylOkY0rdm45FSYXKDYPMQqzQix1jG4GZJjM9En+
+M848LupnHBFsmHyQqyyRlsg1gb+wtvAw7Y44wcfAhGbiAT5DVX+zxg==
+-----END RSA PRIVATE KEY-----
diff --git a/t/keys/u4.pub b/t/keys/u4.pub
new file mode 100644
index 0000000..06f3648
--- /dev/null
+++ b/t/keys/u4.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZaDduXtPQLxW5Hwd1sCqbC9Afj9P+ZpqqVB4mpwBNGBZA0O9W2ERqxbkrHwPNxJVFZLRwnkSlMsa+uzzhpeB2+DpPAVko7+6OuQUNmbD2F3gj8O/R2n9juGKZLy8C+edZ9jKokGJfimUTH0qDyhYfDLworcccqg7yMBYAc5Y7cYwHwFXXbxUui8YHpKn30auCW3M/1SE1Ee392hre97z58OTvzZJqd1VNNH0w0u3uqExI4qtuagVa0vfpbmGZWMUZNkLVk0hTo+KrKBSo+3IBhuXC/+dnfmzvYqbE/tJ5qDdKy5lQ+dGMzg9n7tgTu1w9M5TFxy3zG5NleCOHZaI3 g3@sita-lt.atc.tcs.com
diff --git a/t/keys/u5 b/t/keys/u5
new file mode 100644
index 0000000..ed65131
--- /dev/null
+++ b/t/keys/u5
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA41A0bY6+0akNSJlR2PeRATNtncARXVOUar7CNaxwPqVXQR1+
+TmU9evmIEkRLf0kFAa7L3QDFroFu4sDiSJjvfYIkHdoxO4Fk128PJBhObIXaarKc
+UBIZ29/8I3dTedq5CY/YHL/AjaT+0VktWX7YwigvGDamrXAKqjnW1mWKp6TNj8bp
+vppBvj3yvdYoOLYvCs/SKfKdlayngiihTbKELPcRu8NzYxHiF2b/u0t31evdP0fA
+/apAslKhHdCm8ScZJuyIdztcogBFE5dGm0h3qlbJKvKT8JmTcgQ5TBBcmqPVmUkm
+nPvIzd2G7D1smbExG7LnqekHcS9dvaPnF0B3oQIDAQABAoIBAGl1e21crXDN0mjd
+INjdOnvpJTDru+KldRT0/VszbjvSL6H5EfFDDPvxqsx2vOQHt3fpZZFZ21yzlgND
+Y3g0499BsonbAb5OsL82OjsPv8qfaw7XYKfRTgfxaaP2p1bAP9qMzsG/wJC2fLYZ
+fm2n6N5jED5WlIugkIIbJW4AXAycDB2ZU0xtPTHJ8nrSj1otDCsD2VbbVHXN7H6g
+HrTqP4RqD+uVDIxSrXllz4Udwe/I1wUrvY9HjH2qS6Liqb5kw8SOf4a2et18+KB2
+NNk6ZEAlmOx1ddC2eZPxm8XxAxdSwTggcwvtixCeoXR53OwTZAZ8BBzwusrMklZm
+/n5zPWUCgYEA88Lf4i0WFu9h4FnQ6qxCAKDS1XyRpCYt5cyTpZPMV3LuUV0DPS3K
++EZeU689BSOiwav8omCtrrrOHtE9fHjOH85Gd4IRyOwUQWI7RLpFiI8Lbs4tMbP5
+k1UOzzTji++N47XfTTQVdwHbx4jac80LVavbRd3AKd6oRjc/gQOpgasCgYEA7rnr
+uoWqT76xVNLmE+g93x33hzES2HitgjHzvm5B2Cu6fwlXi7cSnLEWVn7W5QXEuhek
+6uhq4NC0ahL/qAyJsFhVve32qrR7yacDVlMWZ5iQPfqtvS9CsxyafADBWsgeN4L1
+5oqQofNlh0l+UvMFp8INNbKfxTOPKCMfGxIEd+MCgYEA7UsJky380PrbtwD4JVrn
+LaFhXL3VMYyRJaFPIeKNC5wwbzgyjP3lFme6L5Dpv/T+3bZFSvT+XpgvS0S5rFAV
+qFSvuGsAUS2wUi4EMFV8lwFZSdafnEDtdgVZU1DTKkhbQg6sgIVxV9aRUt7gedZj
+cFTKMms6RAgim6fww/ECs90CgYBdI1pt9jJhVHPZNUMgpy5ke0uUijfhDwwazKRd
+OqUj0sO7RojKcM2pJoohivEKf3qmZA0qvSzds2+AJxNpnCKoE364UDw5k5rsLOXn
+axlFp8c29zOLqQGr4c//60eExKjNXaHUpWESXmTRKIJJmJkvP01qEtu0043ZygIb
+zKbDowKBgHhacDR3aat3kkaTp/4AhekhDhMgZ1u2NIi0ycsA6zRlmFucHMwtKCM0
+VMuu40D9N1lTmcdndNKkTJx6CpbS7g/y0ctkyGwgFdmTjjHYl1nATt9G2oRzKx6I
+nQ+sBUwBPieIqEmiFh+KAsDox6plrtvhSSwp8FYLWBJXZHlLJIsd
+-----END RSA PRIVATE KEY-----
diff --git a/t/keys/u5.pub b/t/keys/u5.pub
new file mode 100644
index 0000000..96a0045
--- /dev/null
+++ b/t/keys/u5.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDjUDRtjr7RqQ1ImVHY95EBM22dwBFdU5RqvsI1rHA+pVdBHX5OZT16+YgSREt/SQUBrsvdAMWugW7iwOJImO99giQd2jE7gWTXbw8kGE5shdpqspxQEhnb3/wjd1N52rkJj9gcv8CNpP7RWS1ZftjCKC8YNqatcAqqOdbWZYqnpM2Pxum+mkG+PfK91ig4ti8Kz9Ip8p2VrKeCKKFNsoQs9xG7w3NjEeIXZv+7S3fV690/R8D9qkCyUqEd0KbxJxkm7Ih3O1yiAEUTl0abSHeqVskq8pPwmZNyBDlMEFyao9WZSSac+8jN3YbsPWyZsTEbsuep6QdxL129o+cXQHeh g3@sita-lt.atc.tcs.com
diff --git a/t/keys/u6 b/t/keys/u6
new file mode 100644
index 0000000..86deee7
--- /dev/null
+++ b/t/keys/u6
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAxyRjRT8RoSDnAnbZdrTjXhBMrkfNfolWFqc3qAjAo8Tmp7Ns
+n7R+KCl31RDkC9u4ll4AOfF9bIdP9ovDVvXoTMoiOdPTYqsnaMBBpDH04vxID2U5
+rEOhSzbvTazzaSlYNFED2a9bMfULuF6WxchFPBQGhTZaC8cDAkS5YYXvjHXAn1GN
+kpv0k5qltD8vjJoM7Mg5JLm84WejPQ5CmwOXqsQj69ZUZEyA8J3oqWVYkEhpYkbK
+IuzaHkrTC0PUQJ8MS3wM8358bWp53zYFJMkyL1a/zJQPOMaDiFTXXj1Uue/5ZkIM
+/fkkOMLbaCgEFCMbL8HkVEq4Q0dLhJKl1IdzbQIDAQABAoIBACQd91shuyLMAtmx
+kHM1D1+J+T5Ki3x9j/1/ylpRbA7HsUWNBxBX/eFu0+ryq0lzSiELX2Mi5yp9yATh
+CEaHRuBWcKqoPlhQzk7zP3R2EwHv22nfY/xYL7KiffhKe8MA2pxybQ5X/WQsGzoO
+/a1VSylAQIZ8ewxTxbntmOmVDwMcLZdY5X4NAmzUw1lFD2TVOlHyhjvhgIzhO1tp
+fBvsX3OL+Pk0tTQebuhoqEy2R01BLc+0ZFAKAFZpZgualqZqjyIU0isQsEo+EK4s
+Pn7Ccxy+156aWAEdgD5Aj7oMn3zA9YIt/JoX8muaceUOj3E7B5ZKBtYZkFPVnKO9
+rPMNTIkCgYEA/v5IeFDZjcAMLWqhuPekcUQYMXc+I0ZTn7vT3aksWxhARG96CPdG
+HNLMw4y8yZLnCAsdKUJGtrVcn4y4K0FzUYn0tDm9cmtFoFcQvLPp4mR1kB84urIG
+7qEwv4djeLhl0cYsqMpyl7qflbTVwKa09Fy7HX9ZFL58nXVX63uRXlMCgYEAx+2o
+L/GKkKgRPpCudsGoplECjzqP4SWHXaYVOupVzpnvxFKVcpBl6Un6p6U9V6W16k2C
+XUg9XqFdleawfAwGQ2D/Ip/u4r8GKOphv0QCjO88ld15Ky9mr9Sk4/Z2EAMUrmKg
+CS4hGBF7VIeA3FnzJwLkYL0+WQKpKBt/zojQLz8CgYEAnmtEgttYDeTeq+iviMbx
+9xyjGzhF9oxer8J1oiTUVdP/OYU4gBGAEbA1Xtg1AdauiiS9fUCbxi9u2AEI+naz
+OllHGiE1Pby/iRoOX+42xFw9XcjH6dVo0SB7tMJcXkfRmj5QyJzeDL35H301v3bS
+vW5PIchYg7bEnN6mPLqMWdkCgYBVWaX1Yb5v5vAFr6prVF11MxxOnQeTbHwPhLmH
+f0bGfn0XaNIYKID5SPXS3/4CDuJMdm5y+EYKwgS729H4AwIhfaUt2O0Yq8gra3Pz
+PUuBcxiAOh5iS0ghRDxofW0FhOstTzlW8fR62+u0uGxQpa3iN5/blK6rPTGNx7+W
+Il4N7QKBgHRTxUZraVt3n0kJ4+tdc+F8XK1wLyVC9PmtW+28tiyx14k8o8H/6W94
+r3lyI13sUnvW6zU0VMAALQujRcJDLac/zSUdFGmgU3P3QatU5yUm0Xpt1uFM49Zw
+xZLBTDJxe2Qrc3Z3SKReRqxLz6tTU1soH7TVw/S36tBovKjTw7Cc
+-----END RSA PRIVATE KEY-----
diff --git a/t/keys/u6.pub b/t/keys/u6.pub
new file mode 100644
index 0000000..de5b06b
--- /dev/null
+++ b/t/keys/u6.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHJGNFPxGhIOcCdtl2tONeEEyuR81+iVYWpzeoCMCjxOans2yftH4oKXfVEOQL27iWXgA58X1sh0/2i8NW9ehMyiI509NiqydowEGkMfTi/EgPZTmsQ6FLNu9NrPNpKVg0UQPZr1sx9Qu4XpbFyEU8FAaFNloLxwMCRLlhhe+MdcCfUY2Sm/STmqW0Py+MmgzsyDkkubzhZ6M9DkKbA5eqxCPr1lRkTIDwneipZViQSGliRsoi7NoeStMLQ9RAnwxLfAzzfnxtannfNgUkyTIvVr/MlA84xoOIVNdePVS57/lmQgz9+SQ4wttoKAQUIxsvweRUSrhDR0uEkqXUh3Nt g3@sita-lt.atc.tcs.com
diff --git a/t/listers.t b/t/listers.t
new file mode 100755
index 0000000..5fbf0ae
--- /dev/null
+++ b/t/listers.t
@@ -0,0 +1,134 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# the various list-* commands
+# ----------------------------------------------------------------------
+
+try 'plan 30';
+
+try "## info";
+
+confreset;confadd '
+ @oss = git gitolite gitolite3
+ @prop = cc p4
+ @crypto = alice bob carol
+ @dilbert = alice wally ashok
+
+ repo @oss
+ RW = u1 @crypto
+ R = u2 @dilbert
+ repo @prop
+ RW = u2 @dilbert
+ R = u1
+ repo t3
+ RW = u3
+ R = u4
+';
+
+try "ADMIN_PUSH info; !/FATAL/" or die text();
+try "
+ /Initialized.*empty.*cc.git/
+ /Initialized.*empty.*p4.git/
+ /Initialized.*empty.*git.git/
+ /Initialized.*empty.*gitolite.git/
+ /Initialized.*empty.*gitolite3.git/
+ /Initialized.*empty.*t3.git/
+";
+
+try "gitolite list-groups"; cmp
+'@crypto
+@dilbert
+@oss
+@prop
+';
+
+try "gitolite list-users"; cmp
+'@all
+@crypto
+@dilbert
+admin
+u1
+u2
+u3
+u4
+';
+try "gitolite list-repos"; cmp
+'@oss
+@prop
+gitolite-admin
+t3
+testing
+';
+
+try "gitolite list-phy-repos"; cmp
+'cc
+git
+gitolite
+gitolite-admin
+gitolite3
+p4
+t3
+testing
+';
+
+try "gitolite list-memberships -u alice"; cmp
+'@crypto
+@dilbert
+';
+
+try "gitolite list-memberships -u ashok"; cmp
+'@dilbert
+';
+
+try "gitolite list-memberships -u carol"; cmp
+'@crypto
+';
+
+try "gitolite list-memberships -r git"; cmp
+'@oss
+';
+
+try "gitolite list-memberships -r gitolite"; cmp
+'@oss
+';
+
+try "gitolite list-memberships -r gitolite3"; cmp
+'@oss
+';
+
+try "gitolite list-memberships -r cc"; cmp
+'@prop
+';
+
+try "gitolite list-memberships -r p4"; cmp
+'@prop
+';
+
+try "gitolite list-members \@crypto"; cmp
+'alice
+bob
+carol
+';
+
+try "gitolite list-members \@dilbert"; cmp
+'alice
+ashok
+wally
+';
+
+try "gitolite list-members \@oss"; cmp
+'git
+gitolite
+gitolite3
+';
+
+try "gitolite list-members \@prop"; cmp
+'cc
+p4
+';
+
diff --git a/t/manjaro-root-smart-http-test-setup b/t/manjaro-root-smart-http-test-setup
new file mode 100755
index 0000000..f254b55
--- /dev/null
+++ b/t/manjaro-root-smart-http-test-setup
@@ -0,0 +1,114 @@
+#!/bin/bash
+
+# gitolite http mode TESTING setup for Manjaro
+# - Probably works for Arch also; if someone tests it let me know
+# - Use the comments to create a version for your distro if needed
+
+# CAUTION: This script needs to be run as root, so you best eyeball it at
+# least once to make sure you know what changes it is making.
+
+# WARNING: clobbers /srv/http/gitolite-home, and also creates 7 http
+# users with trivial passwords FOR TESTING.
+
+# HOWEVER: if you remove some of that, especially the part that creates test
+# users, this *should* work as a quick "setup gitolite http mode" script.
+
+# CAUTION: This script assumes the httpd.conf file is pretty much the default
+# "as shipped" version. If you fiddled with it, this script *may* break.
+# It's on you to determine if that is the case and manually simulate the
+# actions of this script. It's not that hard, and anyway it's just once (for
+# a given server) so it's not too bad.
+
+# ----------------------------------------------------------------------
+# BEGIN APACHE CONF CHANGES
+
+# Unlike Fedora, Manjaro's default httpd.conf does not contain a wildcard
+# include for stuff in conf.d; they're all explicitly included, so we need to
+# include gitolite.conf.
+cd /etc/httpd/conf
+grep ^Include.*gitolite.conf httpd.conf ||
+ printf "\n%s\n%s\n" '# gitolite http mode' 'Include conf/extra/gitolite.conf' >> httpd.conf
+
+# Again, unlike Fedora, Manjaro's default conf does not come with cgi enabled.
+# In fact, the directive is both commented out *and* inside an "IF" block for
+# some other module. Since I don't plan to be an expert on apache, I will
+# punt by including the required LoadModule line before the first LoadModule
+# line that is not in an "if" block (i.e., not indented).
+grep '^LoadModule cgi_module modules/mod_cgi.so' httpd.conf ||
+ perl -i -pE 'say "LoadModule cgi_module modules/mod_cgi.so" if /^LoadModule/ and not $flag++' httpd.conf
+
+# END APACHE CONF CHANGES
+# ----------------------------------------------------------------------
+
+cd ~http
+# should be /srv/http; you may want to check just to be safe
+export GITOLITE_HTTP_HOME=$PWD/gitolite-home
+
+[[ -d gitolite-home ]] && {
+ [[ $GITOLITE_TEST != y ]] && {
+ echo "If you're OK with clobbering $GITOLITE_HTTP_HOME, please rerun with
+environment variable GITOLITE_TEST set to 'y'."
+ exit 1;
+ }
+}
+
+rm -rf gitolite-home
+mkdir gitolite-home
+
+# setup apache conf for gitolite
+cd /etc/httpd/conf/extra
+[[ -f gitolite.conf ]] || {
+ cat > gitolite.conf <<-EOF
+ SetEnv GIT_PROJECT_ROOT $GITOLITE_HTTP_HOME/repositories
+ ScriptAlias /git/ $GITOLITE_HTTP_HOME/gitolite-source/src/gitolite-shell/
+ ScriptAlias /gitmob/ $GITOLITE_HTTP_HOME/gitolite-source/src/gitolite-shell/
+ SetEnv GITOLITE_HTTP_HOME $GITOLITE_HTTP_HOME
+ SetEnv GIT_HTTP_EXPORT_ALL
+
+ <Location /git>
+ AuthType Basic
+ AuthName "Private Git Access"
+ Require valid-user
+ AuthUserFile $GITOLITE_HTTP_HOME/gitolite-http-authuserfile
+ </Location>
+ EOF
+}
+
+# get the gitolite sources
+cd $GITOLITE_HTTP_HOME
+
+if [[ -d /tmp/gitolite.git ]]; then
+ git clone /tmp/gitolite.git gitolite-source
+ # I do this because I have to test stuff *before* it gets to github, so I
+ # can't simply clone what's on github. Instead, I use a local
+ # world-readable bare repo cloned from my dev environment.
+else
+ git clone 'https://github.com/sitaramc/gitolite' gitolite-source
+fi
+
+# make the bin directory, and add it to PATH
+cd gitolite-source
+mkdir $GITOLITE_HTTP_HOME/bin
+./install -ln $GITOLITE_HTTP_HOME/bin
+export PATH=$PATH:$GITOLITE_HTTP_HOME/bin
+
+# come back to base, then run setup. Notice that you have to point HOME to
+# the right place, even if it is just for this command
+cd $GITOLITE_HTTP_HOME
+HOME=$GITOLITE_HTTP_HOME gitolite setup -a admin
+
+# insert some essential lines at the beginning of the rc file
+echo '$ENV{PATH} .= ":$ENV{GITOLITE_HTTP_HOME}/bin";' > 1
+echo >> 1
+cat .gitolite.rc >> 1
+\mv 1 .gitolite.rc
+
+# create users "admin" and "u1" thru "u6" for testing
+htpasswd -bc $GITOLITE_HTTP_HOME/gitolite-http-authuserfile admin admin
+seq 6 | xargs -I % htpasswd -b $GITOLITE_HTTP_HOME/gitolite-http-authuserfile u% u%
+
+# fix up ownership
+chown -R http:http $GITOLITE_HTTP_HOME
+
+# restart httpd to make it pick up all the new stuff
+systemctl restart httpd
diff --git a/t/merge-check.t b/t/merge-check.t
new file mode 100755
index 0000000..fdea318
--- /dev/null
+++ b/t/merge-check.t
@@ -0,0 +1,95 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# merge check -- the M flag
+# ----------------------------------------------------------------------
+
+try "plan 55";
+
+confreset;confadd '
+ repo foo
+ RW+M = u1
+ RW+ = u2
+ RWM .= u3
+ RW = u4
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+# setup a merged push
+
+try "
+ cd ..
+ [ -d foo ]; !ok
+ glt clone u1 file:///foo
+ ok; /Cloning into/
+ /You appear to have cloned an empty/
+";
+
+try "
+ cd foo; ok
+ [ -d .git ]; ok
+ test-commit aa; ok; /1 file changed, 1 insertion/
+ tag start; ok
+ glt push u1 origin master
+ ok; /new branch.*master.-..master/
+ /create.delete ignored.*merge-check/
+ checkout -b new; ok; /Switched to a new branch 'new'/
+ test-commit bb cc; ok
+ checkout master; ok; /Switched to branch 'master'/
+ test-commit dd ee; ok
+ git merge new; ok; /Merge made.*recursive/
+ test-commit ff; ok
+ tag end; ok
+";
+
+# push by u4 should fail
+try "
+ glt push u4 file:///foo master
+ !ok; /WM refs/heads/master foo u4 DENIED by fallthru/
+ /To file:///foo/
+ /remote rejected.*hook declined/
+ /failed to push some refs/
+";
+
+# push by u3 should succeed
+try "
+ glt push u3 file:///foo master
+ ok; /To file:///foo/; /master.-..master/
+";
+
+# rewind by u3 should fail
+try "
+ reset-h start; ok; /HEAD is now at .* aa /
+ glt push u3 file:///foo +master
+ !ok; /rejected.*hook declined/
+ /failed to push some refs/
+";
+
+# rewind by u2 should succeed
+try "
+ glt push u2 file:///foo +master
+ ok; /To file:///foo/
+ /forced update/
+";
+
+# push by u2 should fail
+try "
+ reset-h end; ok; /HEAD is now at .* ff /
+ glt push u2 file:///foo master
+ !ok; /WM refs/heads/master foo u2 DENIED by fallthru/
+ /To file:///foo/
+ /remote rejected.*hook declined/
+ /failed to push some refs/
+";
+
+# push by u1 should succeed
+try "
+ glt push u1 file:///foo master
+ ok; /master.-..master/
+";
diff --git a/t/mirror-test b/t/mirror-test
new file mode 100755
index 0000000..3ace59b
--- /dev/null
+++ b/t/mirror-test
@@ -0,0 +1,445 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# you need 3 disposable userids: sam, frodo, gollum. Then the test user (say
+# "g3") needs to be able to sudo into them. Put this in /etc/sudoers:
+
+# g3 ALL = (sam,frodo,gollum) NOPASSWD: ALL
+
+$ENV{TSH_ERREXIT} = 1;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+use Cwd;
+my $workdir = getcwd();
+my $h = $ENV{HOME};
+my ($t, $t2); # temp vars
+
+# basic tests
+# ----------------------------------------------------------------------
+
+try "plan 152";
+## try "DEF POK = !/DENIED/; !/failed to push/";
+
+## confreset;confadd '
+
+## ';
+
+## try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+# ----------------------------------------------------------------------
+
+# switch keys
+sub swk {
+ my $h = $ENV{HOME};
+ my $k = shift;
+ system("cp $h/.ssh/$k $h/.ssh/id_rsa");
+ system("cp $h/.ssh/$k.pub $h/.ssh/id_rsa.pub");
+}
+
+sub all {
+ try "F " . join(" ", @_);
+ try "S " . join(" ", @_);
+ try "G " . join(" ", @_);
+}
+
+try "
+ DEF F = sudo -u frodo -i
+ DEF S = sudo -u sam -i
+ DEF G = sudo -u gollum -i
+";
+
+my $bd = `gitolite query-rc -n GL_BINDIR`;
+
+try "
+ $bd/../t/mirror-test-setup.sh; ok or die mirror setup shell script failed
+ /hello server-frodo, this is frodo/
+ /hello server-sam, this is frodo/
+ /hello server-gollum, this is frodo/
+ /hello server-frodo, this is sam/
+ /hello server-sam, this is sam/
+ /hello server-gollum, this is sam/
+ /hello server-frodo, this is gollum/
+ /hello server-sam, this is gollum/
+ /hello server-gollum, this is gollum/
+ /hello admin, this is frodo/
+ /Initialized empty .*/gitolite-admin.git/
+ /Initialized empty .*/r1.git/
+ /Initialized empty .*/r2.git/
+ /Initialized empty .*/testing.git/
+ /Initialized empty .*/gitolite-admin.git/
+ /Initialized empty .*/r1.git/
+ /Initialized empty .*/r2.git/
+ /Initialized empty .*/testing.git/
+ /Initialized empty .*/gitolite-admin.git/
+ /Initialized empty .*/r1.git/
+ /Initialized empty .*/r2.git/
+ /Initialized empty .*/testing.git/
+";
+
+# ----------------------------------------------------------------------
+# SECTION 1: gitolite-admin shenanigans
+
+# push to frodo and see sam and gollum change
+try "
+ git clone frodo\@localhost:gitolite-admin fga
+ ok; /Cloning into 'fga'.../
+ cd fga; ok
+ cp $h/.ssh/u?.pub keydir; ok
+ git add keydir; ok
+ git commit -m 6keys; ok
+ git push; ok
+ /To localhost:gitolite-admin/
+ /master -> master/
+ sleep 5
+ git rev-parse HEAD
+";
+
+chomp($t = text());
+
+try "
+ git ls-remote sam\@localhost:gitolite-admin
+ ok; /$t/
+ git ls-remote gollum\@localhost:gitolite-admin
+ ok; /$t/
+";
+
+try "
+ cd ..
+
+";
+
+# push to sam and see frodo and gollum change
+try "
+ git clone sam\@localhost:gitolite-admin sga
+ ok; /Cloning into 'sga'.../
+ cd sga; ok
+ empty; ok
+ git push; ok
+ /To localhost:gitolite-admin/
+ /master -> master/
+ sleep 5
+ git rev-parse HEAD
+";
+
+chomp($t = text());
+
+try "
+ git ls-remote frodo\@localhost:gitolite-admin
+ ok; /$t/
+ git ls-remote gollum\@localhost:gitolite-admin
+ ok; /$t/
+";
+
+try "
+ cd ..
+
+";
+
+# push to gollum and fail at gollum
+try "
+ git clone gollum\@localhost:gitolite-admin gga
+ ok; /Cloning into 'gga'.../
+ cd gga; ok
+ empty; ok
+ git push; !ok
+ !/To localhost:gitolite-admin/
+ !/master -> master/
+ /gollum: pushing 'gitolite-admin' to copy 'gollum' not allowed/
+ git rev-parse HEAD
+";
+
+chomp($t2 = text());
+
+try "
+ git ls-remote frodo\@localhost:gitolite-admin
+ ok; /$t/; !/$t2/
+ git ls-remote sam\@localhost:gitolite-admin
+ ok; /$t/; !/$t2/
+ git ls-remote gollum\@localhost:gitolite-admin
+ ok; /$t/; !/$t2/
+";
+
+# fake out the gollum failure to continue the redirected push and fail at frodo
+try "
+ sudo -u gollum -i gitolite git-config -r gitolite-admin .
+ ok
+ /redirectOK.*sam/
+ !/redirectOK.*gollum/
+
+ sudo -u gollum -i bash -c 'echo repo gitolite-admin > junk'
+ sudo -u gollum -i bash -c 'echo option mirror.redirectOK-1 = gollum >> junk'
+ sudo -u gollum -i bash -c 'cat junk >> .gitolite/conf/gitolite.conf'
+ sudo -u gollum -i gitolite compile
+ sudo -u gollum -i gitolite git-config -r gitolite-admin .
+ ok
+ /redirectOK.*sam/
+ /redirectOK.*gollum/
+
+ git push; !ok
+ /frodo: redirection not allowed from 'gollum'/
+ !/To gollum\@localhost:gitolite-admin/
+ !/master -> master/
+";
+
+# reset gollum via frodo
+try "
+ cd ..
+ rm -rf fga
+ git clone frodo\@localhost:gitolite-admin fga
+ ok; /Cloning into 'fga'.../
+ cd fga; ok
+ empty; ok
+ git push; ok
+ /To localhost:gitolite-admin/
+ /master -> master/
+ sleep 5
+
+ sudo -u gollum -i gitolite git-config -r gitolite-admin .
+ ok
+ /redirectOK.*sam/
+ !/redirectOK.*gollum/
+
+ git rev-parse HEAD
+";
+
+chomp($t = text());
+
+try "
+ git ls-remote sam\@localhost:gitolite-admin
+ ok; /$t/
+ git ls-remote gollum\@localhost:gitolite-admin
+ ok; /$t/
+";
+
+# ----------------------------------------------------------------------
+# user repo shenanigans
+
+# for a recap of the perms see t/mirror-test-setup.sh
+
+try "
+ cd ..
+ pwd
+ /tmp/tsh_tempdir/ or die not in the right place
+" or die;
+
+swk('u1');
+
+# u1 sam r1, R ok, W ok
+try "
+ rm -rf fga sga gga
+
+ git clone sam\@localhost:r1 sr1
+ /Cloning into 'sr1'.../
+ /warning: You appear to have cloned an empty repository/
+ cd sr1
+ empty
+ git push origin master
+ /new branch/; /master -> master/
+ sleep 5
+ git rev-parse HEAD
+";
+chomp($t = text());
+
+# u1 sam r1, W mirrors to frodo but not gollum
+try "
+ git ls-remote sam\@localhost:r1
+ /$t/
+ git ls-remote frodo\@localhost:r1
+ /$t/
+ git ls-remote gollum\@localhost:r1
+ /gollum: 'r1' is mirrored but not here/
+";
+
+swk("u2");
+try "
+ empty
+ git rev-parse HEAD
+";
+chomp($t2 = text());
+
+# u2 sam r2 W ok, mirrors to all
+try "
+ git push sam\@localhost:r2 master
+ /new branch/; /master -> master/
+ /master -> master/
+ sleep 5
+ git ls-remote frodo\@localhost:r2
+ !/$t/
+ /$t2/
+ git ls-remote gollum\@localhost:r2
+ !/$t/
+ /$t2/
+";
+
+swk("u1");
+
+# u1 gollum r1 -- "known unknown" :-)
+# u1 frodo r1 R ok, W not ok
+# u1 sam r1 R ok, W ok
+try "
+ cd ..
+ rm -rf sr1
+
+ git clone gollum\@localhost:r1 fr1
+ /gollum: 'r1' is mirrored but not here/
+
+ git clone frodo\@localhost:r1 fr1; ok
+ cd fr1
+ empty
+ git push
+ /frodo: pushing 'r1' to copy 'frodo' not allowed/
+ cd ..
+ git clone sam\@localhost:r1 sr1; ok
+ cd sr1
+ empty
+ git push; ok
+ /master -> master/
+ sleep 5
+ git rev-parse HEAD
+";
+chomp($t = text());
+
+# u1 sam r1 W mirrored to frodo but not gollum
+try "
+ git ls-remote sam\@localhost:r1
+ /$t/
+ git ls-remote frodo\@localhost:r1
+ /$t/
+
+ git ls-remote gollum\@localhost:r1
+ /gollum: 'r1' is mirrored but not here/
+
+ git reset --hard HEAD^; ok
+ tc a
+ git push; !ok
+ /rejected/
+ /failed to push/
+
+ git push -f
+ /\\+ .......\\.\\.\\........ master -> master .forced update/
+ sleep 5
+";
+
+swk("u2");
+
+# u2 frodo r1 R ok, W not allowed (no redirectOK)
+# u2 frodo r2 W ok
+try "
+ cd ..
+ rm -rf fr1 sr1
+
+ git clone frodo\@localhost:r1 fr1; ok
+ cd fr1
+ tc b
+ git push
+ /frodo: pushing 'r1' to copy 'frodo' not allowed/
+ cd ..
+ git clone frodo\@localhost:r2 fr2; ok
+ cd fr2
+ tc c
+ git push
+ /master -> master/
+ sleep 5
+ git rev-parse HEAD
+";
+chomp($t = text());
+
+# u2 frodo r2 W mirrors to sam and gollum
+try "
+ git ls-remote sam\@localhost:r2
+ /$t/
+ git ls-remote gollum\@localhost:r2
+ /$t/
+
+ git reset --hard HEAD^; ok
+ tc d
+ git push
+ /rejected/
+ /failed to push/
+
+ git push -f
+ /\\+ .......\\.\\.\\........ master -> master .forced update/
+ sleep 5
+
+ cd ..
+ rm -rf fr1 fr2
+";
+
+swk("u3");
+
+# u3 frodo r2 R ok W ok
+try "
+ git clone frodo\@localhost:r2 fr2; ok
+ cd fr2
+ tc e
+ git push; ok
+ sleep 5
+
+ git rev-parse HEAD
+";
+chomp($t = text());
+
+# u3 frodo r2 W mirrors to sam and gollum
+try "
+ git ls-remote sam\@localhost:r2
+ /$t/
+ git ls-remote gollum\@localhost:r2
+ /$t/
+
+ git reset --hard HEAD^; ok
+ tc f
+ git push
+ /rejected/
+ /failed to push/
+
+ sleep 10
+ git push -f
+ /\\+ refs/heads/master r2 u3 DENIED by fallthru/
+ /hook declined/
+ /rejected/
+";
+
+# ----------------------------------------------------------------------
+# all those vague edge cases where the two servers have totally wrong ideas
+# about each other
+
+swk('u1');
+
+try "sudo -u frodo -i ls .gitolite/logs";
+chomp($t = text());
+my $lfn = ".gitolite/logs/$t";
+
+try "
+ ssh sam\@localhost mirror push frodo lfrodo; !ok
+ /FATAL: frodo: 'lfrodo' is local/
+
+ ssh sam\@localhost mirror push frodo mboth; !ok
+ /FATAL: frodo: 'mboth' is native/
+
+ ssh sam\@localhost mirror push frodo mnotsam; !ok
+ /FATAL: frodo: 'sam' is not the master for 'mnotsam'/
+
+ cd ..
+ git clone sam\@localhost:lfrodo2 lfrodo2; ok
+ cd lfrodo2
+ empty
+ git push origin master; !ok
+ /FATAL: frodo: 'lfrodo2' is local/
+
+ cd ..
+ git clone sam\@localhost:nnfrodo nnfrodo; ok
+ cd nnfrodo
+ empty
+ git push origin master; !ok
+ /FATAL: frodo: 'nnfrodo' is not native/
+
+ cd ..
+ git clone sam\@localhost:nvsfrodo nvsfrodo; ok
+ cd nvsfrodo
+ empty
+ git push origin master; !ok
+ /FATAL: frodo: 'sam' is not a valid copy for 'nvsfrodo'/
+";
diff --git a/t/mirror-test-rc b/t/mirror-test-rc
new file mode 100644
index 0000000..1d76783
--- /dev/null
+++ b/t/mirror-test-rc
@@ -0,0 +1,162 @@
+# This file is in perl syntax. But you do NOT need to know perl to edit it --
+# just mind the commas, use single quotes unless you know what you're doing,
+# and make sure the brackets and braces stay matched up!
+
+# (Tip: perl allows a comma after the last item in a list also!)
+
+# HELP for commands (see COMMANDS list below) can be had by running the
+# command with "-h" as the sole argument.
+
+# HELP for all the other FEATURES can be found in the documentation (look for
+# "list of non-core programs shipped with gitolite" in the master index) or
+# directly in the corresponding source file.
+
+%RC = (
+
+ # ------------------------------------------------------------------
+
+ HOSTNAME => '%HOSTNAME',
+
+ # default umask gives you perms of '0700'; see the rc file docs for
+ # how/why you might change this
+ UMASK => 0077,
+
+ # look in the "GIT-CONFIG" section in the README for what to do
+ GIT_CONFIG_KEYS => '',
+
+ # comment out if you don't need all the extra detail in the logfile
+ LOG_EXTRA => 1,
+
+ # roles. add more roles (like MANAGER, TESTER, ...) here.
+ # WARNING: if you make changes to this hash, you MUST run 'gitolite
+ # compile' afterward, and possibly also 'gitolite trigger POST_COMPILE'
+ ROLES => {
+ READERS => 1,
+ WRITERS => 1,
+ },
+ # uncomment (and change) this if you wish
+ # DEFAULT_ROLE_PERMS => 'READERS @all',
+
+ # CACHE => 'Redis',
+
+ # ------------------------------------------------------------------
+
+ # rc variables used by various features
+
+ # the 'info' command prints this as additional info, if it is set
+ # SITE_INFO => 'Please see http://blahblah/gitolite for more help',
+
+ # the 'desc' command uses this
+ # WRITER_CAN_UPDATE_DESC => 1,
+
+ # the CpuTime feature uses these
+ # display user, system, and elapsed times to user after each git operation
+ # DISPLAY_CPU_TIME => 1,
+ # display a warning if total CPU times (u, s, cu, cs) crosses this limit
+ # CPU_TIME_WARN_LIMIT => 0.1,
+
+ # the Mirroring feature needs this
+ # HOSTNAME => "foo",
+
+ # if you enabled 'Shell', you need this
+ # SHELL_USERS_LIST => "$ENV{HOME}/.gitolite.shell-users",
+
+ # ------------------------------------------------------------------
+
+ # List of commands and features to enable
+
+ ENABLE => [
+
+ # COMMANDS
+
+ # These are the commands enabled by default
+ 'help',
+ 'desc',
+ 'info',
+ 'perms',
+ 'writable',
+
+ 'mirror',
+
+ # Uncomment or add new commands here.
+ # 'create',
+ # 'fork',
+ # 'mirror',
+ # 'sskm',
+ # 'D',
+
+ # These FEATURES are enabled by default.
+
+ # essential (unless you're using smart-http mode)
+ 'ssh-authkeys',
+
+ # creates git-config enties from gitolite.conf file entries like 'config foo.bar = baz'
+ 'git-config',
+
+ # creates git-daemon-export-ok files; if you don't use git-daemon, comment this out
+ 'daemon',
+
+ # creates projects.list file; if you don't use gitweb, comment this out
+ 'gitweb',
+
+ # These FEATURES are disabled by default; uncomment to enable. If you
+ # need to add new ones, ask on the mailing list :-)
+
+ # user-visible behaviour
+
+ # prevent wild repos auto-create on fetch/clone
+ # 'no-create-on-read',
+ # no auto-create at all (don't forget to enable the 'create' command!)
+ # 'no-auto-create',
+
+ # access a repo by another (possibly legacy) name
+ # 'Alias',
+
+ # give some users direct shell access
+ # 'Shell',
+
+ # system admin stuff
+
+ # enable mirroring (don't forget to set the HOSTNAME too!)
+ 'Mirroring',
+
+ # allow people to submit pub files with more than one key in them
+ # 'ssh-authkeys-split',
+
+ # selective read control hack
+ # 'partial-copy',
+
+ # manage local, gitolite-controlled, copies of read-only upstream repos
+ # 'upstream',
+
+ # updates 'description' file instead of 'gitweb.description' config item
+ # 'cgit',
+
+ # performance, logging, monitoring...
+
+ # be nice
+ # 'renice 10',
+
+ # log CPU times (user, system, cumulative user, cumulative system)
+ # 'CpuTime',
+
+ # syntactic_sugar for gitolite.conf and included files
+
+ # allow backslash-escaped continuation lines in gitolite.conf
+ # 'continuation-lines',
+
+ # create implicit user groups from directory names in keydir/
+ # 'keysubdirs-as-groups',
+
+ ],
+
+);
+
+# ------------------------------------------------------------------------------
+# per perl rules, this should be the last line in such a file:
+1;
+
+# Local variables:
+# mode: perl
+# End:
+# vim: set syn=perl:
diff --git a/t/mirror-test-setup.sh b/t/mirror-test-setup.sh
new file mode 100755
index 0000000..32bc462
--- /dev/null
+++ b/t/mirror-test-setup.sh
@@ -0,0 +1,196 @@
+#!/bin/bash
+
+set -e
+hosts="frodo sam gollum"
+mainhost=frodo
+
+# setup software
+bd=`gitolite query-rc -n GL_BINDIR`
+mkdir -p /tmp/g3
+rm -rf /tmp/g3/src
+cp -a $bd /tmp/g3/src
+chmod -R go+rX /tmp/g3
+
+# setup symlinks in frodo, sam, and gollum's accounts
+for h in $hosts
+do
+ sudo -u $h -i bash -c "rm -rf *.pub bin .ssh projects.list repositories .gitolite .gitolite.rc"
+done
+
+[ "$1" = "clear" ] && exit
+
+cd /tmp/g3
+[ -d keys ] || {
+ mkdir keys
+ cd keys
+ for h in $hosts
+ do
+ ssh-keygen -N '' -q -f server-$h -C $h
+ chmod go+r /tmp/g3/keys/server-$h
+ done
+ cp $bd/../t/mirror-test-ssh-config ssh-config
+}
+chmod -R go+rX /tmp/g3
+
+for h in $hosts
+do
+ sudo -u $h -i bash -c "mkdir -p bin; ln -sf /tmp/g3/src/gitolite bin; mkdir -p .ssh; chmod 0700 .ssh"
+
+ sudo -u $h -i cp /tmp/g3/keys/ssh-config .ssh/config
+ sudo -u $h -i cp /tmp/g3/keys/server-$h .ssh/id_rsa
+ sudo -u $h -i cp /tmp/g3/keys/server-$h.pub .ssh/id_rsa.pub
+ sudo -u $h -i chmod go-rwx .ssh/id_rsa .ssh/config
+
+done
+
+# add all pubkeys to all servers
+for h in $hosts
+do
+ sudo -u $h -i gitolite setup -a admin
+ for j in $hosts
+ do
+ sudo -u $h -i gitolite setup -pk /tmp/g3/keys/server-$j.pub
+ echo sudo _u $j _i ssh $h@localhost info
+ sudo -u $j -i ssh -o StrictHostKeyChecking=no $h@localhost info
+ done
+ echo ----
+done
+
+# now copy our admin key to the main host
+cd;cd .ssh
+cp admin id_rsa; cp admin.pub id_rsa.pub
+cp admin.pub /tmp/g3/keys; chmod go+r /tmp/g3/keys/admin.pub
+sudo -u $mainhost -i gitolite setup -pk /tmp/g3/keys/admin.pub
+ssh $mainhost@localhost info
+
+lines="
+repo gitolite-admin
+ option mirror.master = frodo
+ option mirror.copies-1 = sam gollum
+ option mirror.redirectOK = sam
+
+repo r1
+ RW+ = u1
+ RW = u2
+ R = u3
+ option mirror.master = sam
+ option mirror.copies-1 = frodo
+
+repo r2
+ RW+ = u2
+ RW = u3
+ R = u4
+ option mirror.master = sam
+ option mirror.copies-1 = frodo gollum
+ option mirror.redirectOK = all
+
+include \"%HOSTNAME.conf\"
+"
+
+lines2="
+repo l-%HOSTNAME
+RW = u1
+"
+
+# for each server, set the HOSTNAME to the rc, add the mirror options to the
+# conf file, and compile
+for h in $hosts
+do
+ cat $bd/../t/mirror-test-rc | perl -pe "s/%HOSTNAME/$h/" > /tmp/g3/temp
+ chmod go+rX /tmp/g3/temp
+ sudo -u $h -i cp /tmp/g3/temp .gitolite.rc
+ echo "$lines" | sudo -u $h -i sh -c 'cat >> .gitolite/conf/gitolite.conf'
+ echo "$lines2" | sudo -u $h -i sh -c "cat >> .gitolite/conf/$h.conf"
+ sudo -u $h -i gitolite setup
+done
+
+# goes on frodo
+lines="
+# local to frodo but sam thinks frodo is a copy
+repo lfrodo
+RW = u1
+
+# both think they're master
+repo mboth
+RW = u1
+option mirror.master = frodo
+option mirror.copies = sam
+
+# frodo thinks someone else is the master but sam thinks he is
+repo mnotsam
+RW = u1
+option mirror.master = merry
+option mirror.copies = frodo
+
+# local to frodo but sam thinks frodo is a master and redirect is OK
+repo lfrodo2
+RW = u1
+
+# non-native to frodo but sam thinks frodo is master
+repo nnfrodo
+RW = u1
+option mirror.master = gollum
+option mirror.copies = frodo
+option mirror.redirectOK = all
+
+# sam is not a valid copy to send stuff to frodo
+repo nvsfrodo
+RW = u1
+option mirror.master = frodo
+option mirror.copies = gollum
+option mirror.redirectOK = all
+"
+
+echo "$lines" | sudo -u frodo -i sh -c "cat >> .gitolite/conf/frodo.conf"
+
+# goes on sam
+lines="
+# local to frodo but sam thinks frodo is a copy
+repo lfrodo
+RW = u1
+option mirror.master = sam
+option mirror.copies = frodo
+
+# both think they're master
+repo mboth
+RW = u1
+option mirror.master = sam
+option mirror.copies = frodo
+
+# frodo thinks someone else is the master but sam thinks he is
+repo mnotsam
+RW = u1
+option mirror.master = sam
+option mirror.copies = frodo
+
+# local to frodo but sam thinks frodo is a master and redirect is OK
+repo lfrodo2
+RW = u1
+option mirror.master = frodo
+option mirror.copies = sam
+option mirror.redirectOK = all
+
+# non-native to frodo but sam thinks frodo is master
+repo nnfrodo
+RW = u1
+option mirror.master = frodo
+option mirror.copies = sam
+option mirror.redirectOK = all
+
+# sam is not a valid copy to send stuff to frodo
+repo nvsfrodo
+RW = u1
+option mirror.master = frodo
+option mirror.copies = sam
+option mirror.redirectOK = all
+"
+
+echo "$lines" | sudo -u sam -i sh -c "cat >> .gitolite/conf/sam.conf"
+
+for h in $hosts
+do
+ sudo -u $h -i gitolite setup
+done
+
+# that ends the setup phase
+echo ======================================================================
diff --git a/t/mirror-test-ssh-config b/t/mirror-test-ssh-config
new file mode 100644
index 0000000..40de6d7
--- /dev/null
+++ b/t/mirror-test-ssh-config
@@ -0,0 +1,11 @@
+host frodo
+ user frodo
+ hostname localhost
+
+host sam
+ user sam
+ hostname localhost
+
+host gollum
+ user gollum
+ hostname localhost
diff --git a/t/partial-copy.t b/t/partial-copy.t
new file mode 100755
index 0000000..493f0d4
--- /dev/null
+++ b/t/partial-copy.t
@@ -0,0 +1,181 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# test script for partial copy feature
+# ----------------------------------------------------------------------
+
+try "plan 82";
+try "DEF POK = !/DENIED/; !/failed to push/";
+my $h = $ENV{HOME};
+
+try "
+ cat $h/.gitolite.rc
+ perl s/GIT_CONFIG_KEYS.*/GIT_CONFIG_KEYS => '.*',/
+ perl s/# 'partial-copy'/'partial-copy'/
+ put $h/.gitolite.rc
+";
+
+confreset;confadd '
+ repo foo
+ RW+ = u1 u2
+
+ repo foo-pc
+ - secret-1$ = u4
+ R = u4 # marker 01
+ RW next = u4
+ RW+ dev/USER/ = u4
+ RW refs/tags/USER/ = u4
+
+ - VREF/partial-copy = @all
+ config gitolite.partialCopyOf = foo
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ /Init.*empty.*foo\\.git/
+ /Init.*empty.*foo-pc\\.git/
+";
+
+try "
+ cd ..
+
+ ## populate repo foo, by user u1
+ # create foo with a bunch of branches and tags
+ CLONE u1 foo
+ /appear.*cloned/
+ cd foo
+ tc a1 a2
+ checkout -b dev/u1/foo; tc f1 f2
+ checkout master; tc m1 m2
+ checkout master; checkout -b next; tc n1 n2; tag nt1
+ checkout -b secret-1; tc s11 s12; tag s1t1
+ checkout next; checkout -b secret-2; tc s21 s22; tag s2t1
+ glt push u1 --all
+ /new branch/; /secret-1/; /secret-2/
+ glt push u1 --tags
+ /new tag/; /s1t1/; /s2t1/
+
+ ## user u4 tries foo, fails, tries foo-pc
+ cd ..
+ CLONE u4 foo foo4; !ok
+ /R any foo u4 DENIED by fallthru/
+ CLONE u4 foo-pc ; ok;
+ /Cloning into 'foo-pc'/
+ /new branch.* dev/u1/foo .* dev/u1/foo/
+ /new branch.* master .* master/
+ /new branch.* next .* next/
+ /new branch.* secret-2 .* secret-2/
+ !/new branch.* secret-1 .* secret-1/
+ /new tag.* nt1 .* nt1/
+ /new tag.* s2t1 .* s2t1/
+ !/new tag.* s1t1 .* s1t1/
+
+ ## user u4 pushes to foo-pc
+ cd foo-pc
+ checkout master
+ tc u4m1 u4m2; PUSH u4; !ok
+ /W refs/heads/master foo-pc u4 DENIED by fallthru/
+ /hook declined to update refs/heads/master/
+ /To file:///foo-pc/
+ /remote rejected/
+ /failed to push some refs to 'file:///foo-pc'/
+
+ checkout next
+ tc u4n1 u4n2
+ PUSH u4 next; ok
+ /To .*/foo.git/
+ /new branch\\] ca3787119b7e8b9914bc22c939cefc443bc308da -> refs/partial/br-\\d+/
+ /file:///foo-pc/
+ /52c7716..ca37871 next -> next/
+ tag u4/nexttag; glt push u4 --tags
+ /To file:///foo-pc/
+ /\\[new tag\\] u4/nexttag +-> +u4/nexttag/
+ /\\[new branch\\] ca3787119b7e8b9914bc22c939cefc443bc308da -> refs/partial/br-\\d+/
+
+ checkout master
+ checkout -b dev/u4/u4master
+ tc devu4m1 devu4m2
+ PUSH u4 HEAD; ok
+ /To .*/foo.git/
+ /new branch\\] 228353950557ed1eb13679c1fce4d2b4718a2060 -> refs/partial/br-\\d+/
+ /file:///foo-pc/
+ /new branch.* HEAD -> dev/u4/u4master/
+
+ ## user u1 gets u4's updates, makes some more
+ cd ../foo
+ glt fetch u1
+ /From file:///foo/
+ /new branch\\] dev/u4/u4master -> origin/dev/u4/u4master/
+ /new tag\\] u4/nexttag +-> +u4/nexttag/
+ /52c7716..ca37871 next +-> +origin/next/
+ checkout master; tc u1ma1 u1ma2;
+ /\\[master 8ab1ff5\\] u1ma2 at Thu Jul 7 06:23:20 2011/
+ tag mt2; PUSH u1 master; ok
+ checkout secret-1; tc u1s1b1 u1s1b2
+ /\\[secret-1 5f96cb5\\] u1s1b2 at Thu Jul 7 06:23:20 2011/
+ tag s1t2; PUSH u1 HEAD; ok
+ checkout secret-2; tc u1s2b1 u1s2b2
+ /\\[secret-2 1ede682\\] u1s2b2 at Thu Jul 7 06:23:20 2011/
+ tag s2t2; PUSH u1 HEAD; ok
+ glt push u1 --tags; ok
+
+ glt ls-remote u1 origin
+ /8ab1ff512faf5935dc0fbff357b6f453b66bb98b\trefs/tags/mt2/
+ /5f96cb5ff73c730fb040eb2d01981f7677ca6dba\trefs/tags/s1t2/
+ /1ede6829ec7b75a53cd6acb7da64e5a8011e6050\trefs/tags/s2t2/
+
+ ## u4 gets updates but without the tag in secret-1
+ cd ../foo-pc
+ glt ls-remote u4 origin
+ !/ refs/heads/secret-1/; !/s1t1/; !/s1t2/
+ /8ab1ff512faf5935dc0fbff357b6f453b66bb98b\tHEAD/
+ /8ced4a374b3935bac1a5ba27ef8dd950bd867d47\trefs/heads/dev/u1/foo/
+ /228353950557ed1eb13679c1fce4d2b4718a2060\trefs/heads/dev/u4/u4master/
+ /8ab1ff512faf5935dc0fbff357b6f453b66bb98b\trefs/heads/master/
+ /ca3787119b7e8b9914bc22c939cefc443bc308da\trefs/heads/next/
+ /1ede6829ec7b75a53cd6acb7da64e5a8011e6050\trefs/heads/secret-2/
+ /8ab1ff512faf5935dc0fbff357b6f453b66bb98b\trefs/tags/mt2/
+ /52c7716c6b029963dd167c647c1ff6222a366499\trefs/tags/nt1/
+ /01f04ece6519e7c0e6aea3d26c7e75e9c4e4b06d\trefs/tags/s2t1/
+ /1ede6829ec7b75a53cd6acb7da64e5a8011e6050\trefs/tags/s2t2/
+
+ glt fetch u4
+ /3ea704d..8ab1ff5 master -> origin/master/
+ /01f04ec..1ede682 secret-2 -> origin/secret-2/
+ /\\[new tag\\] mt2 -> mt2/
+ /\\[new tag\\] s2t2 -> s2t2/
+ !/ refs/heads/secret-1/; !/s1t1/; !/s1t2/
+";
+__END__
+
+# last words...
+glt ls-remote u4 file:///foo-pc
+
+cd ../gitolite-admin
+cat conf/gitolite.conf
+perl s/.*marker 01.*//;
+put conf/gitolite.conf
+add conf; commit -m erdel; ok; PUSH admin; ok
+
+glt ls-remote u4 file:///foo-pc
+# see rant below at this point
+
+cd $h/repositories/foo-pc.git
+git branch -D secret-2
+git tag -d s2t1 s2t2
+git gc --prune=now
+glt ls-remote u4 file:///foo-pc
+# only *now* does the rant get addressed
+
+__END__
+
+RANT...
+
+This is where things go all screwy. Because we still have the *objects*
+pointed to by tags s2t1 and s2t2, we still get them back from the main repo.
diff --git a/t/perm-default-roles.t b/t/perm-default-roles.t
new file mode 100755
index 0000000..c417903
--- /dev/null
+++ b/t/perm-default-roles.t
@@ -0,0 +1,169 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# permissions using role names
+# ----------------------------------------------------------------------
+
+try "plan 33";
+try "DEF POK = !/DENIED/; !/failed to push/";
+
+my $rb = `gitolite query-rc -n GL_REPO_BASE`;
+
+try "pwd";
+my $od = text();
+chomp($od);
+
+my $t;
+
+confreset; confadd '
+ @g1 = u1
+ @g2 = u2
+ @g3 = u3
+ @g4 = u4
+ repo foo/CREATOR/..*
+ C = @g1 @g2
+ RW+ = CREATOR
+ - refs/tags/ = WRITERS
+ RW = WRITERS
+ R = READERS
+
+ repo bar/CREATOR/..*
+ C = @g3 @g4
+ RW+ = CREATOR
+ - refs/tags/ = WRITERS
+ RW = WRITERS
+ R = READERS
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+# create repos - 1; no gl-perms files expected
+try "
+
+cd ..
+
+# make foo/u1/u1r1
+glt clone u1 file:///foo/u1/u1r1
+ /Initialized empty Git repository in .*/foo/u1/u1r1.git//
+
+# make bar/u3/u3r1
+glt clone u3 file:///bar/u3/u3r1
+ /Initialized empty Git repository in .*/bar/u3/u3r1.git//
+
+cd u3r1
+";
+
+try "cd $rb; find . -name gl-perms; cd $od"; cmp text(), '';
+
+# enable set-default-roles feature
+try "
+ cat $ENV{HOME}/.gitolite.rc
+ perl s/# 'set-default-roles'/'set-default-roles'/
+ put $ENV{HOME}/.gitolite.rc
+";
+
+# create repos - 2; empty gl-perms files expected
+try "
+
+cd ..
+
+# make foo/u1/u1r2
+glt clone u1 file:///foo/u1/u1r2
+ /Initialized empty Git repository in .*/foo/u1/u1r2.git//
+
+# make bar/u3/u3r2
+glt clone u3 file:///bar/u3/u3r2
+ /Initialized empty Git repository in .*/bar/u3/u3r2.git//
+
+cd u3r2
+";
+
+try "cd $rb; find . -name gl-perms";
+$t = md5sum(sort (lines()));
+cmp $t, 'd41d8cd98f00b204e9800998ecf8427e ./bar/u3/u3r2.git/gl-perms
+d41d8cd98f00b204e9800998ecf8427e ./foo/u1/u1r2.git/gl-perms
+';
+try "cd $od";
+
+# enable per repo default roles
+confadd '
+ repo foo/CREATOR/..*
+ option default.roles-1 = READERS u3
+ option default.roles-2 = WRITERS u4
+
+ repo bar/CREATOR/..*
+ option default.roles-1 = READERS u5
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+# create repos - 3; filled gl-perms expected
+try "
+
+cd ..
+
+gitolite access foo/u1/u1r3 u4 W
+ !ok
+ !/refs/../
+ /W any foo/u1/u1r3 u4 DENIED by fallthru/
+
+# make foo/u1/u1r3
+glt clone u1 file:///foo/u1/u1r3
+ /Initialized empty Git repository in .*/foo/u1/u1r3.git//
+
+gitolite access foo/u1/u1r3 u4 W
+ ok
+ /refs/../
+ !/W any foo/u1/u1r3 u4 DENIED by fallthru/
+
+# make bar/u3/u3r3
+glt clone u3 file:///bar/u3/u3r3
+ /Initialized empty Git repository in .*/bar/u3/u3r3.git//
+
+cd u3r3
+";
+
+try "cd $rb; find . -name gl-perms";
+$t = md5sum(sort (lines()));
+cmp $t, 'd41d8cd98f00b204e9800998ecf8427e ./bar/u3/u3r2.git/gl-perms
+b09856c1addc8e46f6ce0d21a666a633 ./bar/u3/u3r3.git/gl-perms
+d41d8cd98f00b204e9800998ecf8427e ./foo/u1/u1r2.git/gl-perms
+1b5af29692fad391318573bbe633b476 ./foo/u1/u1r3.git/gl-perms
+';
+try "cd $od";
+
+# add perms to an old repo
+try "
+echo WRITERS \@h1 | glt perms u1 -c foo/u1/u1r1
+";
+
+try "cd $rb; find . -name gl-perms";
+$t = md5sum(sort (lines()));
+cmp $t, 'd41d8cd98f00b204e9800998ecf8427e ./bar/u3/u3r2.git/gl-perms
+b09856c1addc8e46f6ce0d21a666a633 ./bar/u3/u3r3.git/gl-perms
+f8f0fd8e139ddb64cd5572914b98750a ./foo/u1/u1r1.git/gl-perms
+d41d8cd98f00b204e9800998ecf8427e ./foo/u1/u1r2.git/gl-perms
+1b5af29692fad391318573bbe633b476 ./foo/u1/u1r3.git/gl-perms
+';
+try "cd $od";
+
+# add perms to a new repo
+try "
+echo WRITERS \@h2 | glt perms u1 -c foo/u1/u1r4
+";
+
+try "cd $rb; find . -name gl-perms";
+$t = md5sum(sort (lines()));
+cmp $t, 'd41d8cd98f00b204e9800998ecf8427e ./bar/u3/u3r2.git/gl-perms
+b09856c1addc8e46f6ce0d21a666a633 ./bar/u3/u3r3.git/gl-perms
+f8f0fd8e139ddb64cd5572914b98750a ./foo/u1/u1r1.git/gl-perms
+d41d8cd98f00b204e9800998ecf8427e ./foo/u1/u1r2.git/gl-perms
+1b5af29692fad391318573bbe633b476 ./foo/u1/u1r3.git/gl-perms
+df17cd2d47e4d99642d7c5ce4093d115 ./foo/u1/u1r4.git/gl-perms
+';
+try "cd $od";
diff --git a/t/perm-roles.t b/t/perm-roles.t
new file mode 100755
index 0000000..c4d017f
--- /dev/null
+++ b/t/perm-roles.t
@@ -0,0 +1,218 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# permissions using role names
+# ----------------------------------------------------------------------
+
+try "plan 91";
+try "DEF POK = !/DENIED/; !/failed to push/";
+
+confreset; confadd '
+ @g1 = u1
+ @g2 = u2
+ @g3 = u3
+ @g4 = u4
+ repo foo/CREATOR/..*
+ C = @g1
+ RW+ = CREATOR
+ - refs/tags/ = WRITERS
+ RW = WRITERS
+ R = READERS
+ RW+D = MANAGERS
+ RW refs/tags/ = TESTERS
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+
+cd ..
+
+# make foo/u1/u1r1
+rm -rf ~/td/u1r1
+glt clone u1 file:///foo/u1/u1r1
+ /Initialized empty Git repository in .*/foo/u1/u1r1.git//
+cd u1r1
+
+# CREATOR can push
+tc e-549 e-550
+glt push u1 file:///foo/u1/u1r1 master:master
+ POK; /master -> master/
+# CREATOR can create branch
+tc w-277 w-278
+glt push u1 file:///foo/u1/u1r1 master:b1
+ POK; /master -> b1/
+# CREATOR can rewind branch
+git reset --hard HEAD^
+tc d-987 d-988
+glt push u1 file:///foo/u1/u1r1 +master:b1
+ POK; /master -> b1 \\(forced update\\)/
+# CREATOR cannot delete branch
+glt push u1 file:///foo/u1/u1r1 :b1
+ /D refs/heads/b1 foo/u1/u1r1 u1 DENIED by fallthru/
+ reject
+
+# CREATOR can push a tag
+git tag t1 HEAD^^
+glt push u1 file:///foo/u1/u1r1 t1
+ POK; /\\[new tag\\] t1 -> t1/
+
+# add u2 to WRITERS
+echo WRITERS \@g2 | glt perms u1 -c foo/u1/u1r1
+glt perms u1 foo/u1/u1r1 -l
+ /WRITERS \@g2/
+
+glt fetch u1
+git reset --hard origin/master
+
+# WRITERS can push
+tc j-185 j-186
+glt push u2 file:///foo/u1/u1r1 master:master
+ POK; /master -> master/
+# WRITERS can create branch
+tc u-420 u-421
+glt push u2 file:///foo/u1/u1r1 master:b2
+ POK; /master -> b2/
+# WRITERS cannot rewind branch
+git reset --hard HEAD^
+tc l-136 l-137
+glt push u2 file:///foo/u1/u1r1 +master:b2
+ /\\+ refs/heads/b2 foo/u1/u1r1 u2 DENIED by fallthru/
+ reject
+# WRITERS cannot delete branch
+glt push u2 file:///foo/u1/u1r1 :b2
+ /D refs/heads/b2 foo/u1/u1r1 u2 DENIED by fallthru/
+ reject
+# WRITERS cannot push a tag
+git tag t2 HEAD^^
+glt push u2 file:///foo/u1/u1r1 t2
+ /W refs/tags/t2 foo/u1/u1r1 u2 DENIED by refs/tags//
+ reject
+
+# change u2 to READERS
+echo READERS u2 | glt perms u1 -c foo/u1/u1r1
+glt perms u1 foo/u1/u1r1 -l
+ /READERS u2/
+
+glt fetch u1
+git reset --hard origin/master
+
+# READERS cannot push at all
+tc v-753 v-754
+glt push u2 file:///foo/u1/u1r1 master:master
+ /W any foo/u1/u1r1 u2 DENIED by fallthru/
+
+# add invalid category MANAGERS
+ /usr/bin/printf 'READERS u6\\nMANAGERS u2\\n' | glt perms u1 -c foo/u1/u1r1
+ !ok
+ /Invalid role 'MANAGERS'/
+";
+
+# make MANAGERS valid
+put "$ENV{HOME}/g3trc", "\$rc{ROLES}{MANAGERS} = 1;\n";
+
+# add u2 to now valid MANAGERS
+try "
+ ENV G3T_RC=$ENV{HOME}/g3trc
+ gitolite compile; ok or die compile failed
+ /usr/bin/printf 'READERS u6\\nMANAGERS u2\\n' | glt perms u1 -c foo/u1/u1r1
+ ok; !/Invalid role 'MANAGERS'/
+ glt perms u1 foo/u1/u1r1 -l
+";
+
+cmp 'READERS u6
+MANAGERS u2
+';
+
+try "
+glt fetch u1
+git reset --hard origin/master
+
+# MANAGERS can push
+tc d-714 d-715
+glt push u2 file:///foo/u1/u1r1 master:master
+ POK; /master -> master/
+
+# MANAGERS can create branch
+tc n-614 n-615
+glt push u2 file:///foo/u1/u1r1 master:b3
+ POK; /master -> b3/
+# MANAGERS can rewind branch
+git reset --hard HEAD^
+tc a-511 a-512
+glt push u2 file:///foo/u1/u1r1 +master:b3
+ POK; /master -> b3 \\(forced update\\)/
+# MANAGERS cannot delete branch
+glt push u2 file:///foo/u1/u1r1 :b3
+ / - \\[deleted\\] b3/
+# MANAGERS can push a tag
+git tag t3 HEAD^^
+glt push u2 file:///foo/u1/u1r1 t3
+ POK; /\\[new tag\\] t3 -> t3/
+
+# add invalid category TESTERS
+echo TESTERS u2 | glt perms u1 -c foo/u1/u1r1
+ !ok
+ /Invalid role 'TESTERS'/
+";
+
+# make TESTERS valid
+put "|cat >> $ENV{HOME}/g3trc", "\$rc{ROLES}{TESTERS} = 1;\n";
+
+try "
+gitolite compile; ok or die compile failed
+# add u2 to now valid TESTERS
+echo TESTERS u2 | glt perms u1 -c foo/u1/u1r1
+ !/Invalid role 'TESTERS'/
+glt perms u1 foo/u1/u1r1 -l
+";
+
+cmp 'TESTERS u2
+';
+
+try "
+glt fetch u1
+git reset --hard origin/master
+
+# TESTERS cannot push
+tc d-134 d-135
+glt push u2 file:///foo/u1/u1r1 master:master
+ /W refs/heads/master foo/u1/u1r1 u2 DENIED by fallthru/
+ reject
+# TESTERS cannot create branch
+tc p-668 p-669
+glt push u2 file:///foo/u1/u1r1 master:b4
+ /W refs/heads/b4 foo/u1/u1r1 u2 DENIED by fallthru/
+ reject
+# TESTERS cannot delete branch
+glt push u2 file:///foo/u1/u1r1 :b2
+ /D refs/heads/b2 foo/u1/u1r1 u2 DENIED by fallthru/
+ reject
+# TESTERS can push a tag
+git tag t4 HEAD^^
+glt push u2 file:///foo/u1/u1r1 t4
+ POK; /\\[new tag\\] t4 -> t4/
+";
+
+# make TESTERS invalid again
+put "$ENV{HOME}/g3trc", "\$rc{ROLES}{MANAGERS} = 1;\n";
+
+try "
+gitolite compile; ok or die compile failed
+# CREATOR can push
+glt fetch u1
+git reset --hard origin/master
+tc y-626 y-627
+glt push u1 file:///foo/u1/u1r1 master:master
+ POK; /master -> master/
+# TESTERS is an invalid category
+git tag t5 HEAD^^
+glt push u2 file:///foo/u1/u1r1 t5
+ /role 'TESTERS' not allowed, ignoring/
+ /W any foo/u1/u1r1 u2 DENIED by fallthru/
+";
diff --git a/t/perms-groups.t b/t/perms-groups.t
new file mode 100755
index 0000000..eb51bc7
--- /dev/null
+++ b/t/perms-groups.t
@@ -0,0 +1,81 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# assigning roles to groups instead of users
+# ----------------------------------------------------------------------
+
+try "plan 31";
+
+try "DEF POK = !/DENIED/; !/failed to push/";
+
+confreset; confadd '
+ @leads = u1 u2
+ @devs = u1 u2 u3 u4
+
+ @gbar = bar/CREATOR/..*
+ repo @gbar
+ C = @leads
+ RW+ = CREATOR
+ RW = WRITERS
+ R = READERS
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+
+ # u1 auto-creates a repo
+ glt ls-remote u1 file:///bar/u1/try1
+ /Initialized empty Git repository in .*/bar/u1/try1.git//
+ # default permissions for u2 and u4
+ glt info u1 -lc
+ /R W *\tbar/u1/try1\tu1/
+ glt info u2 -lc
+ !/R W *\tbar/u1/try1\tu1/
+ glt info u4 -lc
+ !/R W *\tbar/u1/try1\tu1/
+
+ # \@leads can RW try1
+ echo WRITERS \@leads | glt perms u1 -c bar/u1/try1; ok
+ glt info u1 -lc
+ /R W *\tbar/u1/try1\tu1/
+ glt info u2 -lc
+ /R W *\tbar/u1/try1\tu1/
+ glt info u4 -lc
+ !/R W *\tbar/u1/try1\tu1/
+
+ # \@devs can R try1
+ echo READERS \@devs | glt perms u1 -c bar/u1/try1; ok
+ glt perms u1 bar/u1/try1 -l
+ /READERS \@devs/
+ !/WRITERS \@leads/
+
+ glt info u1 -lc
+ /R W *\tbar/u1/try1\tu1/
+
+ glt info u2 -lc
+ !/R W *\tbar/u1/try1\tu1/
+ /R *\tbar/u1/try1\tu1/
+
+ glt info u4 -lc
+ !/R W *\tbar/u1/try1\tu1/
+ /R *\tbar/u1/try1\tu1/
+
+# combo of previous 2
+ /usr/bin/printf 'READERS \@devs\\nWRITERS \@leads\\n' | glt perms u1 -c bar/u1/try1; ok
+ glt perms u1 bar/u1/try1 -l
+ /READERS \@devs/
+ /WRITERS \@leads/
+ glt info u1 -lc
+ /R W *\tbar/u1/try1\tu1/
+ glt info u2 -lc
+ /R W *\tbar/u1/try1\tu1/
+ glt info u4 -lc
+ !/R W *\tbar/u1/try1\tu1/
+ /R *\tbar/u1/try1\tu1/
+";
diff --git a/t/personal-branches.t b/t/personal-branches.t
new file mode 100755
index 0000000..8a08128
--- /dev/null
+++ b/t/personal-branches.t
@@ -0,0 +1,100 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# personal branches
+# ----------------------------------------------------------------------
+
+try "plan 64";
+
+confreset;confadd '
+ @admins = admin dev1
+ repo gitolite-admin
+ RW+ = admin
+
+ repo testing
+ RW+ = @all
+
+ @g1 = t1
+ repo @g1
+ R = u2
+ RW = u3
+ RW+ = u4
+ RW a/USER/ = @all
+ RW+ p/USER/ = u1 u6
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+
+ gitolite access t1 u1; ok; /refs/heads/p/u1//; !/DENIED/
+ gitolite access t1 u5; !ok; /\\+ any t1 u5 DENIED by fallthru/
+ gitolite access \@g1 u5 W; ok; /refs/heads/a/u5//; !/DENIED/
+
+ gitolite access t1 u1 W refs/heads/a/user1/foo; !ok; /W refs/heads/a/user1/foo t1 u1 DENIED by fallthru/
+ gitolite access \@g1 u1 + refs/heads/a/user1/foo; !ok; /\\+ refs/heads/a/user1/foo \@g1 u1 DENIED by fallthru/
+ gitolite access t1 u1 W refs/heads/p/user1/foo; !ok; /W refs/heads/p/user1/foo t1 u1 DENIED by fallthru/
+ gitolite access \@g1 u1 + refs/heads/p/user1/foo; !ok; /\\+ refs/heads/p/user1/foo \@g1 u1 DENIED by fallthru/
+
+ gitolite access \@g1 u1 W refs/heads/a/u1/foo; ok; /refs/heads/a/u1//; !/DENIED/
+ gitolite access t1 u1 + refs/heads/a/u1/foo; !ok; /\\+ refs/heads/a/u1/foo t1 u1 DENIED by fallthru/
+ gitolite access \@g1 u1 W refs/heads/p/u1/foo; ok; /refs/heads/p/u1//; !/DENIED/
+ gitolite access t1 u1 + refs/heads/p/u1/foo; ok; /refs/heads/p/u1//; !/DENIED/
+
+ gitolite access \@g1 u1 W refs/heads/p/u2/foo; !ok; /W refs/heads/p/u2/foo \@g1 u1 DENIED by fallthru/
+ gitolite access t1 u1 + refs/heads/p/u2/foo; !ok; /\\+ refs/heads/p/u2/foo t1 u1 DENIED by fallthru/
+";
+
+confreset; confadd '
+ @staff = u1 u2 u3 u4 u5 u6
+ @gfoo = foo
+ repo @gfoo
+ RW+ = u1 u2
+ RW+ p/USER/ = u3 u4
+ RW temp = u5 u6
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ DEF OK = gitolite access foo %1 %2 refs/heads/%3; ok
+ DEF NOK = gitolite access foo %1 %2 refs/heads/%3; !ok
+";
+
+try "
+
+# u1 and u2 can push
+ OK u1 W master
+ OK u2 W master
+ OK u2 W p/u1/foo
+ OK u1 W p/u2/foo
+ OK u1 W p/u3/foo
+
+# u3 cant push u1/u4 personal branches
+ NOK u3 W p/u1/foo
+ NOK u3 W p/u4/doo
+
+# u4 can push u4 personal branch
+ OK u4 W p/u4/foo
+# u5 push temp
+ OK u5 W temp
+
+# u1 and u2 can rewind
+ OK u1 + master
+ OK u2 + p/u1/foo
+ OK u1 + p/u2/foo
+ OK u1 + p/u3/foo
+
+# u3 cant rewind u1/u4 personal branches
+ NOK u3 + p/u1/foo
+ NOK u3 + p/u4/foo
+# u4 can rewind u4 personal branch
+ OK u4 + p/u4/foo
+# u5 cant rewind temp
+ NOK u5 + temp
+";
diff --git a/t/reference.t b/t/reference.t
new file mode 100755
index 0000000..fefb5bc
--- /dev/null
+++ b/t/reference.t
@@ -0,0 +1,48 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+my $h = $ENV{HOME};
+
+# fork command
+# ----------------------------------------------------------------------
+
+try "plan 16";
+
+my $rb = `gitolite query-rc -n GL_REPO_BASE`;
+
+try "sed -ie 's%.Mirroring.,%\"Mirroring\",\\n\"create-with-reference\",%' ~/.gitolite.rc";
+
+confreset;confadd '
+
+ repo source
+ RW+ = u1 u2
+
+ repo fork
+ RW+ = u1 u2
+ option reference.repo = source
+
+ repo multifork
+ RW+ = u1 u2
+ option reference.repo-1 = source
+ option reference.repo-2 = fork
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try " # Verify files
+ # source doesn't have alternates
+ ls $rb/source.git/objects/info/alternates; !ok
+
+ # fork has source as an alternate
+ ls $rb/fork.git/objects/info/alternates; ok
+ cat $rb/fork.git/objects/info/alternates; ok; /$rb/source.git/objects/
+
+ # multifork has multiple alternates
+ ls $rb/multifork.git/objects/info/alternates; ok
+ cat $rb/multifork.git/objects/info/alternates; ok; /$rb/source.git/objects/
+ /$rb/fork.git/objects/
+";
diff --git a/t/refex-expr-test-1 b/t/refex-expr-test-1
new file mode 100755
index 0000000..1372a1e
--- /dev/null
+++ b/t/refex-expr-test-1
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+# not part of the official test suite (yet); just some q&d testing
+
+# to be run from ~/gitolite as ./$0
+
+set -e
+exec 3>&2
+exec > /dev/null
+exec 2> /dev/null
+print2() { echo -n "$@" >&3; }
+say2() { echo "$@" >&3; }
+die() { echo FATAL: "$@" >&3; exit 1; }
+
+export od=$PWD
+export tmp=$(mktemp -d)
+echo $tmp >&3
+trap "rm -rf $tmp" 0
+cd $tmp
+
+print2 setting up...
+( cd $od; t/reset )
+echo "push @{ \$RC{ENABLE} }, 'refex-expr';" >> ~/.gitolite.rc
+cat <<EOF >> ~/.gitolite/conf/gitolite.conf
+
+repo r1
+ RW+ = u1 u2 # line 1
+
+ RW+ master = u3 u4 # line 2
+ RW+ = u3 u4 # line 3
+ RW+ VREF/NAME/Makefile = u3 u4 # line 4
+ - master and VREF/NAME/Makefile = u3 u4 # line 5
+
+EOF
+gitolite setup
+say2 done
+
+# ----------------------------------------------------------------------
+
+rm -rf u1
+git clone u1:r1 u1
+cd u1
+tsh 'tc f1'
+git push u1:r1 master
+tsh 'tc f2'
+git push u2:r1 master
+tsh 'tc f3'
+git push u3:r1 master
+tsh 'tc f4'
+git push u4:r1 master
+say2 everyone master no Makefile
+
+tsh 'tc f5 Makefile'
+git push u1:r1 master
+tsh 'tc f5 Makefile'
+git push u1:r1 master:m1
+say2 u1 Makefile master
+
+tsh 'tc f5 Makefile'
+git push u3:r1 master && die u3 r1 master should have failed
+git push u3:r1 master:m2
+say2 u3 Makefile master fail m2 pass
diff --git a/t/refex-expr-test-2 b/t/refex-expr-test-2
new file mode 100755
index 0000000..773e42c
--- /dev/null
+++ b/t/refex-expr-test-2
@@ -0,0 +1,60 @@
+#!/bin/bash
+
+# not part of the official test suite (yet); just some q&d testing
+
+# to be run from ~/gitolite as ./$0
+
+set -e
+exec 3>&2
+exec > /dev/null
+exec 2> /dev/null
+print2() { echo -n "$@" >&3; }
+say2() { echo "$@" >&3; }
+die() { echo FATAL: "$@" >&3; exit 1; }
+
+export od=$PWD
+export tmp=$(mktemp -d)
+echo $tmp >&3
+trap "rm -rf $tmp" 0
+cd $tmp
+
+print2 setting up...
+( cd $od; t/reset )
+echo "push @{ \$RC{ENABLE} }, 'refex-expr';" >> ~/.gitolite.rc
+cat <<EOF >> ~/.gitolite/conf/gitolite.conf
+
+ repo r2
+ RW+ = @all
+
+ RW+ VREF/NAME/doc/ = u2
+ RW+ VREF/NAME/src/ = u2
+ - VREF/NAME/doc/ and VREF/NAME/src/ = u2
+
+EOF
+gitolite setup
+say2 done
+
+# ----------------------------------------------------------------------
+
+git clone u2:r2
+cd r2
+
+tsh 'tc aa'
+git push origin master
+say2 aa pass
+
+mkdir doc src
+
+tsh 'tc doc/d1'
+git push origin master
+say2 doc pass
+
+tsh 'tc src/s1'
+tsh 'tc src/s2'
+git push origin master
+say2 src src pass
+
+tsh 'tc doc/d2 src/s3'
+git push origin master && die 1
+git push u1:r2 master
+say2 doc src u2 fail u1 pass
diff --git a/t/refex-expr-test-3 b/t/refex-expr-test-3
new file mode 100755
index 0000000..47599eb
--- /dev/null
+++ b/t/refex-expr-test-3
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+# not part of the official test suite (yet); just some q&d testing
+
+# to be run from ~/gitolite as ./$0
+
+set -e
+exec 3>&2
+exec > /dev/null
+exec 2> /dev/null
+print2() { echo -n "$@" >&3; }
+say2() { echo "$@" >&3; }
+die() { echo FATAL: "$@" >&3; exit 1; }
+
+export od=$PWD
+export tmp=$(mktemp -d)
+echo $tmp >&3
+trap "rm -rf $tmp" 0
+cd $tmp
+
+print2 setting up...
+( cd $od; t/reset )
+echo "push @{ \$RC{ENABLE} }, 'refex-expr';" >> ~/.gitolite.rc
+cat <<EOF >> ~/.gitolite/conf/gitolite.conf
+
+ repo r3
+ RW+ = u1 u2 u3
+
+ RW+ VREF/NAME/conf/ = u3
+ - VREF/NAME/conf/ -gt 2 = u3
+
+EOF
+gitolite setup
+say2 done
+
+# ----------------------------------------------------------------------
+
+git clone u3:r3
+cd r3
+
+tsh 'tc aa'
+git push origin master
+say2 aa pass
+
+mkdir doc conf
+
+tsh 'tc doc/d1 doc/d2 doc/d3 doc/d4 conf/c1'
+git push origin master
+say2 4 doc 1 conf pass
+
+tsh 'tc conf/c2 conf/c3 conf/c4'
+git push origin master && die 1
+
+git push u2:r3 master
+say2 3 conf u3 fail u2 pass
diff --git a/t/refex-expr-test-9 b/t/refex-expr-test-9
new file mode 100755
index 0000000..b3a9f09
--- /dev/null
+++ b/t/refex-expr-test-9
@@ -0,0 +1,107 @@
+#!/bin/bash
+
+# not part of the official test suite (yet); just some q&d testing
+
+# to be run from ~/gitolite as ./$0
+
+set -e
+exec 3>&2
+exec > /dev/null
+exec 2> /dev/null
+print2() { echo -n "$@" >&3; }
+say2() { echo "$@" >&3; }
+die() { echo FATAL: "$@" >&3; exit 1; }
+
+export od=$PWD
+export tmp=$(mktemp -d)
+echo $tmp >&3
+trap "rm -rf $tmp" 0
+cd $tmp
+
+print2 setting up...
+( cd $od; t/reset )
+echo "push @{ \$RC{ENABLE} }, 'refex-expr';" >> ~/.gitolite.rc
+cat <<EOF >> ~/.gitolite/conf/gitolite.conf
+
+repo r9
+
+ RW+ = u3 u4
+
+ # u1 and u2 have some restrictions
+
+ # cant push master
+ - master = u1 u2
+ # cant push versioned tags, but other tags are fine
+ - refs/tags/v[0-9] = u1 u2
+ # everything else is fine, but we need to recognise when they're pushing
+ # tags, so that the refex expr will have the correct info
+ RW+ refs/tags/ = u1 u2
+ RW+ = u1 u2
+
+ # can push files in "foo/" only to a tag
+ RW+ VREF/NAME/foo/ = u1 u2
+
+ RW+ VREF/NAME/foo/ and refs/tags/ = u1 u2
+ - VREF/NAME/foo/ and not refs/tags/ = u1 u2
+
+EOF
+gitolite setup
+say2 done
+
+# ----------------------------------------------------------------------
+
+# make sure u3 is not constrained in any way
+
+git clone u3:r9 refex-test.repo
+cd refex-test.repo
+
+tsh 'tc u3-f1'
+git pom
+
+mkdir bar foo
+tsh 'tc bar/thr'
+git pom
+git tag v3
+git push origin v3
+tsh 'tc foo/rht'
+git pom
+git tag 3v
+git push origin 3v
+
+say2 u3 no limits
+
+# now test u1's constraints
+
+cd ..
+rm -rf refex-test.repo
+
+rm -rf ~/repositories/r9.git
+gitolite setup
+
+git clone u1:r9 refex-test.repo
+cd refex-test.repo
+
+tsh 'tc u1-f1'
+# cant push master
+git pom && die 1
+# can push other branches
+git push origin master:m1
+say2 master fail m1 pass
+
+mkdir bar foo
+tsh 'tc bar/one'
+git push origin master:m1
+git tag v1
+# cant push v-tag
+git push origin v1 && die 2
+say2 v-tag fail
+
+# cant push foo/ to a branch
+tsh 'tc foo/eno'
+git push origin master:m1 && die 3
+say2 foo/ m1 fail
+
+# but can push to a non-v-tag
+git tag 1v
+git push origin 1v
+say2 foo/ non-v-tag pass
diff --git a/t/repo-specific-hooks.t b/t/repo-specific-hooks.t
new file mode 100755
index 0000000..90a23c6
--- /dev/null
+++ b/t/repo-specific-hooks.t
@@ -0,0 +1,231 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# test script for partial copy feature
+# ----------------------------------------------------------------------
+
+try "plan 128";
+my $h = $ENV{HOME};
+my $rb = `gitolite query-rc -n GL_REPO_BASE`;
+
+try 'cd tsh_tempdir; mkdir -p local/hooks/repo-specific';
+
+foreach my $h (qw/first second/) {
+ put "local/hooks/repo-specific/$h", "#!/bin/sh
+echo \$0
+if [ \$# -ne 0 ]; then
+ echo \$0 has args: \$@
+else
+ echo \$0 has stdin: `cat`
+fi
+";
+}
+try 'chmod +x local/hooks/repo-specific/*';
+
+try 'pwd';
+my $tempdir = join("\n", sort (lines()));
+try 'cd gitolite-admin';
+
+try "# Enable LOCAL_CODE and repo-specific-hooks
+ cat $h/.gitolite.rc
+ perl s/# 'repo-specific-hooks'/'repo-specific-hooks'/
+ perl s%# LOCAL_CODE%LOCAL_CODE => '$tempdir/local', #%
+ put $h/.gitolite.rc
+";
+
+confreset;confadd '
+ repo foo
+ RW+ = @all
+
+ repo bar
+ RW+ = @all
+
+ repo baz
+ RW+ = @all
+
+ repo frob
+ RW+ = @all
+';
+
+try "ADMIN_PUSH repo-specific-hooks-0; !/FATAL/" or die text();
+
+try "
+ /Init.*empty.*foo\\.git/
+ /Init.*empty.*bar\\.git/
+ /Init.*empty.*baz\\.git/
+ /Init.*empty.*frob\\.git/
+";
+
+my $failing_hook = "#!/bin/sh
+exit 1
+";
+
+# Place a existing hooks in repos
+put "$rb/foo.git/hooks/post-recieve", $failing_hook;
+put "$rb/bar.git/hooks/pre-recieve", $failing_hook;
+put "$rb/baz.git/hooks/post-update", $failing_hook;
+put "$rb/frob.git/hooks/post-update", $failing_hook;
+
+try "# Verify hooks
+ ls -l $rb/foo.git/hooks/*; ok; !/post-receive -. .*local/hooks/multi-hook-driver/
+ ls -l $rb/bar.git/hooks/*; ok; !/pre-receive -. .*local/hooks/multi-hook-driver/
+ ls -l $rb/baz.git/hooks/*; ok; !/post-update -. .*local/hooks/multi-hook-driver/
+ ls -l $rb/frob.git/hooks/*; ok; !/post-update -. .*local/hooks/multi-hook-driver/
+";
+
+confreset;confadd '
+ repo foo
+ RW+ = @all
+ option hook.post-receive = first
+
+ repo bar
+ RW+ = @all
+ option hook.pre-receive = first second
+
+ repo baz
+ RW+ = @all
+ option hook.post-receive = first
+ option hook.post-update = first second
+
+ repo frob
+ RW+ = @all
+ option hook.post-receive.b = first
+ option hook.post-receive.a = second
+
+ repo gitolite-admin
+ option hook.post-receive = second
+';
+
+
+try "ADMIN_PUSH repo-specific-hooks-1; !/FATAL/" or die text();
+
+try "# Verify hooks
+ ls -l $rb/foo.git/hooks/*; ok; /post-receive.h00-first/
+ !/post-receive.h01/
+ /post-receive -. .*local/hooks/multi-hook-driver/
+ ls -l $rb/bar.git/hooks/*; ok; /pre-receive.h00-first/
+ /pre-receive.h01-second/
+ /pre-receive -. .*local/hooks/multi-hook-driver/
+ ls -l $rb/baz.git/hooks/*; ok; /post-receive.h00-first/
+ /post-update.h00-first/
+ /post-update.h01-second/
+ /post-update -. .*local/hooks/multi-hook-driver/
+ ls -l $rb/frob.git/hooks/*; ok; /post-receive.h00-second/
+ /post-receive.h01-first/
+ /post-receive -. .*local/hooks/multi-hook-driver/
+ ls -l $rb/gitolite-admin.git/hooks/*
+ ok; /post-receive.h/
+ /post-receive -. .*local/hooks/multi-hook-driver/
+ !/post-update -. .*local/hooks/multi-hook-driver/
+";
+
+try "
+ cd ..
+
+ # Single hook still works
+ [ -d foo ]; !ok;
+ CLONE admin foo; ok; /empty/; /cloned/
+ cd foo
+ tc a1; ok; /ee47f8b/
+ PUSH admin master; ok; /new.*master -. master/
+ /hooks/post-receive.h00-first/
+ !/post-receive.*has args:/
+ /post-receive.h00-first has stdin: 0000000000000000000000000000000000000000 ee47f8b6be2160ad1a3f69c97a0cb3d488e6657e refs/heads/master/
+
+ cd ..
+
+ # Multiple hooks fired
+ [ -d bar ]; !ok;
+ CLONE admin bar; ok; /empty/; /cloned/
+ cd bar
+ tc a2; ok; /cfc8561/
+ PUSH admin master; ok; /new.*master -. master/
+ /hooks/pre-receive.h00-first/
+ !/hooks/pre-recieve.*has args:/
+ /hooks/pre-receive.h00-first has stdin: 0000000000000000000000000000000000000000 cfc8561c7827a8b94df6c5dad156383d4cb210f5 refs/heads/master/
+ /hooks/pre-receive.h01-second/
+ !/hooks/pre-receive.h01.*has args:/
+ /hooks/pre-receive.h01-second has stdin: 0000000000000000000000000000000000000000 cfc8561c7827a8b94df6c5dad156383d4cb210f5 refs/heads/master/
+
+ cd ..
+
+ # Post-update has stdin instead of arguments
+ [ -d baz ]; !ok;
+ CLONE admin baz; ok; /empty/; /cloned/
+ cd baz
+ tc a3; ok; /2863617/
+ PUSH admin master; ok; /new.*master -. master/
+ /hooks/post-receive.h00-first/
+ !/hooks/post-receive.h00.*has args:/
+ /hooks/post-receive.h00-first has stdin: 0000000000000000000000000000000000000000 28636171ae703f42fb17c312c6b6a078ed07a2cd refs/heads/master/
+ /hooks/post-update.h00-first/
+ /hooks/post-update.h00-first has args: refs/heads/master/
+ !/hooks/post-update.h00.*has stdin:/
+ /hooks/post-update.h01-second/
+ /hooks/post-update.h01-second has args: refs/heads/master/
+ !/hooks/post-update.h01.*has stdin:/
+";
+
+# Verify hooks are removed properly
+
+confadd '
+ repo foo
+ RW+ = @all
+ option hook.post-receive = ""
+
+ repo bar
+ RW+ = @all
+ option hook.pre-receive = second
+
+ repo baz
+ RW+ = @all
+ option hook.post-receive = ""
+ option hook.post-update = second
+';
+
+try "ADMIN_PUSH repo-specific-hooks-02; !/FATAL/" or die text();
+
+try "
+ ls $rb/foo.git/hooks/*; ok; !/post-receive.h0/
+ ls $rb/bar.git/hooks/*; ok; !/pre-receive.*first/
+ /pre-receive.h00-second/
+ ls $rb/baz.git/hooks/*; ok; !/post-receive.h0/
+ !/post-update.*first/
+ /post-update.h00-second/
+";
+
+try "
+ cd ..
+
+ # Foo has no hooks
+ cd foo
+ tc b1; ok; /7ef69de/
+ PUSH admin master; ok; /master -. master/
+ !/hooks/post-receive/
+
+ cd ..
+
+ # Bar only has the second hook
+ cd bar
+ tc b2; ok; /cc7808f/
+ PUSH admin master; ok; /master -. master/
+ /hooks/pre-receive.h00-second/
+ !/hooks/pre-receive.*has args:/
+ /hooks/pre-receive.h00-second has stdin: cfc8561c7827a8b94df6c5dad156383d4cb210f5 cc7808f77c7c7d705f82dc54dc3152146175768f refs/heads/master/
+
+ cd ..
+
+ # Baz has no post-receive and keeps the second hook for post-update
+ cd baz
+ tc b3; ok; /8d20101/
+ PUSH admin master; ok; /master -. master/
+ !/hooks/post-receive.*/
+ /hooks/post-update.h00-second/
+ /hooks/post-update.h00-second has args: refs/heads/master/
+ !/hooks/post-update.*has stdin/
+";
diff --git a/t/reset b/t/reset
new file mode 100755
index 0000000..502de2b
--- /dev/null
+++ b/t/reset
@@ -0,0 +1,33 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+BEGIN {
+ unlink "$ENV{HOME}/.ssh/authorized_keys";
+}
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+use Cwd;
+my $workdir = getcwd();
+
+confreset;confadd '
+repo foo/..*
+ C = u1 u2 u3
+ RW+ = CREATOR
+ RW = WRITERS
+ R = READERS
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ mkdir -p keydir
+ cp \$HOME/.ssh/u*.pub keydir
+ cp \$HOME/.ssh/admin.pub keydir
+ git add keydir
+ git commit -m 6k
+ glt push admin origin
+";
diff --git a/t/rule-seq.t b/t/rule-seq.t
new file mode 100755
index 0000000..0d97558
--- /dev/null
+++ b/t/rule-seq.t
@@ -0,0 +1,87 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# rule sequence
+# ----------------------------------------------------------------------
+
+# this is the specific example in commit 32056e0 of g2
+
+try "plan 27";
+
+try "DEF POK = !/DENIED/; !/failed to push/";
+
+confreset; confadd '
+ @private-owners = u1 u2
+ @experienced-private-owners = u3 u4
+
+ repo CREATOR/.*
+ C = @private-owners @experienced-private-owners
+ RWD = CREATOR
+ RW = WRITERS
+ R = READERS
+ - = @private-owners
+ RW+D = CREATOR
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd ..
+ glt clone u1 file:///u1/r1
+ /Initialized empty Git repository in .*/u1/r1.git//
+ cd r1
+ tc h-395
+ glt push u1 origin master
+ git checkout -b br1
+ tc m-367
+ tc i-747
+
+ # u1 create branch
+ glt push u1 origin br1
+ /\\* \\[new branch\\] br1 -> br1/
+ POK; /br1 -> br1/
+
+ # u1 rewind branch
+ git reset --hard HEAD^
+ tc e-633
+ glt push u1 origin +br1
+ /\\+ refs/heads/br1 u1/r1 u1 DENIED by refs//
+ /error: hook declined to update refs/heads/br1/
+ reject
+
+ # u1 delete branch
+ glt push u1 origin :br1
+ /\\[deleted\\] br1/
+
+ cd ..
+ rm -rf r1
+ glt clone u3 file:///u3/r1
+ /Initialized empty Git repository in .*/u3/r1.git//
+ cd r1
+ tc p-274
+ glt push u3 origin master
+ git checkout -b br1
+ tc s-613
+ tc k-988
+
+ # u3 create branch
+ glt push u3 origin br1
+ /\\* \\[new branch\\] br1 -> br1/
+ POK; /br1 -> br1/
+
+ # u3 rewind branch
+ git reset --hard HEAD^
+ tc n-919
+ glt push u3 origin +br1
+ /To file:///u3/r1/
+ /\\+ .......\\.\\.\\........ br1 -> br1 \\(forced update\\)/
+
+ # u3 delete branch
+ glt push u3 origin :br1
+ /\\[deleted\\] br1/
+";
diff --git a/t/sequence.t b/t/sequence.t
new file mode 100755
index 0000000..8d66d03
--- /dev/null
+++ b/t/sequence.t
@@ -0,0 +1,116 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# uhh, seems to be another rule sequence test
+# ----------------------------------------------------------------------
+
+try "plan 48";
+
+confreset;confadd '
+ @staff = u1 u2 u3
+ @gfoo = foo/CREATOR/..*
+ repo @gfoo
+ C = u1
+ RW+ = CREATOR
+ RW = WRITERS
+ - = @staff
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd ..
+ glt clone u1 file:///foo/u1/bar; ok
+ /Initialized empty Git repository in .*/foo/u1/bar.git//
+
+ cd bar
+ tc p-906
+ glt push u1 origin master
+ /To file:///foo/u1/bar/
+ /\\[new branch\\] master -> master/
+ echo WRITERS u2 | glt perms u1 -c foo/u1/bar
+ glt perms u1 foo/u1/bar -l
+ /WRITERS u2/
+ # expand
+ glt info u2
+ /R W *\tfoo/u1/bar/
+ /R W *\ttesting/
+
+ # push
+ cd ..
+ glt clone u2 file:///foo/u1/bar u2bar
+ /Cloning into 'u2bar'.../
+ cd u2bar
+ tc p-222
+ glt push u2
+ /master -> master/
+ !/DENIED/
+ !/failed to push/
+";
+
+confreset;confadd '
+ @staff = u1 u2 u3
+ @gfoo = foo/CREATOR/..*
+ repo @gfoo
+ C = u1
+ RW+ = CREATOR
+ - = @staff
+ RW = WRITERS
+ R = READERS
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ cd ..
+ rm -rf bar u2bar
+ glt clone u1 file:///foo/u1/bar; ok
+ /Initialized empty Git repository in .*/foo/u1/bar.git//
+
+ cd bar
+ tc p-906
+ glt push u1 origin master
+ /To file:///foo/u1/bar/
+ /\\[new branch\\] master -> master/
+ echo WRITERS u2 | glt perms u1 -c foo/u1/bar
+ glt perms u1 foo/u1/bar -l
+ /WRITERS u2/
+ # expand
+ glt info u2
+ !/R W *\tfoo/u1/baz/
+ /R W *\tfoo/u1/bar/
+ /R W *\ttesting/
+
+ # push
+ cd ..
+ glt clone u2 file:///foo/u1/bar u2bar
+ /Cloning into 'u2bar'.../
+ cd u2bar
+ tc p-222
+ glt push u2
+ !ok
+ reject
+ /W refs/heads/master foo/u1/bar u2 DENIED by refs/\\.\\*/
+
+ # auto-create using perms fail
+ echo READERS u5 | glt perms u4 -c foo/u4/baz
+ !/Initialized empty Git repository in .*/foo/u4/baz.git/
+ /FATAL: repo does not exist, or you are not authorised/
+
+ # auto-create using perms
+ echo READERS u2 | glt perms u1 -c foo/u1/baz
+ /Initialized empty Git repository in .*/foo/u1/baz.git/
+
+ glt perms u1 foo/u1/baz -l
+ /READERS u2/
+ # expand
+ glt info u2
+ /R *\tfoo/u1/baz/
+ /R W *\tfoo/u1/bar/
+ /R W *\ttesting/
+";
diff --git a/t/smart-http b/t/smart-http
new file mode 100755
index 0000000..3ac910b
--- /dev/null
+++ b/t/smart-http
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+die() { echo "$@"; exit 1; }
+
+# git clone `url u1 r1`
+url() {
+ echo http://$1:$1@localhost/git/$2.git
+}
+
+# `cmd sitaram info`
+cmd() {
+ c="curl http://$1:$1@localhost/git"
+ shift
+ c="$c/$1"
+ shift
+
+ if [ -n "$1" ]
+ then
+ c="$c?$1"
+ shift
+ fi
+ while [ -n "$1" ]
+ do
+ c="$c+$1"
+ shift
+ done
+
+ echo $c
+}
+
+export tmp=$(mktemp -d);
+trap "rm -rf $tmp" 0;
+cd $tmp
+
+tsh "plan 28"
+
+tsh "
+ ## ls-remote admin admin
+ git ls-remote `url admin gitolite-admin`
+ ok
+ /HEAD/
+ /refs.heads.master/
+ ## clone
+ git clone `url admin gitolite-admin`
+ ok
+ /Cloning into/
+ ls -al gitolite-admin/conf
+ /gitolite.conf/
+" || die "step 1"
+
+cd gitolite-admin
+echo repo t2 >> conf/gitolite.conf
+echo 'RW+ = u1 u2' >> conf/gitolite.conf
+
+tsh "
+ ## add, commit, push
+ git add conf/gitolite.conf
+ ok
+ !/./
+ git commit -m t2
+ ok
+ /1 file.*changed/
+ git push
+ ok
+ /Initialized.*gitolite-home.repositories.t2.git/
+ /To http:..localhost.git.gitolite-admin.git/
+ /master -. master/
+ ## various ls-remotes
+ git ls-remote `url u1 gitolite-admin`
+ !ok
+ /FATAL: R any gitolite-admin u1 DENIED by fallthru/
+ git ls-remote `url u1 t2`
+ ok
+ !/./
+ git ls-remote `url u2 t2`
+ ok
+ !/./
+ git ls-remote `url u3 t2`
+ !ok
+ /FATAL: R any t2 u3 DENIED by fallthru/
+ ## push to u1:t2
+ git push `url u1 t2` master:master
+ ok
+ /To http:..localhost.git.t2.git/
+ /master -. master/
+ git ls-remote `url u2 t2`
+ ok
+ /HEAD/
+ /refs.heads.master/
+" || die "step 2"
diff --git a/t/smart-http.root-setup b/t/smart-http.root-setup
new file mode 100755
index 0000000..f22dcb5
--- /dev/null
+++ b/t/smart-http.root-setup
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+# ----------------------------------------------------------------------
+# please do not even LOOK at this file without reading doc/http.mkd
+# ----------------------------------------------------------------------
+
+die() { echo "$@"; exit 1; }
+
+# scare the sh*t out of people who run it blindly
+[ -f /tmp/gitolite-smart-http-test-OK ] || {
+ # scary message
+ echo '+ rm -rf /'
+ # lots of disk activity
+ find / >/dev/null 2>/dev/null
+ # and it he's still clueless, God bless!
+ echo 'root file system erased successfully. Goodbye and God bless!'
+ exit 1
+}
+
+# ----------------------------------------------------------------------
+# are we *BSD or Linux?
+uname_s=`uname -s` # could be Linux or FreeBSD or some other BSD
+if [ "$uname_s" = "Linux" ]
+then
+ bsd=:
+else
+ lnx=:
+fi
+
+# ----------------------------------------------------------------------
+# main
+
+[ $EUID = 0 ] || die "you must run this as root"
+
+# delete any existing apache conf for gitolite
+$lnx rm /etc/httpd/conf.d/gitolite.conf
+$bsd rm /usr/local/etc/apache24/Includes/gitolite.conf
+
+# build your "home within a home"
+$lnx cd ~apache
+$bsd rm -rf /tmp/usr.share.httpd
+$bsd mkdir -p /tmp/usr.share.httpd
+$bsd chown www:www /tmp/usr.share.httpd
+$bsd cd /tmp/usr.share.httpd
+
+rm -rf gitolite-home
+mkdir gitolite-home
+export GITOLITE_HTTP_HOME=$PWD/gitolite-home
+
+# get the gitolite sources
+cd gitolite-home
+git clone /tmp/gitolite.git gitolite-source
+# NOTE: I use a bare repo in /tmp for convenience; you'd use
+# 'https://github.com/sitaramc/gitolite'
+
+# make the bin directory, and add it to PATH
+cd gitolite-source
+mkdir $GITOLITE_HTTP_HOME/bin
+./install -ln $GITOLITE_HTTP_HOME/bin
+export PATH=$PATH:$GITOLITE_HTTP_HOME/bin
+
+# come back to base, then run setup. Notice that you have to point HOME to
+# the right place, even if it is just for this command
+cd $GITOLITE_HTTP_HOME
+HOME=$GITOLITE_HTTP_HOME gitolite setup -a admin
+
+# insert some essential lines at the beginning of the rc file
+echo '$ENV{PATH} .= ":$ENV{GITOLITE_HTTP_HOME}/bin";' >> 1
+echo >> 1
+cat .gitolite.rc >> 1
+\mv 1 .gitolite.rc
+
+# fix up ownership
+$lnx chown -R apache:apache $GITOLITE_HTTP_HOME
+$bsd chown -R www:www $GITOLITE_HTTP_HOME
+
+# create the apache config. Note the trailing slashes on the 2 ScriptAlias
+# lines. (The second one is optional for most sites). NOTE: you also need to
+# give the AuthUserFile a better name/location than what I have below.
+cat <<EOF1 > 1
+SetEnv GIT_PROJECT_ROOT $GITOLITE_HTTP_HOME/repositories
+ScriptAlias /git/ $GITOLITE_HTTP_HOME/gitolite-source/src/gitolite-shell/
+ScriptAlias /gitmob/ $GITOLITE_HTTP_HOME/gitolite-source/src/gitolite-shell/
+SetEnv GITOLITE_HTTP_HOME $GITOLITE_HTTP_HOME
+SetEnv GIT_HTTP_EXPORT_ALL
+
+<Location /git>
+ AuthType Basic
+ AuthName "Private Git Access"
+ Require valid-user
+ AuthUserFile $GITOLITE_HTTP_HOME/gitolite-http-authuserfile
+</Location>
+EOF1
+$lnx mv 1 /etc/httpd/conf.d/gitolite.conf
+$bsd mv 1 /usr/local/etc/apache24/Includes/gitolite.conf
+
+# NOTE: this is for testing only
+htpasswd -bc $GITOLITE_HTTP_HOME/gitolite-http-authuserfile admin admin
+map "htpasswd -b $GITOLITE_HTTP_HOME/gitolite-http-authuserfile % %" u{1..6}
+$lnx chown apache:apache $GITOLITE_HTTP_HOME/gitolite-http-authuserfile
+$bsd chown www:www $GITOLITE_HTTP_HOME/gitolite-http-authuserfile
+
+# restart httpd to make it pick up all the new stuff
+$lnx service httpd restart
+$bsd /usr/local/etc/rc.d/apache24 restart
diff --git a/t/ssh-authkeys.t b/t/ssh-authkeys.t
new file mode 100755
index 0000000..46b9413
--- /dev/null
+++ b/t/ssh-authkeys.t
@@ -0,0 +1,77 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# testing the (separate) authkeys handler
+# ----------------------------------------------------------------------
+
+$ENV{GL_BINDIR} = "$ENV{PWD}/src";
+
+my $ak = "$ENV{HOME}/.ssh/authorized_keys";
+mkdir("$ENV{HOME}/.ssh", 0700) if not -d "$ENV{HOME}/.ssh";
+my $kd = `gitolite query-rc -n GL_ADMIN_BASE` . "/keydir";
+
+try "plan 49";
+
+my $pgm = "gitolite ../triggers/post-compile/ssh-authkeys";
+
+try "
+ # prep
+ rm -rf $ak; ok
+
+ $pgm; ok
+ mkdir $kd; ok
+ cd $kd; ok
+ $pgm; ok; /authorized_keys missing/
+ /creating/
+ wc < $ak; ok; /2 *6 *32/
+ # some gl keys
+ ssh-keygen -N '' -q -f alice -C alice
+ ssh-keygen -N '' -q -f bob -C bob
+ ssh-keygen -N '' -q -f carol -C carol
+ ssh-keygen -N '' -q -f dave -C dave
+ ssh-keygen -N '' -q -f eve -C eve
+ rm alice bob carol dave eve
+ ls -a; ok; /alice.pub/; /bob.pub/; /carol.pub/; /dave.pub/; /eve.pub/
+ $pgm; ok;
+ wc < $ak; ok; /^ *7 .*/;
+ grep gitolite $ak; ok; /start/
+ /end/
+
+ # some normal keys
+ mv alice.pub $ak; ok
+ cat carol.pub >> $ak; ok
+ $pgm; ok; /carol.pub duplicates.*non-gitolite key/
+ wc < $ak; ok; /^ *8 .*/;
+
+ # moving normal keys up
+ mv dave.pub dave
+ $pgm; ok
+ cat dave >> $ak; ok
+ grep -n dave $ak; ok; /8:ssh-rsa/
+ mv dave dave.pub
+ $pgm; ok; /carol.pub duplicates.*non-gitolite key/
+ /dave.pub duplicates.*non-gitolite key/
+ grep -n dave $ak; ok; /3:ssh-rsa/
+
+ # a bad key
+ ls -al > bad.pub
+ $pgm; !ok; /fingerprinting failed for \\'keydir/bad.pub\\'/
+ wc < $ak; ok; /^ *9 .*/;
+ # a good key doesn't get added
+ ssh-keygen -N '' -q -f good
+ $pgm; !ok; /fingerprinting failed for \\'keydir/bad.pub\\'/
+ wc < $ak; ok; /^ *9 .*/;
+ # till the bad key is removed
+ rm bad.pub
+ $pgm; ok;
+ wc < $ak; ok; /^ *10 .*/;
+
+ # duplicate gl key
+ cp bob.pub robert.pub
+ $pgm; ok; /robert.pub duplicates.*bob.pub/
+";
diff --git a/t/ssh-basic.t b/t/ssh-basic.t
new file mode 100755
index 0000000..ebed2d2
--- /dev/null
+++ b/t/ssh-basic.t
@@ -0,0 +1,57 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Common;
+use Gitolite::Test;
+
+# basic tests using ssh
+# ----------------------------------------------------------------------
+
+my $bd = `gitolite query-rc -n GL_BINDIR`;
+my $h = $ENV{HOME};
+my $ab = `gitolite query-rc -n GL_ADMIN_BASE`;
+umask 0077;
+
+try "
+ plan 26
+
+ # reset stuff
+ rm -f $h/.ssh/authorized_keys; ok or die 1
+
+ cp $bd/../t/keys/u[1-6]* $h/.ssh; ok or die 2
+ cp $bd/../t/keys/admin* $h/.ssh; ok or die 3
+ cp $bd/../t/keys/config $h/.ssh; ok or die 4
+ cat $h/.ssh/config
+ perl s/%USER/$ENV{USER}/
+ put $h/.ssh/config
+
+ mkdir $ab/keydir; ok or die 5
+ cp $bd/../t/keys/*.pub $ab/keydir; ok or die 6
+";
+
+system("gitolite ../triggers/post-compile/ssh-authkeys");
+
+# basic tests
+# ----------------------------------------------------------------------
+
+confreset; confadd '
+ @g1 = u1
+ @g2 = u2
+ repo foo
+ RW = @g1 u3
+ R = @g2 u4
+';
+
+try "ADMIN_PUSH set3; !/FATAL/" or die text();
+
+try "
+ ssh u1 info; ok; /R W\tfoo/
+ ssh u2 info; ok; /R \tfoo/
+ ssh u3 info; ok; /R W\tfoo/
+ ssh u4 info; ok; /R \tfoo/
+ ssh u5 info; ok; !/foo/
+ ssh u6 info; ok; !/foo/
+"
diff --git a/t/templates.t b/t/templates.t
new file mode 100755
index 0000000..a705167
--- /dev/null
+++ b/t/templates.t
@@ -0,0 +1,1322 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use 5.10.0;
+use Data::Dumper;
+
+# this is hardcoded; change it if needed
+use lib "$ENV{PWD}/src/lib";
+
+use Gitolite::Test;
+
+BEGIN {
+ $ENV{G3T_RC} = "$ENV{HOME}/g3trc";
+ put "$ENV{G3T_RC}", "\$rc{ROLES} = {
+ FORCERS => 1,
+ MASTERS => 1,
+ READERS => 1,
+ ROOT => 1,
+ TEAM => 1,
+ WRITERS => 1
+ }";
+}
+
+use Gitolite::Rc;
+use Gitolite::Common;
+use Gitolite::Conf::Load;
+
+# permissions using role names
+# ----------------------------------------------------------------------
+
+try "plan 1163";
+try "DEF POK = !/DENIED/; !/failed to push/";
+
+# basic push admin repo
+confreset; confadd '
+# order is important for these next few repo group definitions, because an
+# individual repo may pick and choose any combination of them, and they should
+# apply sensibly. In this example, "BASE" is pretty much required; the others
+# are optional.
+
+# if you want someone to have "ultimate" power over all refs in the repo,
+# add them to the ROOT role.
+repo @BASE
+ RW+CD = ROOT
+
+# add this to the repo group list to allow personal branches
+repo @PERSONAL
+ RW+CD dev/USER/ = TEAM
+ - dev/ = TEAM
+ RW+CD refs/tags/dev/USER/ = TEAM
+ - refs/tags/dev/ = TEAM
+
+# add this to the repo group list to control tagging for release versions
+repo @RELEASES
+ RWC refs/tags/v[0-9] = RELEASERS
+ - refs/tags/v[0-9] = @all
+
+# (the basic set of access rules continues)
+repo @BASE
+ # Note that "FORCERS" here, even though they have RW+CD,
+ # 1. cannot touch other users personal branches or tags if you added
+ # PER_BR to the repo group list, and
+ # 2. create a release tag unless they are also in RELEASE_TAGGERS if
+ # you added TAGS to the repo group list
+ RW+CD = FORCERS
+ RWC master = MASTERS
+ - master = @all
+ RWC = RELEASERS MASTERS WRITERS
+ # Note you can define "@all" to have the READERS role, and then this will
+ # effectively be public (albeit authenticated public) readable.
+ R = READERS
+
+=begin template-data
+
+repo base = BASE
+ FORCERS = u1
+ MASTERS = u2
+ WRITERS = u3
+ READERS = u4
+
+repo baseroot = BASE
+ ROOT = admin
+ FORCERS = u1
+ MASTERS = u2
+ WRITERS = u3
+ READERS = u4
+
+repo basepers = BASE PERSONAL
+ FORCERS = u1
+ MASTERS = u2
+ WRITERS = u3
+ READERS = u4 u5
+ TEAM = u1 u2 u3 u5 u6
+
+repo baserel = BASE RELEASES
+ FORCERS = u1
+ MASTERS = u2
+ WRITERS = u3
+ READERS = u4 u5
+ TEAM = u1 u2 u3 u5 u6
+
+repo baseall = BASE PERSONAL RELEASES
+ ROOT = admin
+ FORCERS = u1
+ MASTERS = u2
+ WRITERS = u3
+ READERS = u4 u5
+ TEAM = u1 u2 u3 u5 u6
+
+=end
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+# now we step outside tsh, into pure perl
+
+sub _access {
+ push @_, 'any' if @_ < 4;
+ my $ref = pop;
+ $ref =~ s(^)(refs/heads/) if $ref ne 'any' and $ref !~ m(^(refs|VREF)/);
+ push @_, $ref;
+
+ return access(@_);
+}
+
+sub ok {
+ say STDOUT (_access(@_) !~ /DENIED/ ? "ok" : "not ok");
+}
+sub nok {
+ say STDOUT (_access(@_) =~ /DENIED/ ? "ok" : "not ok");
+}
+
+nok qw( base admin R );
+nok qw( base admin W master );
+nok qw( base admin W notmaster );
+nok qw( base admin W refs/tags/boo );
+nok qw( base admin W refs/tags/v1 );
+nok qw( base admin W dev/admin/foo );
+nok qw( base admin W refs/tags/dev/admin/foo );
+nok qw( base admin W dev/alice/foo );
+nok qw( base admin W refs/tags/dev/alice/foo );
+nok qw( base admin + master );
+nok qw( base admin + notmaster );
+nok qw( base admin + refs/tags/boo );
+nok qw( base admin + refs/tags/v1 );
+nok qw( base admin + dev/admin/foo );
+nok qw( base admin + refs/tags/dev/admin/foo );
+nok qw( base admin + dev/alice/foo );
+nok qw( base admin + refs/tags/dev/alice/foo );
+nok qw( base admin C master );
+nok qw( base admin C notmaster );
+nok qw( base admin C refs/tags/boo );
+nok qw( base admin C refs/tags/v1 );
+nok qw( base admin C dev/admin/foo );
+nok qw( base admin C refs/tags/dev/admin/foo );
+nok qw( base admin C dev/alice/foo );
+nok qw( base admin C refs/tags/dev/alice/foo );
+nok qw( base admin D master );
+nok qw( base admin D notmaster );
+nok qw( base admin D refs/tags/boo );
+nok qw( base admin D refs/tags/v1 );
+nok qw( base admin D dev/admin/foo );
+nok qw( base admin D refs/tags/dev/admin/foo );
+nok qw( base admin D dev/alice/foo );
+nok qw( base admin D refs/tags/dev/alice/foo );
+
+ok qw( base u1 R );
+ok qw( base u1 W master );
+ok qw( base u1 W notmaster );
+ok qw( base u1 W refs/tags/boo );
+ok qw( base u1 W refs/tags/v1 );
+ok qw( base u1 W dev/u1/foo );
+ok qw( base u1 W refs/tags/dev/u1/foo );
+ok qw( base u1 W dev/alice/foo );
+ok qw( base u1 W refs/tags/dev/alice/foo );
+ok qw( base u1 + master );
+ok qw( base u1 + notmaster );
+ok qw( base u1 + refs/tags/boo );
+ok qw( base u1 + refs/tags/v1 );
+ok qw( base u1 + dev/u1/foo );
+ok qw( base u1 + refs/tags/dev/u1/foo );
+ok qw( base u1 + dev/alice/foo );
+ok qw( base u1 + refs/tags/dev/alice/foo );
+ok qw( base u1 C master );
+ok qw( base u1 C notmaster );
+ok qw( base u1 C refs/tags/boo );
+ok qw( base u1 C refs/tags/v1 );
+ok qw( base u1 C dev/u1/foo );
+ok qw( base u1 C refs/tags/dev/u1/foo );
+ok qw( base u1 C dev/alice/foo );
+ok qw( base u1 C refs/tags/dev/alice/foo );
+ok qw( base u1 D master );
+ok qw( base u1 D notmaster );
+ok qw( base u1 D refs/tags/boo );
+ok qw( base u1 D refs/tags/v1 );
+ok qw( base u1 D dev/u1/foo );
+ok qw( base u1 D refs/tags/dev/u1/foo );
+ok qw( base u1 D dev/alice/foo );
+ok qw( base u1 D refs/tags/dev/alice/foo );
+
+ok qw( base u2 R );
+ok qw( base u2 W master );
+ok qw( base u2 W notmaster );
+ok qw( base u2 W refs/tags/boo );
+ok qw( base u2 W refs/tags/v1 );
+ok qw( base u2 W dev/u2/foo );
+ok qw( base u2 W refs/tags/dev/u2/foo );
+ok qw( base u2 W dev/alice/foo );
+ok qw( base u2 W refs/tags/dev/alice/foo );
+nok qw( base u2 + master );
+nok qw( base u2 + notmaster );
+nok qw( base u2 + refs/tags/boo );
+nok qw( base u2 + refs/tags/v1 );
+nok qw( base u2 + dev/u2/foo );
+nok qw( base u2 + refs/tags/dev/u2/foo );
+nok qw( base u2 + dev/alice/foo );
+nok qw( base u2 + refs/tags/dev/alice/foo );
+ok qw( base u2 C master );
+ok qw( base u2 C notmaster );
+ok qw( base u2 C refs/tags/boo );
+ok qw( base u2 C refs/tags/v1 );
+ok qw( base u2 C dev/u2/foo );
+ok qw( base u2 C refs/tags/dev/u2/foo );
+ok qw( base u2 C dev/alice/foo );
+ok qw( base u2 C refs/tags/dev/alice/foo );
+nok qw( base u2 D master );
+nok qw( base u2 D notmaster );
+nok qw( base u2 D refs/tags/boo );
+nok qw( base u2 D refs/tags/v1 );
+nok qw( base u2 D dev/u2/foo );
+nok qw( base u2 D refs/tags/dev/u2/foo );
+nok qw( base u2 D dev/alice/foo );
+nok qw( base u2 D refs/tags/dev/alice/foo );
+
+ok qw( base u3 R );
+nok qw( base u3 W master );
+ok qw( base u3 W notmaster );
+ok qw( base u3 W refs/tags/boo );
+ok qw( base u3 W refs/tags/v1 );
+ok qw( base u3 W dev/u3/foo );
+ok qw( base u3 W refs/tags/dev/u3/foo );
+ok qw( base u3 W dev/alice/foo );
+ok qw( base u3 W refs/tags/dev/alice/foo );
+nok qw( base u3 + master );
+nok qw( base u3 + notmaster );
+nok qw( base u3 + refs/tags/boo );
+nok qw( base u3 + refs/tags/v1 );
+nok qw( base u3 + dev/u3/foo );
+nok qw( base u3 + refs/tags/dev/u3/foo );
+nok qw( base u3 + dev/alice/foo );
+nok qw( base u3 + refs/tags/dev/alice/foo );
+nok qw( base u3 C master );
+ok qw( base u3 C notmaster );
+ok qw( base u3 C refs/tags/boo );
+ok qw( base u3 C refs/tags/v1 );
+ok qw( base u3 C dev/u3/foo );
+ok qw( base u3 C refs/tags/dev/u3/foo );
+ok qw( base u3 C dev/alice/foo );
+ok qw( base u3 C refs/tags/dev/alice/foo );
+nok qw( base u3 D master );
+nok qw( base u3 D notmaster );
+nok qw( base u3 D refs/tags/boo );
+nok qw( base u3 D refs/tags/v1 );
+nok qw( base u3 D dev/u3/foo );
+nok qw( base u3 D refs/tags/dev/u3/foo );
+nok qw( base u3 D dev/alice/foo );
+nok qw( base u3 D refs/tags/dev/alice/foo );
+
+ok qw( base u4 R );
+nok qw( base u4 W master );
+nok qw( base u4 W notmaster );
+nok qw( base u4 W refs/tags/boo );
+nok qw( base u4 W refs/tags/v1 );
+nok qw( base u4 W dev/u4/foo );
+nok qw( base u4 W refs/tags/dev/u4/foo );
+nok qw( base u4 W dev/alice/foo );
+nok qw( base u4 W refs/tags/dev/alice/foo );
+nok qw( base u4 + master );
+nok qw( base u4 + notmaster );
+nok qw( base u4 + refs/tags/boo );
+nok qw( base u4 + refs/tags/v1 );
+nok qw( base u4 + dev/u4/foo );
+nok qw( base u4 + refs/tags/dev/u4/foo );
+nok qw( base u4 + dev/alice/foo );
+nok qw( base u4 + refs/tags/dev/alice/foo );
+nok qw( base u4 C master );
+nok qw( base u4 C notmaster );
+nok qw( base u4 C refs/tags/boo );
+nok qw( base u4 C refs/tags/v1 );
+nok qw( base u4 C dev/u4/foo );
+nok qw( base u4 C refs/tags/dev/u4/foo );
+nok qw( base u4 C dev/alice/foo );
+nok qw( base u4 C refs/tags/dev/alice/foo );
+nok qw( base u4 D master );
+nok qw( base u4 D notmaster );
+nok qw( base u4 D refs/tags/boo );
+nok qw( base u4 D refs/tags/v1 );
+nok qw( base u4 D dev/u4/foo );
+nok qw( base u4 D refs/tags/dev/u4/foo );
+nok qw( base u4 D dev/alice/foo );
+nok qw( base u4 D refs/tags/dev/alice/foo );
+
+nok qw( base u5 R );
+nok qw( base u5 W master );
+nok qw( base u5 W notmaster );
+nok qw( base u5 W refs/tags/boo );
+nok qw( base u5 W refs/tags/v1 );
+nok qw( base u5 W dev/u5/foo );
+nok qw( base u5 W refs/tags/dev/u5/foo );
+nok qw( base u5 W dev/alice/foo );
+nok qw( base u5 W refs/tags/dev/alice/foo );
+nok qw( base u5 + master );
+nok qw( base u5 + notmaster );
+nok qw( base u5 + refs/tags/boo );
+nok qw( base u5 + refs/tags/v1 );
+nok qw( base u5 + dev/u5/foo );
+nok qw( base u5 + refs/tags/dev/u5/foo );
+nok qw( base u5 + dev/alice/foo );
+nok qw( base u5 + refs/tags/dev/alice/foo );
+nok qw( base u5 C master );
+nok qw( base u5 C notmaster );
+nok qw( base u5 C refs/tags/boo );
+nok qw( base u5 C refs/tags/v1 );
+nok qw( base u5 C dev/u5/foo );
+nok qw( base u5 C refs/tags/dev/u5/foo );
+nok qw( base u5 C dev/alice/foo );
+nok qw( base u5 C refs/tags/dev/alice/foo );
+nok qw( base u5 D master );
+nok qw( base u5 D notmaster );
+nok qw( base u5 D refs/tags/boo );
+nok qw( base u5 D refs/tags/v1 );
+nok qw( base u5 D dev/u5/foo );
+nok qw( base u5 D refs/tags/dev/u5/foo );
+nok qw( base u5 D dev/alice/foo );
+nok qw( base u5 D refs/tags/dev/alice/foo );
+
+nok qw( base u6 R );
+nok qw( base u6 W master );
+nok qw( base u6 W notmaster );
+nok qw( base u6 W refs/tags/boo );
+nok qw( base u6 W refs/tags/v1 );
+nok qw( base u6 W dev/u6/foo );
+nok qw( base u6 W refs/tags/dev/u6/foo );
+nok qw( base u6 W dev/alice/foo );
+nok qw( base u6 W refs/tags/dev/alice/foo );
+nok qw( base u6 + master );
+nok qw( base u6 + notmaster );
+nok qw( base u6 + refs/tags/boo );
+nok qw( base u6 + refs/tags/v1 );
+nok qw( base u6 + dev/u6/foo );
+nok qw( base u6 + refs/tags/dev/u6/foo );
+nok qw( base u6 + dev/alice/foo );
+nok qw( base u6 + refs/tags/dev/alice/foo );
+nok qw( base u6 C master );
+nok qw( base u6 C notmaster );
+nok qw( base u6 C refs/tags/boo );
+nok qw( base u6 C refs/tags/v1 );
+nok qw( base u6 C dev/u6/foo );
+nok qw( base u6 C refs/tags/dev/u6/foo );
+nok qw( base u6 C dev/alice/foo );
+nok qw( base u6 C refs/tags/dev/alice/foo );
+nok qw( base u6 D master );
+nok qw( base u6 D notmaster );
+nok qw( base u6 D refs/tags/boo );
+nok qw( base u6 D refs/tags/v1 );
+nok qw( base u6 D dev/u6/foo );
+nok qw( base u6 D refs/tags/dev/u6/foo );
+nok qw( base u6 D dev/alice/foo );
+nok qw( base u6 D refs/tags/dev/alice/foo );
+
+ok qw( baseroot admin R );
+ok qw( baseroot admin W master );
+ok qw( baseroot admin W notmaster );
+ok qw( baseroot admin W refs/tags/boo );
+ok qw( baseroot admin W refs/tags/v1 );
+ok qw( baseroot admin W dev/admin/foo );
+ok qw( baseroot admin W refs/tags/dev/admin/foo );
+ok qw( baseroot admin W dev/alice/foo );
+ok qw( baseroot admin W refs/tags/dev/alice/foo );
+ok qw( baseroot admin + master );
+ok qw( baseroot admin + notmaster );
+ok qw( baseroot admin + refs/tags/boo );
+ok qw( baseroot admin + refs/tags/v1 );
+ok qw( baseroot admin + dev/admin/foo );
+ok qw( baseroot admin + refs/tags/dev/admin/foo );
+ok qw( baseroot admin + dev/alice/foo );
+ok qw( baseroot admin + refs/tags/dev/alice/foo );
+ok qw( baseroot admin C master );
+ok qw( baseroot admin C notmaster );
+ok qw( baseroot admin C refs/tags/boo );
+ok qw( baseroot admin C refs/tags/v1 );
+ok qw( baseroot admin C dev/admin/foo );
+ok qw( baseroot admin C refs/tags/dev/admin/foo );
+ok qw( baseroot admin C dev/alice/foo );
+ok qw( baseroot admin C refs/tags/dev/alice/foo );
+ok qw( baseroot admin D master );
+ok qw( baseroot admin D notmaster );
+ok qw( baseroot admin D refs/tags/boo );
+ok qw( baseroot admin D refs/tags/v1 );
+ok qw( baseroot admin D dev/admin/foo );
+ok qw( baseroot admin D refs/tags/dev/admin/foo );
+ok qw( baseroot admin D dev/alice/foo );
+ok qw( baseroot admin D refs/tags/dev/alice/foo );
+
+ok qw( baseroot u1 R );
+ok qw( baseroot u1 W master );
+ok qw( baseroot u1 W notmaster );
+ok qw( baseroot u1 W refs/tags/boo );
+ok qw( baseroot u1 W refs/tags/v1 );
+ok qw( baseroot u1 W dev/u1/foo );
+ok qw( baseroot u1 W refs/tags/dev/u1/foo );
+ok qw( baseroot u1 W dev/alice/foo );
+ok qw( baseroot u1 W refs/tags/dev/alice/foo );
+ok qw( baseroot u1 + master );
+ok qw( baseroot u1 + notmaster );
+ok qw( baseroot u1 + refs/tags/boo );
+ok qw( baseroot u1 + refs/tags/v1 );
+ok qw( baseroot u1 + dev/u1/foo );
+ok qw( baseroot u1 + refs/tags/dev/u1/foo );
+ok qw( baseroot u1 + dev/alice/foo );
+ok qw( baseroot u1 + refs/tags/dev/alice/foo );
+ok qw( baseroot u1 C master );
+ok qw( baseroot u1 C notmaster );
+ok qw( baseroot u1 C refs/tags/boo );
+ok qw( baseroot u1 C refs/tags/v1 );
+ok qw( baseroot u1 C dev/u1/foo );
+ok qw( baseroot u1 C refs/tags/dev/u1/foo );
+ok qw( baseroot u1 C dev/alice/foo );
+ok qw( baseroot u1 C refs/tags/dev/alice/foo );
+ok qw( baseroot u1 D master );
+ok qw( baseroot u1 D notmaster );
+ok qw( baseroot u1 D refs/tags/boo );
+ok qw( baseroot u1 D refs/tags/v1 );
+ok qw( baseroot u1 D dev/u1/foo );
+ok qw( baseroot u1 D refs/tags/dev/u1/foo );
+ok qw( baseroot u1 D dev/alice/foo );
+ok qw( baseroot u1 D refs/tags/dev/alice/foo );
+
+ok qw( baseroot u2 R );
+ok qw( baseroot u2 W master );
+ok qw( baseroot u2 W notmaster );
+ok qw( baseroot u2 W refs/tags/boo );
+ok qw( baseroot u2 W refs/tags/v1 );
+ok qw( baseroot u2 W dev/u2/foo );
+ok qw( baseroot u2 W refs/tags/dev/u2/foo );
+ok qw( baseroot u2 W dev/alice/foo );
+ok qw( baseroot u2 W refs/tags/dev/alice/foo );
+nok qw( baseroot u2 + master );
+nok qw( baseroot u2 + notmaster );
+nok qw( baseroot u2 + refs/tags/boo );
+nok qw( baseroot u2 + refs/tags/v1 );
+nok qw( baseroot u2 + dev/u2/foo );
+nok qw( baseroot u2 + refs/tags/dev/u2/foo );
+nok qw( baseroot u2 + dev/alice/foo );
+nok qw( baseroot u2 + refs/tags/dev/alice/foo );
+ok qw( baseroot u2 C master );
+ok qw( baseroot u2 C notmaster );
+ok qw( baseroot u2 C refs/tags/boo );
+ok qw( baseroot u2 C refs/tags/v1 );
+ok qw( baseroot u2 C dev/u2/foo );
+ok qw( baseroot u2 C refs/tags/dev/u2/foo );
+ok qw( baseroot u2 C dev/alice/foo );
+ok qw( baseroot u2 C refs/tags/dev/alice/foo );
+nok qw( baseroot u2 D master );
+nok qw( baseroot u2 D notmaster );
+nok qw( baseroot u2 D refs/tags/boo );
+nok qw( baseroot u2 D refs/tags/v1 );
+nok qw( baseroot u2 D dev/u2/foo );
+nok qw( baseroot u2 D refs/tags/dev/u2/foo );
+nok qw( baseroot u2 D dev/alice/foo );
+nok qw( baseroot u2 D refs/tags/dev/alice/foo );
+
+ok qw( baseroot u3 R );
+nok qw( baseroot u3 W master );
+ok qw( baseroot u3 W notmaster );
+ok qw( baseroot u3 W refs/tags/boo );
+ok qw( baseroot u3 W refs/tags/v1 );
+ok qw( baseroot u3 W dev/u3/foo );
+ok qw( baseroot u3 W refs/tags/dev/u3/foo );
+ok qw( baseroot u3 W dev/alice/foo );
+ok qw( baseroot u3 W refs/tags/dev/alice/foo );
+nok qw( baseroot u3 + master );
+nok qw( baseroot u3 + notmaster );
+nok qw( baseroot u3 + refs/tags/boo );
+nok qw( baseroot u3 + refs/tags/v1 );
+nok qw( baseroot u3 + dev/u3/foo );
+nok qw( baseroot u3 + refs/tags/dev/u3/foo );
+nok qw( baseroot u3 + dev/alice/foo );
+nok qw( baseroot u3 + refs/tags/dev/alice/foo );
+nok qw( baseroot u3 C master );
+ok qw( baseroot u3 C notmaster );
+ok qw( baseroot u3 C refs/tags/boo );
+ok qw( baseroot u3 C refs/tags/v1 );
+ok qw( baseroot u3 C dev/u3/foo );
+ok qw( baseroot u3 C refs/tags/dev/u3/foo );
+ok qw( baseroot u3 C dev/alice/foo );
+ok qw( baseroot u3 C refs/tags/dev/alice/foo );
+nok qw( baseroot u3 D master );
+nok qw( baseroot u3 D notmaster );
+nok qw( baseroot u3 D refs/tags/boo );
+nok qw( baseroot u3 D refs/tags/v1 );
+nok qw( baseroot u3 D dev/u3/foo );
+nok qw( baseroot u3 D refs/tags/dev/u3/foo );
+nok qw( baseroot u3 D dev/alice/foo );
+nok qw( baseroot u3 D refs/tags/dev/alice/foo );
+
+ok qw( baseroot u4 R );
+nok qw( baseroot u4 W master );
+nok qw( baseroot u4 W notmaster );
+nok qw( baseroot u4 W refs/tags/boo );
+nok qw( baseroot u4 W refs/tags/v1 );
+nok qw( baseroot u4 W dev/u4/foo );
+nok qw( baseroot u4 W refs/tags/dev/u4/foo );
+nok qw( baseroot u4 W dev/alice/foo );
+nok qw( baseroot u4 W refs/tags/dev/alice/foo );
+nok qw( baseroot u4 + master );
+nok qw( baseroot u4 + notmaster );
+nok qw( baseroot u4 + refs/tags/boo );
+nok qw( baseroot u4 + refs/tags/v1 );
+nok qw( baseroot u4 + dev/u4/foo );
+nok qw( baseroot u4 + refs/tags/dev/u4/foo );
+nok qw( baseroot u4 + dev/alice/foo );
+nok qw( baseroot u4 + refs/tags/dev/alice/foo );
+nok qw( baseroot u4 C master );
+nok qw( baseroot u4 C notmaster );
+nok qw( baseroot u4 C refs/tags/boo );
+nok qw( baseroot u4 C refs/tags/v1 );
+nok qw( baseroot u4 C dev/u4/foo );
+nok qw( baseroot u4 C refs/tags/dev/u4/foo );
+nok qw( baseroot u4 C dev/alice/foo );
+nok qw( baseroot u4 C refs/tags/dev/alice/foo );
+nok qw( baseroot u4 D master );
+nok qw( baseroot u4 D notmaster );
+nok qw( baseroot u4 D refs/tags/boo );
+nok qw( baseroot u4 D refs/tags/v1 );
+nok qw( baseroot u4 D dev/u4/foo );
+nok qw( baseroot u4 D refs/tags/dev/u4/foo );
+nok qw( baseroot u4 D dev/alice/foo );
+nok qw( baseroot u4 D refs/tags/dev/alice/foo );
+
+nok qw( baseroot u5 R );
+nok qw( baseroot u5 W master );
+nok qw( baseroot u5 W notmaster );
+nok qw( baseroot u5 W refs/tags/boo );
+nok qw( baseroot u5 W refs/tags/v1 );
+nok qw( baseroot u5 W dev/u5/foo );
+nok qw( baseroot u5 W refs/tags/dev/u5/foo );
+nok qw( baseroot u5 W dev/alice/foo );
+nok qw( baseroot u5 W refs/tags/dev/alice/foo );
+nok qw( baseroot u5 + master );
+nok qw( baseroot u5 + notmaster );
+nok qw( baseroot u5 + refs/tags/boo );
+nok qw( baseroot u5 + refs/tags/v1 );
+nok qw( baseroot u5 + dev/u5/foo );
+nok qw( baseroot u5 + refs/tags/dev/u5/foo );
+nok qw( baseroot u5 + dev/alice/foo );
+nok qw( baseroot u5 + refs/tags/dev/alice/foo );
+nok qw( baseroot u5 C master );
+nok qw( baseroot u5 C notmaster );
+nok qw( baseroot u5 C refs/tags/boo );
+nok qw( baseroot u5 C refs/tags/v1 );
+nok qw( baseroot u5 C dev/u5/foo );
+nok qw( baseroot u5 C refs/tags/dev/u5/foo );
+nok qw( baseroot u5 C dev/alice/foo );
+nok qw( baseroot u5 C refs/tags/dev/alice/foo );
+nok qw( baseroot u5 D master );
+nok qw( baseroot u5 D notmaster );
+nok qw( baseroot u5 D refs/tags/boo );
+nok qw( baseroot u5 D refs/tags/v1 );
+nok qw( baseroot u5 D dev/u5/foo );
+nok qw( baseroot u5 D refs/tags/dev/u5/foo );
+nok qw( baseroot u5 D dev/alice/foo );
+nok qw( baseroot u5 D refs/tags/dev/alice/foo );
+
+nok qw( baseroot u6 R );
+nok qw( baseroot u6 W master );
+nok qw( baseroot u6 W notmaster );
+nok qw( baseroot u6 W refs/tags/boo );
+nok qw( baseroot u6 W refs/tags/v1 );
+nok qw( baseroot u6 W dev/u6/foo );
+nok qw( baseroot u6 W refs/tags/dev/u6/foo );
+nok qw( baseroot u6 W dev/alice/foo );
+nok qw( baseroot u6 W refs/tags/dev/alice/foo );
+nok qw( baseroot u6 + master );
+nok qw( baseroot u6 + notmaster );
+nok qw( baseroot u6 + refs/tags/boo );
+nok qw( baseroot u6 + refs/tags/v1 );
+nok qw( baseroot u6 + dev/u6/foo );
+nok qw( baseroot u6 + refs/tags/dev/u6/foo );
+nok qw( baseroot u6 + dev/alice/foo );
+nok qw( baseroot u6 + refs/tags/dev/alice/foo );
+nok qw( baseroot u6 C master );
+nok qw( baseroot u6 C notmaster );
+nok qw( baseroot u6 C refs/tags/boo );
+nok qw( baseroot u6 C refs/tags/v1 );
+nok qw( baseroot u6 C dev/u6/foo );
+nok qw( baseroot u6 C refs/tags/dev/u6/foo );
+nok qw( baseroot u6 C dev/alice/foo );
+nok qw( baseroot u6 C refs/tags/dev/alice/foo );
+nok qw( baseroot u6 D master );
+nok qw( baseroot u6 D notmaster );
+nok qw( baseroot u6 D refs/tags/boo );
+nok qw( baseroot u6 D refs/tags/v1 );
+nok qw( baseroot u6 D dev/u6/foo );
+nok qw( baseroot u6 D refs/tags/dev/u6/foo );
+nok qw( baseroot u6 D dev/alice/foo );
+nok qw( baseroot u6 D refs/tags/dev/alice/foo );
+
+nok qw( basepers admin R );
+nok qw( basepers admin W master );
+nok qw( basepers admin W notmaster );
+nok qw( basepers admin W refs/tags/boo );
+nok qw( basepers admin W refs/tags/v1 );
+nok qw( basepers admin W dev/admin/foo );
+nok qw( basepers admin W refs/tags/dev/admin/foo );
+nok qw( basepers admin W dev/alice/foo );
+nok qw( basepers admin W refs/tags/dev/alice/foo );
+nok qw( basepers admin + master );
+nok qw( basepers admin + notmaster );
+nok qw( basepers admin + refs/tags/boo );
+nok qw( basepers admin + refs/tags/v1 );
+nok qw( basepers admin + dev/admin/foo );
+nok qw( basepers admin + refs/tags/dev/admin/foo );
+nok qw( basepers admin + dev/alice/foo );
+nok qw( basepers admin + refs/tags/dev/alice/foo );
+nok qw( basepers admin C master );
+nok qw( basepers admin C notmaster );
+nok qw( basepers admin C refs/tags/boo );
+nok qw( basepers admin C refs/tags/v1 );
+nok qw( basepers admin C dev/admin/foo );
+nok qw( basepers admin C refs/tags/dev/admin/foo );
+nok qw( basepers admin C dev/alice/foo );
+nok qw( basepers admin C refs/tags/dev/alice/foo );
+nok qw( basepers admin D master );
+nok qw( basepers admin D notmaster );
+nok qw( basepers admin D refs/tags/boo );
+nok qw( basepers admin D refs/tags/v1 );
+nok qw( basepers admin D dev/admin/foo );
+nok qw( basepers admin D refs/tags/dev/admin/foo );
+nok qw( basepers admin D dev/alice/foo );
+nok qw( basepers admin D refs/tags/dev/alice/foo );
+
+ok qw( basepers u1 R );
+ok qw( basepers u1 W master );
+ok qw( basepers u1 W notmaster );
+ok qw( basepers u1 W refs/tags/boo );
+ok qw( basepers u1 W refs/tags/v1 );
+ok qw( basepers u1 W dev/u1/foo );
+ok qw( basepers u1 W refs/tags/dev/u1/foo );
+nok qw( basepers u1 W dev/alice/foo );
+nok qw( basepers u1 W refs/tags/dev/alice/foo );
+ok qw( basepers u1 + master );
+ok qw( basepers u1 + notmaster );
+ok qw( basepers u1 + refs/tags/boo );
+ok qw( basepers u1 + refs/tags/v1 );
+ok qw( basepers u1 + dev/u1/foo );
+ok qw( basepers u1 + refs/tags/dev/u1/foo );
+nok qw( basepers u1 + dev/alice/foo );
+nok qw( basepers u1 + refs/tags/dev/alice/foo );
+ok qw( basepers u1 C master );
+ok qw( basepers u1 C notmaster );
+ok qw( basepers u1 C refs/tags/boo );
+ok qw( basepers u1 C refs/tags/v1 );
+ok qw( basepers u1 C dev/u1/foo );
+ok qw( basepers u1 C refs/tags/dev/u1/foo );
+nok qw( basepers u1 C dev/alice/foo );
+nok qw( basepers u1 C refs/tags/dev/alice/foo );
+ok qw( basepers u1 D master );
+ok qw( basepers u1 D notmaster );
+ok qw( basepers u1 D refs/tags/boo );
+ok qw( basepers u1 D refs/tags/v1 );
+ok qw( basepers u1 D dev/u1/foo );
+ok qw( basepers u1 D refs/tags/dev/u1/foo );
+nok qw( basepers u1 D dev/alice/foo );
+nok qw( basepers u1 D refs/tags/dev/alice/foo );
+
+ok qw( basepers u2 R );
+ok qw( basepers u2 W master );
+ok qw( basepers u2 W notmaster );
+ok qw( basepers u2 W refs/tags/boo );
+ok qw( basepers u2 W refs/tags/v1 );
+ok qw( basepers u2 W dev/u2/foo );
+ok qw( basepers u2 W refs/tags/dev/u2/foo );
+nok qw( basepers u2 W dev/alice/foo );
+nok qw( basepers u2 W refs/tags/dev/alice/foo );
+nok qw( basepers u2 + master );
+nok qw( basepers u2 + notmaster );
+nok qw( basepers u2 + refs/tags/boo );
+nok qw( basepers u2 + refs/tags/v1 );
+ok qw( basepers u2 + dev/u2/foo );
+ok qw( basepers u2 + refs/tags/dev/u2/foo );
+nok qw( basepers u2 + dev/alice/foo );
+nok qw( basepers u2 + refs/tags/dev/alice/foo );
+ok qw( basepers u2 C master );
+ok qw( basepers u2 C notmaster );
+ok qw( basepers u2 C refs/tags/boo );
+ok qw( basepers u2 C refs/tags/v1 );
+ok qw( basepers u2 C dev/u2/foo );
+ok qw( basepers u2 C refs/tags/dev/u2/foo );
+nok qw( basepers u2 C dev/alice/foo );
+nok qw( basepers u2 C refs/tags/dev/alice/foo );
+nok qw( basepers u2 D master );
+nok qw( basepers u2 D notmaster );
+nok qw( basepers u2 D refs/tags/boo );
+nok qw( basepers u2 D refs/tags/v1 );
+ok qw( basepers u2 D dev/u2/foo );
+ok qw( basepers u2 D refs/tags/dev/u2/foo );
+nok qw( basepers u2 D dev/alice/foo );
+nok qw( basepers u2 D refs/tags/dev/alice/foo );
+
+ok qw( basepers u3 R );
+nok qw( basepers u3 W master );
+ok qw( basepers u3 W notmaster );
+ok qw( basepers u3 W refs/tags/boo );
+ok qw( basepers u3 W refs/tags/v1 );
+ok qw( basepers u3 W dev/u3/foo );
+ok qw( basepers u3 W refs/tags/dev/u3/foo );
+nok qw( basepers u3 W dev/alice/foo );
+nok qw( basepers u3 W refs/tags/dev/alice/foo );
+nok qw( basepers u3 + master );
+nok qw( basepers u3 + notmaster );
+nok qw( basepers u3 + refs/tags/boo );
+nok qw( basepers u3 + refs/tags/v1 );
+ok qw( basepers u3 + dev/u3/foo );
+ok qw( basepers u3 + refs/tags/dev/u3/foo );
+nok qw( basepers u3 + dev/alice/foo );
+nok qw( basepers u3 + refs/tags/dev/alice/foo );
+nok qw( basepers u3 C master );
+ok qw( basepers u3 C notmaster );
+ok qw( basepers u3 C refs/tags/boo );
+ok qw( basepers u3 C refs/tags/v1 );
+ok qw( basepers u3 C dev/u3/foo );
+ok qw( basepers u3 C refs/tags/dev/u3/foo );
+nok qw( basepers u3 C dev/alice/foo );
+nok qw( basepers u3 C refs/tags/dev/alice/foo );
+nok qw( basepers u3 D master );
+nok qw( basepers u3 D notmaster );
+nok qw( basepers u3 D refs/tags/boo );
+nok qw( basepers u3 D refs/tags/v1 );
+ok qw( basepers u3 D dev/u3/foo );
+ok qw( basepers u3 D refs/tags/dev/u3/foo );
+nok qw( basepers u3 D dev/alice/foo );
+nok qw( basepers u3 D refs/tags/dev/alice/foo );
+
+ok qw( basepers u4 R );
+nok qw( basepers u4 W master );
+nok qw( basepers u4 W notmaster );
+nok qw( basepers u4 W refs/tags/boo );
+nok qw( basepers u4 W refs/tags/v1 );
+nok qw( basepers u4 W dev/u4/foo );
+nok qw( basepers u4 W refs/tags/dev/u4/foo );
+nok qw( basepers u4 W dev/alice/foo );
+nok qw( basepers u4 W refs/tags/dev/alice/foo );
+nok qw( basepers u4 + master );
+nok qw( basepers u4 + notmaster );
+nok qw( basepers u4 + refs/tags/boo );
+nok qw( basepers u4 + refs/tags/v1 );
+nok qw( basepers u4 + dev/u4/foo );
+nok qw( basepers u4 + refs/tags/dev/u4/foo );
+nok qw( basepers u4 + dev/alice/foo );
+nok qw( basepers u4 + refs/tags/dev/alice/foo );
+nok qw( basepers u4 C master );
+nok qw( basepers u4 C notmaster );
+nok qw( basepers u4 C refs/tags/boo );
+nok qw( basepers u4 C refs/tags/v1 );
+nok qw( basepers u4 C dev/u4/foo );
+nok qw( basepers u4 C refs/tags/dev/u4/foo );
+nok qw( basepers u4 C dev/alice/foo );
+nok qw( basepers u4 C refs/tags/dev/alice/foo );
+nok qw( basepers u4 D master );
+nok qw( basepers u4 D notmaster );
+nok qw( basepers u4 D refs/tags/boo );
+nok qw( basepers u4 D refs/tags/v1 );
+nok qw( basepers u4 D dev/u4/foo );
+nok qw( basepers u4 D refs/tags/dev/u4/foo );
+nok qw( basepers u4 D dev/alice/foo );
+nok qw( basepers u4 D refs/tags/dev/alice/foo );
+
+ok qw( basepers u5 R );
+nok qw( basepers u5 W master );
+nok qw( basepers u5 W notmaster );
+nok qw( basepers u5 W refs/tags/boo );
+nok qw( basepers u5 W refs/tags/v1 );
+ok qw( basepers u5 W dev/u5/foo );
+ok qw( basepers u5 W refs/tags/dev/u5/foo );
+nok qw( basepers u5 W dev/alice/foo );
+nok qw( basepers u5 W refs/tags/dev/alice/foo );
+nok qw( basepers u5 + master );
+nok qw( basepers u5 + notmaster );
+nok qw( basepers u5 + refs/tags/boo );
+nok qw( basepers u5 + refs/tags/v1 );
+ok qw( basepers u5 + dev/u5/foo );
+ok qw( basepers u5 + refs/tags/dev/u5/foo );
+nok qw( basepers u5 + dev/alice/foo );
+nok qw( basepers u5 + refs/tags/dev/alice/foo );
+nok qw( basepers u5 C master );
+nok qw( basepers u5 C notmaster );
+nok qw( basepers u5 C refs/tags/boo );
+nok qw( basepers u5 C refs/tags/v1 );
+ok qw( basepers u5 C dev/u5/foo );
+ok qw( basepers u5 C refs/tags/dev/u5/foo );
+nok qw( basepers u5 C dev/alice/foo );
+nok qw( basepers u5 C refs/tags/dev/alice/foo );
+nok qw( basepers u5 D master );
+nok qw( basepers u5 D notmaster );
+nok qw( basepers u5 D refs/tags/boo );
+nok qw( basepers u5 D refs/tags/v1 );
+ok qw( basepers u5 D dev/u5/foo );
+ok qw( basepers u5 D refs/tags/dev/u5/foo );
+nok qw( basepers u5 D dev/alice/foo );
+nok qw( basepers u5 D refs/tags/dev/alice/foo );
+
+ok qw( basepers u6 R );
+nok qw( basepers u6 W master );
+nok qw( basepers u6 W notmaster );
+nok qw( basepers u6 W refs/tags/boo );
+nok qw( basepers u6 W refs/tags/v1 );
+ok qw( basepers u6 W dev/u6/foo );
+ok qw( basepers u6 W refs/tags/dev/u6/foo );
+nok qw( basepers u6 W dev/alice/foo );
+nok qw( basepers u6 W refs/tags/dev/alice/foo );
+nok qw( basepers u6 + master );
+nok qw( basepers u6 + notmaster );
+nok qw( basepers u6 + refs/tags/boo );
+nok qw( basepers u6 + refs/tags/v1 );
+ok qw( basepers u6 + dev/u6/foo );
+ok qw( basepers u6 + refs/tags/dev/u6/foo );
+nok qw( basepers u6 + dev/alice/foo );
+nok qw( basepers u6 + refs/tags/dev/alice/foo );
+nok qw( basepers u6 C master );
+nok qw( basepers u6 C notmaster );
+nok qw( basepers u6 C refs/tags/boo );
+nok qw( basepers u6 C refs/tags/v1 );
+ok qw( basepers u6 C dev/u6/foo );
+ok qw( basepers u6 C refs/tags/dev/u6/foo );
+nok qw( basepers u6 C dev/alice/foo );
+nok qw( basepers u6 C refs/tags/dev/alice/foo );
+nok qw( basepers u6 D master );
+nok qw( basepers u6 D notmaster );
+nok qw( basepers u6 D refs/tags/boo );
+nok qw( basepers u6 D refs/tags/v1 );
+ok qw( basepers u6 D dev/u6/foo );
+ok qw( basepers u6 D refs/tags/dev/u6/foo );
+nok qw( basepers u6 D dev/alice/foo );
+nok qw( basepers u6 D refs/tags/dev/alice/foo );
+
+nok qw( baserel admin R );
+nok qw( baserel admin W master );
+nok qw( baserel admin W notmaster );
+nok qw( baserel admin W refs/tags/boo );
+nok qw( baserel admin W refs/tags/v1 );
+nok qw( baserel admin W dev/admin/foo );
+nok qw( baserel admin W refs/tags/dev/admin/foo );
+nok qw( baserel admin W dev/alice/foo );
+nok qw( baserel admin W refs/tags/dev/alice/foo );
+nok qw( baserel admin + master );
+nok qw( baserel admin + notmaster );
+nok qw( baserel admin + refs/tags/boo );
+nok qw( baserel admin + refs/tags/v1 );
+nok qw( baserel admin + dev/admin/foo );
+nok qw( baserel admin + refs/tags/dev/admin/foo );
+nok qw( baserel admin + dev/alice/foo );
+nok qw( baserel admin + refs/tags/dev/alice/foo );
+nok qw( baserel admin C master );
+nok qw( baserel admin C notmaster );
+nok qw( baserel admin C refs/tags/boo );
+nok qw( baserel admin C refs/tags/v1 );
+nok qw( baserel admin C dev/admin/foo );
+nok qw( baserel admin C refs/tags/dev/admin/foo );
+nok qw( baserel admin C dev/alice/foo );
+nok qw( baserel admin C refs/tags/dev/alice/foo );
+nok qw( baserel admin D master );
+nok qw( baserel admin D notmaster );
+nok qw( baserel admin D refs/tags/boo );
+nok qw( baserel admin D refs/tags/v1 );
+nok qw( baserel admin D dev/admin/foo );
+nok qw( baserel admin D refs/tags/dev/admin/foo );
+nok qw( baserel admin D dev/alice/foo );
+nok qw( baserel admin D refs/tags/dev/alice/foo );
+
+ok qw( baserel u1 R );
+ok qw( baserel u1 W master );
+ok qw( baserel u1 W notmaster );
+ok qw( baserel u1 W refs/tags/boo );
+nok qw( baserel u1 W refs/tags/v1 );
+ok qw( baserel u1 W dev/u1/foo );
+ok qw( baserel u1 W refs/tags/dev/u1/foo );
+ok qw( baserel u1 W dev/alice/foo );
+ok qw( baserel u1 W refs/tags/dev/alice/foo );
+ok qw( baserel u1 + master );
+ok qw( baserel u1 + notmaster );
+ok qw( baserel u1 + refs/tags/boo );
+nok qw( baserel u1 + refs/tags/v1 );
+ok qw( baserel u1 + dev/u1/foo );
+ok qw( baserel u1 + refs/tags/dev/u1/foo );
+ok qw( baserel u1 + dev/alice/foo );
+ok qw( baserel u1 + refs/tags/dev/alice/foo );
+ok qw( baserel u1 C master );
+ok qw( baserel u1 C notmaster );
+ok qw( baserel u1 C refs/tags/boo );
+nok qw( baserel u1 C refs/tags/v1 );
+ok qw( baserel u1 C dev/u1/foo );
+ok qw( baserel u1 C refs/tags/dev/u1/foo );
+ok qw( baserel u1 C dev/alice/foo );
+ok qw( baserel u1 C refs/tags/dev/alice/foo );
+ok qw( baserel u1 D master );
+ok qw( baserel u1 D notmaster );
+ok qw( baserel u1 D refs/tags/boo );
+nok qw( baserel u1 D refs/tags/v1 );
+ok qw( baserel u1 D dev/u1/foo );
+ok qw( baserel u1 D refs/tags/dev/u1/foo );
+ok qw( baserel u1 D dev/alice/foo );
+ok qw( baserel u1 D refs/tags/dev/alice/foo );
+
+ok qw( baserel u2 R );
+ok qw( baserel u2 W master );
+ok qw( baserel u2 W notmaster );
+ok qw( baserel u2 W refs/tags/boo );
+nok qw( baserel u2 W refs/tags/v1 );
+ok qw( baserel u2 W dev/u2/foo );
+ok qw( baserel u2 W refs/tags/dev/u2/foo );
+ok qw( baserel u2 W dev/alice/foo );
+ok qw( baserel u2 W refs/tags/dev/alice/foo );
+nok qw( baserel u2 + master );
+nok qw( baserel u2 + notmaster );
+nok qw( baserel u2 + refs/tags/boo );
+nok qw( baserel u2 + refs/tags/v1 );
+nok qw( baserel u2 + dev/u2/foo );
+nok qw( baserel u2 + refs/tags/dev/u2/foo );
+nok qw( baserel u2 + dev/alice/foo );
+nok qw( baserel u2 + refs/tags/dev/alice/foo );
+ok qw( baserel u2 C master );
+ok qw( baserel u2 C notmaster );
+ok qw( baserel u2 C refs/tags/boo );
+nok qw( baserel u2 C refs/tags/v1 );
+ok qw( baserel u2 C dev/u2/foo );
+ok qw( baserel u2 C refs/tags/dev/u2/foo );
+ok qw( baserel u2 C dev/alice/foo );
+ok qw( baserel u2 C refs/tags/dev/alice/foo );
+nok qw( baserel u2 D master );
+nok qw( baserel u2 D notmaster );
+nok qw( baserel u2 D refs/tags/boo );
+nok qw( baserel u2 D refs/tags/v1 );
+nok qw( baserel u2 D dev/u2/foo );
+nok qw( baserel u2 D refs/tags/dev/u2/foo );
+nok qw( baserel u2 D dev/alice/foo );
+nok qw( baserel u2 D refs/tags/dev/alice/foo );
+
+ok qw( baserel u3 R );
+nok qw( baserel u3 W master );
+ok qw( baserel u3 W notmaster );
+ok qw( baserel u3 W refs/tags/boo );
+nok qw( baserel u3 W refs/tags/v1 );
+ok qw( baserel u3 W dev/u3/foo );
+ok qw( baserel u3 W refs/tags/dev/u3/foo );
+ok qw( baserel u3 W dev/alice/foo );
+ok qw( baserel u3 W refs/tags/dev/alice/foo );
+nok qw( baserel u3 + master );
+nok qw( baserel u3 + notmaster );
+nok qw( baserel u3 + refs/tags/boo );
+nok qw( baserel u3 + refs/tags/v1 );
+nok qw( baserel u3 + dev/u3/foo );
+nok qw( baserel u3 + refs/tags/dev/u3/foo );
+nok qw( baserel u3 + dev/alice/foo );
+nok qw( baserel u3 + refs/tags/dev/alice/foo );
+nok qw( baserel u3 C master );
+ok qw( baserel u3 C notmaster );
+ok qw( baserel u3 C refs/tags/boo );
+nok qw( baserel u3 C refs/tags/v1 );
+ok qw( baserel u3 C dev/u3/foo );
+ok qw( baserel u3 C refs/tags/dev/u3/foo );
+ok qw( baserel u3 C dev/alice/foo );
+ok qw( baserel u3 C refs/tags/dev/alice/foo );
+nok qw( baserel u3 D master );
+nok qw( baserel u3 D notmaster );
+nok qw( baserel u3 D refs/tags/boo );
+nok qw( baserel u3 D refs/tags/v1 );
+nok qw( baserel u3 D dev/u3/foo );
+nok qw( baserel u3 D refs/tags/dev/u3/foo );
+nok qw( baserel u3 D dev/alice/foo );
+nok qw( baserel u3 D refs/tags/dev/alice/foo );
+
+ok qw( baserel u4 R );
+nok qw( baserel u4 W master );
+nok qw( baserel u4 W notmaster );
+nok qw( baserel u4 W refs/tags/boo );
+nok qw( baserel u4 W refs/tags/v1 );
+nok qw( baserel u4 W dev/u4/foo );
+nok qw( baserel u4 W refs/tags/dev/u4/foo );
+nok qw( baserel u4 W dev/alice/foo );
+nok qw( baserel u4 W refs/tags/dev/alice/foo );
+nok qw( baserel u4 + master );
+nok qw( baserel u4 + notmaster );
+nok qw( baserel u4 + refs/tags/boo );
+nok qw( baserel u4 + refs/tags/v1 );
+nok qw( baserel u4 + dev/u4/foo );
+nok qw( baserel u4 + refs/tags/dev/u4/foo );
+nok qw( baserel u4 + dev/alice/foo );
+nok qw( baserel u4 + refs/tags/dev/alice/foo );
+nok qw( baserel u4 C master );
+nok qw( baserel u4 C notmaster );
+nok qw( baserel u4 C refs/tags/boo );
+nok qw( baserel u4 C refs/tags/v1 );
+nok qw( baserel u4 C dev/u4/foo );
+nok qw( baserel u4 C refs/tags/dev/u4/foo );
+nok qw( baserel u4 C dev/alice/foo );
+nok qw( baserel u4 C refs/tags/dev/alice/foo );
+nok qw( baserel u4 D master );
+nok qw( baserel u4 D notmaster );
+nok qw( baserel u4 D refs/tags/boo );
+nok qw( baserel u4 D refs/tags/v1 );
+nok qw( baserel u4 D dev/u4/foo );
+nok qw( baserel u4 D refs/tags/dev/u4/foo );
+nok qw( baserel u4 D dev/alice/foo );
+nok qw( baserel u4 D refs/tags/dev/alice/foo );
+
+ok qw( baserel u5 R );
+nok qw( baserel u5 W master );
+nok qw( baserel u5 W notmaster );
+nok qw( baserel u5 W refs/tags/boo );
+nok qw( baserel u5 W refs/tags/v1 );
+nok qw( baserel u5 W dev/u5/foo );
+nok qw( baserel u5 W refs/tags/dev/u5/foo );
+nok qw( baserel u5 W dev/alice/foo );
+nok qw( baserel u5 W refs/tags/dev/alice/foo );
+nok qw( baserel u5 + master );
+nok qw( baserel u5 + notmaster );
+nok qw( baserel u5 + refs/tags/boo );
+nok qw( baserel u5 + refs/tags/v1 );
+nok qw( baserel u5 + dev/u5/foo );
+nok qw( baserel u5 + refs/tags/dev/u5/foo );
+nok qw( baserel u5 + dev/alice/foo );
+nok qw( baserel u5 + refs/tags/dev/alice/foo );
+nok qw( baserel u5 C master );
+nok qw( baserel u5 C notmaster );
+nok qw( baserel u5 C refs/tags/boo );
+nok qw( baserel u5 C refs/tags/v1 );
+nok qw( baserel u5 C dev/u5/foo );
+nok qw( baserel u5 C refs/tags/dev/u5/foo );
+nok qw( baserel u5 C dev/alice/foo );
+nok qw( baserel u5 C refs/tags/dev/alice/foo );
+nok qw( baserel u5 D master );
+nok qw( baserel u5 D notmaster );
+nok qw( baserel u5 D refs/tags/boo );
+nok qw( baserel u5 D refs/tags/v1 );
+nok qw( baserel u5 D dev/u5/foo );
+nok qw( baserel u5 D refs/tags/dev/u5/foo );
+nok qw( baserel u5 D dev/alice/foo );
+nok qw( baserel u5 D refs/tags/dev/alice/foo );
+
+nok qw( baserel u6 R );
+nok qw( baserel u6 W master );
+nok qw( baserel u6 W notmaster );
+nok qw( baserel u6 W refs/tags/boo );
+nok qw( baserel u6 W refs/tags/v1 );
+nok qw( baserel u6 W dev/u6/foo );
+nok qw( baserel u6 W refs/tags/dev/u6/foo );
+nok qw( baserel u6 W dev/alice/foo );
+nok qw( baserel u6 W refs/tags/dev/alice/foo );
+nok qw( baserel u6 + master );
+nok qw( baserel u6 + notmaster );
+nok qw( baserel u6 + refs/tags/boo );
+nok qw( baserel u6 + refs/tags/v1 );
+nok qw( baserel u6 + dev/u6/foo );
+nok qw( baserel u6 + refs/tags/dev/u6/foo );
+nok qw( baserel u6 + dev/alice/foo );
+nok qw( baserel u6 + refs/tags/dev/alice/foo );
+nok qw( baserel u6 C master );
+nok qw( baserel u6 C notmaster );
+nok qw( baserel u6 C refs/tags/boo );
+nok qw( baserel u6 C refs/tags/v1 );
+nok qw( baserel u6 C dev/u6/foo );
+nok qw( baserel u6 C refs/tags/dev/u6/foo );
+nok qw( baserel u6 C dev/alice/foo );
+nok qw( baserel u6 C refs/tags/dev/alice/foo );
+nok qw( baserel u6 D master );
+nok qw( baserel u6 D notmaster );
+nok qw( baserel u6 D refs/tags/boo );
+nok qw( baserel u6 D refs/tags/v1 );
+nok qw( baserel u6 D dev/u6/foo );
+nok qw( baserel u6 D refs/tags/dev/u6/foo );
+nok qw( baserel u6 D dev/alice/foo );
+nok qw( baserel u6 D refs/tags/dev/alice/foo );
+
+ok qw( baseall admin R );
+ok qw( baseall admin W master );
+ok qw( baseall admin W notmaster );
+ok qw( baseall admin W refs/tags/boo );
+ok qw( baseall admin W refs/tags/v1 );
+ok qw( baseall admin W dev/admin/foo );
+ok qw( baseall admin W refs/tags/dev/admin/foo );
+ok qw( baseall admin W dev/alice/foo );
+ok qw( baseall admin W refs/tags/dev/alice/foo );
+ok qw( baseall admin + master );
+ok qw( baseall admin + notmaster );
+ok qw( baseall admin + refs/tags/boo );
+ok qw( baseall admin + refs/tags/v1 );
+ok qw( baseall admin + dev/admin/foo );
+ok qw( baseall admin + refs/tags/dev/admin/foo );
+ok qw( baseall admin + dev/alice/foo );
+ok qw( baseall admin + refs/tags/dev/alice/foo );
+ok qw( baseall admin C master );
+ok qw( baseall admin C notmaster );
+ok qw( baseall admin C refs/tags/boo );
+ok qw( baseall admin C refs/tags/v1 );
+ok qw( baseall admin C dev/admin/foo );
+ok qw( baseall admin C refs/tags/dev/admin/foo );
+ok qw( baseall admin C dev/alice/foo );
+ok qw( baseall admin C refs/tags/dev/alice/foo );
+ok qw( baseall admin D master );
+ok qw( baseall admin D notmaster );
+ok qw( baseall admin D refs/tags/boo );
+ok qw( baseall admin D refs/tags/v1 );
+ok qw( baseall admin D dev/admin/foo );
+ok qw( baseall admin D refs/tags/dev/admin/foo );
+ok qw( baseall admin D dev/alice/foo );
+ok qw( baseall admin D refs/tags/dev/alice/foo );
+
+ok qw( baseall u1 R );
+ok qw( baseall u1 W master );
+ok qw( baseall u1 W notmaster );
+ok qw( baseall u1 W refs/tags/boo );
+nok qw( baseall u1 W refs/tags/v1 );
+ok qw( baseall u1 W dev/u1/foo );
+ok qw( baseall u1 W refs/tags/dev/u1/foo );
+nok qw( baseall u1 W dev/alice/foo );
+nok qw( baseall u1 W refs/tags/dev/alice/foo );
+ok qw( baseall u1 + master );
+ok qw( baseall u1 + notmaster );
+ok qw( baseall u1 + refs/tags/boo );
+nok qw( baseall u1 + refs/tags/v1 );
+ok qw( baseall u1 + dev/u1/foo );
+ok qw( baseall u1 + refs/tags/dev/u1/foo );
+nok qw( baseall u1 + dev/alice/foo );
+nok qw( baseall u1 + refs/tags/dev/alice/foo );
+ok qw( baseall u1 C master );
+ok qw( baseall u1 C notmaster );
+ok qw( baseall u1 C refs/tags/boo );
+nok qw( baseall u1 C refs/tags/v1 );
+ok qw( baseall u1 C dev/u1/foo );
+ok qw( baseall u1 C refs/tags/dev/u1/foo );
+nok qw( baseall u1 C dev/alice/foo );
+nok qw( baseall u1 C refs/tags/dev/alice/foo );
+ok qw( baseall u1 D master );
+ok qw( baseall u1 D notmaster );
+ok qw( baseall u1 D refs/tags/boo );
+nok qw( baseall u1 D refs/tags/v1 );
+ok qw( baseall u1 D dev/u1/foo );
+ok qw( baseall u1 D refs/tags/dev/u1/foo );
+nok qw( baseall u1 D dev/alice/foo );
+nok qw( baseall u1 D refs/tags/dev/alice/foo );
+
+ok qw( baseall u2 R );
+ok qw( baseall u2 W master );
+ok qw( baseall u2 W notmaster );
+ok qw( baseall u2 W refs/tags/boo );
+nok qw( baseall u2 W refs/tags/v1 );
+ok qw( baseall u2 W dev/u2/foo );
+ok qw( baseall u2 W refs/tags/dev/u2/foo );
+nok qw( baseall u2 W dev/alice/foo );
+nok qw( baseall u2 W refs/tags/dev/alice/foo );
+nok qw( baseall u2 + master );
+nok qw( baseall u2 + notmaster );
+nok qw( baseall u2 + refs/tags/boo );
+nok qw( baseall u2 + refs/tags/v1 );
+ok qw( baseall u2 + dev/u2/foo );
+ok qw( baseall u2 + refs/tags/dev/u2/foo );
+nok qw( baseall u2 + dev/alice/foo );
+nok qw( baseall u2 + refs/tags/dev/alice/foo );
+ok qw( baseall u2 C master );
+ok qw( baseall u2 C notmaster );
+ok qw( baseall u2 C refs/tags/boo );
+nok qw( baseall u2 C refs/tags/v1 );
+ok qw( baseall u2 C dev/u2/foo );
+ok qw( baseall u2 C refs/tags/dev/u2/foo );
+nok qw( baseall u2 C dev/alice/foo );
+nok qw( baseall u2 C refs/tags/dev/alice/foo );
+nok qw( baseall u2 D master );
+nok qw( baseall u2 D notmaster );
+nok qw( baseall u2 D refs/tags/boo );
+nok qw( baseall u2 D refs/tags/v1 );
+ok qw( baseall u2 D dev/u2/foo );
+ok qw( baseall u2 D refs/tags/dev/u2/foo );
+nok qw( baseall u2 D dev/alice/foo );
+nok qw( baseall u2 D refs/tags/dev/alice/foo );
+
+ok qw( baseall u3 R );
+nok qw( baseall u3 W master );
+ok qw( baseall u3 W notmaster );
+ok qw( baseall u3 W refs/tags/boo );
+nok qw( baseall u3 W refs/tags/v1 );
+ok qw( baseall u3 W dev/u3/foo );
+ok qw( baseall u3 W refs/tags/dev/u3/foo );
+nok qw( baseall u3 W dev/alice/foo );
+nok qw( baseall u3 W refs/tags/dev/alice/foo );
+nok qw( baseall u3 + master );
+nok qw( baseall u3 + notmaster );
+nok qw( baseall u3 + refs/tags/boo );
+nok qw( baseall u3 + refs/tags/v1 );
+ok qw( baseall u3 + dev/u3/foo );
+ok qw( baseall u3 + refs/tags/dev/u3/foo );
+nok qw( baseall u3 + dev/alice/foo );
+nok qw( baseall u3 + refs/tags/dev/alice/foo );
+nok qw( baseall u3 C master );
+ok qw( baseall u3 C notmaster );
+ok qw( baseall u3 C refs/tags/boo );
+nok qw( baseall u3 C refs/tags/v1 );
+ok qw( baseall u3 C dev/u3/foo );
+ok qw( baseall u3 C refs/tags/dev/u3/foo );
+nok qw( baseall u3 C dev/alice/foo );
+nok qw( baseall u3 C refs/tags/dev/alice/foo );
+nok qw( baseall u3 D master );
+nok qw( baseall u3 D notmaster );
+nok qw( baseall u3 D refs/tags/boo );
+nok qw( baseall u3 D refs/tags/v1 );
+ok qw( baseall u3 D dev/u3/foo );
+ok qw( baseall u3 D refs/tags/dev/u3/foo );
+nok qw( baseall u3 D dev/alice/foo );
+nok qw( baseall u3 D refs/tags/dev/alice/foo );
+
+ok qw( baseall u4 R );
+nok qw( baseall u4 W master );
+nok qw( baseall u4 W notmaster );
+nok qw( baseall u4 W refs/tags/boo );
+nok qw( baseall u4 W refs/tags/v1 );
+nok qw( baseall u4 W dev/u4/foo );
+nok qw( baseall u4 W refs/tags/dev/u4/foo );
+nok qw( baseall u4 W dev/alice/foo );
+nok qw( baseall u4 W refs/tags/dev/alice/foo );
+nok qw( baseall u4 + master );
+nok qw( baseall u4 + notmaster );
+nok qw( baseall u4 + refs/tags/boo );
+nok qw( baseall u4 + refs/tags/v1 );
+nok qw( baseall u4 + dev/u4/foo );
+nok qw( baseall u4 + refs/tags/dev/u4/foo );
+nok qw( baseall u4 + dev/alice/foo );
+nok qw( baseall u4 + refs/tags/dev/alice/foo );
+nok qw( baseall u4 C master );
+nok qw( baseall u4 C notmaster );
+nok qw( baseall u4 C refs/tags/boo );
+nok qw( baseall u4 C refs/tags/v1 );
+nok qw( baseall u4 C dev/u4/foo );
+nok qw( baseall u4 C refs/tags/dev/u4/foo );
+nok qw( baseall u4 C dev/alice/foo );
+nok qw( baseall u4 C refs/tags/dev/alice/foo );
+nok qw( baseall u4 D master );
+nok qw( baseall u4 D notmaster );
+nok qw( baseall u4 D refs/tags/boo );
+nok qw( baseall u4 D refs/tags/v1 );
+nok qw( baseall u4 D dev/u4/foo );
+nok qw( baseall u4 D refs/tags/dev/u4/foo );
+nok qw( baseall u4 D dev/alice/foo );
+nok qw( baseall u4 D refs/tags/dev/alice/foo );
+
+ok qw( baseall u5 R );
+nok qw( baseall u5 W master );
+nok qw( baseall u5 W notmaster );
+nok qw( baseall u5 W refs/tags/boo );
+nok qw( baseall u5 W refs/tags/v1 );
+ok qw( baseall u5 W dev/u5/foo );
+ok qw( baseall u5 W refs/tags/dev/u5/foo );
+nok qw( baseall u5 W dev/alice/foo );
+nok qw( baseall u5 W refs/tags/dev/alice/foo );
+nok qw( baseall u5 + master );
+nok qw( baseall u5 + notmaster );
+nok qw( baseall u5 + refs/tags/boo );
+nok qw( baseall u5 + refs/tags/v1 );
+ok qw( baseall u5 + dev/u5/foo );
+ok qw( baseall u5 + refs/tags/dev/u5/foo );
+nok qw( baseall u5 + dev/alice/foo );
+nok qw( baseall u5 + refs/tags/dev/alice/foo );
+nok qw( baseall u5 C master );
+nok qw( baseall u5 C notmaster );
+nok qw( baseall u5 C refs/tags/boo );
+nok qw( baseall u5 C refs/tags/v1 );
+ok qw( baseall u5 C dev/u5/foo );
+ok qw( baseall u5 C refs/tags/dev/u5/foo );
+nok qw( baseall u5 C dev/alice/foo );
+nok qw( baseall u5 C refs/tags/dev/alice/foo );
+nok qw( baseall u5 D master );
+nok qw( baseall u5 D notmaster );
+nok qw( baseall u5 D refs/tags/boo );
+nok qw( baseall u5 D refs/tags/v1 );
+ok qw( baseall u5 D dev/u5/foo );
+ok qw( baseall u5 D refs/tags/dev/u5/foo );
+nok qw( baseall u5 D dev/alice/foo );
+nok qw( baseall u5 D refs/tags/dev/alice/foo );
+
+ok qw( baseall u6 R );
+nok qw( baseall u6 W master );
+nok qw( baseall u6 W notmaster );
+nok qw( baseall u6 W refs/tags/boo );
+nok qw( baseall u6 W refs/tags/v1 );
+ok qw( baseall u6 W dev/u6/foo );
+ok qw( baseall u6 W refs/tags/dev/u6/foo );
+nok qw( baseall u6 W dev/alice/foo );
+nok qw( baseall u6 W refs/tags/dev/alice/foo );
+nok qw( baseall u6 + master );
+nok qw( baseall u6 + notmaster );
+nok qw( baseall u6 + refs/tags/boo );
+nok qw( baseall u6 + refs/tags/v1 );
+ok qw( baseall u6 + dev/u6/foo );
+ok qw( baseall u6 + refs/tags/dev/u6/foo );
+nok qw( baseall u6 + dev/alice/foo );
+nok qw( baseall u6 + refs/tags/dev/alice/foo );
+nok qw( baseall u6 C master );
+nok qw( baseall u6 C notmaster );
+nok qw( baseall u6 C refs/tags/boo );
+nok qw( baseall u6 C refs/tags/v1 );
+ok qw( baseall u6 C dev/u6/foo );
+ok qw( baseall u6 C refs/tags/dev/u6/foo );
+nok qw( baseall u6 C dev/alice/foo );
+nok qw( baseall u6 C refs/tags/dev/alice/foo );
+nok qw( baseall u6 D master );
+nok qw( baseall u6 D notmaster );
+nok qw( baseall u6 D refs/tags/boo );
+nok qw( baseall u6 D refs/tags/v1 );
+ok qw( baseall u6 D dev/u6/foo );
+ok qw( baseall u6 D refs/tags/dev/u6/foo );
+nok qw( baseall u6 D dev/alice/foo );
+nok qw( baseall u6 D refs/tags/dev/alice/foo );
+
diff --git a/t/vrefs-1.t b/t/vrefs-1.t
new file mode 100755
index 0000000..eea4b24
--- /dev/null
+++ b/t/vrefs-1.t
@@ -0,0 +1,138 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# VREFs - part 1
+# ----------------------------------------------------------------------
+
+try "plan 88";
+
+put "conf/gitolite.conf", "
+ repo gitolite-admin
+ RW+ = admin
+
+ \@gfoo = foo
+ \@lead = u1
+ \@dev2 = u2
+ \@dev4 = u4
+ \@devs = \@dev2 \@dev4 u6
+ repo \@gfoo
+ RW+ = \@lead \@devs
+ # intentional mis-spelling
+ - VREF/MISCOUNT/2 = \@dev2
+ - VREF/MISCOUNT/4 = \@dev4
+ - VREF/MISCOUNT/3/NEWFILES = u6
+ - VREF/MISCOUNT/6 = u6
+";
+
+try "
+ ADMIN_PUSH vr1a
+ cd ..
+ [ -d foo ]; !ok
+ CLONE u1 foo; ok; /Cloning into/
+ /You appear to have cloned an empty/
+ cd foo; ok
+ [ -d .git ]; ok
+
+ # VREF not called for u1
+ tc a1 a2 a3 a4 a5; ok; /aaf9e8e/
+ PUSH u1 master; ok; /new branch.*master -. master/
+ !/helper program missing/
+ !/hook declined/
+ !/remote rejected/
+ # VREF is called for u2
+ tc b1; ok; /1f440d3/
+ PUSH u2; !ok; /helper program missing/
+ /hook declined/
+ /remote rejected/
+";
+
+put "../gitolite-admin/conf/gitolite.conf", "
+ repo gitolite-admin
+ RW+ = admin
+
+ \@gfoo = foo
+ \@lead = u1
+ \@dev2 = u2
+ \@dev4 = u4
+ \@devs = \@dev2 \@dev4 u6
+ repo \@gfoo
+ RW+ = \@lead \@devs
+ - VREF/COUNT/2 = \@dev2
+ - VREF/COUNT/4 = \@dev4
+ - VREF/COUNT/3/NEWFILES = u6
+ - VREF/COUNT/6 = u6
+";
+
+try "
+ ADMIN_PUSH vr1b
+ cd ../foo; ok
+
+ # u2 1 file
+ PUSH u2; ok; /aaf9e8e..1f440d3.*master -. master/
+
+ # u2 2 files
+ tc b2 b3; ok; /c3397f7/
+ PUSH u2; ok; /1f440d3..c3397f7.*master -. master/
+
+ # u2 3 files
+ tc c1 c2 c3; ok; /be242d7/
+ PUSH u2; !ok; /W VREF/COUNT/2 foo u2 DENIED by VREF/COUNT/2/
+ /too many changed files in this push/
+ /hook declined/
+ /remote rejected/
+
+ # u4 3 files
+ PUSH u4; ok; /c3397f7..be242d7.*master -. master/
+
+ # u4 4 files
+ tc d1 d2 d3 d4; ok; /88d80e2/
+ PUSH u4; ok; /be242d7..88d80e2.*master -. master/
+
+ # u4 5 files
+ tc d5 d6 d7 d8 d9; ok; /e9c60b0/
+ PUSH u4; !ok; /W VREF/COUNT/4 foo u4 DENIED by VREF/COUNT/4/
+ /too many changed files in this push/
+ /hook declined/
+ /remote rejected/
+
+ # u1 all files
+ PUSH u1; ok; /88d80e2..e9c60b0.*master -. master/
+
+ # u6 6 old files
+ test-tick
+ tc d1 d2 d3 d4 d5 d6
+ ok; /2773f0a/
+ PUSH u6; ok; /e9c60b0..2773f0a.*master -. master/
+ tag six
+
+ # u6 updates 7 old files
+ test-tick; test-tick
+ tc d1 d2 d3 d4 d5 d6 d7
+ ok; /d3fb574/
+ PUSH u6; !ok; /W VREF/COUNT/6 foo u6 DENIED by VREF/COUNT/6/
+ /too many changed files in this push/
+ /hook declined/
+ /remote rejected/
+ reset-h six; ok; /HEAD is now at 2773f0a/
+
+ # u6 4 new 2 old files
+ test-tick; test-tick
+ tc d1 d2 n1 n2 n3 n4
+ ok; /9e90848/
+ PUSH u6; !ok; /W VREF/COUNT/3/NEWFILES foo u6 DENIED by VREF/COUNT/3/NEWFILES/
+ /too many new files in this push/
+ /hook declined/
+ /remote rejected/
+ reset-h six; ok; /HEAD is now at 2773f0a/
+
+ # u6 3 new 3 old files
+ test-tick; test-tick
+ tc d1 d2 d3 n1 n2 n3
+ ok; /e47ff5d/
+ PUSH u6; ok; /2773f0a..e47ff5d.*master -. master/
+";
diff --git a/t/vrefs-2.t b/t/vrefs-2.t
new file mode 100755
index 0000000..40db308
--- /dev/null
+++ b/t/vrefs-2.t
@@ -0,0 +1,109 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# VREFs - part 2
+# ----------------------------------------------------------------------
+
+try "plan 72";
+
+put "../gitolite-admin/conf/gitolite.conf", "
+ \@gfoo = foo
+ \@lead = u1
+ \@senior_devs = u2 u3
+ \@junior_devs = u4 u5 u6
+ repo \@gfoo
+
+ RW+ = \@all
+
+ RW+ VREF/COUNT/2/NO_SIGNOFF = \@lead
+ - VREF/COUNT/2/NO_SIGNOFF = \@all
+
+ - VREF/COUNT/10/NEWFILES = \@junior_devs
+
+ - VREF/FILETYPE/AUTOGENERATED = \@all
+";
+
+try "
+ ADMIN_PUSH vr2a
+ cd ..
+ # setup
+ [ -d foo ]; !ok
+ CLONE u1 foo; ok; /Cloning into/
+ /You appear to have cloned an empty/
+ cd foo; ok
+ [ -d .git ]; ok
+
+ # u1 push 15 new files
+ tc a b c d e f g h i j k l m n o
+ ok; /d8c0392/
+ PUSH u1 master; ok; /new branch.*master -. master/
+
+ # u2 push 2 new 10 old without signoff
+ tc a b c d e f g h i j u2a u2b
+ ok; /6787ac9/
+ PUSH u2; ok; /d8c0392..6787ac9.*master -. master/
+
+ # u2 fail to push 3 new files without signoff
+ tc u2c u2d u2e; ok; /a74562b/
+ PUSH u2; !ok; /W VREF/COUNT/2/NO_SIGNOFF foo u2 DENIED by VREF/COUNT/2/NO_SIGNOFF/
+ /top commit message should include the text .3 new files signed-off by: tester.example.com./
+ /hook declined/
+ /remote rejected/
+ # u2 push 15 new files with signoff
+ tc u2f u2g u2h u2i u2j u2k u2l u2m u2n u2o u2p u2q
+ ok; /8dd31aa/
+ git commit --allow-empty -m '15 new files signed-off by: tester\@example.com'
+ ok; /.master 6126489. 15 new files signed-off by: tester.example.com/
+ PUSH u2; ok; /6787ac9..6126489.*master -. master/
+
+ # u4 push 2 new 10 old files without signoff
+ tc u4a u4b a b c d e f g h i j
+ ok; /76c5593/
+ PUSH u4; ok; /6126489..76c5593.*master -. master/
+
+ # u4 fail push 3 new files withoug signoff
+ tc u4c u4d u4e; ok; /2a84398/
+ PUSH u4; !ok; /W VREF/COUNT/2/NO_SIGNOFF foo u4 DENIED by VREF/COUNT/2/NO_SIGNOFF/
+ /top commit message should include the text .3 new files signed-off by: tester.example.com./
+ /hook declined/
+ /remote rejected/
+
+ # u4 push 10 new 5 old with signoff
+ tc u4f u4g u4h u4i u4j u4k u4l a b c d e
+ ok; /09b646a/
+ git commit --allow-empty -m '10 new files signed-off by: tester\@example.com'
+ ok; /.master 47f84b0. 10 new files signed-off by: tester.example.com/
+ PUSH u4; ok; /76c5593..47f84b0.*master -. master/
+
+ # u4 fail push 11 new files even with signoff
+ tc u4ab u4ac u4ad u4ae u4af u4ag u4ah u4ai u4aj u4ak u4al
+ ok; /90e7344/
+ git commit --allow-empty -m '11 new files signed-off by: tester\@example.com'
+ ok; /.master 1f36537. 11 new files signed-off by: tester.example.com/
+ PUSH u4; !ok; /W VREF/COUNT/10/NEWFILES foo u4 DENIED by VREF/COUNT/10/NEWFILES/
+ /too many new files in this push/
+ /hook declined/
+ /remote rejected/
+
+ # test AUTOGENERATED vref
+ glt fetch u1 origin; ok;
+ reset-h origin/master; ok;
+ tc not-really.java; ok; /0f88b2e/
+ PUSH u4; ok; /47f84b0..0f88b2e.*master -. master/
+";
+
+put "|cat >> not-really.java", "
+ Generated by the protocol buffer compiler. DO NOT EDIT
+";
+
+try "
+ commit -am pbc; ok; /b2df6ef/
+ PUSH u4; !ok; /W VREF/FILETYPE/AUTOGENERATED foo u4 DENIED by VREF/FILETYPE/AUTOGENERATED/
+ /hook declined/
+ /remote rejected/
+";
diff --git a/t/wild-1.t b/t/wild-1.t
new file mode 100755
index 0000000..7a8f766
--- /dev/null
+++ b/t/wild-1.t
@@ -0,0 +1,126 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# wild repos - part 1
+# ----------------------------------------------------------------------
+
+try "plan 66";
+
+confreset;confadd '
+ @prof = u1
+ @TAs = u2 u3
+ @students = u4 u5 u6
+
+ @gfoo = foo/CREATOR/a[0-9][0-9]
+ repo @gfoo
+ C = @all
+ RW+ = CREATOR
+ RW = WRITERS @TAs
+ R = READERS @prof
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+# reasonably complex setup; we'll do everything from one repo though
+cd ..
+
+# u1 create success
+glt clone u1 file:///foo/u1/a01; ok; /Initialized empty Git repository in .*/foo/u1/a01.git//
+
+# u2 create success
+glt clone u2 file:///foo/u2/a02; ok; /Initialized empty Git repository in .*/foo/u2/a02.git//
+
+# u4 tries to create u2 repo
+glt clone u4 file:///foo/u2/a12; !ok; /R any foo/u2/a12 u4 DENIED by fallthru/
+
+# line anchored regexes
+glt clone u4 file:///foo/u4/a1234; !ok; /R any foo/u4/a1234 u4 DENIED by fallthru/
+
+# u4 tries to create his own repo
+glt clone u4 file:///foo/u4/a12; ok; /Initialized empty Git repository in .*/foo/u4/a12.git//
+ /warning: You appear to have cloned an empty repository./
+
+# u4 push success
+cd a12
+tc p-728 p-729 p-730 p-731; ok
+glt push u4 origin master; ok; /To file:///foo/u4/a12/
+ /\\* \\[new branch\\] master -> master/
+
+# u1 clone success
+cd ..
+glt clone u1 file:///foo/u4/a12 u1a12; ok; /Cloning into 'u1a12'.../
+
+# u1 push fail
+cd u1a12
+tc m-778 m-779; ok;
+glt push u1 origin; !ok; /W any foo/u4/a12 u1 DENIED by fallthru/
+
+# u2 clone success
+cd ..
+glt clone u2 file:///foo/u4/a12 u2a12; ok; /Cloning into 'u2a12'.../
+
+# u2 push success
+cd u2a12
+tc s-708 s-709; ok;
+glt push u2 origin; ok; /To file:///foo/u4/a12/
+ /master -> master/
+
+# u2 rewind fail
+glt push u2 -f origin master^:master; !ok; /\\+ refs/heads/master foo/u4/a12 u2 DENIED by fallthru/
+ reject
+
+# u4 pull to sync up
+cd ../a12
+glt pull u4; ok; /Fast-forward/
+ /From file:///foo/u4/a12/
+ /master -> origin/master/
+
+# u4 rewind success
+git reset --hard HEAD^; ok
+glt push u4 -f; ok; /To file:///foo/u4/a12/
+ /\\+ .* master -> master \\(forced update\\)/
+
+# u5 clone fail
+cd ..
+glt clone u5 file:///foo/u4/a12 u5a12; !ok; /R any foo/u4/a12 u5 DENIED by fallthru/
+
+glt perms u4 foo/u4/a12 + READERS u5
+glt perms u4 foo/u4/a12 + WRITERS u6
+
+glt perms u4 foo/u4/a12 -l
+";
+
+cmp 'READERS u5
+WRITERS u6
+';
+
+try "
+# u5 clone success
+glt clone u5 file:///foo/u4/a12 u5a12; ok; /Cloning into 'u5a12'.../
+
+# u5 push fail
+cd u5a12
+tc y-743 y-744; ok
+glt push u5; !ok; /W any foo/u4/a12 u5 DENIED by fallthru/
+
+# u6 clone success
+cd ..
+glt clone u6 file:///foo/u4/a12 u6a12; ok; /Cloning into 'u6a12'.../
+
+# u6 push success
+cd u6a12
+tc k-68 k-69; ok
+glt push u6 file:///foo/u4/a12; ok; /To file:///foo/u4/a12/
+ /master -> master/
+
+# u6 rewind fail
+glt push u6 -f file:///foo/u4/a12 master^:master
+ !ok; /\\+ refs/heads/master foo/u4/a12 u6 DENIED by fallthru/
+ reject
+";
diff --git a/t/wild-2.t b/t/wild-2.t
new file mode 100755
index 0000000..cbba4f8
--- /dev/null
+++ b/t/wild-2.t
@@ -0,0 +1,128 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+# wild repos - part 2
+# ----------------------------------------------------------------------
+
+try "plan 65";
+
+confreset;confadd '
+ @prof = u1
+ @TAs = u2 u3
+ @students = u4 u5 u6
+
+ @gfoo = foo/CREATOR/a[0-9][0-9]
+ repo @gfoo
+ C = @students
+ RW+ = CREATOR
+ RW = WRITERS @TAs
+ R = READERS @prof
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+cd ..
+
+# u1 create fail
+glt clone u1 file:///foo/u1/a01; !ok; /R any foo/u1/a01 u1 DENIED by fallthru/
+
+# u2 create fail
+glt clone u2 file:///foo/u2/a02; !ok; /R any foo/u2/a02 u2 DENIED by fallthru/
+
+# u4 tries to create u2 repo
+glt clone u4 file:///foo/u2/a12; !ok; /R any foo/u2/a12 u4 DENIED by fallthru/
+
+# line anchored regexes
+glt clone u4 file:///foo/u4/a1234; !ok; /R any foo/u4/a1234 u4 DENIED by fallthru/
+
+# u4 tries to create his own repo
+glt clone u4 file:///foo/u4/a12; ok; /Initialized empty Git repository in .*/foo/u4/a12.git//
+ /warning: You appear to have cloned an empty repository./
+
+# u4 push success
+cd a12
+tc n-770 n-771 n-772 n-773; ok
+glt push u4 origin master; ok; /To file:///foo/u4/a12/
+ /\\* \\[new branch\\] master -> master/
+
+# u1 clone success
+cd ..
+glt clone u1 file:///foo/u4/a12 u1a12; ok; /Cloning into 'u1a12'.../
+
+# u1 push fail
+cd u1a12
+tc c-442 c-443; ok
+glt push u1; !ok; /W any foo/u4/a12 u1 DENIED by fallthru/
+
+# u2 clone success
+cd ..
+glt clone u2 file:///foo/u4/a12 u2a12; ok; /Cloning into 'u2a12'.../
+
+# u2 push success
+cd u2a12
+tc e-393 e-394; ok;
+glt push u2; ok; /To file:///foo/u4/a12/
+ /master -> master/
+
+# u2 rewind fail
+glt push u2 -f origin master^:master; !ok; /\\+ refs/heads/master foo/u4/a12 u2 DENIED by fallthru/
+ reject
+
+# u4 pull to sync up
+cd ../a12
+glt pull u4; ok; /Fast-forward/
+ /From file:///foo/u4/a12/
+ /master -> origin/master/
+
+# u4 rewind success
+git reset --hard HEAD^; ok
+glt push u4 -f; ok; /To file:///foo/u4/a12/
+ /\\+ .* master -> master \\(forced update\\)/
+
+# u5 clone fail
+cd ..
+glt clone u5 file:///foo/u4/a12 u5a12; !ok; /R any foo/u4/a12 u5 DENIED by fallthru/
+
+# setperm
+glt perms u4 foo/u4/a12 + READERS u5
+glt perms u4 foo/u4/a12 + WRITERS u6
+
+# getperms
+glt perms u4 foo/u4/a12 -l
+";
+
+cmp 'READERS u5
+WRITERS u6
+';
+
+try "
+# u5 clone success
+glt clone u5 file:///foo/u4/a12 u5a12; ok; /Cloning into 'u5a12'.../
+
+# u5 push fail
+cd u5a12
+tc g-809 g-810; ok
+glt push u5; !ok; /W any foo/u4/a12 u5 DENIED by fallthru/
+
+# u6 clone success
+cd ..
+glt clone u6 file:///foo/u4/a12 u6a12; ok; /Cloning into 'u6a12'.../
+
+# u6 push success
+cd u6a12
+tc f-912 f-913
+glt push u6 file:///foo/u4/a12; ok; /To file:///foo/u4/a12/
+ /master -> master/
+
+# u6 rewind fail
+glt push u6 -f file:///foo/u4/a12 master^:master
+ !ok; /\\+ refs/heads/master foo/u4/a12 u6 DENIED by fallthru/
+ reject
+
+";
diff --git a/t/writable.t b/t/writable.t
new file mode 100755
index 0000000..a649323
--- /dev/null
+++ b/t/writable.t
@@ -0,0 +1,124 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+use Cwd;
+my $workdir = getcwd();
+
+# 'gitolite writable' command
+# ----------------------------------------------------------------------
+
+my $sf = ".gitolite.down";
+
+try "plan 66";
+try "DEF POK = !/DENIED/; !/failed to push/";
+
+# delete the down file
+unlink "$ENV{HOME}/$sf";
+
+# add foo, bar/..* repos to the config and push
+confreset;confadd '
+ repo foo
+ RW = u1
+ R = u2
+
+ repo bar/..*
+ C = u2 u4 u6
+ RW = CREATOR u3
+';
+
+try "ADMIN_PUSH set1; !/FATAL/" or die text();
+
+try "
+ # clone and push to foo
+ CLONE u1 foo; ok
+ cd foo; ok
+ tc f1; ok
+ PUSH u1 master; ok; /new branch/
+
+ # auto-clone and push to bar/u2
+ cd ..
+ CLONE u2 bar/u2; ok; /appear to have cloned an empty/
+ /Initialized empty/
+ cd u2;
+ tc f2
+ PUSH u2 master; ok;
+
+ # disable site with some message
+ gitolite writable \@all off testing site-wide disable; ok
+
+ # try push foo and see fail + message
+ cd ../foo; ok
+ tc f3; ok
+ PUSH u1; !ok; /testing site-wide disable/
+ # try push bar/u2 and ...
+ cd ../u2; ok
+ tc f4; ok
+ PUSH u2; !ok; /testing site-wide disable/
+
+ # try auto-create push bar/u4 and this works!!
+ cd ..
+ CLONE u4 bar/u4; ok; /appear to have cloned an empty/
+ /Initialized empty/
+ !/testing site-wide disable/
+ cd u4; ok
+
+ # enable site
+ gitolite writable \@all on; ok
+
+ # try same 3 again
+
+ # try push foo and see fail + message
+ cd ../foo; ok
+ tc g3; ok
+ PUSH u1; ok; /master -> master/
+ # try push bar/u2 and ...
+ cd ../u2; ok
+ tc g4; ok
+ PUSH u2; ok; /master -> master/
+
+ # try auto-create push bar/u4 and this works!!
+ cd ..
+ CLONE u6 bar/u6; ok; /appear to have cloned an empty/
+ /Initialized empty/
+ !/testing site-wide disable/
+ cd u6; ok
+
+ # disable just foo
+ gitolite writable foo off foo down
+
+ # try push foo and see the message
+ cd ../foo; ok
+ tc g3; ok
+ PUSH u1; !ok; /foo down/
+ !/testing site-wide disable/
+ # push bar/u2 ok
+ cd ../u2
+ tc g4
+ PUSH u2; ok; /master -> master/
+
+ # enable foo, disable bar/u2
+ gitolite writable foo on
+ gitolite writable bar/u2 off the bar is closed
+
+ # try both
+ cd ../foo; ok
+ tc h3; ok
+ PUSH u1; ok; /master -> master/
+ # push bar/u2 ok
+ cd ../u2
+ tc h4
+ PUSH u2; !ok; /the bar is closed/
+
+ ssh u3 writable bar/u2 on; !ok; /you are not authorized/
+ ssh u3 writable \@all on; !ok; /you are not authorized/
+
+ ssh u2 writable bar/u2 on; ok
+ ssh u2 writable \@all on; !ok; /you are not authorized/
+
+ ssh admin writable \@all on;
+ ok
+";
diff --git a/t/z-end.t b/t/z-end.t
new file mode 100755
index 0000000..6c98fe4
--- /dev/null
+++ b/t/z-end.t
@@ -0,0 +1,14 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# this is hardcoded; change it if needed
+use lib "src/lib";
+use Gitolite::Test;
+
+try "plan 1; cd $ENV{PWD}; git status -s -uno; !/./ or die" or die "dirty tree";
+try "git log -1 --format='%h %ai %s'";
+put "|cat >> prove.log", text();
+
+
+