summaryrefslogtreecommitdiffstats
path: root/contrib
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 /contrib
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 'contrib')
-rwxr-xr-xcontrib/commands/compile-1139
-rwxr-xr-xcontrib/commands/ukm732
-rwxr-xr-xcontrib/hooks/repo-specific/save-push-signatures188
-rw-r--r--contrib/lib/Apache/gitolite.conf47
-rw-r--r--contrib/lib/Gitolite/Triggers/RedmineUserAlias.pm55
-rw-r--r--contrib/t/ukm.t447
-rwxr-xr-xcontrib/triggers/IP-check43
-rwxr-xr-xcontrib/triggers/file_mirror172
-rwxr-xr-xcontrib/utils/ad_groups.sh40
-rwxr-xr-xcontrib/utils/gitolite-local136
-rwxr-xr-xcontrib/utils/ipa_groups.pl229
-rwxr-xr-xcontrib/utils/ldap_groups.sh22
-rwxr-xr-xcontrib/utils/rc-format-v3.4212
-rwxr-xr-xcontrib/utils/testconf130
-rw-r--r--contrib/vim/indent/gitolite.vim49
-rw-r--r--contrib/vim/syntax/gitolite.vim94
16 files changed, 2735 insertions, 0 deletions
diff --git a/contrib/commands/compile-1 b/contrib/commands/compile-1
new file mode 100755
index 0000000..a5b5356
--- /dev/null
+++ b/contrib/commands/compile-1
@@ -0,0 +1,139 @@
+#!/usr/bin/perl -s
+use strict;
+use warnings;
+
+# DESCRIPTION:
+
+# This program is meant to re-compile the access rules (and 'config' or
+# 'option' lines) of exactly ONE actual repo (i.e., not a repo group or a
+# repo pattern).
+
+# MOTIVATION:
+
+# Fedora has a huge number of repos, as well as lot of churn in permissions.
+# The combination of having a large conf *and* frequent compiles were not
+# working out, hence this solution. Not sure if any others have such a
+# situation, so it's a standalone program, separate from "core" gitolite,
+# shipped in "contrib" instead of "src".
+
+# SETUP:
+
+# It expects to run as a gitolite sub-command, which means you will need to
+# copy it from contrib to src/commands, or the equivalent location inside
+# LOCAL_CODE; see non-core.html in the docs for details.
+
+# INVOCATION:
+
+# It takes one argument: the name of a file that contains the new ruleset
+# you want to use. (This cannot be STDIN or "-" or something).
+
+# example:
+#
+# gitolite compile-1 <file-containing-rules-for-exactly-one-repo>
+
+# WARNING:
+
+# If the main gitolite.conf changes significantly (specifically, if the
+# number of effective rules in it increase quite a bit), you may have to run
+# this command on ALL repos to update their individual gl-conf files.
+#
+# (TBD: explain this in more concrete terms)
+
+# ----------------------------------------------------------------------
+# THERE IS NO ERROR CHECKING ON THE WARNING ABOVE, NOR ON THE ASSUMPTIONS AND
+# REQUIREMENTS BELOW. PLEASE USE CAREFULLY!
+# ----------------------------------------------------------------------
+
+# ASSUMPTIONS/REQUIREMENTS:
+
+# The file given must contain exactly one 'repo' line, with exactly one repo
+# name, followed by the rules, configs, and options for that repo in the
+# normal gitolite.conf syntax.
+
+# The file must not have any group definitions, though it may use group
+# definitions already setup in the main gitolite.conf file.
+
+# Rules for this repo need not be already defined in the main gitolite.conf.
+# If they are, they will cease to have any effect once you run this command
+# - only the rules you supply in the file passed to this command will apply,
+# and they will be considered to be placed at the end of gitolite.conf.
+
+# If the repo does not exist, it must be first created using:
+#
+# GL_USER=admin gitolite create <reponame>
+#
+# where <reponame> is the gitolite-style name (i.e., "foo", not "foo.git" or
+# "~/repositories/foo" or "~/repositories/foo.git")
+#
+# This, of course, requires the main gitolite.conf to have the following
+# lines at the top:
+#
+# repo [A-Za-z].*
+# C = admin
+
+# Any change to the main gitolite.conf is followed by a full 'gitolite
+# compile'; i.e., ~/.gitolite/conf/gitolite.conf-compiled.pm, the main
+# "compiled" conf file, is consistent with the latest gitolite.conf.
+
+use 5.10.0;
+use Data::Dumper;
+
+use lib $ENV{GL_LIBDIR};
+use Gitolite::Rc;
+use Gitolite::Common;
+use Gitolite::Conf;
+use Gitolite::Conf::Store;
+use Gitolite::Conf::Sugar;
+
+my ($cf, $repo) = args(); # conffile from @ARGV, repo from first line of conffile
+my $startseq = getseq(); # get the starting sequence number by looking in the (common) compiled conf file
+parse_and_store($cf, $repo); # parse the ruleset and write out just the gl-conf file
+ # (this is the only part that uses core gitolite functions)
+update_seq($repo, $startseq); # update gl-conf with adjusted sequence numbers
+
+exit 0;
+
+# ----------------------------------------------------------------------
+
+sub args {
+ my $cf = shift @ARGV or _die "need conffile";
+ $cf = $ENV{PWD} . "/" . $cf unless $cf =~ m(^/);
+
+ my $t = slurp($cf);
+ _die "bad conf file" unless $t =~ /^\s*repo\s+(\S+)\s*$/m;
+ my $repo = $1;
+
+ return ($cf, $repo);
+}
+
+sub getseq {
+ my @main_cc = slurp "$rc{GL_ADMIN_BASE}/conf/gitolite.conf-compiled.pm";
+ my $max = 0;
+ for (@main_cc) {
+ $max = $1 if m/^ +(\d+),$/ and $max < $1;
+ }
+
+ return $max;
+}
+
+sub parse_and_store {
+ my ($cf, $repo) = @_;
+
+ parse(sugar($cf));
+ _chdir( $rc{GL_REPO_BASE} );
+ Gitolite::Conf::Store::store_1($repo);
+}
+
+sub update_seq {
+ my ($repo, $startseq) = @_;
+
+ _chdir("$rc{GL_REPO_BASE}/$repo.git");
+ my $text = slurp("gl-conf");
+
+ $startseq+=1000;
+ # just for safety, in case someone adds a few rules to the main conf later, but neglects to update repo confs
+
+ $text =~ s/^( +)(\d+),$/"$1" . ($2+$startseq) . ","/gme;
+
+ _print("gl-conf", $text);
+}
diff --git a/contrib/commands/ukm b/contrib/commands/ukm
new file mode 100755
index 0000000..3683154
--- /dev/null
+++ b/contrib/commands/ukm
@@ -0,0 +1,732 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use lib $ENV{GL_LIBDIR};
+use Gitolite::Rc;
+use Gitolite::Common;
+use Gitolite::Easy;
+
+=for usage
+Usage for this command is not that simple. Please read the full
+documentation in
+https://github.com/sitaramc/gitolite-doc/blob/master/contrib/ukm.mkd
+or online at http://gitolite.com/gitolite/ukm.html.
+=cut
+
+usage() if @ARGV and $ARGV[0] eq '-h';
+
+# Terms used in this file.
+# pubkeypath: the (relative) filename of a public key starting from
+# gitolite-admin/keydir. Examples: alice.pub, foo/bar/alice.pub,
+# alice@home.pub, foo/alice@laptop.pub. You get more examples, if you
+# replace "alice" by "bob@example.com".
+# userid: computed from a pubkeypath by removing any directory
+# part, the '.pub' extension and the "old-style" @NAME classifier.
+# The userid identifies a user in the gitolite.conf file.
+# keyid: an identifier for a key given on the command line.
+# If the script is called by one of the super_key_managers, then the
+# keyid is the pubkeypath without the '.pub' extension. Otherwise it
+# is the userid for a guest.
+# The keyid is normalized to lowercase letters.
+
+my $rb = $rc{GL_REPO_BASE};
+my $ab = $rc{GL_ADMIN_BASE};
+
+# This will be the subdirectory under "keydir" in which the guest
+# keys will be stored. To prevent denial of service, this directory
+# should better start with 'zzz'.
+# The actual value can be set through the GUEST_DIRECTORY resource.
+# WARNING: If this value is changed you must understand the consequences.
+# There will be no support if guestkeys_dir is anything else than
+# 'zzz/guests'.
+my $guestkeys_dir = 'zzz/guests';
+
+# A guest key cannot have arbitrary names (keyid). Only keys that do *not*
+# match $forbidden_guest_pattern are allowed. Super-key-managers can add
+# any keyid.
+
+# This is the directory for additional keys of a self key manager.
+my $selfkeys_dir = 'zzz/self';
+# There is no flexibility for selfkeys. One must specify a keyid that
+# matches the regular expression '^@[a-z0-9]+$'. Note that all keyids
+# are transformed to lowercase before checking.
+my $required_self_pattern = qr([a-z0-9]+);
+my $selfkey_management = 0; # disable selfkey managment
+
+# For guest key managers the keyid must pass two tests.
+# 1) It must match the $required_guest_pattern regular expression.
+# 2) It must not match the $forbidden_guest_pattern regular expression.
+# Default for $forbidden_guest_pattern is qr(.), i.e., every keyid is
+# forbidden, or in other words, only the gitolite-admin can manage keys.
+# Default for $required_guest_pattern is such that the keyid must look
+# like an email address, i.e. must have exactly one @ and at least one
+# dot after the @.
+# Just setting 'ukm' => 1 in .gitolite.rc only allows the super-key-managers
+# (i.e., only the gitolite admin(s)) to manage keys.
+my $required_guest_pattern =
+ qr(^[0-9a-z][-0-9a-z._+]*@[-0-9a-z._+]+[.][-0-9a-z._+]+$);
+my $forbidden_guest_pattern = qr(.);
+
+die "The command 'ukm' is not enabled.\n" if ! $rc{'COMMANDS'}{'ukm'};
+
+my $km = $rc{'UKM_CONFIG'};
+if(ref($km) eq 'HASH') {
+ # If not set we only allow keyids that look like emails
+ my $rgp = $rc{'UKM_CONFIG'}{'REQUIRED_GUEST_PATTERN'} || '';
+ $required_guest_pattern = qr(^($rgp)$) if $rgp;
+ $forbidden_guest_pattern = $rc{'UKM_CONFIG'}{'FORBIDDEN_GUEST_PATTERN'}
+ || $forbidden_guest_pattern;
+ $selfkey_management = $rc{'UKM_CONFIG'}{'SELFKEY_MANAGEMENT'} || 0;
+}
+
+# get the actual userid
+my $gl_user = $ENV{GL_USER};
+my $super_key_manager = is_admin(); # or maybe is_super_admin() ?
+
+# save arguments for later
+my $operation = shift || 'list';
+my $keyid = shift || '';
+$keyid = lc $keyid; # normalize to lowercase ids
+
+my ($zop, $zfp, $zselector, $zuser) = get_pending($gl_user);
+# The following will only be true if a selfkey manager logs in to
+# perform a pending operation.
+my $pending_self = ($zop ne '');
+
+die "You are not a key manager.\n"
+ unless $super_key_manager || $pending_self
+ || in_group('guest-key-managers')
+ || in_group('self-key-managers');
+
+# Let's deal with the pending user first. The only allowed operations
+# that are to confirm the add operation with the random code
+# that must be provided via stdin or to undo a pending del operation.
+if ($pending_self) {
+ pending_user($gl_user, $zop, $zfp, $zselector, $zuser);
+ exit;
+}
+
+my @available_operations = ('list','add','del');
+die "unknown ukm subcommand: $operation\n"
+ unless grep {$operation eq $_} @available_operations;
+
+# get to the keydir
+_chdir("$ab/keydir");
+
+# Note that the program warns if it finds a fingerprint that maps to
+# different userids.
+my %userids = (); # mapping from fingerprint to userid
+my %fingerprints = (); # mapping from pubkeypath to fingerprint
+my %pubkeypaths = (); # mapping from userid to pubkeypaths
+ # note that the result is a list of pubkeypaths
+
+# Guest keys are managed by people in the @guest-key-managers group.
+# They can only add/del keys in the $guestkeys_dir directory. In fact,
+# the guest key manager $gl_user has only access to keys inside
+# %guest_pubkeypaths.
+my %guest_pubkeypaths = (); # mapping from userid to pubkeypath for $gl_user
+
+# Self keys are managed by people in the @self-key-managers group.
+# They can only add/del keys in the $selfkeys_dir directory. In fact,
+# the self key manager $gl_user has only access to keys inside
+# %self_pubkeypaths.
+my %self_pubkeypaths = ();
+
+# These are the keys that are managed by a super key manager.
+my @all_pubkeypaths = `find . -type f -name "*.pub" 2>/dev/null | sort`;
+
+for my $pubkeypath (@all_pubkeypaths) {
+ chomp($pubkeypath);
+ my $fp = fingerprint($pubkeypath);
+ $fingerprints{$pubkeypath} = $fp;
+ my $userid = get_userid($pubkeypath);
+ my ($zop, $zfp, $zselector, $zuser) = get_pending($userid);
+ $userid = $zuser if $zop;
+ if (! defined $userids{$fp}) {
+ $userids{$fp} = $userid;
+ } else {
+ warn "key $fp is used for different user ids\n"
+ unless $userids{$fp} eq $userid;
+ }
+ push @{$pubkeypaths{$userid}}, $pubkeypath;
+ if ($pubkeypath =~ m|^./$guestkeys_dir/([^/]+)/[^/]+\.pub$|) {
+ push @{$guest_pubkeypaths{$userid}}, $pubkeypath if $gl_user eq $1;
+ }
+ if ($pubkeypath =~ m|^./$selfkeys_dir/([^/]+)/[^/]+\.pub$|) {
+ push @{$self_pubkeypaths{$userid}}, $pubkeypath if $gl_user eq $1;
+ }
+}
+
+###################################################################
+# do stuff according to the operation
+###################################################################
+
+if ( $operation eq 'list' ) {
+ list_pubkeys();
+ print "\n\n";
+ exit;
+}
+
+die "keyid required\n" unless $keyid;
+die "Not allowed to use '..' in keyid.\n" if $keyid =~ /\.\./;
+
+if ( $operation eq 'add' ) {
+ if ($super_key_manager) {
+ add_pubkey($gl_user, "$keyid.pub", safe_stdin());
+ } elsif (selfselector($keyid)) {
+ add_self($gl_user, $keyid, safe_stdin());
+ } else {
+ # assert ingroup('guest-key-managers');
+ add_guest($gl_user, $keyid, safe_stdin());
+ }
+} elsif ( $operation eq 'del' ) {
+ if ($super_key_manager) {
+ del_super($gl_user, "$keyid.pub");
+ } elsif (selfselector($keyid)) {
+ del_self($gl_user, $keyid);
+ } else {
+ # assert ingroup('guest-key-managers');
+ del_guest($gl_user, $keyid);
+ }
+}
+
+exit;
+
+
+###################################################################
+# only function definitions are following
+###################################################################
+
+# make a temp clone and switch to it
+our $TEMPDIR;
+BEGIN { $TEMPDIR = `mktemp -d -t tmp.XXXXXXXXXX`; chomp($TEMPDIR) }
+END { my $err = $?; `/bin/rm -rf $TEMPDIR`; $? = $err; }
+
+sub cd_temp_clone {
+ chomp($TEMPDIR);
+ hushed_git( "clone", "$rb/gitolite-admin.git", "$TEMPDIR/gitolite-admin" );
+ chdir("$TEMPDIR/gitolite-admin");
+ my $ip = $ENV{SSH_CONNECTION};
+ $ip =~ s/ .*//;
+ my ($zop, $zfp, $zselector, $zuser) = get_pending($ENV{GL_USER});
+ my $email = $zuser;
+ $email .= '@' . $ip unless $email =~ m(@);
+ my $name = $zop ? "\@$zselector" : $zuser;
+ # Record the keymanager in the gitolite-admin repo as author of the change.
+ hushed_git( "config", "user.email", "$email" );
+ hushed_git( "config", "user.name", "'$name from $ip'" );
+}
+
+# compute the fingerprint from the full path of a pubkey file
+sub fingerprint {
+ my ($fp, $output) = ssh_fingerprint_file(shift);
+ # Do not print the output of $output to an untrusted destination.
+ die "does not seem to be a valid pubkey\n" unless $fp;
+ return $fp;
+}
+
+
+# Read one line from STDIN and return it.
+# If no data is available on STDIN after one second, the empty string
+# is returned.
+# If there is more than one line or there was an error in reading, the
+# function dies.
+sub safe_stdin {
+ use IO::Select;
+ my $s=IO::Select->new(); $s->add(\*STDIN);
+ return '' unless $s->can_read(1);
+ my $data;
+ my $ret = read STDIN, $data, 4096;
+ # current pubkeys are approx 400 bytes so we go a little overboard
+ die "could not read pubkey data" . ( defined($ret) ? "" : ": $!" ) . "\n"
+ unless $ret;
+ die "pubkey data seems to have more than one line\n" if $data =~ /\n./;
+ return $data;
+}
+
+# call git, be quiet
+sub hushed_git {
+ system("git " . join(" ", @_) . ">/dev/null 2>/dev/null");
+}
+
+# Extract the userid from the full path of the pubkey file (relative
+# to keydir/ and including the '.pub' extension.
+sub get_userid {
+ my ($u) = @_; # filename of pubkey relative to keydir/.
+ $u =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub
+ $u =~ s/(\@[^.]+)?\.pub$//; # baz.pub, baz@home.pub -> baz
+ return $u;
+}
+
+# Extract the @selector part from the full path of the pubkey file
+# (relative to keydir/ and including the '.pub' extension).
+# If there is no @selector part, the empty string is returned.
+# We also correctly extract the selector part from pending keys.
+sub get_selector {
+ my ($u) = @_; # filename of pubkey relative to keydir/.
+ $u =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub
+ $u =~ s(\.pub$)(); # baz@home.pub -> baz@home
+ return $1 if $u =~ m/.\@($required_self_pattern)$/; # baz@home -> home
+ my ($zop, $zfp, $zselector, $zuser) = get_pending($u);
+ # If $u was not a pending key, then $zselector is the empty string.
+ return $zselector;
+}
+
+# Extract fingerprint, operation, selector, and true userid from a
+# pending userid.
+sub get_pending {
+ my ($gl_user) = @_;
+ return ($1, $2, $3, $4)
+ if ($gl_user=~/^zzz-(...)-([0-9a-f]{32})-($required_self_pattern)-(.*)/);
+ return ('', '', '', $gl_user)
+}
+
+# multiple / and are simplified to one / and the path is made relative
+sub sanitize_pubkeypath {
+ my ($pubkeypath) = @_;
+ $pubkeypath =~ s|//|/|g; # normalize path
+ $pubkeypath =~ s,\./,,g; # remove './' from path
+ return './'.$pubkeypath; # Don't allow absolute paths.
+}
+
+# This function is only relavant for guest key managers.
+# It returns true if the pattern is OK and false otherwise.
+sub required_guest_keyid {
+ my ($_) = @_;
+ /$required_guest_pattern/ and ! /$forbidden_guest_pattern/;
+}
+
+# The function takes a $keyid as input and returns the keyid with the
+# initial @ stripped if everything is fine. It aborts with an error if
+# selfkey management is not enabled or the function is called for a
+# non-self-key-manager.
+# If the required selfkey pattern is not matched, it returns an empty string.
+# Thus the function can be used to check whether a given keyid is a
+# proper selfkeyid.
+sub selfselector {
+ my ($keyid) = @_;
+ return '' unless $keyid =~ m(^\@($required_self_pattern)$);
+ $keyid = $1;
+ die "selfkey management is not enabled\n" unless $selfkey_management;
+ die "You are not a selfkey manager.\n" if ! in_group('self-key-managers');
+ return $keyid;
+}
+
+# Return the number of characters reserved for the userid field.
+sub userid_width {
+ my ($paths) = @_;
+ my (%pkpaths) = %{$paths};
+ my (@userid_lengths) = sort {$a <=> $b} (map {length($_)} keys %pkpaths);
+ @userid_lengths ? $userid_lengths[-1] : 0;
+}
+
+# List the keys given by a reference to a hash.
+# The regular expression $re is used to remove the initial part of the
+# keyid and replace it by what is matched inside the parentheses.
+# $format and $width are used for pretty printing
+sub list_keys {
+ my ($paths, $tokeyid, $format, $width) = @_;
+ my (%pkpaths) = %{$paths};
+ for my $userid (sort keys %pkpaths) {
+ for my $pubkeypath (sort @{$pkpaths{$userid}}) {
+ my $fp = $fingerprints{$pubkeypath};
+ my $userid = $userids{$fp};
+ my $keyid = &{$tokeyid}($pubkeypath);
+ printf $format,$fp,$userid,$width+1-length($userid),"",$keyid
+ if ($super_key_manager
+ || required_guest_keyid($keyid)
+ || $keyid=~m(^\@));
+ }
+ }
+}
+
+# Turn a pubkeypath into a keyid for super-key-managers, guest-keys,
+# and self-keys.
+sub superkeyid {
+ my ($keyid) = @_;
+ $keyid =~ s(\.pub$)();
+ $keyid =~ s(^\./)();
+ return $keyid;
+}
+
+sub guestkeyid {
+ my ($keyid) = @_;
+ $keyid =~ s(\.pub$)();
+ $keyid =~ s(^.*/)();
+ return $keyid;
+}
+
+sub selfkeyid {
+ my ($keyid) = @_;
+ $keyid =~ s(\.pub$)();
+ $keyid =~ s(^.*/)();
+ my ($zop, $zfp, $zselector, $zuser) = get_pending($keyid);
+ return "\@$zselector (pending $zop)" if $zop;
+ $keyid =~ s(.*@)(@);
+ return $keyid;
+}
+
+###################################################################
+
+# List public keys managed by the respective user.
+# The fingerprints, userids and keyids are printed.
+# keyids are shown in a form that can be used for add and del
+# subcommands.
+sub list_pubkeys {
+ print "Hello $gl_user, you manage the following keys:\n";
+ my $format = "%-47s %s%*s%s\n";
+ my $width = 0;
+ if ($super_key_manager) {
+ $width = userid_width(\%pubkeypaths);
+ $width = 6 if $width < 6; # length("userid")==6
+ printf $format, "fingerprint", "userid", ($width-5), "", "keyid";
+ list_keys(\%pubkeypaths, , \&superkeyid, $format, $width);
+ } else {
+ my $widths = $selfkey_management?userid_width(\%self_pubkeypaths):0;
+ my $widthg = userid_width(\%guest_pubkeypaths);
+ $width = $widths > $widthg ? $widths : $widthg; # maximum width
+ return unless $width; # there are no keys
+ $width = 6 if $width < 6; # length("userid")==6
+ printf $format, "fingerprint", "userid", ($width-5), "", "keyid";
+ list_keys(\%self_pubkeypaths, \&selfkeyid, $format, $width)
+ if $selfkey_management;
+ list_keys(\%guest_pubkeypaths, \&guestkeyid, $format, $width);
+ }
+}
+
+
+###################################################################
+
+# Add a public key for the user $gl_user.
+# $pubkeypath is the place where the new key will be stored.
+# If the file or its fingerprint already exists, the operation is
+# rejected.
+sub add_pubkey {
+ my ( $gl_user, $pubkeypath, $keymaterial ) = @_;
+ if(! $keymaterial) {
+ print STDERR "Please supply the new key on STDIN.\n";
+ print STDERR "Try something like this:\n";
+ print STDERR "cat FOO.pub | ssh GIT\@GITOLITESERVER ukm add KEYID\n";
+ die "missing public key data\n";
+ }
+ # clean pubkeypath a bit
+ $pubkeypath = sanitize_pubkeypath($pubkeypath);
+ # Check that there is not yet something there already.
+ die "cannot override existing key\n" if $fingerprints{$pubkeypath};
+
+ my $userid = get_userid($pubkeypath);
+ # Super key managers shouldn't be able to add a that leads to
+ # either an empty userid or to a userid that starts with @.
+ #
+ # To avoid confusion, all keyids for super key managers must be in
+ # a full path format. Having a public key of the form
+ # gitolite-admin/keydir/@foo.pub might be confusing and might lead
+ # to other problems elsewhere.
+ die "cannot add key that starts with \@\n" if (!$userid) || $userid=~/^@/;
+
+ cd_temp_clone();
+ _chdir("keydir");
+ $pubkeypath =~ m((.*)/); # get the directory part
+ _mkdir($1);
+ _print($pubkeypath, $keymaterial);
+ my $fp = fingerprint($pubkeypath);
+
+ # Maybe we are adding a selfkey.
+ my ($zop, $zfp, $zselector, $zuser) = get_pending($userid);
+ my $user = $zop ? "$zuser\@$zselector" : $userid;
+ $userid = $zuser;
+ # Check that there isn't a key with the same fingerprint under a
+ # different userid.
+ if (defined $userids{$fp}) {
+ if ($userid ne $userids{$fp}) {
+ print STDERR "Found $fp $userids{$fp}\n" if $super_key_manager;
+ print STDERR "Same key is already available under another userid.\n";
+ die "cannot add key\n";
+ } elsif ($zop) {
+ # Because of the way a key is confirmed with ukm, it is
+ # impossible to confirm the initial key of the user as a
+ # new selfkey. (It will lead to the function list_pubkeys
+ # instead of pending_user_add, because the gl_user will
+ # not be that of a pending user.) To avoid confusion, we,
+ # therefore, forbid to add the user's initial key
+ # altogether.
+ # In fact, we here also forbid to add any key for that
+ # user that is already in the system.
+ die "You cannot add a key that already belongs to you.\n";
+ }
+ } else {# this fingerprint does not yet exist
+ my @paths = @{$pubkeypaths{$userid}} if defined $pubkeypaths{$userid};
+ if (@paths) {# there are already keys for $userid
+ if (grep {$pubkeypath eq $_} @paths) {
+ print STDERR "The keyid is already present. Nothing changed.\n";
+ } elsif ($super_key_manager) {
+ # It's OK to add new selfkeys, but here we are in the case
+ # of adding multiple keys for guests. That is forbidden.
+ print STDERR "Adding new public key for $userid.\n";
+ } elsif ($pubkeypath =~ m(^\./$guestkeys_dir/)) {
+ # Arriving here means we are about to add a *new*
+ # guest key, because the fingerprint is not yet
+ # existing. This would be for an already existing
+ # userid (added by another guest key manager). Since
+ # that effectively means to (silently) add an
+ # additional key for an existing user, it must be
+ # forbidden.
+ die "cannot add another public key for an existing user\n";
+ }
+ }
+ }
+ exit if (`git status -s` eq ''); # OK to add identical keys twice
+ hushed_git( "add", "." ) and die "git add failed\n";
+ hushed_git( "commit", "-m", "'ukm add $gl_user $userid\n\n$fp'" )
+ and die "git commit failed\n";
+ system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
+}
+
+# Guest key managers should not be allowed to add directories or
+# multiple keys via the @domain mechanism, since this might allow
+# another guest key manager to give an attacker access to another
+# user's repositories.
+#
+# Example: Alice adds bob.pub for bob@example.org. David adds eve.pub
+# (where only Eve but not Bob has the private key) under the keyid
+# bob@example.org@foo. This basically gives Eve the same rights as
+# Bob.
+sub add_guest {
+ my ( $gl_user, $keyid, $keymaterial ) = @_;
+ die "keyid not allowed: '$keyid'\n"
+ if $keyid =~ m(@.*@) or $keyid =~ m(/) or !required_guest_keyid($keyid);
+ add_pubkey($gl_user, "$guestkeys_dir/$gl_user/$keyid.pub", $keymaterial);
+}
+
+# Add a new selfkey for user $gl_user.
+sub add_self {
+ my ( $gl_user, $keyid, $keymaterial ) = @_;
+ my $selector = "";
+ $selector = selfselector($keyid); # might return empty string
+ die "keyid not allowed: $keyid\n" unless $selector;
+
+ # Check that the new selector is not already in use even not in a
+ # pending state.
+ die "keyid already in use: $keyid\n"
+ if grep {selfkeyid($_)=~/^\@$selector( .*)?$/} @{$self_pubkeypaths{$gl_user}};
+ # generate new pubkey create fingerprint
+ system("ssh-keygen -N '' -q -f \"$TEMPDIR/session\" -C $gl_user");
+ my $sessionfp = fingerprint("$TEMPDIR/session.pub");
+ $sessionfp =~ s/://g;
+ my $user = "zzz-add-$sessionfp-$selector-$gl_user";
+ add_pubkey($gl_user, "$selfkeys_dir/$gl_user/$user.pub", $keymaterial);
+ print `cat "$TEMPDIR/session.pub"`;
+}
+
+###################################################################
+
+
+# Delete a key of user $gl_user.
+sub del_pubkey {
+ my ($gl_user, $pubkeypath) = @_;
+ $pubkeypath = sanitize_pubkeypath($pubkeypath);
+ my $fp = $fingerprints{$pubkeypath};
+ die "key not found\n" unless $fp;
+ cd_temp_clone();
+ chdir("keydir");
+ hushed_git( "rm", "$pubkeypath" ) and die "git rm failed\n";
+ my $userid = get_userid($pubkeypath);
+ hushed_git( "commit", "-m", "'ukm del $gl_user $userid\n\n$fp'" )
+ and die "git commit failed\n";
+ system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
+}
+
+# $gl_user is a super key manager. This function aborts if the
+# superkey manager tries to remove his last key.
+sub del_super {
+ my ($gl_user, $pubkeypath) = @_;
+ $pubkeypath = sanitize_pubkeypath($pubkeypath);
+ die "You are not managing the key $keyid.\n"
+ unless grep {$_ eq $pubkeypath} @all_pubkeypaths;
+ my $userid = get_userid($pubkeypath);
+ if ($gl_user eq $userid) {
+ my @paths = @{$pubkeypaths{$userid}};
+ die "You cannot delete your last key.\n"
+ if scalar(grep {$userid eq get_userid($_)} @paths)<2;
+ }
+ del_pubkey($gl_user, $pubkeypath);
+}
+
+sub del_guest {
+ my ($gl_user, $keyid) = @_;
+ my $pubkeypath = sanitize_pubkeypath("$guestkeys_dir/$gl_user/$keyid.pub");
+ my $userid = get_userid($pubkeypath);
+ # Check whether $gl_user actually manages $keyid.
+ my @paths = ();
+ @paths = @{$guest_pubkeypaths{$userid}}
+ if defined $guest_pubkeypaths{$userid};
+ die "You are not managing the key $keyid.\n"
+ unless grep {$_ eq $pubkeypath} @paths;
+ del_pubkey($gl_user, $pubkeypath);
+}
+
+# Delete a selfkey of $gl_user. The first delete is a preparation of
+# the deletion and only a second call will actually delete the key. If
+# the second call is done with the key that is scheduled for deletion,
+# it is basically undoing the previous del call. This last case is
+# handled in function pending_user_del.
+sub del_self {
+ my ($gl_user, $keyid) = @_;
+ my $selector = selfselector($keyid); # might return empty string
+ die "keyid not allowed: '$keyid'\n" unless $selector;
+
+ # Does $gl_user actually manage that keyid?
+ # All (non-pending) selfkeys have an @selector part in their pubkeypath.
+ my @paths = @{$self_pubkeypaths{$gl_user}};
+ die "You are not managing the key $keyid.\n"
+ unless grep {$selector eq get_selector($_)} @paths;
+
+ cd_temp_clone();
+ _chdir("keydir");
+ my $fp = '';
+ # Is it the first or the second del call? It's the second call, if
+ # there is a scheduled-for-deletion or scheduled-for-addition
+ # selfkey which has the given keyid as a selector part.
+ @paths = grep {
+ my ($zop, $zfp, $zselector, $zuser) = get_pending(get_userid($_));
+ $zselector eq $selector
+ } @paths;
+ if (@paths) {# start actual deletion of the key (second call)
+ my $pubkeypath = $paths[0];
+ $fp = fingerprint($pubkeypath);
+ my ($zop, $zf, $zs, $zu) = get_pending(get_userid($pubkeypath));
+ $zop = $zop eq 'add' ? 'undo-add' : 'confirm-del';
+ hushed_git("rm", "$pubkeypath") and die "git rm failed\n";
+ hushed_git("commit", "-m", "'ukm $zop $gl_user\@$selector\n\n$fp'")
+ and die "git commit failed\n";
+ system("gitolite push >/dev/null 2>/dev/null")
+ and die "git push failed\n";
+ print STDERR "pending keyid deleted: \@$selector\n";
+ return;
+ }
+ my $oldpubkeypath = "$selfkeys_dir/$gl_user/$gl_user\@$selector.pub";
+ # generate new pubkey and create fingerprint to get a random number
+ system("ssh-keygen -N '' -q -f \"$TEMPDIR/session\" -C $gl_user");
+ my $sessionfp = fingerprint("$TEMPDIR/session.pub");
+ $sessionfp =~ s/://g;
+ my $user = "zzz-del-$sessionfp-$selector-$gl_user";
+ my $newpubkeypath = "$selfkeys_dir/$gl_user/$user.pub";
+
+ # A key for gitolite access that is in authorized_keys and not
+ # existing in the expected place under keydir/ should actually not
+ # happen, but one never knows.
+ die "key not available\n" unless -r $oldpubkeypath;
+
+ # For some strange reason the target key already exists.
+ die "cannot override existing key\n" if -e $newpubkeypath;
+
+ $fp = fingerprint($oldpubkeypath);
+ print STDERR "prepare deletion of key \@$selector\n";
+ hushed_git("mv", "$oldpubkeypath", "$newpubkeypath")
+ and die "git mv failed\n";
+ hushed_git("commit", "-m", "'ukm prepare-del $gl_user\@$selector\n\n$fp'")
+ and die "git commit failed\n";
+ system("gitolite push >/dev/null 2>/dev/null")
+ and die "git push failed\n";
+}
+
+###################################################################
+# Adding a selfkey should be done as follows.
+#
+# cat newkey.pub | ssh git@host ukm add @selector > session
+# cat session | ssh -i newkey git@host ukm
+#
+# The provided random data will come from a newly generated ssh key
+# whose fingerprint will be stored in $gl_user. So we compute the
+# fingerprint of the data that is given to us. If it doesn't match the
+# fingerprint, then something went wrong and the confirm operation is
+# forbidden, in fact, the pending key will be removed from the system.
+sub pending_user_add {
+ my ($gl_user, $zfp, $zselector, $zuser) = @_;
+ my $oldpubkeypath = "$selfkeys_dir/$zuser/$gl_user.pub";
+ my $newpubkeypath = "$selfkeys_dir/$zuser/$zuser\@$zselector.pub";
+
+ # A key for gitolite access that is in authorized_keys and not
+ # existing in the expected place under keydir/ should actually not
+ # happen, but one never knows.
+ die "key not available\n" unless -r $oldpubkeypath;
+
+ my $keymaterial = safe_stdin();
+ # If there is no keymaterial (which corresponds to a session key
+ # for the confirm-add operation), logging in to this key, removes
+ # it from the system.
+ my $session_key_not_provided = '';
+ if (!$keymaterial) {
+ $session_key_not_provided = "missing session key";
+ } else {
+ _print("$TEMPDIR/session.pub", $keymaterial);
+ my $sessionfp = fingerprint("$TEMPDIR/session.pub");
+ $sessionfp =~ s/://g;
+ $session_key_not_provided = "session key not accepted"
+ unless ($zfp eq $sessionfp)
+ }
+ my $fp = fingerprint($oldpubkeypath);
+ if ($session_key_not_provided) {
+ print STDERR "$session_key_not_provided\n";
+ print STDERR "pending keyid deleted: \@$zselector\n";
+ hushed_git("rm", "$oldpubkeypath") and die "git rm failed\n";
+ hushed_git("commit", "-m", "'ukm del $zuser\@$zselector\n\n$fp'")
+ and die "git commit failed\n";
+ system("gitolite push >/dev/null 2>/dev/null")
+ and die "git push failed\n";
+ return;
+ }
+
+ # For some strange reason the target key already exists.
+ die "cannot override existing key\n" if -e $newpubkeypath;
+
+ print STDERR "pending keyid added: \@$zselector\n";
+ hushed_git("mv", "$oldpubkeypath", "$newpubkeypath")
+ and die "git mv failed\n";
+ hushed_git("commit", "-m", "'ukm confirm-add $zuser\@$zselector\n\n$fp'")
+ and die "git commit failed\n";
+ system("gitolite push >/dev/null 2>/dev/null")
+ and die "git push failed\n";
+}
+
+# To delete a key, one must first bring the key into a pending state
+# and then truely delete it with another key. In case, the login
+# happens with the pending key (implemented below), it means that the
+# delete operation has to be undone.
+sub pending_user_del {
+ my ($gl_user, $zfp, $zselector, $zuser) = @_;
+ my $oldpubkeypath = "$selfkeys_dir/$zuser/$gl_user.pub";
+ my $newpubkeypath = "$selfkeys_dir/$zuser/$zuser\@$zselector.pub";
+ print STDERR "undo pending deletion of keyid \@$zselector\n";
+ # A key for gitolite access that is in authorized_keys and not
+ # existing in the expected place under keydir/ should actually not
+ # happen, but one never knows.
+ die "key not available\n" unless -r $oldpubkeypath;
+ # For some strange reason the target key already exists.
+ die "cannot override existing key\n" if -e $newpubkeypath;
+ my $fp = fingerprint($oldpubkeypath);
+ hushed_git("mv", "$oldpubkeypath", "$newpubkeypath")
+ and die "git mv failed\n";
+ hushed_git("commit", "-m", "'ukm undo-del $zuser\@$zselector\n\n$fp'")
+ and die "git commit failed\n";
+}
+
+# A user whose key is in pending state cannot do much. In fact,
+# logging in as such a user simply takes back the "bringing into
+# pending state", i.e. a key scheduled for adding is remove and a key
+# scheduled for deletion is brought back into its properly added state.
+sub pending_user {
+ my ($gl_user, $zop, $zfp, $zselector, $zuser) = @_;
+ cd_temp_clone();
+ _chdir("keydir");
+ if ($zop eq 'add') {
+ pending_user_add($gl_user, $zfp, $zselector, $zuser);
+ } elsif ($zop eq 'del') {
+ pending_user_del($gl_user, $zfp, $zselector, $zuser);
+ } else {
+ die "unknown operation\n";
+ }
+ system("gitolite push >/dev/null 2>/dev/null")
+ and die "git push failed\n";
+}
diff --git a/contrib/hooks/repo-specific/save-push-signatures b/contrib/hooks/repo-specific/save-push-signatures
new file mode 100755
index 0000000..2470491
--- /dev/null
+++ b/contrib/hooks/repo-specific/save-push-signatures
@@ -0,0 +1,188 @@
+#!/bin/sh
+
+# ----------------------------------------------------------------------
+# post-receive hook to adopt push certs into 'refs/push-certs'
+
+# Collects the cert blob on push and saves it, then, if a certain number of
+# signed pushes have been seen, processes all the "saved" blobs in one go,
+# adding them to the special ref 'refs/push-certs'. This is done in a way
+# that allows searching for all the certs pertaining to one specific branch
+# (thanks to Junio Hamano for this idea plus general brainstorming).
+
+# The "collection" happens only if $GIT_PUSH_CERT_NONCE_STATUS = OK; again,
+# thanks to Junio for pointing this out; see [1]
+#
+# [1]: https://groups.google.com/forum/#!topic/gitolite/7cSrU6JorEY
+
+# WARNINGS:
+# Does not check that GIT_PUSH_CERT_STATUS = "G". If you want to check that
+# and FAIL the push, you'll have to write a simple pre-receive hook
+# (post-receive is not the place for that; see 'man githooks').
+#
+# Gitolite users: failing the hook cannot be done as a VREF because git does
+# not set those environment variables in the update hook. You'll have to
+# write a trivial pre-receive hook and add that in.
+
+# Relevant gitolite doc links:
+# repo-specific environment variables
+# http://gitolite.com/gitolite/dev-notes.html#rsev
+# repo-specific hooks
+# http://gitolite.com/gitolite/non-core.html#rsh
+# http://gitolite.com/gitolite/cookbook.html#v3.6-variation-repo-specific-hooks
+
+# Environment:
+# GIT_PUSH_CERT_NONCE_STATUS should be "OK" (as mentioned above)
+#
+# GL_OPTIONS_GPC_PENDING (optional; defaults to 1). This is the number of
+# git push certs that should be waiting in order to trigger the post
+# processing. You can set it within gitolite like so:
+#
+# repo foo bar # or maybe just 'repo @all'
+# option ENV.GPC_PENDING = 5
+
+# Setup:
+# Set up this code as a post-receive hook for whatever repos you need to.
+# Then arrange to have the environment variable GL_OPTION_GPC_PENDING set to
+# some number, as shown above. (This is only required if you need it to be
+# greater than 1.) It could of course be different for different repos.
+# Also see "Invocation" section below.
+
+# Invocation:
+# Normally via git (see 'man githooks'), once it is setup as a post-receive
+# hook.
+#
+# However, if you set the "pending" limit high, and want to periodically
+# "clean up" pending certs without necessarily waiting for the counter to
+# trip, do the following (untested):
+#
+# RB=$(gitolite query-rc GL_REPO_BASE)
+# for r in $(gitolite list-phy-repos)
+# do
+# cd $RB/$repo.git
+# unset GL_OPTIONS_GPC_PENDING # if it is set higher up
+# hooks/post-receive post_process
+# done
+#
+# That will take care of it.
+
+# Using without gitolite:
+# Just set GL_OPTIONS_GPC_PENDING within the script (maybe read it from git
+# config). Everything else is independent of gitolite.
+
+# ----------------------------------------------------------------------
+# make it work on BSD also (but NOT YET TESTED on FreeBSD!)
+uname_s=`uname -s`
+if [ "$uname_s" = "Linux" ]
+then
+ _lock() { flock "$@"; }
+else
+ _lock() { lockf -k "$@"; }
+ # I'm assuming other BSDs also have this; I only have FreeBSD.
+fi
+
+# ----------------------------------------------------------------------
+# standard stuff
+die() { echo "$@" >&2; exit 1; }
+warn() { echo "$@" >&2; }
+
+# ----------------------------------------------------------------------
+# if there are no arguments, we're running as a "post-receive" hook
+if [ -z "$1" ]
+then
+ # ignore if it may be a replay attack
+ [ "$GIT_PUSH_CERT_NONCE_STATUS" = "OK" ] || exit 1
+ # I don't think "exit 1" does anything in a post-receive anyway, so that's
+ # just a symbolic gesture!
+
+ # note the lock file used
+ _lock .gpc.lock $0 cat_blob
+
+ # if you want to initiate the post-processing ONLY from outside (for
+ # example via cron), comment out the next line.
+ exec $0 post_process
+fi
+
+# ----------------------------------------------------------------------
+# the 'post_process' part; see "Invocation" section in the doc at the top
+if [ "$1" = "post_process" ]
+then
+ # this is the same lock file as above
+ _lock .gpc.lock $0 count_and_rotate $$
+
+ [ -d git-push-certs.$$ ] || exit 0
+
+ # but this is a different one
+ _lock .gpc.ref.lock $0 update_ref $$
+
+ exit 0
+fi
+
+# ----------------------------------------------------------------------
+# other values for "$1" are internal use only
+
+if [ "$1" = "cat_blob" ]
+then
+ mkdir -p git-push-certs
+ git cat-file blob $GIT_PUSH_CERT > git-push-certs/$GIT_PUSH_CERT
+ echo $GIT_PUSH_CERT >> git-push-certs/.blob.list
+fi
+
+if [ "$1" = "count_and_rotate" ]
+then
+ count=$(ls git-push-certs | wc -l)
+ if test $count -ge ${GL_OPTIONS_GPC_PENDING:-1}
+ then
+ # rotate the directory
+ mv git-push-certs git-push-certs.$2
+ fi
+fi
+
+if [ "$1" = "update_ref" ]
+then
+ # use a different index file for all this
+ GIT_INDEX_FILE=push_certs_index; export GIT_INDEX_FILE
+
+ # prepare the special ref to receive commits
+ PUSH_CERTS=refs/push-certs
+ if git rev-parse -q --verify $PUSH_CERTS >/dev/null
+ then
+ git read-tree $PUSH_CERTS
+ else
+ git read-tree --empty
+ T=$(git write-tree)
+ C=$(echo 'start' | git commit-tree $T)
+ git update-ref $PUSH_CERTS $C
+ fi
+
+ # for each cert blob...
+ for b in `cat git-push-certs.$2/.blob.list`
+ do
+ cf=git-push-certs.$2/$b
+
+ # it's highly unlikely that the blob got GC-ed already but write it
+ # back anyway, just in case
+ B=$(git hash-object -w $cf)
+
+ # bit of a sanity check
+ [ "$B" = "$b" ] || warn "this should not happen: $B is not equal to $b"
+
+ # for each ref described within the cert, update the index
+ for ref in `cat $cf | egrep '^[a-f0-9]+ [a-f0-9]+ refs/' | cut -f3 -d' '`
+ do
+ git update-index --add --cacheinfo 100644,$b,$ref
+ # we're using the ref name as a "fake" filename, so people can,
+ # for example, 'git log refs/push-certs -- refs/heads/master', to
+ # see all the push certs pertaining to the master branch. This
+ # idea came from Junio Hamano, the git maintainer (I certainly
+ # don't deal with git plumbing enough to have thought of it!)
+ done
+
+ T=$(git write-tree)
+ C=$( git commit-tree -p $PUSH_CERTS $T < $cf )
+ git update-ref $PUSH_CERTS $C
+
+ rm -f $cf
+ done
+ rm -f git-push-certs.$2/.blob.list
+ rmdir git-push-certs.$2
+fi
diff --git a/contrib/lib/Apache/gitolite.conf b/contrib/lib/Apache/gitolite.conf
new file mode 100644
index 0000000..87ba843
--- /dev/null
+++ b/contrib/lib/Apache/gitolite.conf
@@ -0,0 +1,47 @@
+# Apache Gitolite smart-http install Active Directory Authentication
+
+# Author: Jonathan Gray
+
+# It is assumed you already have mod_ssl, mod_ldap, & mod_authnz configured for apache
+# It is also assumed you are disabling http on port 80 and requiring the use of https on port 443
+
+# Boiler plate configuration from the smart-http deployment documentation script
+# Adjust paths if you use something other than the default
+SetEnv GIT_PROJECT_ROOT /var/www/gitolite-home/repositories
+ScriptAlias /git/ /var/www/gitolite-home/gitolite-source/src/gitolite-shell/
+ScriptAlias /gitmob/ /var/www/gitolite-home/gitolite-source/src/gitolite-shell/
+SetEnv GITOLITE_HTTP_HOME /var/www/gitolite-home
+SetEnv GIT_HTTP_EXPORT_ALL
+
+# Setup LDAP trusted root certificate from your domain
+LDAPTrustedGlobalCert CA_BASE64 /etc/httpd/conf.d/domain.ca.cer
+
+# In case you havn't setup proper SSL certificates in ssl.conf, go ahead and do it here to save headache later with git
+SSLCertificateFile /etc/httpd/conf.d/gitolite.server.crt
+SSLCertificateKeyFile /etc/httpd/conf.d/gitolite.server.key
+SSLCertificateChainFile /etc/httpd/conf.d/DigiCertCA.crt
+
+<Location /git>
+ Order deny,allow
+ # In case you want to restrict access to a given ip/subnet
+ #Allow from my.ip.range/cidr
+ #Deny from All
+ AuthType Basic
+ AuthName "Git"
+ AuthBasicProvider ldap
+ AuthUserFile /dev/null
+ AuthzLDAPAuthoritative on
+ AuthLDAPURL ldaps://AD.DC1.local:3269 AD.DC2.local:3269 AD.DC3.local:3269/?sAMAccountName?sub
+ AuthLDAPBindDN git@domain.local
+ AuthLDAPBindPassword super.secret.password
+ AuthLDAPGroupAttributeIsDN on
+
+ # You must use one of the two following approaches to handle authentication via active directory
+
+ # Require membership in the gitolite users group in AD
+ # The ldap-filter option is used to handle nested groups on the AD server rather than multiple calls to traverse from apache
+ # Require ldap-filter memberof:1.2.840.113556.1.4.1941:=cn=Gitolite Users,ou=Security Groups,dc=domain,dc=local
+
+ # Alternatively, require a valid user account only since you're going to control authorization in gitolite anyway
+ Require valid-user
+</Location>
diff --git a/contrib/lib/Gitolite/Triggers/RedmineUserAlias.pm b/contrib/lib/Gitolite/Triggers/RedmineUserAlias.pm
new file mode 100644
index 0000000..8fde513
--- /dev/null
+++ b/contrib/lib/Gitolite/Triggers/RedmineUserAlias.pm
@@ -0,0 +1,55 @@
+package Gitolite::Triggers::RedmineUserAlias;
+
+use Gitolite::Rc;
+use Gitolite::Common;
+use Gitolite::Conf::Load;
+
+use strict;
+use warnings;
+
+# aliasing a redmine username to a more user-friendly one
+# ----------------------------------------------------------------------
+
+=for usage
+
+Why:
+
+ Redmine creates users like "redmine_alice_123"; we want the users to just
+ see "alice" instead of that.
+
+Assumption:
+
+* Redmine does not allow duplicates in the middle bit; i.e., you can't
+ create redmine_alice_123 and redmine_alice_456 also.
+
+How:
+
+* add this code as lib/Gitolite/Triggers/RedmineUserAlias.pm to your
+ site-local code directory; see this link for how:
+
+ http://gitolite.com/gitolite/non-core.html#ncloc
+
+* add the following to the rc file, just before the ENABLE section (don't
+ forget the trailing comma):
+
+ INPUT => [ 'RedmineUserAlias::input' ],
+
+Notes:
+
+* http mode has not been tested and will not be. If someone has the time to
+ test it and make it work please let me know.
+
+* not tested with mirroring.
+
+Quote:
+
+* "All that for what is effectively one line of code. I need a life".
+
+=cut
+
+sub input {
+ $ARGV[0] or _die "no username???";
+ $ARGV[0] =~ s/^redmine_(\S+)_\d+$/$1/;
+}
+
+1;
diff --git a/contrib/t/ukm.t b/contrib/t/ukm.t
new file mode 100644
index 0000000..da4fc0b
--- /dev/null
+++ b/contrib/t/ukm.t
@@ -0,0 +1,447 @@
+#!/usr/bin/perl
+
+# Call like this:
+# TSH_VERBOSE=1 TSH_ERREXIT=1 HARNESS_ACTIVE=1 GITOLITE_TEST=y prove t/ukm.t
+
+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`;
+my $pd = "$bd/../t/keys"; # source for pubkeys
+umask 0077;
+
+_mkdir( "$h/.ssh", 0700 ) if not -d "$h/.ssh";
+
+try "plan 204";
+
+
+# Reset everything.
+# Only admin and u1, u2, and u3 keys are available initially
+# Keys u4, u5, and u6 are used as guests later.
+# For easy access, we put the keys into ~/.ssh/, though.
+try "
+ rm -f $h/.ssh/authorized_keys; ok or die 1
+ cp $pd/u[1-6]* $h/.ssh; ok or die 2
+ cp $pd/admin* $h/.ssh; ok or die 3
+ cp $pd/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 $pd/u[1-3].pub $ab/keydir; ok or die 6
+ cp $pd/admin.pub $ab/keydir; ok or die 7
+";
+
+# Put the keys into ~/.ssh/authorized_keys
+system("gitolite ../triggers/post-compile/ssh-authkeys");
+
+# enable user key management in a simple form.
+# Guest key managers can add keyids looking like email addresses, but
+# cannot add emails containing example.com or hemmecke.org.
+system("sed -i \"s/.*ENABLE =>.*/'UKM_CONFIG'=>{'FORBIDDEN_GUEST_PATTERN'=>'example.com|hemmecke.org'}, ENABLE => ['ukm',/\" $h/.gitolite.rc");
+
+# super-key-managers can add/del any key
+# super-key-managers should in fact agree with people having write
+# access to gitolite-admin repo.
+# guest-key-managers can add/del guest keys
+confreset; confadd '
+ @guest-key-managers = u2 u3
+ @creators = u2 u3
+ repo pub/CREATOR/..*
+ C = @creators
+ RW+ = CREATOR
+ RW = WRITERS
+ R = READERS
+';
+
+# Populate the gitolite-admin/keydir in the same way as it was used for
+# the initialization of .ssh/authorized_keys above.
+try "
+ mkdir keydir; ok or die 8
+ cp $pd/u[1-3].pub keydir; ok or die 9;
+ cp $pd/admin.pub keydir; ok or die 10;
+ git add conf keydir; ok
+ git commit -m ukm; ok; /master.* ukm/
+";
+
+# Activate new config data.
+try "PUSH admin; ok; gsh; /master -> master/; !/FATAL/" or die text();
+
+# Check whether the above setup yields the expected behavior for ukm.
+# The admin is super-key-manager, thus can manage every key.
+try "
+ ssh admin ukm; ok; /Hello admin, you manage the following keys:/
+ / admin +admin/
+ / u1 +u1/
+ / u2 +u2/
+ / u3 +u3/
+";
+
+# u1 isn't a key manager, so shouldn't be above to manage keys.
+try "ssh u1 ukm; !ok; /FATAL: You are not a key manager./";
+
+# u2 and u3 are guest key managers, but don't yet manage any key.
+try "ssh u2 ukm; ok"; cmp "Hello u2, you manage the following keys:\n\n\n";
+try "ssh u3 ukm; ok"; cmp "Hello u3, you manage the following keys:\n\n\n";
+
+
+###################################################################
+# Unknows subkommands abort ukm.
+try "ssh u2 ukm fake; !ok; /FATAL: unknown ukm subcommand: fake/";
+
+
+###################################################################
+# Addition of keys.
+
+# If no data is provided on stdin, we don't block, but rather timeout
+# after one second and abort the program.
+try "ssh u2 ukm add u4\@example.org; !ok; /FATAL: missing public key data/";
+
+# If no keyid is given, we cannot add a key.
+try "ssh u2 ukm add; !ok; /FATAL: keyid required/";
+
+try "
+ DEF ADD = cat $pd/%1.pub|ssh %2 ukm add %3
+ DEF ADDOK = ADD %1 %2 %3; ok
+ DEF ADDNOK = ADD %1 %2 %3; !ok
+ DEF FP = ADDNOK u4 u2 %1
+ DEF FORBIDDEN_PATTERN = FP %1; /FATAL: keyid not allowed:/
+";
+
+# Neither a guest key manager nor a super key manager can add keys that have
+# double dot in their keyid. This is hardcoded to forbid paths with .. in it.
+try "
+ ADDNOK u4 u2 u4\@hemmecke..org; /Not allowed to use '..' in keyid./
+ ADDNOK u4 admin u4\@hemmecke..org; /Not allowed to use '..' in keyid./
+ ADDNOK u4 admin ./../.myshrc; /Not allowed to use '..' in keyid./
+";
+
+# guest-key-managers can only add keys that look like emails.
+try "
+ FORBIDDEN_PATTERN u4
+ FORBIDDEN_PATTERN u4\@example
+ FORBIDDEN_PATTERN u4\@foo\@example.org
+
+ # No support for 'old style' multiple keys.
+ FORBIDDEN_PATTERN u4\@example.org\@foo
+
+ # No path delimiter in keyid
+ FORBIDDEN_PATTERN foo/u4\@example.org
+
+ # Certain specific domains listed in FORBIDDEN_GUEST_PATTERN are forbidden.
+ # Note that also u4\@example-com would be rejected, because MYDOMAIN
+ # contains a regular expression --> I don't care.
+ FORBIDDEN_PATTERN u4\@example.com
+ FORBIDDEN_PATTERN u4\@hemmecke.org
+";
+
+# Accept one guest key.
+try "ADDOK u4 u2 u4\@example.org";
+try "ssh u2 ukm; ok; /Hello u2, you manage the following keys:/
+ / u4\@example.org *u4\@example.org/";
+
+# Various ways how a key must be rejected.
+try "
+ # Cannot add the same key again.
+ ADDNOK u4 u2 u4\@example.org; /FATAL: cannot override existing key/
+
+ # u2 can also not add u4.pub under another keyid
+ ADDNOK u4 u2 u4\@example.net; /FATAL: cannot add key/
+ /Same key is already available under another userid./
+
+ # u2 can also not add another key under the same keyid.
+ ADDNOK u5 u2 u4\@example.org; /FATAL: cannot override existing key/
+
+ # Also u3 cannot not add another key under the same keyid.
+ ADDNOK u5 u3 u4\@example.org
+ /FATAL: cannot add another public key for an existing user/
+
+ # And u3 cannot not add u4.pub under another keyid.
+ ADDNOK u4 u3 u4\@example.net; /FATAL: cannot add key/
+ /Same key is already available under another userid./
+
+ # Not even the admin can add the same key u4 under a different userid.
+ ADDNOK u4 admin u4\@example.net; /FATAL: cannot add key/
+ /Same key is already available under another userid./
+ /Found .* u4\@example.org/
+
+ # Super key managers cannot add keys that start with @.
+ # We don't care about @ in the dirname, though.
+ ADDNOK u4 admin foo/\@ex.net; /FATAL: cannot add key that starts with \@/
+ ADDNOK u4 admin foo/\@ex; /FATAL: cannot add key that starts with \@/
+ ADDNOK u4 admin \@ex.net; /FATAL: cannot add key that starts with \@/
+ ADDNOK u4 admin \@ex; /FATAL: cannot add key that starts with \@/
+";
+
+# But u3 can add u4.pub under the same keyid.
+try "ADDOK u4 u3 u4\@example.org";
+
+try "ssh u3 ukm; ok; /Hello u3, you manage the following keys:/
+ / u4\@example.org *u4\@example.org/";
+
+# The admin can add multiple keys for the same userid.
+try "
+ ADDOK u5 admin u4\@example.org
+ ADDOK u5 admin u4\@example.org\@home
+ ADDOK u5 admin laptop/u4\@example.org
+ ADDOK u5 admin laptop/u4\@example.org\@home
+";
+
+# And admin can also do this for other guest key managers. Note,
+# however, that the gitolite-admin must be told where the
+# GUEST_DIRECTORY is. But he/she could find out by cloning the
+# gitolite-admin repository and adding the same key directly.
+try "
+ ADDOK u5 admin zzz/guests/u2/u4\@example.org\@foo
+ ADDOK u6 admin zzz/guests/u3/u6\@example.org
+";
+
+try "ssh admin ukm; ok"; cmp "Hello admin, you manage the following keys:
+fingerprint userid keyid
+a4:d1:11:1d:25:5c:55:9b:5f:91:37:0e:44:a5:a5:f2 admin admin
+00:2c:1f:dd:a3:76:5a:1e:c4:3c:01:15:65:19:a5:2e u1 u1
+69:6f:b5:8a:f5:7b:d8:40:ce:94:09:a2:b8:95:79:5b u2 u2
+26:4b:20:24:98:a4:e4:a5:b9:97:76:9a:15:92:27:2d u3 u3
+78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org laptop/u4\@example.org
+78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org laptop/u4\@example.org\@home
+78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org u4\@example.org
+78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org u4\@example.org\@home
+8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org zzz/guests/u2/u4\@example.org
+78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org zzz/guests/u2/u4\@example.org\@foo
+8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org zzz/guests/u3/u4\@example.org
+fc:0f:eb:52:7a:d2:35:da:89:96:f5:15:0e:85:46:e7 u6\@example.org zzz/guests/u3/u6\@example.org
+\n\n";
+
+# Now, u2 has two keys in his directory, but u2 can manage only one of
+# them, since the one added by the admin has two @ in it. Thus the key
+# added by admin is invisible to u2.
+try "ssh u2 ukm; ok"; cmp "Hello u2, you manage the following keys:
+fingerprint userid keyid
+8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org u4\@example.org
+\n\n";
+
+# Since admin added key u6@example.org to the directory of u2, u2 is
+# also able to see it and, in fact, to manage it.
+try "ssh u3 ukm; ok"; cmp "Hello u3, you manage the following keys:
+fingerprint userid keyid
+8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org u4\@example.org
+fc:0f:eb:52:7a:d2:35:da:89:96:f5:15:0e:85:46:e7 u6\@example.org u6\@example.org
+\n\n";
+
+###################################################################
+# Deletion of keys.
+try "
+ DEF DEL = ssh %1 ukm del %2
+ DEF DELOK = DEL %1 %2; ok
+ DEF DELNOK = DEL %1 %2; !ok
+ DEF DELNOMGR = DELNOK %1 %2; /FATAL: You are not managing the key /
+";
+
+# Deletion requires a keyid.
+try "ssh u3 ukm del; !ok; /FATAL: keyid required/";
+
+# u3 can, of course, not remove any unmanaged key.
+try "DELNOMGR u3 u2";
+
+# But u3 can delete u4@example.org and u6@example.org. This will, of course,
+# not remove the key u4@example.org that u2 manages.
+try "
+ DELOK u3 u4\@example.org
+ DELOK u3 u6\@example.org
+";
+
+# After having deleted u4@example.org, u3 cannot remove it again,
+# even though, u2 still manages that key.
+try "DELNOMGR u3 u4\@example.org";
+
+# Of course a super-key-manager can remove any (existing) key.
+try "
+ DELOK admin zzz/guests/u2/u4\@example.org
+ DELNOK admin zzz/guests/u2/u4\@example.org
+ /FATAL: You are not managing the key zzz/guests/u2/u4\@example.org./
+ DELNOK admin zzz/guests/u2/u4\@example.org\@x
+ /FATAL: You are not managing the key zzz/guests/u2/u4\@example.org./
+ DELOK admin zzz/guests/u2/u4\@example.org\@foo
+";
+
+# As the admin could do that via pushing to the gitolite-admin manually,
+# it's also allowed to delete even non-guest keys.
+try "DELOK admin u3";
+
+# Let's clean the environment again.
+try "
+ DELOK admin laptop/u4\@example.org\@home
+ DELOK admin laptop/u4\@example.org
+ DELOK admin u4\@example.org\@home
+ DELOK admin u4\@example.org
+ ADDOK u3 admin u3
+ ";
+
+# Currently the admin has just one key. It cannot be removed.
+# But after adding another key, deletion should work fine.
+try "
+ DELNOK admin admin; /FATAL: You cannot delete your last key./
+ ADDOK u6 admin second/admin; /Adding new public key for admin./
+ DELOK admin admin
+ DELNOK u6 admin; /FATAL: You are not managing the key admin./
+ DELNOK u6 second/admin; /FATAL: You cannot delete your last key./
+ ADDOK admin u6 admin; /Adding new public key for admin./
+ DELOK u6 second/admin
+";
+
+###################################################################
+# Selfkey management.
+
+# If self key management is not switched on in the .gitolite.rc file,
+# it's not allowed at all.
+try "ssh u2 ukm add \@second; !ok; /FATAL: selfkey management is not enabled/";
+
+# Let's enable it.
+system("sed -i \"/'UKM_CONFIG'=>/s/=>{/=>{'SELFKEY_MANAGEMENT'=>1,/\" $h/.gitolite.rc");
+
+# And add self-key-managers to gitolite.conf
+# chdir("../gitolite-admin") or die "in `pwd`, could not cd ../g-a";
+try "glt pull admin origin master; ok";
+put "|cut -c5- > conf/gitolite.conf", '
+ repo gitolite-admin
+ RW+ = admin
+ repo testing
+ RW+ = @all
+ @guest-key-managers = u2 u3
+ @self-key-managers = u1 u2
+ @creators = u2 u3
+ repo pub/CREATOR/..*
+ C = @creators
+ RW+ = CREATOR
+ RW = WRITERS
+ R = READERS
+';
+try "
+ git add conf keydir; ok
+ git commit -m selfkey; ok; /master.* selfkey/
+";
+try "PUSH admin; ok; gsh; /master -> master/; !/FATAL/" or die text();
+
+# Now we can start with the tests.
+
+# Only self key managers are allowed to use selfkey management.
+# See variable @self-key-managers.
+try "ssh u3 ukm add \@second; !ok; /FATAL: You are not a selfkey manager./";
+
+# Cannot add keyid that are not alphanumeric.
+try "ssh u1 ukm add \@second-key; !ok; /FATAL: keyid not allowed:/";
+
+# Add a second key for u1, but leave it pending by not feeding in the
+# session key. The new user can login, but he/she lives under a quite
+# random gl_user name and thus is pretty much excluded from everything
+# except permissions given to @all. If this new id calls ukm without
+# providing the session key, this (pending) key is automatically
+# removed from the system.
+# If a certain keyid is in the system, then it cannot be added again.
+try "
+ ADDOK u4 u1 \@second
+ ssh admin ukm; ok; /u1 zzz/self/u1/zzz-add-[a-z0-9]{32}-second-u1/
+ ssh u1 ukm; ok; /u1 \@second .pending add./
+ ADDNOK u4 u1 \@second; /FATAL: keyid already in use: \@second/
+ ssh u4 ukm; ok; /pending keyid deleted: \@second/
+ ssh admin ukm; ok; !/zzz/; !/second/
+";
+
+# Not providing a proper ssh public key will abort. Providing a good
+# ssh public key, which is not a session key makes the key invalid.
+# The key will, therefore, be deleted by this operation.
+try "
+ ADDOK u4 u1 \@second
+ echo fake|ssh u4 ukm; !ok; /FATAL: does not seem to be a valid pubkey/
+ cat $pd/u5.pub | ssh u4 ukm; ok;
+ /session key not accepted/
+ /pending keyid deleted: \@second/
+";
+
+# True addition of a new selfkey is done via piping it to a second ssh
+# call that uses the new key to call ukm. Note that the first ssh must
+# have completed its job before the second ssh is able to successfully
+# log in. This can be done via sleep or via redirecting to a file and
+# then reading from it.
+try "
+ # ADDOK u4 u1 \@second | (sleep 2; ssh u4 ukm); ok
+ ADD u4 u1 \@second > session; ok
+ cat session | ssh u4 ukm; ok; /pending keyid added: \@second/
+";
+
+# u1 cannot add his/her initial key, since that key can never be
+# confirmed via ukm, so it is forbidden altogether. In fact, u1 is not
+# allowed to add any key twice.
+try "
+ ADDNOK u1 u1 \@first
+ /FATAL: You cannot add a key that already belongs to you./
+ ADDNOK u4 u1 \@first
+ /FATAL: You cannot add a key that already belongs to you./
+";
+
+# u1 also can add more keys, but not under an existing keyid. That can
+# be done by any of his/her identities (here we choose u4).
+try "
+ ADDNOK u5 u1 \@second; /FATAL: keyid already in use: \@second/
+ ADD u5 u4 \@third > session; ok
+ cat session | ssh u5 ukm; ok; /pending keyid added: \@third/
+";
+
+# u2 cannot add the same key, but is allowed to use the same name (@third).
+try "
+ ADDNOK u5 u2 \@third; /FATAL: cannot add key/
+ /Same key is already available under another userid./
+ ADD u6 u2 \@third > session; ok
+ cat session | ssh u6 ukm; ok; /pending keyid added: \@third/
+";
+
+# u6 can schedule his/her own key for deletion, but cannot actually
+# remove it. Trying to do so results in bringing back the key. Actual
+# deletion must be confirmed by another key.
+try "
+ ssh u6 ukm del \@third; /prepare deletion of key \@third/
+ ssh u2 ukm; ok; /u2 \@third .pending del./
+ ssh u6 ukm; ok; /undo pending deletion of keyid \@third/
+ ssh u6 ukm del \@third; /prepare deletion of key \@third/
+ ssh u2 ukm del \@third; ok; /pending keyid deleted: \@third/
+";
+
+# While in pending-deletion state, it's forbidden to add another key
+# with the same keyid. It's also forbidden to add a key with the same
+# fingerprint as the to-be-deleted key).
+# A new key under another keyid, is OK.
+try "
+ ssh u1 ukm del \@third; /prepare deletion of key \@third/
+ ADDNOK u4 u1 \@third; /FATAL: keyid already in use: \@third/
+ ADDNOK u5 u1 \@fourth;
+ /FATAL: You cannot add a key that already belongs to you./
+ ADD u6 u1 \@fourth > session; ok
+ ssh u1 ukm; ok;
+ /u1 \@second/
+ /u1 \@fourth .pending add./
+ /u1 \@third .pending del./
+";
+# We can remove a pending-for-addition key (@fourth) by logging in
+# with a non-pending key. Trying to do anything with key u5 (@third)
+# will just bring it back to its normal state, but not change the
+# state of any other key. As already shown above, using u6 (@fourth)
+# without a proper session key, would remove it from the system.
+# Here we want to demonstrate that key u1 can delete u6 immediately.
+try "ssh u1 ukm del \@fourth; /pending keyid deleted: \@fourth/";
+
+# The pending-for-deletion key @third can also be removed via the u4
+# (@second) key.
+try "ssh u4 ukm del \@third; ok; /pending keyid deleted: \@third/";
+
+# Non-existing selfkeys cannot be deleted.
+try "ssh u4 ukm del \@x; !ok; /FATAL: You are not managing the key \@x./";
diff --git a/contrib/triggers/IP-check b/contrib/triggers/IP-check
new file mode 100755
index 0000000..9a4fda1
--- /dev/null
+++ b/contrib/triggers/IP-check
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+# Check an IP before allowing access.
+
+# This is also a generic example of how to add arbitrary checks at the PRE_GIT
+# stage, in order to control fetch/clone as well, not just push operations
+# (VREFs, in contrast, only work for pushes).
+
+# Notice how repo-specific information is being passed to this code (bullet 3
+# below). For more on that, see:
+# https://gitolite.com/gitolite/dev-notes/#appendix-1-repo-specific-environment-variables
+
+# Instructions:
+
+# 1. put this in an appropriate triggers directory (read about non-core
+# code at http://gitolite.com/gitolite/non-core/ for more on this; the
+# cookbook may also help here).
+
+# 2. add a line:
+# PRE_GIT => [ 'IP-check' ],
+# just before the "ENABLE" line in the rc file
+
+# 3. add a line like this to the "repo ..." section in gitolite.conf:
+# option ENV.IP_allowed = 1.2.3.0/24
+# take care that this expression is valid, in the sense that passing it
+# to 'ipcalc -n' will return the part before the "/". I.e., in this
+# example, 'ipcalc -n 1.2.3.0/24' should (and does) return 1.2.3.0.
+
+# ----
+
+[ -n "$GL_OPTION_IP_allowed" ] || exit 0
+
+expected=${GL_OPTION_IP_allowed%/*}
+ mask=${GL_OPTION_IP_allowed#*/}
+
+current_ip=${SSH_CONNECTION%% *}
+
+eval `ipcalc -n $current_ip/$mask`
+
+[ "$expected" == "$NETWORK" ] && exit 0
+
+echo >&2 "IP $current_ip does not match allowed block $GL_OPTION_IP_allowed"
+exit 1
diff --git a/contrib/triggers/file_mirror b/contrib/triggers/file_mirror
new file mode 100755
index 0000000..755ce86
--- /dev/null
+++ b/contrib/triggers/file_mirror
@@ -0,0 +1,172 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+# Use an external (non-gitolite) mirror to backup gitolite repos. They will
+# be automatically kept uptodate as people push to your gitolite server. If
+# your server should die and you create a new one, you can quickly and easily
+# get everything back from the external mirror with a few simple commands.
+
+# -------------------------------------------------------------
+# SEE WARNINGS/CAVEATS AND INSTRUCTIONS AT THE END OF THIS FILE
+# -------------------------------------------------------------
+
+# ----------------------------------------------------------------------
+
+use lib $ENV{GL_LIBDIR};
+use Gitolite::Easy;
+
+my ( $trigger, $repo, $dummy, $op ) = @ARGV;
+exit 0 unless $trigger eq 'POST_GIT' or $trigger eq 'POST_CREATE';
+exit 0 if $trigger eq 'POST_GIT' and $op ne 'W';
+
+chdir("$rc{GL_REPO_BASE}/$repo.git") or _die "chdir failed: $!\n";
+
+my %config = config( $repo, "gitolite-options\\.mirror\\.extcopy" );
+for my $copy ( values %config ) {
+ _do($copy);
+
+ # processing one copy is sufficient for restoring!
+ last if $trigger eq 'POST_CREATE';
+}
+
+# in shell, that would be something like:
+# gitolite git-config -r $repo gitolite-options\\.mirror\\.extcopy | cut -f3 | while read copy
+# do
+# ...
+
+# ----------------------------------------------------------------------
+
+sub _do {
+ my $url = shift;
+
+ if ( $trigger eq 'POST_CREATE' ) {
+ # brand new repo just created; needs to be populated from mirror
+
+ # For your urls you will need a way to somehow query the server and
+ # ask if the repo is present; it's upto you how you do it.
+ my $path = $url;
+ $path =~ s(^file://)();
+ return unless -d $path;
+
+ # now fetch. Maybe we can put a "-q" in there?
+ system( "git", "fetch", $url, "+refs/*:refs/*" );
+
+ } elsif ( $trigger eq 'POST_GIT' ) {
+ # someone just pushed; we need to update our mirrors
+
+ # need to create the repo on the mirror. Again, it's upto you how you
+ # make sure there's a repo on the mirror that can receive the push.
+ make_repo($url); # in case it doesn't already exist
+
+ # now push
+ system( "git", "push", "--mirror", $url );
+ }
+}
+
+sub make_repo {
+ my $url = shift;
+ # in this example, the URL is 'file:///...'; for other urls, presumably
+ # the url tells you enough about how to *create* a repo.
+
+ my $path = $url;
+ $path =~ s(^file://)();
+ return if -d $path;
+ system( "git", "init", "--bare", $path );
+}
+
+__END__
+
+WARNINGS
+--------
+
+1. THIS IS SAMPLE CODE. You will AT LEAST have to customise the _do() and
+ make_repo() functions above based on what your remote URLs are. For
+ example, I don't even know how to create a repo from the command line if
+ your external store is, say, github!
+
+2. THIS DOES NOT WORK FOR WILD REPOs. It can be made to work, with a few
+ extra steps to backup and restore the "gl-perms" and "gl-creator" files.
+
+ "Left as an exercise for the reader!"
+
+DESIGN NOTES
+------------
+
+This is really just a combination of "upstream" (see src/triggers/upstream)
+and mirroring (gitolite mirroring does allow a copy to be non-gitolite, as
+long as the ssh stuff is done the same way).
+
+The main difference is that gitolite mirroring expects peers to all talk ssh,
+whereas this method lets you use other protocols. Specifically, since this
+whole thing was started off by someone wanting to put his repos on s3
+(apparently jgit can talk to s3 directly), you can modify the two functions to
+deal with whatever remote server you have.
+
+LANGUAGE
+--------
+
+This doesn't have to be in perl. Shell equivalent for the only gitolite
+specific code is supplied; the rest of the code is fairly straightforward.
+
+SETUP
+-----
+
+1. Put this code into your LOCAL_CODE directory under "triggers"; see
+ non-core.html for details.
+
+2. Add these lines to your rc file, just before the ENABLE line. (I'm
+ assuming a v3.4 or later installation here).
+
+ POST_CREATE => [ 'file_mirror' ],
+ POST_GIT => [ 'file_mirror' ],
+
+3. Backup your rc file, since you may have other changes in it that you'll
+ want to preserve.
+
+4. Do something like this in your gitolite.conf file:
+
+ repo @all
+ option mirror.extcopy-1 = file:///tmp/he1/%GL_REPO.git
+ option mirror.extcopy-2 = file:///tmp/he2/%GL_REPO.git
+
+ As you can see, since this is just for demo/test, we're using a couple of
+ temp directories to serve as our "remotes" using the file:// protocol.
+
+5. Do a one-time manual sync of all the repos (subsequent syncs happen on
+ each push):
+
+ gitolite list-phy-repos | xargs -I xx gitolite trigger POST_GIT xx admin W
+
+ (This is a little trick we're playing on the trigger stuff, but it should
+ work fine. Just make sure that, if you have other things in your POST_GIT
+ trigger list, they're not affected in some way. 'gitolite query-rc
+ POST_GIT' will tell you what else you have.)
+
+That takes care of the "setup" and "regular backup".
+
+RESTORE
+-------
+
+1. Install gitolite normally. You'll get the usual two repos.
+
+2. Restore the previously backed up rc file to replace the default one that
+ gitolite created. At the very least, the rc file should have the
+ POST_CREATE and POST_GIT entries.
+
+ ---------------------------------------------------------
+ IF YOU FORGET THIS STEP, NASTY THINGS WILL HAPPEN TO YOU!
+ ---------------------------------------------------------
+
+3. Clone the admin repo from one of your backup servers to some temp dir. In
+ our example,
+
+ git clone /tmp/he1/gitolite-admin.git old-ga
+
+4. 'cd' to that clone and force push to your *new* admin repo:
+
+ cd old-ga
+ git push -f admin:gitolite-admin
+
+That's it. As each repo gets created by the admin push, they'll get populated
+by the backed up stuff due to the POST_CREATE trigger.
diff --git a/contrib/utils/ad_groups.sh b/contrib/utils/ad_groups.sh
new file mode 100755
index 0000000..cc86692
--- /dev/null
+++ b/contrib/utils/ad_groups.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# author derived from: damien.nozay@gmail.com
+# author: Jonathan Gray
+
+# Given a username,
+# Provides a space-separated list of groups that the user is a member of.
+#
+# see http://gitolite.com/gitolite/conf.html#ldap
+# GROUPLIST_PGM => /path/to/ldap_groups.sh
+
+# Be sure to add your domain CA to the trusted certificates in /etc/openldap/ldap.conf using the TLS_CACERT option or you'll get certificate validation errors
+
+ldaphost='ldap://AD.DC1.local:3268,ldap://AD.DC2.local:3268,ldap://AD.DC3.local:3268'
+ldapuser='git@domain.local'
+ldappass='super.secret.password'
+binddn='dc=domain,dc=local'
+username=$1;
+
+# I don't assume your users share a common OU, so I search the entire domain
+ldap_groups() {
+ # Go fetch the full user CN as it could be anywhere inside the DN
+ usercn=$(
+ ldapsearch -ZZ -H ${ldaphost} -D ${ldapuser} -w ${ldappass} -b ${binddn} -LLL -o ldif-wrap=no "(sAMAccountName=${username})" \
+ | grep "^dn:" \
+ | perl -pe 's|dn: (.*?)|\1|'
+ )
+
+ # Using a proprietary AD extension, let the AD Controller resolve all nested group memberships
+ # http://ddkonline.blogspot.com/2010/05/how-to-recursively-get-group-membership.html
+ # Also, substitute spaces in AD group names for '_' since gitolite expects a space separated list
+ echo $(
+ ldapsearch -ZZ -H ${ldaphost} -D ${ldapuser} -w ${ldappass} -b ${binddn} -LLL -o ldif-wrap=no "(member:1.2.840.113556.1.4.1941:=${usercn})" \
+ | grep "^dn:" \
+ | perl -pe 's|dn: CN=(.*?),.*|\1|' \
+ | sed 's/ /_/g'
+ )
+}
+
+ldap_groups $@
diff --git a/contrib/utils/gitolite-local b/contrib/utils/gitolite-local
new file mode 100755
index 0000000..903b868
--- /dev/null
+++ b/contrib/utils/gitolite-local
@@ -0,0 +1,136 @@
+#!/bin/bash
+
+# ----------------------------------------------------------------------
+# change these lines to suit
+testconf=$HOME/GITOLITE-TESTCONF
+gitolite_url=https://github.com/sitaramc/gitolite
+ # change it to something local for frequent use
+ # gitolite_url=file:///tmp/gitolite.git
+
+# ----------------------------------------------------------------------
+# Usage: gitolite-local <options>
+#
+# Test your gitolite.conf rule lists on your LOCAL machine (without even
+# pushing to the server!)
+#
+# (one-time)
+#
+# 1. put this code somewhere in your $PATH if you wish
+# 2. edit the line near the top of the script if you want to use some other
+# directory than the default, for "testconf".
+# 2. prepare the "testconf" directory by running:
+# gitolite-local prep
+#
+# (lather, rinse, repeat)
+#
+# 1. edit the conf (see notes below for more)
+# gitolite-local edit
+# 2. compile the conf
+# gitolite-local compile
+# 3. check permissions using "info" command:
+# gitolite-local info USERNAME
+# 4. check permissions using "access" command:
+# gitolite-local access <options for gitolite access command>
+# 5. clone, fetch, and push if you like!
+# gitolite-local clone <username> <reponame> <other options for clone>
+# gitolite-local fetch <username> <options for fetch>
+# gitolite-local push <username> <options for push>
+#
+# note on editing the conf: you don't have to use the edit command; you can
+# also directly edit '.gitolite/conf/gitolite.conf' in the 'testconf'
+# directory. You'll need to do that if your gitolite conf consists of more
+# than just one file (like if you have includes, etc.)
+#
+# note on the clone command: most of the options won't work for clone, unless
+# git is ok with them being placed *after* the repo name.
+
+# ----------------------------------------------------------------------
+die() { echo "$@" >&2; exit 1; }
+usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; }
+[ -z "$1" ] && usage
+
+# ----------------------------------------------------------------------
+if [ $1 == prep ]
+then
+ set -e
+
+ [ -d $testconf ] && die "directory '$testconf' already exists"
+
+ mkdir $testconf
+ cd $testconf
+
+ export HOME=$PWD
+
+ echo getting gitolite source...
+ git clone $gitolite_url gitolite
+ echo
+
+ echo installing gitolite...
+ gitolite/install >/dev/null
+ echo
+
+ echo setting up gitolite...
+ export PATH=$PWD/gitolite/src:$PATH
+ gitolite setup -a admin
+ echo
+
+ exit 0
+fi
+
+od=$PWD
+cd $testconf
+export HOME=$PWD
+export PATH=$PWD/gitolite/src:$PATH
+
+if [ $1 = edit ]
+then
+ editor=${EDITOR:-vim}
+ $editor .gitolite/conf/gitolite.conf
+elif [ $1 = compile ]
+then
+ gitolite compile
+elif [ $1 = compile+ ]
+then
+ gitolite compile\; gitolite trigger POST_COMPILE
+elif [ $1 = info ]
+then
+ shift
+ user=$1
+ shift
+
+ GL_USER=$user gitolite info "$@"
+elif [ $1 = access ]
+then
+ shift
+
+ gitolite access "$@"
+elif [ $1 = clone ]
+then
+ shift
+ export G3T_USER=$1
+ shift
+
+ cd $od
+ export GL_BINDIR=$HOME/gitolite/t
+ # or you could do it the long way, using 'gitolite query-rc GL_BINDIR'
+ repo=$1; shift
+ git clone --upload-pack=$GL_BINDIR/gitolite-upload-pack file:///$repo "$@"
+elif [ $1 = fetch ]
+then
+ shift
+ export G3T_USER=$1
+ shift
+
+ cd $od
+ export GL_BINDIR=$HOME/gitolite/t
+ git fetch --upload-pack=$GL_BINDIR/gitolite-upload-pack "$@"
+elif [ $1 = push ]
+then
+ shift
+ export G3T_USER=$1
+ shift
+
+ cd $od
+ export GL_BINDIR=$HOME/gitolite/t
+ git push --receive-pack=$GL_BINDIR/gitolite-receive-pack "$@"
+fi
diff --git a/contrib/utils/ipa_groups.pl b/contrib/utils/ipa_groups.pl
new file mode 100755
index 0000000..9cffa40
--- /dev/null
+++ b/contrib/utils/ipa_groups.pl
@@ -0,0 +1,229 @@
+#!/usr/bin/env perl
+#
+# ipa_groups.pl
+#
+# See perldoc for usage
+#
+use Net::LDAP;
+use Net::LDAP::Control::Paged;
+use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED);
+use strict;
+use warnings;
+
+my $usage = <<EOD;
+Usage: $0 \$uid
+This script returns a list of groups that \$uid is a member of
+EOD
+
+my $uid = shift or die $usage;
+
+## CONFIG SECTION
+
+# If you want to do plain-text LDAP, then set ldap_opts to an empty hash and
+# then set protocols of ldap_hosts to ldap://
+my @ldap_hosts = [
+ 'ldaps://auth-ldap-001.prod.example.net',
+ 'ldaps://auth-ldap-002.prod.example.net',
+];
+my %ldap_opts = (
+ verify => 'require',
+ cafile => '/etc/pki/tls/certs/prod.example.net_CA.crt'
+);
+
+# Base DN to search
+my $base_dn = 'dc=prod,dc=example,dc=net';
+
+# User for binding to LDAP server with
+my $user = 'uid=svc_gitolite_bind,cn=sysaccounts,cn=etc,dc=prod,dc=example,dc=net';
+my $pass = 'reallysecurepasswordstringhere';
+
+## Below variables should not need to be changed under normal circumstances
+
+# OU where groups are located. Anything return that is not within this OU is
+# removed from results. This OU is static on FreeIPA so will only need updating
+# if you want to support other LDAP servers. This is a regex so can be set to
+# anything you want (E.G '.*').
+my $groups_ou = qr/cn=groups,cn=accounts,${base_dn}$/;
+
+# strip path - if you want to return the full path of the group object then set
+# this to 0
+my $strip_group_paths = 1;
+
+# Number of seconds before timeout (for each query)
+my $timeout=5;
+
+# user object class
+my $user_oclass = 'person';
+
+# group attribute
+my $group_attrib = 'memberOf';
+
+## END OF CONFIG SECTION
+
+# Catch timeouts here
+$SIG{'ALRM'} = sub {
+ die "LDAP queries timed out";
+};
+
+alarm($timeout);
+
+# try each server until timeout is reached, has very fast failover if a server
+# is totally unreachable
+my $ldap = Net::LDAP->new(@ldap_hosts, %ldap_opts) ||
+ die "Error connecting to specified servers: $@ \n";
+
+my $mesg = $ldap->bind(
+ dn => $user,
+ password => $pass
+);
+
+if ($mesg->code()) {
+ die ("error:", $mesg->code(),"\n",
+ "error name: ",$mesg->error_name(),"\n",
+ "error text: ",$mesg->error_text(),"\n");
+}
+
+# How many LDAP query results to grab for each paged round
+# Set to under 1000 to limit load on LDAP server
+my $page = Net::LDAP::Control::Paged->new(size => 500);
+
+# @queries is an array or array references. We initially fill it up with one
+# arrayref (The first LDAP search) and then add more during the execution.
+# First start by resolving the group.
+my @queries = [ ( base => $base_dn,
+ filter => "(&(objectClass=${user_oclass})(uid=${uid}))",
+ control => [ $page ],
+) ];
+
+# array to store groups matching $groups_ou
+my @verified_groups;
+
+# Loop until @queries is empty...
+foreach my $queryref (@queries) {
+
+ # set cookie for paged querying
+ my $cookie;
+ alarm($timeout);
+ while (1) {
+ # Perform search
+ my $mesg = $ldap->search( @{$queryref} );
+
+ foreach my $entry ($mesg->entries) {
+ my @groups = $entry->get_value($group_attrib);
+ # find any groups matching $groups_ou regex and push onto $verified_groups array
+ foreach my $group (@groups) {
+ if ($group =~ /$groups_ou/) {
+ push @verified_groups, $group;
+ }
+ }
+ }
+
+ # Only continue on LDAP_SUCCESS
+ $mesg->code and last;
+
+ # Get cookie from paged control
+ my($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
+ $cookie = $resp->cookie or last;
+
+ # Set cookie in paged control
+ $page->cookie($cookie);
+ } # END: while(1)
+
+ # Reset the page control for the next query
+ $page->cookie(undef);
+
+ if ($cookie) {
+ # We had an abnormal exit, so let the server know we do not want any more
+ $page->cookie($cookie);
+ $page->size(0);
+ $ldap->search( @{$queryref} );
+ # Then die
+ die("LDAP query unsuccessful");
+ }
+
+} # END: foreach my $queryref (...)
+
+# we're assuming that the group object looks something like
+# cn=name,cn=groups,cn=accounts,dc=X,dc=Y and there are no ',' chars in group
+# names
+if ($strip_group_paths) {
+ for (@verified_groups) { s/^cn=([^,]+),.*$/$1/g };
+}
+
+foreach my $verified (@verified_groups) {
+ print $verified . "\n";
+}
+
+alarm(0);
+
+__END__
+
+=head1 NAME
+
+ipa_groups.pl
+
+=head2 VERSION
+
+0.1.1
+
+=head2 DESCRIPTION
+
+Connects to one or more FreeIPA-based LDAP servers in a first-reachable fashion and returns a newline separated list of groups for a given uid. Uses memberOf attribute and thus supports nested groups.
+
+=head2 AUTHOR
+
+Richard Clark <rclark@telnic.org>
+
+=head2 FreeIPA vs Generic LDAP
+
+This script uses regular LDAP, but is focussed on support for FreeIPA, where users and groups are generally contained within single OUs, and memberOf attributes within the user object are enumerated with a recursive list of groups that the user is a member of.
+
+It is mostly impossible to provide generic out of the box LDAP support due to varying schemas, supported extensions and overlays between implementations.
+
+=head2 CONFIGURATION
+
+=head3 LDAP Bind Account
+
+To setup an LDAP bind user in FreeIPA, create a svc_gitolite_bind.ldif file along the following lines:
+
+ dn: uid=svc_gitolite_bind,cn=sysaccounts,cn=etc,dc=prod,dc=example,dc=net
+ changetype: add
+ objectclass: account
+ objectclass: simplesecurityobject
+ uid: svc_gitolite_bind
+ userPassword: reallysecurepasswordstringhere
+ passwordExpirationTime: 20150201010101Z
+ nsIdleTimeout: 0
+
+Then create the service account user, using ldapmodify authenticating as the the directory manager account (or other acccount with appropriate privileges to the sysaccounts OU):
+
+ $ ldapmodify -h auth-ldap-001.prod.example.net -Z -x -D "cn=Directory Manager" -W -f svc_gitolite_bind.ldif
+
+=head3 Required Configuration
+
+The following variables within the C<## CONFIG SECTION ##> need to be configured before the script will work.
+
+C<@ldap_hosts> - Should be set to an array of URIs or hosts to connect to. Net::LDAP will attempt to connect to each host in this list and stop on the first reachable server. The example shows TLS-supported URIs, if you want to use plain-text LDAP then set the protocol part of the URI to LDAP:// or just provide hostnames as this is the default behavior for Net::LDAP.
+
+C<%ldap_opts> - To use LDAP-over-TLS, provide the CA certificate for your LDAP servers. To use plain-text LDAP, then empty this hash of it's values or provide other valid arguments to Net::LDAP.
+
+C<%base_dn> - This can either be set to the 'true' base DN for your directory, or alternatively you can set it the the OU that your users are located in (E.G cn=users,cn=accounts,dc=prod,dc=example,dc=net).
+
+C<$user> - Provide the full Distinguished Name of your directory bind account as configured above.
+
+C<$pass> - Set to password of your directory bind account as configured above.
+
+=head3 Optional Configuration
+
+C<$groups_ou> - By default this is a regular expression matching the default groups OU. Any groups not matching this regular expression are removed from the search results. This is because FreeIPA enumerates non-user type groups (E.G system, sudoers, policy and other types) within the memberOf attribute. To change this behavior, set C<$groups_ou> to a regex matching anything you want (E.G: '.*').
+
+C<$strip_group_paths> - If this is set to perl boolean false (E.G '0') then groups will be returned in DN format. Default is true, so just the short/CN value is returned.
+
+C<$timeout> - Number of seconds to wait for an LDAP query before determining that it has failed and trying the next server in the list. This does not affect unreachable servers, which are failed immediately.
+
+C<$user_oclass> - Object class of the user to search for.
+
+C<$group_attrib> - Attribute to search for within the user object that denotes the membership of a group.
+
+=cut
+
diff --git a/contrib/utils/ldap_groups.sh b/contrib/utils/ldap_groups.sh
new file mode 100755
index 0000000..01bf5ee
--- /dev/null
+++ b/contrib/utils/ldap_groups.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# author: damien.nozay@gmail.com
+
+# Given a username,
+# Provides a space-separated list of groups that the user is a member of.
+#
+# see http://gitolite.com/gitolite/conf.html#ldap
+# GROUPLIST_PGM => /path/to/ldap_groups.sh
+
+ldap_groups() {
+ username=$1;
+ # this relies on openldap / pam_ldap to be configured properly on your
+ # system. my system allows anonymous search.
+ echo $(
+ ldapsearch -x -LLL "(&(objectClass=posixGroup)(memberUid=${username}))" cn \
+ | grep "^cn" \
+ | cut -d' ' -f2
+ );
+}
+
+ldap_groups $@
diff --git a/contrib/utils/rc-format-v3.4 b/contrib/utils/rc-format-v3.4
new file mode 100755
index 0000000..1a11737
--- /dev/null
+++ b/contrib/utils/rc-format-v3.4
@@ -0,0 +1,212 @@
+#!/usr/bin/perl
+
+# help with rc file format change at v3.4 -- help upgrade v3 rc files from
+# v3.3 and below to the new v3.4 and above format
+
+# once you upgrade gitolite past 3.4, you may want to use the new rc file
+# format, because it's really much nicer (just to recap: the old format will
+# still work, in fact internally the new format gets converted to the old
+# format before actually being used. However, the new format makes it much
+# easier to enable and disable features).
+
+# PLEASE SEE WARNINGS BELOW
+
+# this program helps you upgrade your rc file.
+
+# STEPS
+# cd gitolite-source-repo-clone
+# contrib/utils/upgrade-rc33 /path/to/old.gitolite.rc > new.gitolite.rc
+
+# WARNINGS
+# make sure you also READ ALL ERROR/WARNING MESSAGES GENERATED
+# make sure you EXAMINE THE FILE AND CHECK THAT EVERYTHING LOOKS GOOD before using it
+# be especially careful about
+# variables which contains single/double quotes or other special characters
+# variables that stretch across multiple lines
+# features which take arguments (like 'renice')
+# new features you've enabled which don't exist in the default rc
+
+# ----------------------------------------------------------------------
+
+use strict;
+use warnings;
+use 5.10.0;
+use Cwd;
+use Data::Dumper;
+$Data::Dumper::Terse = 1;
+$Data::Dumper::Indent = 1;
+$Data::Dumper::Sortkeys = 1;
+
+BEGIN {
+ $ENV{HOME} = getcwd;
+ $ENV{HOME} .= "/.home.rcupgrade.$$";
+ mkdir $ENV{HOME} or die "mkdir '$ENV{HOME}': $!\n";
+}
+
+END {
+ system("rm -rf ./.home.rcupgrade.$$");
+}
+
+use lib "./src/lib";
+use Gitolite::Rc;
+{
+ no warnings 'redefine';
+ sub Gitolite::Common::gl_log { }
+}
+
+# ----------------------------------------------------------------------
+
+# everything happens inside a fresh v3.6.1+ gitolite clone; no other
+# directories are used.
+
+# the old rc file to be migrated is somewhere *else* and is supplied as a
+# command line argument.
+
+# ----------------------------------------------------------------------
+
+my $oldrc = shift or die "need old rc filename as arg-1\n";
+
+{
+
+ package rcup;
+ do $oldrc;
+}
+
+my %oldrc;
+{
+ no warnings 'once';
+ %oldrc = %rcup::RC;
+}
+
+delete $rcup::{RC};
+{
+ my @extra = sort keys %rcup::;
+ warn "**** WARNING ****\nyou have variables declared outside the %RC hash; you must handle them manually\n" if @extra;
+}
+
+# this is the new rc text being built up
+my $newrc = glrc('default-text');
+
+# ----------------------------------------------------------------------
+
+# default disable all features in newrc
+map { disable( $_, 'sq' ) } (qw(help desc info perms writable ssh-authkeys git-config daemon gitweb));
+# map { disable($_, '') } (qw(GIT_CONFIG_KEYS));
+
+set_s('HOSTNAME');
+set_s( 'UMASK', 'num' );
+set_s( 'GIT_CONFIG_KEYS', 'sq' );
+set_s( 'LOG_EXTRA', 'num' );
+set_s( 'DISPLAY_CPU_TIME', 'num' );
+set_s( 'CPU_TIME_WARN_LIMIT', 'num' );
+set_s('SITE_INFO');
+
+set_s('LOCAL_CODE');
+
+if ( $oldrc{WRITER_CAN_UPDATE_DESC} ) {
+ die "tell Sitaram he changed the default rc too much" unless $newrc =~ /rc variables used by various features$/m;
+ $newrc =~ s/(rc variables used by various features\n)/$1\n # backward compat\n WRITER_CAN_UPDATE_DESC => 1,\n/;
+
+ delete $oldrc{WRITER_CAN_UPDATE_DESC};
+}
+
+if ( $oldrc{ROLES} ) {
+ my $t = '';
+ for my $r ( sort keys %{ $oldrc{ROLES} } ) {
+ $t .= ( " " x 8 ) . $r . ( " " x ( 28 - length($r) ) ) . "=> 1,\n";
+ }
+ $newrc =~ s/(ROLES *=> *\{\n).*?\n( *\},)/$1$t$2/s;
+
+ delete $oldrc{ROLES};
+}
+
+if ( $oldrc{DEFAULT_ROLE_PERMS} ) {
+ warn "DEFAULT_ROLE_PERMS has been replaced by per repo option\nsee http://gitolite.com/gitolite/wild.html\n";
+ delete $oldrc{DEFAULT_ROLE_PERMS};
+}
+
+# the following is a bit like the reverse of what the new Rc.pm does...
+
+for my $l ( split /\n/, $Gitolite::Rc::non_core ) {
+ next if $l =~ /^ *#/ or $l !~ /\S/;
+
+ my ( $name, $where, $module ) = split ' ', $l;
+ $module = $name if $module eq '.';
+ ( $module = $name ) .= "::" . lc($where) if $module eq '::';
+
+ # if you find $module as an element of $where, enable $name
+ enable($name) if miw( $module, $where );
+}
+
+# now deal with commands
+if ( $oldrc{COMMANDS} ) {
+ for my $c ( sort keys %{ $oldrc{COMMANDS} } ) {
+ if ( $oldrc{COMMANDS}{$c} == 1 ) {
+ enable($c);
+ # we don't handle anything else right (and so far only git-annex
+ # is affected, as far as I remember)
+
+ delete $oldrc{COMMANDS}{$c};
+ }
+ }
+}
+
+print $newrc;
+
+for my $w (qw(INPUT POST_COMPILE PRE_CREATE ACCESS_1 POST_GIT PRE_GIT ACCESS_2 POST_CREATE SYNTACTIC_SUGAR)) {
+ delete $oldrc{$w} unless scalar( @{ $oldrc{$w} } );
+}
+delete $oldrc{COMMANDS} unless scalar keys %{ $oldrc{COMMANDS} };
+
+exit 0 unless %oldrc;
+
+warn "the following parts of the old rc were NOT converted:\n";
+print STDERR Dumper \%oldrc;
+
+# ----------------------------------------------------------------------
+
+# set scalars that the new file defaults to "commented out"
+sub set_s {
+ my ( $key, $type ) = @_;
+ $type ||= '';
+ return unless exists $oldrc{$key};
+
+ # special treatment for UMASK
+ $oldrc{$key} = substr( "00" . sprintf( "%o", $oldrc{$key} ), -4 ) if ( $key eq 'UMASK' );
+
+ $newrc =~ s/# $key /$key /; # uncomment if needed
+ if ( $type eq 'num' ) {
+ $newrc =~ s/$key ( *=> *).*/$key $1$oldrc{$key},/;
+ } elsif ( $type eq 'sq' ) {
+ $newrc =~ s/$key ( *=> *).*/$key $1'$oldrc{$key}',/;
+ } else {
+ $newrc =~ s/$key ( *=> *).*/$key $1"$oldrc{$key}",/;
+ }
+
+ delete $oldrc{$key};
+}
+
+sub disable {
+ my ( $key, $type ) = @_;
+ if ( $type eq 'sq' ) {
+ $newrc =~ s/^( *)'$key'/$1# '$key'/m;
+ } else {
+ $newrc =~ s/^( *)$key\b/$1# $key/m;
+ }
+}
+
+sub enable {
+ my $key = shift;
+ $newrc =~ s/^( *)# *'$key'/$1'$key'/m;
+ return if $newrc =~ /^ *'$key'/m;
+ $newrc =~ s/(add new commands here.*\n)/$1 '$key',\n/;
+}
+
+sub miw {
+ my ( $m, $w ) = @_;
+ return 0 unless $oldrc{$w};
+ my @in = @{ $oldrc{$w} };
+ my @out = grep { !/^$m$/ } @{ $oldrc{$w} };
+ $oldrc{$w} = \@out;
+ return not scalar(@in) == scalar(@out);
+}
diff --git a/contrib/utils/testconf b/contrib/utils/testconf
new file mode 100755
index 0000000..03580f9
--- /dev/null
+++ b/contrib/utils/testconf
@@ -0,0 +1,130 @@
+#!/bin/bash
+
+# this is meant to be run on your *client* (where you edit and commit files
+# in a gitolite-admin *working* repo), not on the gitolite server.
+#
+# TO USE
+# ======
+
+# To use this, first upgrade gitolite to the latest on the server; you need at
+# least v3.6.7.
+#
+# Then, on the client:
+#
+# 1. copy this file (contrib/utils/testconf in the latest gitolite) to
+# somewhere in your $PATH
+# 2. modify the following lines if you wish (default should be fine for
+# most people):
+
+ # a semi-permanent area to play in (please delete it manually if you want to start afresh).
+ testconf=$HOME/GITOLITE-TESTCONF
+ # the gitolite source code
+ gitolite_url=https://github.com/sitaramc/gitolite
+
+# 3. go to your gitolite-admin clone and make suitable changes; see example
+# below. No need to push to the server, yet.
+# 4. run 'testconf`
+#
+# CAVEAT: include files are not handled the same way gitolite parsing handles
+# them -- we just cat all the conf files together, in sorted order.
+#
+# If the tests ran OK, push your changes to the server as usual.
+
+# EXAMPLE changes to gitolite.conf
+# ================================
+# Say you have these rules in the conf file:
+#
+# repo foo
+# R = u1
+# RW = u2
+# RW+ = u3
+#
+# To create test code for this, add the following lines to the conf file.
+#
+# =begin testconf
+# # you can put arbitrary bash code here, but a simple example follows
+#
+# ok() { "$@" && echo ok || echo "not ok ($*)"; }
+# nok() { ! "$@" && echo ok || echo "not ok ($*)"; }
+#
+# ok gitolite access -q foo u1 R
+# nok gitolite access -q foo u1 W
+#
+# ok gitolite access -q foo u2 W
+# nok gitolite access -q foo u2 +
+#
+# ok gitolite access -q foo u3 +
+# =end
+#
+# Note that you can actually put in any bash code between the 'begin' and
+# 'end' lines; the above is just a useful sample/template.
+#
+# Because of the 'begin' and 'end' lines, gitolite will ignore those lines
+# when processing the conf file ON THE SERVER.
+#
+# (optional) TAP compliance
+# =========================
+# if you add a line 'echo 1..5' (in this case, since there are 5 ok/nok lines;
+# you will certainly have more) to the top the file, you can run
+#
+# prove `which testconf`
+#
+# which will give you a much nicer output. The only issue is if you have
+# include files, you will need to put that in the file whose name is sorted
+# first!
+#
+# Using a non-default ".gitolite.rc"
+# ==================================
+#
+# If your conf needs a non-default `~/.gitolite.rc`, copy the file you need as
+# "testconf.gitolite.rc" in the root directory of the gitolite-admin clone
+# where you are running "testconf". (Whether you commit this file to the
+# gitolite-admin repo, or keep it local/untracked, is your call).
+
+# ----------------------------------------------------------------------
+od=$PWD
+
+# prep
+
+mkdir -p $testconf
+cd $testconf
+
+export HOME=$PWD
+export PATH=$PWD/gitolite/src:$PATH
+
+[ -d gitolite ] || {
+
+ echo getting gitolite source...
+ git clone $gitolite_url gitolite
+ echo
+
+ echo installing gitolite...
+ gitolite/install >/dev/null
+ echo
+
+ echo setting up gitolite...
+ gitolite setup -a admin
+ echo
+
+}
+
+# copy conf from $od
+
+rm -rf $testconf/.gitolite/conf
+mkdir -p $testconf/.gitolite/conf
+cp -a $od/conf/* $testconf/.gitolite/conf/
+
+# copy rc from $od, if it exists
+[ -f $od/testconf.gitolite.rc ] && cp $od/testconf.gitolite.rc $testconf/.gitolite.rc
+
+# compile+
+
+gitolite compile
+gitolite trigger POST_COMPILE
+
+# snarf bits of code from conf files and run them
+
+cat `find $testconf/.gitolite/conf -type f -name "*.conf" | sort` |
+ perl -ne '
+ print if /^=begin testconf$/ .. /^=end$/ and not /^=(begin|end)/;
+ ' | /bin/bash
diff --git a/contrib/vim/indent/gitolite.vim b/contrib/vim/indent/gitolite.vim
new file mode 100644
index 0000000..b36f30a
--- /dev/null
+++ b/contrib/vim/indent/gitolite.vim
@@ -0,0 +1,49 @@
+" Vim indent file
+" Language: gitolite configuration
+" URL: https://github.com/sitaramc/gitolite/blob/master/contrib/vim/indent/gitolite.vim
+" (https://raw.githubusercontent.com/sitaramc/gitolite/master/contrib/vim/indent/gitolite.vim)
+" Maintainer: Sitaram Chamarty <sitaramc@gmail.com>
+" (former Maintainer: Teemu Matilainen <teemu.matilainen@iki.fi>)
+" Last Change: 2017 Oct 05
+
+if exists("b:did_indent")
+ finish
+endif
+let b:did_indent = 1
+
+setlocal autoindent
+setlocal indentexpr=GetGitoliteIndent()
+setlocal indentkeys=o,O,*<Return>,!^F,=repo,\",=
+
+" Only define the function once.
+if exists("*GetGitoliteIndent")
+ finish
+endif
+
+let s:cpo_save = &cpo
+set cpo&vim
+
+function! GetGitoliteIndent()
+ let prevln = prevnonblank(v:lnum-1)
+ let pline = getline(prevln)
+ let cline = getline(v:lnum)
+
+ if cline =~ '^\s*\(C\|R\|RW\|RW+\|RWC\|RW+C\|RWD\|RW+D\|RWCD\|RW+CD\|-\)[ \t=]'
+ return shiftwidth()
+ elseif cline =~ '^\s*config\s'
+ return shiftwidth()
+ elseif cline =~ '^\s*option\s'
+ return shiftwidth()
+ elseif pline =~ '^\s*repo\s' && cline =~ '^\s*\(#.*\)\?$'
+ return shiftwidth()
+ elseif cline =~ '^\s*#'
+ return indent(prevln)
+ elseif cline =~ '^\s*$'
+ return -1
+ else
+ return 0
+ endif
+endfunction
+
+let &cpo = s:cpo_save
+unlet s:cpo_save
diff --git a/contrib/vim/syntax/gitolite.vim b/contrib/vim/syntax/gitolite.vim
new file mode 100644
index 0000000..3a6da26
--- /dev/null
+++ b/contrib/vim/syntax/gitolite.vim
@@ -0,0 +1,94 @@
+" Vim syntax file
+" Language: gitolite configuration
+" URL: https://github.com/sitaramc/gitolite/blob/master/contrib/vim/syntax/gitolite.vim
+" (https://raw.githubusercontent.com/sitaramc/gitolite/master/contrib/vim/syntax/gitolite.vim)
+" Maintainer: Sitaram Chamarty <sitaramc@gmail.com>
+" (former Maintainer: Teemu Matilainen <teemu.matilainen@iki.fi>)
+" Last Change: 2017 Oct 05
+
+if exists("b:current_syntax")
+ finish
+endif
+
+let s:cpo_save = &cpo
+set cpo&vim
+
+" this seems to be the best way, for now.
+syntax sync fromstart
+
+" ---- common stuff
+
+syn match gitoliteGroup '@\S\+'
+
+syn match gitoliteComment '#.*' contains=gitoliteTodo
+syn keyword gitoliteTodo TODO FIXME XXX NOT contained
+
+" ---- main section
+
+" catch template-data syntax appearing outside template-data section
+syn match gitoliteRepoError '^\s*repo.*='
+syn match gitoliteRepoError '^\s*\S\+\s*=' " this gets overridden later when first word is a perm, don't worry
+
+" normal gitolite group and repo lines
+syn match gitoliteGroupLine '^\s*@\S\+\s*=\s*\S.*$' contains=gitoliteGroup,gitoliteComment
+syn match gitoliteRepoLine '^\s*repo\s\+[^=]*$' contains=gitoliteRepo,gitoliteGroup,gitoliteComment
+syn keyword gitoliteRepo repo contained
+
+syn keyword gitoliteSpecialRepo CREATOR
+
+" normal gitolite rule lines
+syn match gitoliteRuleLine '^\s*\(-\|C\|R\|RW+\?C\?D\?\)\s[^#]*' contains=gitoliteRule,gitoliteCreateRule,gitoliteDenyRule,gitoliteRefex,gitoliteUsers,gitoliteGroup
+syn match gitoliteRule '\(^\s*\)\@<=\(-\|C\|R\|RW+\?C\?D\?\)\s\@=' contained
+syn match gitoliteRefex '\(^\s*\(-\|R\|RW+\?C\?D\?\)\s\+\)\@<=\S.\{-}\(\s*=\)\@=' contains=gitoliteSpecialRefex
+syn match gitoliteSpecialRefex 'NAME/'
+syn match gitoliteSpecialRefex '/USER/'
+syn match gitoliteCreateRule '\(^\s*C\s.*=\s*\)\@<=\S[^#]*[^# ]' contained contains=gitoliteGroup
+syn match gitoliteDenyRule '\(^\s*-\s.*=\s*\)\@<=\S[^#]*[^# ]' contained
+
+" normal gitolite config (and similar) lines
+syn match gitoliteConfigLine '^\s*\(config\|option\|include\|subconf\)\s[^#]*' contains=gitoliteConfigKW,gitoliteConfigKey,gitoliteConfigVal,gitoliteComment
+syn keyword gitoliteConfigKW config option include subconf contained
+syn match gitoliteConfigKey '\(\(config\|option\)\s\+\)\@<=[^ =]*' contained
+syn match gitoliteConfigVal '\(=\s*\)\@<=\S.*' contained
+
+" ---- template-data section
+
+syn region gitoliteTemplateLine matchgroup=PreProc start='^=begin template-data$' end='^=end$' contains=gitoliteTplRepoLine,gitoliteTplRoleLine,gitoliteGroup,gitoliteComment,gitoliteTplError
+
+syn match gitoliteTplRepoLine '^\s*repo\s\+\S.*=.*' contained contains=gitoliteTplRepo,gitoliteTplTemplates,gitoliteGroup
+syn keyword gitoliteTplRepo repo contained
+syn match gitoliteTplTemplates '\(=\s*\)\@<=\S.*' contained contains=gitoliteGroup,gitoliteComment
+
+syn match gitoliteTplRoleLine '^\s*\S\+\s*=\s*.*' contained contains=gitoliteTplRole,gitoliteGroup,gitoliteComment
+syn match gitoliteTplRole '\S\+\s*='he=e-1 contained
+
+" catch normal gitolite rules appearing in template-data section
+syn match gitoliteTplError '^\s*repo[^=]*$' contained
+syn match gitoliteTplError '^\s*\(-\|R\|RW+\?C\?D\?\)\s'he=e-1 contained
+syn match gitoliteTplError '^\s*\(config\|option\|include\|subconf\)\s'he=e-1 contained
+syn match gitoliteTplError '^\s*@\S\+\s*=' contained contains=NONE
+
+hi def link gitoliteGroup Identifier
+hi def link gitoliteComment Comment
+hi def link gitoliteTodo ToDo
+hi def link gitoliteRepoError Error
+hi def link gitoliteGroupLine PreProc
+hi def link gitoliteRepo Keyword
+hi def link gitoliteSpecialRepo PreProc
+hi def link gitoliteRule Keyword
+hi def link gitoliteCreateRule PreProc
+hi def link gitoliteDenyRule WarningMsg
+hi def link gitoliteRefex Constant
+hi def link gitoliteSpecialRefex PreProc
+hi def link gitoliteConfigKW Keyword
+hi def link gitoliteConfigKey Identifier
+hi def link gitoliteConfigVal String
+hi def link gitoliteTplRepo Keyword
+hi def link gitoliteTplTemplates Constant
+hi def link gitoliteTplRole Constant
+hi def link gitoliteTplError Error
+
+let b:current_syntax = "gitolite"
+
+let &cpo = s:cpo_save
+unlet s:cpo_save