Compare commits

...

No commits in common. "master" and "temp-br--data-dumper-problem-demo" have entirely different histories.

169 changed files with 6234 additions and 13628 deletions

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
conf/* crlf=input
src/* crlf=input
hooks/common/* crlf=input
hooks/gitolite-admin/* crlf=input

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
*.tar
*.tgz
*.tar.gz
*.tar.bz2
conf/VERSION

View file

@ -1,86 +0,0 @@
2012-12-29 v3.3 bug fix: gl-perms propagation to slaves broke sometime
after v3.2 (so if you're only picking up tagged releases
you're OK)
the "D" command now allows rm/unlock to be totally
disabled
new trigger: update-gitweb-daemon-from-options; another
way to update gitweb and daemon access lists
new 'create' command for explicit wild repo creation, and
new AutoCreate trigger to control auto-creation
allow simple macros in conf file
2012-11-14 v3.2 major efficiency boost for large setups
optional support for multi-line pubkeys; see
src/triggers/post-compile/ssh-authkeys-split
bug fix for not creating gl-conf when repo para has only
config lines and no access rules
new 'bg' trigger command to put long jobs started from a
trigger into background
%GL_REPO and %GL_CREATOR now work for 'option's also
test suite now much more BSD friendly
2012-10-05 v3.1 (security) fix path traversal on wild repos
new %GL_CREATOR variable for git-config lines
rsync command to create and send bundles automagically
migrated 'who-pushed'
logical expressions on refexes!!!
2012-06-27 v3.04 documentation graduated and moved out of parents house :)
new trigger for 'repo specific umask'
new 'list-dangling-repos' command
new LOCAL_CODE rc var; allow admin specified programs to
override system-installed ones
new 'upstream' trigger-cum-command to maintain local
copies of external repos
new 'sudo' command
minor backward compat breakage in 'gitolite query-rc'
'perms' command can now create repo if needed
migrated 'symbolic-ref' command
'gitolite setup --hooks-only'
2012-05-23 v3.03 fix major bug that allowed an admin to get a shell
2012-05-20 v3.02 packaging instructions fixed up and smoke tested
make it easier to give some users a full shell
allow aliasing a repo to another name
simulate POST_CREATE for new normal (non-wild) repos
(just for kicks) a VREF that allows for voting on changes
to a branch
bug fix: smart http was not running PRE_ and POST_GIT
triggers
htpasswd migrated
2012-04-29 v3.01 mostly BSD and Solaris compat
also fork command added
2012-04-18 v3.0 first release to "master"
This is a compete rewrite of gitolite; please see
documentation before upgrading.

16
Makefile Normal file
View file

@ -0,0 +1,16 @@
# this is a simple wrapper around "git archive" using make
# "make [refname].tar" produces a tar of refname, then adds a file containing
# the "git describe" output for that refname to the tar. This lets you say
# "cat .GITOLITE-VERSION" to find out which ref produced this tar
# Note: I'm not sure if that "-r" is a GNU tar extension...
.GITOLITE-VERSION:
@touch conf/VERSION
%.tar: .GITOLITE-VERSION
git describe --tags --long $* > conf/VERSION
git archive $* > $@
tar -r -f $@ conf/VERSION
rm conf/VERSION

359
README.md
View file

@ -1,359 +0,0 @@
Github-users: click the 'wiki' link before sending me anything via github.
Existing users: this is gitolite v3.x. If you are upgrading from v2.x this
file will not suffice; you *must* check the online docs (see below for URL).
------------------------------------------------------------------------
This file contains BASIC DOCUMENTATION ONLY.
* It is suitable for a fresh, ssh-based, installation of gitolite and basic
usage of its most important features.
* It is NOT meant to be exhaustive or detailed.
The COMPLETE DOCUMENTATION is at:
http://sitaramc.github.com/gitolite/master-toc.html
Please go there for what/why/how, concepts, background, troubleshooting, more
details on what is covered here, or advanced features not covered here.
------------------------------------------------------------------------
BASIC DOCUMENTATION FOR GITOLITE
================================
This file contains the following sections:
* INSTALLATION AND SETUP
* ADDING USERS AND REPOS
* HELP FOR YOUR USERS
* BASIC SYNTAX
* ACCESS RULES
* GROUPS
* COMMANDS
* THE 'rc' FILE
* GIT-CONFIG
* GIT-DAEMON
* GITWEB
* CONTACT AND SUPPORT
* LICENSE
------------------------------------------------------------------------
INSTALLATION AND SETUP
----------------------
Server requirements:
* any unix system
* sh
* git 1.6.6+
* perl 5.8.8+
* openssh 5.0+
* a dedicated userid to host the repos (in this document, we assume it
is 'git'), with shell access ONLY by 'su - git' from some other userid
on the same server.
Steps to install:
* login as 'git' as described above
* make sure ~/.ssh/authorized_keys is empty or non-existent
* make sure your ssh public key from your workstation is available at $HOME/YourName.pub
* run the following commands:
git clone git://github.com/sitaramc/gitolite
mkdir -p $HOME/bin
gitolite/install -to $HOME/bin
gitolite setup -pk YourName.pub
If the last command doesn't run perhaps 'bin' in not in your 'PATH'.
You can either add it, or just run:
$HOME/bin/gitolite setup -pk YourName.pub
ADDING USERS AND REPOS
----------------------
Do NOT add new repos or users manually on the server. Gitolite users,
repos, and access rules are maintained by making changes to a special repo
called 'gitolite-admin' and pushing those changes to the server.
----
To administer your gitolite installation, start by doing this on your
workstation (if you have not already done so):
git clone git@host:gitolite-admin
**NOTE**: if you are asked for a password, something has gone wrong.
Now if you 'cd gitolite-admin', you will see two subdirectories in it:
'conf' and 'keydir'.
To add new users alice, bob, and carol, obtain their public keys and add
them to 'keydir' as alice.pub, bob.pub, and carol.pub respectively.
To add a new repo 'foo' and give different levels of access to these
users, edit the file 'conf/gitolite.conf' and add lines like this:
repo foo
RW+ = alice
RW = bob
R = carol
See the 'ACCESS RULES' section later for more details.
Once you have made these changes, do something like this:
git add conf
git add keydir
git commit -m 'added foo, gave access to alice, bob, carol'
git push
When the push completes, gitolite will add the new users to
~/.ssh/authorized_keys on the server, as well as create a new, empty, repo
called 'foo'.
HELP FOR YOUR USERS
-------------------
Once a user has sent you their public key and you have added them as
specified above and given them access, you have to tell them what URL to
access their repos at. This is usually 'git clone git@host:reponame'; see
man git-clone for other forms.
**NOTE**: again, if they are asked for a password, something is wrong.
If they need to know what repos they have access to, they just have to run
'ssh git@host info'; see 'COMMANDS' section later for more on this.
BASIC SYNTAX
------------
The basic syntax of the conf file is very simple.
* Everything is space separated; there are no commas, semicolons, etc.,
in the syntax.
* Comments are in the usual perl/shell style.
* User and repo names are as simple as possible; they must start with an
alphanumeric, but after that they can also contain '.', '_', or '-'.
Usernames can optionally be followed by an '@' and a domainname
containing at least one '.'; this allows you to use an email address
as someone's username.
Reponames can contain '/' characters; this allows you to put your
repos in a tree-structure for convenience.
* There are no continuation lines.
ACCESS RULES
------------
This section is mostly 'by example'.
Gitolite's access rules are very powerful. The simplest use was already
shown above. Here is a slightly more detailed example:
repo foo
RW+ = alice
- master = bob
- refs/tags/v[0-9] = bob
RW = bob
RW refs/tags/v[0-9] = carol
R = dave
For clones and fetches, as long as the user is listed with an R, RW
or RW+ in at least one rule, he is allowed to read the repo.
For pushes, rules are processed in sequence until a rule is found
where the user, the permission (see note 1), and the refex (note 2)
*all* match. At that point, if the permission on the matched rule
was '-', the push is denied, otherwise it is allowed. If no rule
matches, the push is denied.
Note 1: permission matching:
* a permission of RW matches only a fast-forward push or create
* a permission of RW+ matches any type of push
* a permission of '-' matches any type of push
Note 2: refex matching:
(refex = optional regex to match the ref being pushed)
* an empty refex is treated as 'refs/.*'
* a refex that does not start with 'refs/' is prefixed with 'refs/heads/'
* finally, a '^' is prefixed
* the ref being pushed is matched against this resulting refex
With all that background, here's what the example rules say:
* alice can do anything to any branch or tag -- create, push, delete, rewind/overwrite etc.
* bob can create or fast-forward push any branch whose name does
not start with 'master' and create any tag whose name does not
start with 'v'+digit.
* carol can create tags whose names start with 'v'+digit.
* dave can clone/fetch.
GROUPS
------
Gitolite allows you to group users or repos for convenience. Here's an
example that creates two groups of users:
@staff = alice bob carol
@interns = ashok
repo secret
RW = @staff
repo foss
RW+ = @staff
RW = @interns
Group lists accumulate. The following two lines have the same effect as
the earlier definition of @staff above:
@staff = alice bob
@staff = carol
You can also use group names in other group names:
@all-devs = @staff @interns
Finally, @all is a special group name that is often convenient to use if
you really mean 'all repos' or 'all users'.
COMMANDS
--------
Users can run certain commands remotely, using ssh. For example:
ssh git@host help
prints a list of available commands.
The most commonly used command is 'info'. All commands respond to a
single argument of '-h' with suitable information.
If you have shell on the server, you have a lot more commands available to
you; try running 'gitolite help'.
THE 'rc' FILE
--------------
Some of the instructions below may require you to edit the rc file
(~/.gitolite.rc on the server).
The rc file is perl code, but you do NOT need to know perl to edit it.
Just mind the commas, use single quotes unless you know what you're doing,
and make sure the brackets and braces stay matched up.
GIT-CONFIG
----------
Gitolite lets you set git-config values for individual repos without
having to log on to the server and run 'git config' commands:
repo foo
config hooks.mailinglist = foo-commits@example.tld
config hooks.emailprefix = '[foo] '
config foo.bar = ''
config foo.baz =
**WARNING**
The last syntax shown above is the *only* way to *delete* a config
variable once you have added it. Merely removing it from the conf
file will *not* delete it from the repo.git/config file.
**SECURITY NOTE**
Some git-config keys allow arbitrary code to be run on the server.
If all of your gitolite admins already have shell access to the server
account hosting it, you can edit the rc file (~/.gitolite.rc) on the
server, and change the GIT_CONFIG_KEYS line to look like this:
GIT_CONFIG_KEYS => '.*',
Otherwise, give it a space-separated list of regular expressions that
define what git-config keys are allowed. For example, this one allows
only variables whose names start with 'gitweb' or with 'gc' to be
defined:
GIT_CONFIG_KEYS => 'gitweb\..* gc\..*',
GIT-DAEMON
----------
Gitolite creates the 'git-daemon-export-ok' file for any repo that is
readable by a special user called 'daemon', like so:
repo foo
R = daemon
GITWEB
------
Any repo that is readable by a special user called 'gitweb' will be added
to the projects.list file.
repo foo
R = gitweb
Or you can set one or more of the following config variables instead:
repo foo
config gitweb.owner = some person's name
config gitweb.description = some description
config gitweb.category = some category
**NOTE**
You will probably need to change the UMASK in the rc file from the
default (0077) to 0027 and add whatever user your gitweb is running as
to the 'git' group. After that, you need to run a one-time 'chmod -R'
on the already created files and directories.
------------------------------------------------------------------------
CONTACT AND SUPPORT
-------------------
Mailing list for support and general discussion:
gitolite@googlegroups.com
subscribe address: gitolite+subscribe@googlegroups.com
Mailing list for announcements and notices:
subscribe address: gitolite-announce+subscribe@googlegroups.com
IRC: #git and #gitolite on freenode. Note that I live in India (UTC+0530
time zone).
Author: sitaramc@gmail.com, but please DO NOT use this for general support
questions. Subscribe to the list and ask there instead.
LICENSE
-------
The gitolite *code* is released under GPL v2. See COPYING for details.
This documentation, which is part of the source code repository, is
provided under a Creative Commons Attribution-ShareAlike 3.0 Unported
License -- see http://creativecommons.org/licenses/by-sa/3.0/

110
README.mkd Normal file
View file

@ -0,0 +1,110 @@
# gitolite
> [Update 2009-10-28: apart from all the nifty new features, there's now an
> "easy install" script in the src directory. This script can be used to
> install as well as upgrade a gitolite install. Please see the INSTALL
> document for details]
----
Gitolite is a rewrite of gitosis, with a completely different config file that
allows (at last!) access control down to the branch level, including
specifying who can and cannot *rewind* a given branch.
In this document:
* what
* why
* extra features
* security
* contact and license
----
### what
Gitolite allows a server to host many git repositories and provide access to
many developers, without having to give them real userids on the server. The
essential magic in doing this is ssh's pubkey access and the `authorized_keys`
file, and the inspiration was an older program called gitosis.
Gitolite can restrict who can read from (clone/fetch) or write to (push) a
repository. It can also restrict who can push to what branch or tag, which is
very important in a corporate environment. Gitolite can be installed without
requiring root permissions, and with no additional software than git itself
and perl. It also has several other neat features described below and
elsewhere in the `doc/` directory.
### why
I have been using gitosis for a while, and have learnt a lot from it. But in
a typical $DAYJOB setting, there are some issues:
* it's not always Linux; you can't just "urpmi gitosis" (or yum or apt-get)
and be done
* often, "python-setuptools" isn't installed (and on a Solaris9 I was trying
to help remotely, we never did manage to install it eventually)
* you don't have root access, or the ability to add users (this is also true
for people who have just one userid on a hosting provider)
* the most requested feature (see below) had to be written anyway
All of this pointed to a rewrite. In perl, naturally :-)
### extra features
The most important feature I needed was **per-branch permissions**. This is
pretty much mandatory in a corporate environment, and is almost the single
reason I started *thinking* about rolling my own gitosis in the first place.
It's not just "read-only" versus "read-write". Rewinding a branch (aka "non
fast forward push") is potentially dangerous, but sometimes needed. So is
deleting a branch (which is really just an extreme form of rewind). I needed
something in between allowing anyone to do it (the default) and disabling it
completely (`receive.denyNonFastForwards` or `receive.denyDeletes`).
Here're **some more features**. All of them, and more, are documented in
detail [here][gsdiff].
[gsdiff]: http://github.com/sitaramc/gitolite/blob/pu/doc/3-faq-tips-etc.mkd#diff
* simpler, yet far more powerful, config file syntax, including specifying
gitweb/daemon access. You'll need this power if you manage lots of
users+repos+combinations of access
* apart from branch-name based restrictions, you can also restrict by
file/dir name changed (i.e., output of `git diff --name-only`)
* config file syntax gets checked upfront, and much more thoroughly
* if your requirements are still too complex, you can split up the config
file and delegate authority over parts of it
* easier to specify gitweb owner, description and gitweb/daemon access
* easier to sync gitweb (http) authorisation with gitolite's access config
* more comprehensive logging [aka: management does not think "blame" is just
a synonym for "annotate" :-)]
* "personal namespace" prefix for each dev
* migration guide and simple converter for gitosis conf file
* "exclude" (or "deny") rights at the branch/tag level
### security
Due to the environment in which this was created and the need it fills, I
consider this a "security" program, albeit a very modest one.
For the first person to find a security hole in it, defined as allowing a
normal user (not the gitolite admin) to read a repo, or write/rewind a ref,
that the config file says he shouldn't, and caused by a bug in *code* that is
in the "master" branch, (not in the other branches, or the configuration file
or in Unix, perl, shell, etc.)... well I can't afford 1000 USD rewards like
djb, so you'll have to settle for 1000 INR (Indian Rupees) as a "token" prize
:-)
However, there are a few optional features (which must be explicitly enabled
in the RC file) where I just haven't had the time to reason about security
thoroughly enough. Please read the comments in `conf/example.gitolite.rc` for
details, looking for the word "security".
----
### contact and license
Gitolite is released under GPL v2. See COPYING for details.
sitaramc@gmail.com

View file

@ -1,99 +0,0 @@
#!/usr/bin/perl
use Cwd;
my $h = $ENV{HOME};
my $rc = "$h/.gitolite.rc";
my %count;
intro();
msg( FATAL => "no rc file found; do you even *have* g2 running?" ) if not -f $rc;
do $rc;
unless ( $return = do $rc ) {
msg( FATAL => "couldn't parse $rc: $@" ) if $@;
msg( FATAL => "couldn't do $rc: $!" ) unless defined $return;
msg( WARNING => "couldn't run $rc" ) unless $return;
}
print "checking rc file...\n";
rc_basic();
rest_of_rc();
print "\n";
print "checking conf file(s)...\n";
conf();
print "\n";
print "checking repos...\n";
repo();
print "\n";
print "...all done...\n";
# ----------------------------------------------------------------------
sub intro {
msg( INFO => "This program only checks for uses that make the new g3 completely unusable" );
msg( '' => "or that might end up giving *more* access to someone if migrated as-is." );
msg( '' => "It does NOT attempt to catch all the differences described in the docs." );
msg( '', '' );
msg( INFO => "'see docs' usually means the pre-migration checklist in" );
msg( '', => "'g2migr.html'; to get there, start from the main migration" );
msg( '', => "page at http://sitaramc.github.com/gitolite/install.html#migr" );
msg( '', '' );
}
sub rc_basic {
msg( FATAL => "GL_ADMINDIR in the wrong place -- aborting; see docs" ) if $GL_ADMINDIR ne "$h/.gitolite";
msg( NOTE => "GL_ADMINDIR is in the right place; assuming you did not mess with" );
msg( '', "GL_CONF, GL_LOGT, GL_KEYDIR, and GL_CONF_COMPILED" );
msg( FATAL => "REPO_BASE in the wrong place -- aborting; see docs" ) if $REPO_BASE ne "$h/repositories" and $REPO_BASE ne "repositories";
# ( abs or rel both ok)
}
sub rest_of_rc {
msg( SEVERE => "GIT_PATH found; see docs" ) if $GIT_PATH;
msg( SEVERE => "GL_ALL_INCLUDES_SPECIAL found; see docs" ) if $GL_ALL_INCLUDES_SPECIAL;
msg( SEVERE => "GL_NO_CREATE_REPOS not yet implemented" ) if $GL_NO_CREATE_REPOS;
msg( SEVERE => "rsync not yet implemented" ) if $RSYNC_BASE;
msg( WARNING => "ADMIN_POST_UPDATE_CHAINS_TO found; see docs" ) if $ADMIN_POST_UPDATE_CHAINS_TO;
msg( WARNING => "GL_NO_DAEMON_NO_GITWEB found; see docs" ) if $GL_NO_DAEMON_NO_GITWEB;
msg( WARNING => "GL_NO_SETUP_AUTHKEYS found; see docs" ) if $GL_NO_SETUP_AUTHKEYS;
msg( WARNING => "UPDATE_CHAINS_TO found; see docs" ) if $UPDATE_CHAINS_TO;
msg( WARNING => "GL_ADC_PATH found; see docs" ) if $GL_ADC_PATH;
msg( WARNING => "non-default GL_WILDREPOS_PERM_CATS found" ) if $GL_WILDREPOS_PERM_CATS ne 'READERS WRITERS';
}
sub conf {
chdir($h);
chdir($GL_ADMINDIR);
my $conf = `find . -name "*.conf" | xargs cat`;
msg( "SEVERE", "NAME rules; see docs" ) if $conf =~ m(NAME/);
msg( "SEVERE", "subconf command in admin repo; see docs" ) if $conf =~ m(NAME/conf/fragments);
msg( "SEVERE", "mirroring used; see docs" ) if $conf =~ m(config +gitolite\.mirror\.);
}
sub repo {
chdir($h);
chdir($REPO_BASE);
my @creater = `find . -name gl-creater`;
if (@creater) {
msg( WARNING => "found " . scalar(@creater) . " gl-creater files; see docs" );
}
my @perms = `find . -name gl-perms | xargs egrep -l -w R\\|RW`;
if (@perms) {
msg( WARNING => "found " . scalar(@perms) . " gl-perms files with R or RW; see docs" );
}
}
sub msg {
my ( $type, $text ) = @_;
print "$type" if $type;
print "\t$text\n";
exit 1 if $type eq 'FATAL';
$count{$type}++ if $type;
}

298
conf/example.conf Normal file
View file

@ -0,0 +1,298 @@
# example conf file for gitolite
# ----------------------------------------------------------------------------
# overall syntax:
# - everything is space-separated; no commas, semicolons, etc (except in
# the description string for gitweb)
# - comments in the normal shell-ish style; no surprises there
# - there are NO continuation lines of any kind
# - user/repo names as simple as possible; they must start with an
# alphanumeric, but after that they can also contain ".", "_", "-".
# - usernames can optionally be followed by an "@" and a domainname
# containing at least one "." (this allows you to use an email
# address as someone's username)
# - reponames can contain "/" characters (this allows you to
# put your repos in a tree-structure for convenience)
# objectives, over and above gitosis:
# - simpler syntax
# - easier gitweb/daemon control
# - specify who can push a branch/tag
# - specify who can rewind a branch/rewrite a tag
# ----------------------------------------------------------------------------
# GROUPS
# ------
# syntax:
# @groupname = [one or more names]
# groups let you club (user or group) names together for convenience
# * a group is like a #define in C except that it can *accumulate* values
# * the config file is parsed in a single-pass, so later *additions* to a
# group name cannot affect earlier *uses* of it
# The following examples should illustrate all this:
# you can have a group of people...
@staff = sitaram some_dev another-dev
# ...or a group of repos
@oss_repos = gitolite linux git perl rakudo entrans vkc
# even sliced and diced differently
@admins = sitaram admin2
# notice that sitaram is in 2 groups (staff and admins)
# if you repeat a group name in another definition line, the
# new ones get added to the old ones (they accumulate)
@staff = au.thor
# so now "@staff" expands to all 4 names
# groups can include other groups, and the included group will
# be expanded to whatever value it currently has
@interns = indy james
@staff = bob @interns
# "@staff" expands to 7 names now
@interns = han
# "@interns" now has 3 names in it, but note that this does
# not change @staff
# WILDCARD REPOSITORIES ("wildrepos" BRANCH ONLY)
# -----------------------------------------
# Please see doc/4-wildcard-repositories.mkd for details
# REPO AND BRANCH PERMISSIONS
# ---------------------------
# syntax:
# start line:
# repo [one or more repos and/or repo groups]
# followed by one or more permissions lines:
# (R|RW|RW+) [zero or more refexes] = [one or more users]
# there are 3 types of permissions: R, RW, and RW+. The "+" means permission
# to "rewind" (force push a non-fast forward to) a branch
# how permissions are matched:
# - user, repo, and access (W or +) are known. For that combination, if
# any of the refexes match the refname being updated, the push succeeds.
# If none of them match, it fails
# what's a refex? a regex to match against the ref being updated (get it?)
# See next section for more on refexes
# BASIC PERMISSIONS (repo level only; apply to all branches/tags in repo)
# most important rule of all -- specify who can make changes
# to *this* file take effect
repo gitolite-admin
RW+ = @admins
# "@all" is a special, predefined, group name of all users
# (everyone who has a pubkey in keydir)
repo testing
RW+ = @all
# this repo is visible to staff but only sitaram can write to it
repo gitolite
R = @staff
RW+ = sitaram
# you can split up access rules for a repo for convenience
# (notice that @oss_repos contains gitolite also)
repo @oss_repos
R = @all
# set permissions to all already defined repos
# (a repository is defined if it has permission rules
# associated, empty "repo" stanza or "@group=..." line is
# not enough). *Please* do see doc/3-faq-tips-etc.mkd for
# some important notes on this feature
repo @all
RW+ = @admins
# SPECIFYING AND USING A REFEX
# - refexes are specified in perl regex syntax
# - refexes are prefix-matched (they are internally anchored with "^"
# before being used), which means a refex like "refs/tags/v[0-9]"
# matches anything *starting with* that pattern. There may be text
# after it (example: refs/tags/v4-r3/p7), and it will still match
# ADVANCED PERMISSIONS USING REFEXES
# - if no refex appears, the rule applies to all refs in that repo
# - a refex is automatically prefixed by "refs/heads/" if it doesn't start
# with "refs/" (so tags have to be explicitly named as
# refs/tags/pattern)
# here's the example from
# Documentation/howto/update-hook-example.txt:
# refs/heads/master junio
# +refs/heads/pu junio
# refs/heads/cogito$ pasky
# refs/heads/bw/.* linus
# refs/heads/tmp/.* .*
# refs/tags/v[0-9].* junio
# and here're the equivalent gitolite refexes
repo git
RW master = junio
RW+ pu = junio
RW cogito$ = pasky
RW bw/ = linus
RW tmp/ = @all
RW refs/tags/v[0-9] = junio
# DENY/EXCLUDE RULES
# ***IMPORTANT NOTES ABOUT "DENY" RULES***:
# - deny rules do NOT affect read access. They only apply to `W` and `+`.
#
# - when using deny rules, the order of your rules starts to matter, where
# earlier it did not. The first matching rule applies, where "matching" is
# defined as either permitting the operation you're attempting (`W` or `+`),
# which results in success, or a "deny" (`-`), which results in failure.
# (As before, a fallthrough also results in failure).
#
# - do not use `@all` when your config has any deny rules; it won't work as
# you probably expect it to!
# in the example above, you cannot easily say "anyone can write any tag,
# except version tags can only be written by junio". The following might look
# like it works but it doesn't:
# RW refs/tags/v[0-9] = junio
# RW refs/tags/ = junio linus pasky @others
# if you use "deny" rules, however, you can do this (a "deny" rule just uses
# "-" instead of "R" or "RW" or "RW+" in the permission field)
RW refs/tags/v[0-9] = junio
- refs/tags/v[0-9] = linus pasky @others
RW refs/tags/ = junio linus pasky @others
# FILE/DIR NAME BASED RESTRICTIONS
# --------------------------------
# Here's a hopefully self-explanatory example. Assume the project has the
# following contents at the top level: a README, a "doc/" directory, and an
# "src/" directory.
repo foo
RW+ = lead_dev # rule 1
RW = dev1 dev2 dev3 dev4 # rule 2
RW NAME/ = lead_dev # rule 3
RW NAME/doc/ = dev1 dev2 # rule 4
RW NAME/src/ = dev1 dev2 dev3 dev4 # rule 5
# Notes
# - the "NAME/" is part of the syntax; think of it as a keyword if you like.
# The rest of it is treated as a refex to match against each file being
# touched (see "SPECIFYING AND USING A REFEX" above for details)
# - file/dir NAME-based restrictions are *in addition* to normal (branch-name
# based) restrictions; they are not a *replacement* for them. This is why
# rule #2 (or something like it, maybe with a more specific branch-name) is
# needed; without it, dev1/2/3/4 cannot push any branches.
# - if a repo has *any* NAME/ rules, then NAME-based restrictions are checked
# for *all* users. This is why rule 3 is needed, even though we don't
# actually have any NAME-based restrictions on lead_dev. Notice the pattern
# on rule 3.
# - *each* file touched by the commits being pushed is checked against those
# rules. So, lead_dev can push changes to any files, dev1/2 can push
# changes to files in "doc/" and "src/" (but not the top level README), and
# dev3/4 can only push changes to files in "src/".
# GITWEB AND DAEMON STUFF
# -----------------------
# No specific syntax for gitweb and daemon access; just make the repo readable
# ("R" access) to the special users "gitweb" and "daemon"
# make "@oss_repos" (all 7 of them!) accessible via git daemon
repo @oss_repos
R = daemon
# make the two *large* repos accessible via gitweb
repo linux perl
R = gitweb
# REPO OWNER/DESCRIPTION LINE FOR GITWEB
# syntax, one of:
# reponame = "some description string in double quotes"
# reponame "owner name" = "some description string in double quotes"
# note: setting a description also gives gitweb access; you do not have to
# give gitweb access as described above if you're specifying a description
gitolite "Sitaram Chamarty" = "fast, secure, access control for git in a corporate environment"
# REPO SPECIFIC GITCONFIG
# -----------------------
# update 2010-02-06; this won't work unless the rc file has the right
# settings; please see comments around the variable $GL_GITCONFIG_KEYS in
# conf/example.gitolite.rc for details and security information.
# (Thanks to teemu dot matilainen at iki dot fi)
# this should be specified within a "repo" stanza
# syntax:
# config sectionname.keyname = [optional value_string]
# example usage: if you placed a hook in hooks/common that requires
# configuration information that is specific to each repo, you could do this:
repo gitolite
config hooks.mailinglist = gitolite-commits@example.tld
config hooks.emailprefix = "[gitolite] "
config foo.bar = ""
config foo.baz =
# This does either a plain "git config section.key value" (for the first 3
# examples above) or "git config --unset-all section.key" (for the last
# example). Other forms (--add, the value_regex, etc) are not supported.
# INCLUDE SOME OTHER FILE
# -----------------------
include "foo.conf"
# this includes the contents of $GL_ADMINDIR/conf/foo.conf here
# Notes:
# - the include statement is not allowed inside delegated fragments for
# security reasons.
# - you can also use an absolute path if you like, although in the interests
# of cloning the admin-repo sanely you should avoid doing this!
# EXTERNAL COMMAND HELPERS -- RSYNC
# ---------------------------------
# If $RSYNC_BASE is non-empty, the following config entries come into play
# (otherwise they are ignored):
# a "fake" git repository to collect rsync rules. Gitolite does not
# auto-create any repo whose name starts with EXTCMD/
repo EXTCMD/rsync
# grant permissions to files/dirs within the $RSYNC_BASE tree. A leading
# NAME/ is required as a prefix; the actual path starts after that. Matching
# follows the same rules as given in "FILE/DIR NAME BASED RESTRICTIONS" above
RW NAME/ = sitaram
RW NAME/foo/ = user1
R NAME/bar/ = user2
# just to remind you that these are perl regexes, not shell globs
RW NAME/baz/.*/*.c = user3

201
conf/example.gitolite.rc Normal file
View file

@ -0,0 +1,201 @@
# paths and configuration variables for gitolite
# please read comments before editing
# this file is meant to be pulled into a perl program using "do" or "require".
# You do NOT need to know perl to edit the paths; it should be fairly
# self-explanatory and easy to maintain perl syntax :-)
# --------------------------------------
# this is where the repos go. If you provide a relative path (not starting
# with "/"), it's relative to your $HOME. You may want to put in something
# like "/bigdisk" or whatever if your $HOME is too small for the repos, for
# example
$REPO_BASE="repositories";
# the default umask for repositories is 0077; change this if you run stuff
# like gitweb and find it can't read the repos. Please note the syntax; the
# leading 0 is required
$REPO_UMASK = 0077; # gets you 'rwx------'
# $REPO_UMASK = 0027; # gets you 'rwxr-x---'
# $REPO_UMASK = 0022; # gets you 'rwxr-xr-x'
# part of the setup of gitweb is a variable called $projects_list (please see
# gitweb documentation for more on this). Set this to the same value:
$PROJECTS_LIST = $ENV{HOME} . "/projects.list";
# --------------------------------------
# I see no reason anyone may want to change the gitolite admin directory, but
# feel free to do so. However, please note that it *must* be an *absolute*
# path (i.e., starting with a "/" character)
# gitolite admin directory, files, etc
$GL_ADMINDIR=$ENV{HOME} . "/.gitolite";
# --------------------------------------
# templates for location of the log files and format of their names
# I prefer this template (note the %y and %m placeholders)
# it produces files like `~/.gitolite/logs/gitolite-2009-09.log`
$GL_LOGT="$GL_ADMINDIR/logs/gitolite-%y-%m.log";
# other choices are below, or you can make your own -- but PLEASE MAKE SURE
# the directory exists and is writable; gitolite won't do that for you (unless
# it is the default, which is "$GL_ADMINDIR/logs")
# $GL_LOGT="$GL_ADMINDIR/logs/gitolite-%y-%m-%d.log";
# $GL_LOGT="$GL_ADMINDIR/logs/gitolite-%y.log";
# --------------------------------------
# Please DO NOT change these three paths
$GL_CONF="$GL_ADMINDIR/conf/gitolite.conf";
$GL_KEYDIR="$GL_ADMINDIR/keydir";
$GL_CONF_COMPILED="$GL_ADMINDIR/conf/gitolite.conf-compiled.pm";
# --------------------------------------
# personal branch prefix; leave it as is (empty) if you don't want to use the
# feature (see the "developer-specific branches" section in the "faq, tips,
# etc" document)
$PERSONAL="";
# uncomment one of these if you do want it. If you change it, remember it
# MUST start with "refs/"
# I recommend this:
# $PERSONAL="refs/personal";
# if you want something more visible/noisy, use this:
# $PERSONAL="refs/heads/personal";
# NOTE: whatever value you choose, for security reasons it is better to make
# it fully qualified -- that is, starting with "refs/"
# --------------------------------------
# if git on your server is on a standard path (that is
# ssh git@server git --version
# works), leave this setting as is. Otherwise, choose one of the
# alternatives, or write your own
$GIT_PATH="";
# $GIT_PATH="/opt/bin/";
# --------------------------------------
# if you want to give shell access to any gitolite user(s), name them here.
# Please see doc/6-ssh-troubleshooting.mkd for details on how this works.
# Do not add people to this list indiscriminately. AUDITABILITY OF ACCESS
# CONTROL CHANGES (AND OF REPO ACCESSES) WILL BE COMPROMISED IF ADMINS CAN
# FIDDLE WITH THE ACTUAL (PLAIN TEXT) LOG FILES THAT GITOLITE KEEPS, WHICH
# THEY CAN EASILY DO IF THEY HAVE A SHELL.
# syntax: space separated list of gitolite usernames in *one* string variable.
# $SHELL_USERS = "alice bob";
# --------------------------------------
# ----------------------------------------------------------------------
# SECURITY SENSITIVE SETTINGS
#
# Settings below this point may have security implications. That
# usually means that I have not thought hard enough about all the
# possible ways to crack security if these settings are enabled.
# Please see details on each setting for specifics, if any.
# ----------------------------------------------------------------------
# --------------------------------------
# ALLOW REPO ADMIN TO SET GITCONFIG KEYS
#
# Gitolite allows you to set git repo options using the "config" keyword; see
# conf/example.conf for details and syntax.
#
# However, if you are in an installation where the repo admin does not (and
# should not) have shell access to the server, then allowing him to set
# arbitrary repo config options *may* be a security risk -- some config
# settings may allow executing arbitrary commands.
#
# You have 3 choices. By default $GL_GITCONFIG_KEYS is left empty, which
# completely disables this feature (meaning you cannot set git configs from
# the repo config).
$GL_GITCONFIG_KEYS = "";
#
# The second choice is to give it a space separated list of settings you
# consider safe. (These are actually treated as a set of regular expression
# patterns, and any one of them must match). For example:
# $GL_GITCONFIG_KEYS = "core\.logAllRefUpdates core\..*compression";
# allows repo admins to set one of those 3 config keys (yes, that second
# pattern matches two settings from "man git-config", if you look)
#
# The third choice (which you may have guessed already if you're familiar with
# regular expressions) is to allow anything and everything:
# $GL_GITCONFIG_KEYS = ".*";
# --------------------------------------
# EXTERNAL COMMAND HELPER -- HTPASSWD
# security note: runs an external command (htpasswd) with specific arguments,
# including a user-chosen "password".
# if you want to enable the "htpasswd" command, give this the absolute path to
# whatever file apache (etc) expect to find the passwords in.
$HTPASSWD_FILE = "";
# Look in doc/3 ("easier to link gitweb authorisation with gitolite" section)
# for more details on using this feature.
# --------------------------------------
# EXTERNAL COMMAND HELPER -- RSYNC
# security note: runs an external command (rsync) with specific arguments, all
# presumably filled in correctly by the client-side rsync.
# base path of all the files that are accessible via rsync. Must be an
# absolute path. Leave it undefined or set to the empty string to disable the
# rsync helper.
$RSYNC_BASE = "";
# $RSYNC_BASE = "/home/git/up-down";
# $RSYNC_BASE = "/tmp/up-down";
# --------------------------------------
# ALLOW REPO CONFIG TO USE WILDCARDS
# security note: this used to in a separate "wildrepos" branch. You can
# create repositories based on wild cards, give "ownership" to the specific
# user who created it, allow him/her to hand out R and RW permissions to other
# users to collaborate, etc. This is powerful stuff, and I've made it as
# secure as I can, but it hasn't had the kind of rigorous line-by-line
# analysis that the old "master" branch had.
# This has now been rolled into master, with all the functionality gated by
# this variable. Set this to 1 if you want to enable the wildrepos features.
# Please see doc/4-wildcard-repositories.mkd for details.
$GL_WILDREPOS = 0;
# --------------------------------------
# per perl rules, this should be the last line in such a file:
1;
# Local variables:
# mode: perl
# End:
# vim: set syn=perl:

11
contrib/vim/README.mkd Normal file
View file

@ -0,0 +1,11 @@
# Vim Syntax Highlight
[Vim][] Syntax highlight for `gitolite.conf` can be found from:
- [vim.org script page][vim.org] (Releases)
- [GitHub][] (Sources)
[Vim]: http://www.vim.org/
[vim.org]: http://www.vim.org/scripts/script.php?script_id=2900
[GitHub]: http://github.com/tmatilai/gitolite.vim

View file

@ -1,127 +0,0 @@
#!/usr/bin/perl -w
#
# migrate gitosis.conf to gitolite.conf format
#
# Based on gl-conf-convert by: Sitaram Chamarty
# Rewritten by: Behan Webster <behanw@websterwood.com>
#
use strict;
use warnings;
if (not @ARGV and -t or @ARGV and $ARGV[0] eq '-h') {
print "Usage:\n gl-conf-convert < gitosis.conf > gitolite.conf\n(please see the documentation for details)\n";
exit 1;
}
my @comments = ();
my $groupname;
my %groups;
my $reponame;
my %repos;
while (<>)
{
# not supported
if (/^repositories *=/ or /^map /) {
print STDERR "not supported: $_";
s/^/NOT SUPPORTED: /;
print;
next;
}
# normalise whitespace to help later regexes
chomp;
s/\s+/ /g;
s/ ?= ?/ = /;
s/^ //;
s/ $//;
if (/^\s*$/ and @comments > 1) {
@{$repos{$reponame}{comments}} = @comments if $reponame;
@{$groups{$groupname}{comments}} = @comments if $groupname;
@comments = ();
} elsif (/^\s*#/) {
push @comments, $_;
} elsif (/^\[repo\s+(.*?)\]$/) {
$groupname = '';
$reponame = $1;
$reponame =~ s/\.git$//;
} elsif (/^\[gitosis\]$/) {
$groupname = '';
$reponame = '@all';
} elsif (/^gitweb\s*=\s*yes/i) {
push @{$repos{$reponame}{R}}, 'gitweb';
} elsif (/^daemon\s*=\s*yes/i) {
push @{$repos{$reponame}{R}}, 'daemon';
} elsif (/^description\s*=\s*(.+?)$/) {
$repos{$reponame}{desc} = $1;
} elsif (/^owner\s*=\s*(.+?)$/) {
$repos{$reponame}{owner} = $1;
} elsif (/^\[group\s+(.*)\]$/) {
$reponame = '';
$groupname = $1;
} elsif (/^members\s*=\s*(.*)/) {
push @{$groups{$groupname}{users}}, map {s/\@([^.]+)$/_$1/g; $_} split(' ', $1);
} elsif (/^write?able\s*=\s*(.*)/) {
foreach my $repo (split(' ', $1)) {
$repo =~ s/\.git$//;
push @{$repos{$repo}{RW}}, "\@$groupname";
}
} elsif (/^readonly\s*=\s*(.*)/) {
foreach my $repo (split(' ', $1)) {
$repo =~ s/\.git$//;
push @{$repos{$repo}{R}}, "\@$groupname";
}
}
}
#use Data::Dumper;
#print Dumper(\%repos);
#print Dumper(\%groups);
# Groups
print "#\n# Groups\n#\n\n";
foreach my $grp (sort keys %groups) {
next unless @{$groups{$grp}{users}};
printf join("\n", @{$groups{$grp}{comments}})."\n" if $groups{$grp}{comments};
printf "\@%-19s = %s\n", $grp, join(' ', @{$groups{$grp}{users}});
}
# Gitweb
print "\n#\n# Gitweb\n#\n\n";
foreach my $repo (sort keys %repos) {
if ($repos{$repo}{desc}) {
@{$repos{$repo}{R}} = grep(!/^gitweb$/, @{$repos{$repo}{R}});
print $repo;
print " \"$repos{$repo}{owner}\"" if $repos{$repo}{owner};
print " = \"$repos{$repo}{desc}\"\n";
}
}
# Repos
print "\n#\n# Repos\n#\n";
foreach my $repo (sort keys %repos) {
print "\n";
printf join("\n", @{$repos{$repo}{comments}})."\n" if $repos{$repo}{comments};
#if ($repos{$repo}{desc}) {
# @{$repos{$repo}{R}} = grep(!/^gitweb$/, @{$repos{$repo}{R}});
#}
print "repo\t$repo\n";
foreach my $access (qw(RW+ RW R)) {
next unless $repos{$repo}{$access};
my @keys;
foreach my $key (@{$repos{$repo}{$access}}) {
if ($key =~ /^\@(.*)/) {
next unless defined $groups{$1} and @{$groups{$1}{users}};
}
push @keys, $key;
}
printf "\t$access\t= %s\n", join(' ', @keys) if @keys;
}
#if ($repos{$repo}{desc}) {
# print $repo;
# print " \"$repos{$repo}{owner}\"" if $repos{$repo}{owner};
# print " = \"$repos{$repo}{desc}\"\n";
#}
}

View file

@ -0,0 +1,712 @@
#!/usr/bin/perl -w
# instructions
#
# run it as is, and the program segfaults (Data::Dumper returns undef)
# perl -w $0 | wc
#
# make it not use a sort sub, and program works; prints 18429 bytes to
# STDOUT:
# NO_SORT_SUB=1 perl -w $0 | wc
#
# run the bizarre workaround that slackorama found (issue #15 on gitolite's
# github repo), and the program works; prints 18429 bytes to STDOUT
# WTF=1 perl -w $0 | wc
#
# run it by populating the hash in one shot (not possible in real life), and
# the program works; prints 18429 bytes to STDOUT
# ONE_SHOT_SETUP=1 perl -w $0 | wc
# step 1 -- setup the %repos hash
our %repos = ();
if (exists $ENV{ONE_SHOT_SETUP}) {
# do it in one shot, from a fully populated hash directly coded into the
# program. This is not useful as a workaround for us in real life but it
# may help debugging the actual problem
&one_shot_setup();
} else {
# or do it in the sequence that gitolite "compile" step actually does
&real_life_setup();
}
use Data::Dumper;
$Data::Dumper::Indent = 1;
# sorting is not a problem...
$Data::Dumper::Sortkeys = 1;
# ...but a custom sort sub is! Not calling one helps, and is the official
# workaround for this problem, currently...
$Data::Dumper::Sortkeys = sub { return [ reverse sort keys %{$_[0]} ]; }
unless exists $ENV{NO_SORT_SUB};
# ...or you could use this totally meaningless operation too! The bizarreness
# of this has prompted me to write this test program
if (exists $ENV{WTF}) {
for my $key (sort keys %repos) {
my @wtf = sort keys %{ $repos{$key} };
}
}
my $dumped_data = Data::Dumper->Dump([\%repos], [qw(*repos)]);
print $dumped_data;
print STDERR "dumped " . length($dumped_data) . " bytes\n";
sub one_shot_setup
{
# set up the %repos hash in one shot... in real life we cannot do this of
# course!
%repos = ('testing' => {'guser13' => [{'refs/.*' =>
'RW+'},{'refs/heads/master' => '-'}],'user88' =>
[{'refs/heads/fun/' => 'RW+'},{'refs/.*' => 'R'}],'user1' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser76' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser25' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user4' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW+'}],'guser36' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'guser2' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser14' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser60' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser57' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser31' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser7' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser66' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser75' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser58' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser1' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser73' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser35' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser74' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user5' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW+'}],'guser11' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'guser33' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser53' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser5' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser4' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser85' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser50' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser38' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser59' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser56' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user3' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW'},{'refs/heads/master' =>
'RW+'}],'guser54' => [{'refs/.*' =>
'RW+'},{'refs/heads/master' => '-'}],'guser20' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'guser27' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user9' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser0' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser32' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user8' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser41' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser26' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser18' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser78' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser52' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser43' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser22' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser29' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser64' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser17' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser34' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user2' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW'},{'refs/heads/master' =>
'RW+'}],'guser82' => [{'refs/.*' =>
'RW+'},{'refs/heads/master' => '-'}],'guser86' =>
[{'refs/heads/fun/' => 'RW+'},{'refs/.*' => 'R'}],'guser44' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser62' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser45' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser48' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser37' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user16' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW+'}],'user6' => [{'refs/.*' =>
'RW+'},{'refs/heads/master' => '-'}],'guser83' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'user7' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'user13' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW'}],'guser55' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'user10' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'W' =>
{'guser13' => 1,'user88' => 1,'user1' => 1,'guser76' =>
1,'guser25' => 1,'user4' => 1,'guser36' => 1,'guser2' =>
1,'guser14' => 1,'guser60' => 1,'guser57' => 1,'guser31' =>
1,'guser7' => 1,'guser66' => 1,'guser75' => 1,'guser58' =>
1,'guser1' => 1,'guser73' => 1,'guser35' => 1,'guser74' =>
1,'user5' => 1,'guser11' => 1,'guser33' => 1,'guser53' =>
1,'guser5' => 1,'guser4' => 1,'guser85' => 1,'guser50' =>
1,'guser38' => 1,'guser59' => 1,'guser56' => 1,'user3' =>
1,'guser54' => 1,'guser20' => 1,'guser27' => 1,'user9' =>
1,'guser0' => 1,'guser32' => 1,'user8' => 1,'guser41' =>
1,'guser26' => 1,'guser18' => 1,'guser78' => 1,'guser52' =>
1,'guser43' => 1,'guser22' => 1,'guser29' => 1,'guser64' =>
1,'guser17' => 1,'guser34' => 1,'user2' => 1,'guser82' =>
1,'guser86' => 1,'guser44' => 1,'guser62' => 1,'guser45' =>
1,'guser48' => 1,'guser37' => 1,'user16' => 1,'user6' =>
1,'guser83' => 1,'user7' => 1,'user13' => 1,'guser55' =>
1,'user10' => 1,'guser9' => 1,'guser8' => 1,'guser23' =>
1,'guser51' => 1,'guser68' => 1,'guser6' => 1,'guser69' =>
1,'user12' => 1,'guser21' => 1,'guser87' => 1,'user11' =>
1,'guser77' => 1,'guser63' => 1,'guser39' => 1,'guser79' =>
1,'guser49' => 1,'guser3' => 1,'guser84' => 1,'guser80' =>
1,'guser65' => 1,'guser10' => 1,'guser12' => 1,'guser42' =>
1,'user15' => 1,'guser15' => 1,'guser71' => 1,'@all' =>
1,'guser47' => 1,'guser40' => 1,'guser70' => 1,'guser28' =>
1,'guser67' => 1,'grussell' => 1,'guser19' => 1,'guser61' =>
1,'user14' => 1,'guser16' => 1,'guser81' => 1,'guser72' =>
1,'guser46' => 1,'guser30' => 1,'guser24' => 1},'guser9' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser8' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser23' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser51' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser68' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser6' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser69' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user12' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW'}],'guser21' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'guser87' =>
[{'refs/heads/fun/' => 'RW+'},{'refs/.*' => 'R'}],'user11' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW'}],'guser77' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'guser63' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser39' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser79' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser49' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser3' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser84' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser80' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser65' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser10' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser12' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser42' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user15' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW'}],'guser15' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'guser71' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'@all' =>
[{'refs/.*' => 'RW+'}],'guser47' => [{'refs/.*' =>
'RW+'},{'refs/heads/master' => '-'}],'guser40' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'guser70' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser28' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser67' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'grussell' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW+'}],'guser19' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'guser61' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'user14' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'},{'refs/heads/master' => 'RW'}],'guser16' => [{'refs/.*'
=> 'RW+'},{'refs/heads/master' => '-'}],'guser81' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser72' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'R' =>
{'guser13' => 1,'user88' => 1,'user1' => 1,'guser76' =>
1,'guser25' => 1,'user4' => 1,'guser36' => 1,'guser2' =>
1,'guser14' => 1,'guser60' => 1,'guser57' => 1,'guser31' =>
1,'guser7' => 1,'guser66' => 1,'guser75' => 1,'guser58' =>
1,'guser1' => 1,'guser73' => 1,'guser35' => 1,'guser74' =>
1,'user5' => 1,'guser11' => 1,'guser33' => 1,'guser53' =>
1,'guser5' => 1,'guser4' => 1,'guser85' => 1,'guser50' =>
1,'guser38' => 1,'guser59' => 1,'guser56' => 1,'user3' =>
1,'guser54' => 1,'guser20' => 1,'guser27' => 1,'user9' =>
1,'guser0' => 1,'guser32' => 1,'user8' => 1,'guser41' =>
1,'guser26' => 1,'guser18' => 1,'guser78' => 1,'guser52' =>
1,'guser43' => 1,'guser22' => 1,'guser29' => 1,'guser64' =>
1,'guser17' => 1,'guser34' => 1,'user2' => 1,'guser82' =>
1,'guser86' => 1,'guser44' => 1,'guser62' => 1,'guser45' =>
1,'guser48' => 1,'guser37' => 1,'user16' => 1,'user6' =>
1,'guser83' => 1,'user7' => 1,'user13' => 1,'guser55' =>
1,'user10' => 1,'guser9' => 1,'guser8' => 1,'guser23' =>
1,'guser51' => 1,'guser68' => 1,'guser6' => 1,'guser69' =>
1,'user12' => 1,'guser21' => 1,'guser87' => 1,'user11' =>
1,'guser77' => 1,'guser63' => 1,'guser39' => 1,'guser79' =>
1,'guser49' => 1,'guser3' => 1,'guser84' => 1,'guser80' =>
1,'guser65' => 1,'guser10' => 1,'guser12' => 1,'guser42' =>
1,'user15' => 1,'guser15' => 1,'guser71' => 1,'@all' =>
1,'guser47' => 1,'guser40' => 1,'guser70' => 1,'guser28' =>
1,'guser67' => 1,'grussell' => 1,'guser19' => 1,'guser61' =>
1,'user14' => 1,'guser16' => 1,'guser81' => 1,'guser72' =>
1,'guser46' => 1,'guser30' => 1,'guser24' => 1},'guser46' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser30' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' => '-'}],'guser24' =>
[{'refs/.*' => 'RW+'},{'refs/heads/master' =>
'-'}]},'gitolite-admin' => {'W' => {'sitaram' => 1},'sitaram'
=> [{'refs/.*' => 'RW+'}],'R' => {'sitaram' => 1}});
}
sub real_life_setup {
# set up the %repos hash in a manner that reflects a real run of
# gitolite's "compiler" script:
$repos{'gitolite-admin'}{R}{'sitaram'} = 1;
$repos{'gitolite-admin'}{W}{'sitaram'} = 1;
push @{ $repos{'gitolite-admin'}{'sitaram'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'@all'} = 1;
$repos{'testing'}{W}{'@all'} = 1;
push @{ $repos{'testing'}{'@all'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser86'} = 1;
$repos{'testing'}{W}{'guser86'} = 1;
push @{ $repos{'testing'}{'guser86'} }, { 'refs/heads/fun/' => 'RW+' };
$repos{'testing'}{R}{'guser87'} = 1;
$repos{'testing'}{W}{'guser87'} = 1;
push @{ $repos{'testing'}{'guser87'} }, { 'refs/heads/fun/' => 'RW+' };
$repos{'testing'}{R}{'user88'} = 1;
$repos{'testing'}{W}{'user88'} = 1;
push @{ $repos{'testing'}{'user88'} }, { 'refs/heads/fun/' => 'RW+' };
$repos{'testing'}{R}{'guser86'} = 1;
push @{ $repos{'testing'}{'guser86'} }, { 'refs/.*' => 'R' };
$repos{'testing'}{R}{'guser87'} = 1;
push @{ $repos{'testing'}{'guser87'} }, { 'refs/.*' => 'R' };
$repos{'testing'}{R}{'user88'} = 1;
push @{ $repos{'testing'}{'user88'} }, { 'refs/.*' => 'R' };
$repos{'testing'}{R}{'grussell'} = 1;
$repos{'testing'}{W}{'grussell'} = 1;
push @{ $repos{'testing'}{'grussell'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser0'} = 1;
$repos{'testing'}{W}{'guser0'} = 1;
push @{ $repos{'testing'}{'guser0'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser1'} = 1;
$repos{'testing'}{W}{'guser1'} = 1;
push @{ $repos{'testing'}{'guser1'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser10'} = 1;
$repos{'testing'}{W}{'guser10'} = 1;
push @{ $repos{'testing'}{'guser10'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser11'} = 1;
$repos{'testing'}{W}{'guser11'} = 1;
push @{ $repos{'testing'}{'guser11'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser12'} = 1;
$repos{'testing'}{W}{'guser12'} = 1;
push @{ $repos{'testing'}{'guser12'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser13'} = 1;
$repos{'testing'}{W}{'guser13'} = 1;
push @{ $repos{'testing'}{'guser13'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser14'} = 1;
$repos{'testing'}{W}{'guser14'} = 1;
push @{ $repos{'testing'}{'guser14'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser15'} = 1;
$repos{'testing'}{W}{'guser15'} = 1;
push @{ $repos{'testing'}{'guser15'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser16'} = 1;
$repos{'testing'}{W}{'guser16'} = 1;
push @{ $repos{'testing'}{'guser16'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser17'} = 1;
$repos{'testing'}{W}{'guser17'} = 1;
push @{ $repos{'testing'}{'guser17'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser18'} = 1;
$repos{'testing'}{W}{'guser18'} = 1;
push @{ $repos{'testing'}{'guser18'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser19'} = 1;
$repos{'testing'}{W}{'guser19'} = 1;
push @{ $repos{'testing'}{'guser19'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser2'} = 1;
$repos{'testing'}{W}{'guser2'} = 1;
push @{ $repos{'testing'}{'guser2'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser20'} = 1;
$repos{'testing'}{W}{'guser20'} = 1;
push @{ $repos{'testing'}{'guser20'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser21'} = 1;
$repos{'testing'}{W}{'guser21'} = 1;
push @{ $repos{'testing'}{'guser21'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser22'} = 1;
$repos{'testing'}{W}{'guser22'} = 1;
push @{ $repos{'testing'}{'guser22'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser23'} = 1;
$repos{'testing'}{W}{'guser23'} = 1;
push @{ $repos{'testing'}{'guser23'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser24'} = 1;
$repos{'testing'}{W}{'guser24'} = 1;
push @{ $repos{'testing'}{'guser24'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser25'} = 1;
$repos{'testing'}{W}{'guser25'} = 1;
push @{ $repos{'testing'}{'guser25'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser26'} = 1;
$repos{'testing'}{W}{'guser26'} = 1;
push @{ $repos{'testing'}{'guser26'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser27'} = 1;
$repos{'testing'}{W}{'guser27'} = 1;
push @{ $repos{'testing'}{'guser27'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser28'} = 1;
$repos{'testing'}{W}{'guser28'} = 1;
push @{ $repos{'testing'}{'guser28'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser29'} = 1;
$repos{'testing'}{W}{'guser29'} = 1;
push @{ $repos{'testing'}{'guser29'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser3'} = 1;
$repos{'testing'}{W}{'guser3'} = 1;
push @{ $repos{'testing'}{'guser3'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser30'} = 1;
$repos{'testing'}{W}{'guser30'} = 1;
push @{ $repos{'testing'}{'guser30'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser31'} = 1;
$repos{'testing'}{W}{'guser31'} = 1;
push @{ $repos{'testing'}{'guser31'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser32'} = 1;
$repos{'testing'}{W}{'guser32'} = 1;
push @{ $repos{'testing'}{'guser32'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser33'} = 1;
$repos{'testing'}{W}{'guser33'} = 1;
push @{ $repos{'testing'}{'guser33'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser34'} = 1;
$repos{'testing'}{W}{'guser34'} = 1;
push @{ $repos{'testing'}{'guser34'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser35'} = 1;
$repos{'testing'}{W}{'guser35'} = 1;
push @{ $repos{'testing'}{'guser35'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser36'} = 1;
$repos{'testing'}{W}{'guser36'} = 1;
push @{ $repos{'testing'}{'guser36'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser37'} = 1;
$repos{'testing'}{W}{'guser37'} = 1;
push @{ $repos{'testing'}{'guser37'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser38'} = 1;
$repos{'testing'}{W}{'guser38'} = 1;
push @{ $repos{'testing'}{'guser38'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser39'} = 1;
$repos{'testing'}{W}{'guser39'} = 1;
push @{ $repos{'testing'}{'guser39'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser4'} = 1;
$repos{'testing'}{W}{'guser4'} = 1;
push @{ $repos{'testing'}{'guser4'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser40'} = 1;
$repos{'testing'}{W}{'guser40'} = 1;
push @{ $repos{'testing'}{'guser40'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser41'} = 1;
$repos{'testing'}{W}{'guser41'} = 1;
push @{ $repos{'testing'}{'guser41'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser42'} = 1;
$repos{'testing'}{W}{'guser42'} = 1;
push @{ $repos{'testing'}{'guser42'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser43'} = 1;
$repos{'testing'}{W}{'guser43'} = 1;
push @{ $repos{'testing'}{'guser43'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser44'} = 1;
$repos{'testing'}{W}{'guser44'} = 1;
push @{ $repos{'testing'}{'guser44'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser45'} = 1;
$repos{'testing'}{W}{'guser45'} = 1;
push @{ $repos{'testing'}{'guser45'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser46'} = 1;
$repos{'testing'}{W}{'guser46'} = 1;
push @{ $repos{'testing'}{'guser46'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser47'} = 1;
$repos{'testing'}{W}{'guser47'} = 1;
push @{ $repos{'testing'}{'guser47'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser48'} = 1;
$repos{'testing'}{W}{'guser48'} = 1;
push @{ $repos{'testing'}{'guser48'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser49'} = 1;
$repos{'testing'}{W}{'guser49'} = 1;
push @{ $repos{'testing'}{'guser49'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser5'} = 1;
$repos{'testing'}{W}{'guser5'} = 1;
push @{ $repos{'testing'}{'guser5'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser50'} = 1;
$repos{'testing'}{W}{'guser50'} = 1;
push @{ $repos{'testing'}{'guser50'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser51'} = 1;
$repos{'testing'}{W}{'guser51'} = 1;
push @{ $repos{'testing'}{'guser51'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser52'} = 1;
$repos{'testing'}{W}{'guser52'} = 1;
push @{ $repos{'testing'}{'guser52'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser53'} = 1;
$repos{'testing'}{W}{'guser53'} = 1;
push @{ $repos{'testing'}{'guser53'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser54'} = 1;
$repos{'testing'}{W}{'guser54'} = 1;
push @{ $repos{'testing'}{'guser54'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser55'} = 1;
$repos{'testing'}{W}{'guser55'} = 1;
push @{ $repos{'testing'}{'guser55'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser56'} = 1;
$repos{'testing'}{W}{'guser56'} = 1;
push @{ $repos{'testing'}{'guser56'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser57'} = 1;
$repos{'testing'}{W}{'guser57'} = 1;
push @{ $repos{'testing'}{'guser57'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser58'} = 1;
$repos{'testing'}{W}{'guser58'} = 1;
push @{ $repos{'testing'}{'guser58'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser59'} = 1;
$repos{'testing'}{W}{'guser59'} = 1;
push @{ $repos{'testing'}{'guser59'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser6'} = 1;
$repos{'testing'}{W}{'guser6'} = 1;
push @{ $repos{'testing'}{'guser6'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser60'} = 1;
$repos{'testing'}{W}{'guser60'} = 1;
push @{ $repos{'testing'}{'guser60'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser61'} = 1;
$repos{'testing'}{W}{'guser61'} = 1;
push @{ $repos{'testing'}{'guser61'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser62'} = 1;
$repos{'testing'}{W}{'guser62'} = 1;
push @{ $repos{'testing'}{'guser62'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser63'} = 1;
$repos{'testing'}{W}{'guser63'} = 1;
push @{ $repos{'testing'}{'guser63'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser64'} = 1;
$repos{'testing'}{W}{'guser64'} = 1;
push @{ $repos{'testing'}{'guser64'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser65'} = 1;
$repos{'testing'}{W}{'guser65'} = 1;
push @{ $repos{'testing'}{'guser65'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser66'} = 1;
$repos{'testing'}{W}{'guser66'} = 1;
push @{ $repos{'testing'}{'guser66'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser67'} = 1;
$repos{'testing'}{W}{'guser67'} = 1;
push @{ $repos{'testing'}{'guser67'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser68'} = 1;
$repos{'testing'}{W}{'guser68'} = 1;
push @{ $repos{'testing'}{'guser68'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser69'} = 1;
$repos{'testing'}{W}{'guser69'} = 1;
push @{ $repos{'testing'}{'guser69'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser7'} = 1;
$repos{'testing'}{W}{'guser7'} = 1;
push @{ $repos{'testing'}{'guser7'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser70'} = 1;
$repos{'testing'}{W}{'guser70'} = 1;
push @{ $repos{'testing'}{'guser70'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser71'} = 1;
$repos{'testing'}{W}{'guser71'} = 1;
push @{ $repos{'testing'}{'guser71'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser72'} = 1;
$repos{'testing'}{W}{'guser72'} = 1;
push @{ $repos{'testing'}{'guser72'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser73'} = 1;
$repos{'testing'}{W}{'guser73'} = 1;
push @{ $repos{'testing'}{'guser73'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser74'} = 1;
$repos{'testing'}{W}{'guser74'} = 1;
push @{ $repos{'testing'}{'guser74'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser75'} = 1;
$repos{'testing'}{W}{'guser75'} = 1;
push @{ $repos{'testing'}{'guser75'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser76'} = 1;
$repos{'testing'}{W}{'guser76'} = 1;
push @{ $repos{'testing'}{'guser76'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser77'} = 1;
$repos{'testing'}{W}{'guser77'} = 1;
push @{ $repos{'testing'}{'guser77'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser78'} = 1;
$repos{'testing'}{W}{'guser78'} = 1;
push @{ $repos{'testing'}{'guser78'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser79'} = 1;
$repos{'testing'}{W}{'guser79'} = 1;
push @{ $repos{'testing'}{'guser79'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser8'} = 1;
$repos{'testing'}{W}{'guser8'} = 1;
push @{ $repos{'testing'}{'guser8'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser80'} = 1;
$repos{'testing'}{W}{'guser80'} = 1;
push @{ $repos{'testing'}{'guser80'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser81'} = 1;
$repos{'testing'}{W}{'guser81'} = 1;
push @{ $repos{'testing'}{'guser81'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser82'} = 1;
$repos{'testing'}{W}{'guser82'} = 1;
push @{ $repos{'testing'}{'guser82'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser83'} = 1;
$repos{'testing'}{W}{'guser83'} = 1;
push @{ $repos{'testing'}{'guser83'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser84'} = 1;
$repos{'testing'}{W}{'guser84'} = 1;
push @{ $repos{'testing'}{'guser84'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser85'} = 1;
$repos{'testing'}{W}{'guser85'} = 1;
push @{ $repos{'testing'}{'guser85'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'guser9'} = 1;
$repos{'testing'}{W}{'guser9'} = 1;
push @{ $repos{'testing'}{'guser9'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user1'} = 1;
$repos{'testing'}{W}{'user1'} = 1;
push @{ $repos{'testing'}{'user1'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user10'} = 1;
$repos{'testing'}{W}{'user10'} = 1;
push @{ $repos{'testing'}{'user10'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user11'} = 1;
$repos{'testing'}{W}{'user11'} = 1;
push @{ $repos{'testing'}{'user11'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user12'} = 1;
$repos{'testing'}{W}{'user12'} = 1;
push @{ $repos{'testing'}{'user12'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user13'} = 1;
$repos{'testing'}{W}{'user13'} = 1;
push @{ $repos{'testing'}{'user13'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user14'} = 1;
$repos{'testing'}{W}{'user14'} = 1;
push @{ $repos{'testing'}{'user14'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user15'} = 1;
$repos{'testing'}{W}{'user15'} = 1;
push @{ $repos{'testing'}{'user15'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user16'} = 1;
$repos{'testing'}{W}{'user16'} = 1;
push @{ $repos{'testing'}{'user16'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user2'} = 1;
$repos{'testing'}{W}{'user2'} = 1;
push @{ $repos{'testing'}{'user2'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user3'} = 1;
$repos{'testing'}{W}{'user3'} = 1;
push @{ $repos{'testing'}{'user3'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user4'} = 1;
$repos{'testing'}{W}{'user4'} = 1;
push @{ $repos{'testing'}{'user4'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user5'} = 1;
$repos{'testing'}{W}{'user5'} = 1;
push @{ $repos{'testing'}{'user5'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user6'} = 1;
$repos{'testing'}{W}{'user6'} = 1;
push @{ $repos{'testing'}{'user6'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user7'} = 1;
$repos{'testing'}{W}{'user7'} = 1;
push @{ $repos{'testing'}{'user7'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user8'} = 1;
$repos{'testing'}{W}{'user8'} = 1;
push @{ $repos{'testing'}{'user8'} }, { 'refs/.*' => 'RW+' };
$repos{'testing'}{R}{'user9'} = 1;
$repos{'testing'}{W}{'user9'} = 1;
push @{ $repos{'testing'}{'user9'} }, { 'refs/.*' => 'RW+' };
push @{ $repos{'testing'}{'grussell'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser0'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser1'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser10'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser11'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser12'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser13'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser14'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser15'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser16'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser17'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser18'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser19'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser2'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser20'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser21'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser22'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser23'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser24'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser25'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser26'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser27'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser28'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser29'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser3'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser30'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser31'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser32'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser33'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser34'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser35'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser36'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser37'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser38'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser39'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser4'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser40'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser41'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser42'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser43'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser44'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser45'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser46'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser47'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser48'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser49'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser5'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser50'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser51'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser52'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser53'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser54'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser55'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser56'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser57'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser58'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser59'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser6'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser60'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser61'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser62'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser63'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser64'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser65'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser66'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser67'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser68'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser69'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser7'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser70'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser71'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser72'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser73'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser74'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser75'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser76'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser77'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser78'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser79'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser8'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser80'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser81'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser82'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser83'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser84'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser85'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'guser9'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user1'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user10'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user11'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user12'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user13'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user14'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user15'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user16'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user2'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user3'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user4'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user5'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user6'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user7'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user8'} }, { 'refs/heads/master' => '-' };
push @{ $repos{'testing'}{'user9'} }, { 'refs/heads/master' => '-' };
$repos{'testing'}{R}{'user11'} = 1;
$repos{'testing'}{W}{'user11'} = 1;
push @{ $repos{'testing'}{'user11'} }, { 'refs/heads/master' => 'RW' };
$repos{'testing'}{R}{'user12'} = 1;
$repos{'testing'}{W}{'user12'} = 1;
push @{ $repos{'testing'}{'user12'} }, { 'refs/heads/master' => 'RW' };
$repos{'testing'}{R}{'user13'} = 1;
$repos{'testing'}{W}{'user13'} = 1;
push @{ $repos{'testing'}{'user13'} }, { 'refs/heads/master' => 'RW' };
$repos{'testing'}{R}{'user14'} = 1;
$repos{'testing'}{W}{'user14'} = 1;
push @{ $repos{'testing'}{'user14'} }, { 'refs/heads/master' => 'RW' };
$repos{'testing'}{R}{'user15'} = 1;
$repos{'testing'}{W}{'user15'} = 1;
push @{ $repos{'testing'}{'user15'} }, { 'refs/heads/master' => 'RW' };
$repos{'testing'}{R}{'user2'} = 1;
$repos{'testing'}{W}{'user2'} = 1;
push @{ $repos{'testing'}{'user2'} }, { 'refs/heads/master' => 'RW' };
$repos{'testing'}{R}{'user3'} = 1;
$repos{'testing'}{W}{'user3'} = 1;
push @{ $repos{'testing'}{'user3'} }, { 'refs/heads/master' => 'RW' };
$repos{'testing'}{R}{'grussell'} = 1;
$repos{'testing'}{W}{'grussell'} = 1;
push @{ $repos{'testing'}{'grussell'} }, { 'refs/heads/master' => 'RW+' };
$repos{'testing'}{R}{'user16'} = 1;
$repos{'testing'}{W}{'user16'} = 1;
push @{ $repos{'testing'}{'user16'} }, { 'refs/heads/master' => 'RW+' };
$repos{'testing'}{R}{'user2'} = 1;
$repos{'testing'}{W}{'user2'} = 1;
push @{ $repos{'testing'}{'user2'} }, { 'refs/heads/master' => 'RW+' };
$repos{'testing'}{R}{'user3'} = 1;
$repos{'testing'}{W}{'user3'} = 1;
push @{ $repos{'testing'}{'user3'} }, { 'refs/heads/master' => 'RW+' };
$repos{'testing'}{R}{'user4'} = 1;
$repos{'testing'}{W}{'user4'} = 1;
push @{ $repos{'testing'}{'user4'} }, { 'refs/heads/master' => 'RW+' };
$repos{'testing'}{R}{'user5'} = 1;
$repos{'testing'}{W}{'user5'} = 1;
push @{ $repos{'testing'}{'user5'} }, { 'refs/heads/master' => 'RW+' };
}

335
doc/0-INSTALL.mkd Normal file
View file

@ -0,0 +1,335 @@
# installing gitolite
[Update 2009-11-18: easy install now works from msysgit also!]
Gitolite is somewhat unusual as far as "server" software goes -- every userid
on the system is a potential "gitolite host" and can install his own version
if he chooses to.
This document tells you how to install gitolite. After the install is done,
you may want to see the [admin document][admin] for adding users, repos, etc.
**Please note** that gitolite depends heavily on proper ssh setup and pubkey
based access. Sadly, most people don't know ssh as well as they think they
do. To make matters worse, ssh problems in gitolite don't always look like
ssh problems. Please read about [ssh troubleshooting][doc6] if you have *any*
kind of trouble installing gitolite!
[admin]: http://github.com/sitaramc/gitolite/blob/pu/doc/2-admin.mkd
[doc6]: http://github.com/sitaramc/gitolite/blob/pu/doc/6-ssh-troubleshooting.mkd
In this document:
* install methods
* user install
* typical example run
* advantages over the older install methods
* disadvantages
* upgrades
* other notes
* system install / user setup
* next steps
* appendix A: server and client requirements for user install
* server
* install workstation
* admin workstation(s)
* appendix B: uninstalling gitolite
* appendix C: NOTE TO PACKAGE MAINTAINERS
----
### install methods
There are 2 ways to install gitolite: The **user-install** mode was the
traditional way, and is used when *any* of the following is true:
* you don't have root on your "server" (some types of hosting setups, many
corporate paranoia setups ;-)
* your server distro does not have gitolite in its package repositories
* your server distro's package repositories have an old version of gitolite
* you want to stay current with the latest gitolite versions
* your server is not Linux (maybe AIX, or Solaris, etc.)
The "user install" section describes this method.
The **system-install followed by user-setup** mode is used when you (or
someone who has root) has installed an RPM or DEB of gitolite and you intend
to use that version.
The "system install / user setup" section describes this method.
----
### user install
There is an easy install script that makes installing very easy for the common
case. **This script will setup everything on the server, but you have to run
it on your workstation, NOT on the server!**
Assumptions/pre-requisites:
* you have a server to host gitolite
* git is installed on that server (and so is perl)
* you have a userid on that server
* you have ssh-pubkey (**password-less**) login to that userid
* if you have only password access, run `ssh-keygen -t rsa` to create a
new keypair if needed, then run `ssh-copy-id user@host`. If you do
not have `ssh-copy-id`, read doc/3-faq-tips-etc.mkd and look for
`ssh-copy-id` in that file for instructions
* you have a clone or an archive of gitolite somewhere on your workstation
* if you don't have one, just run `git clone git://github.com/sitaramc/gitolite`
* your workstation has bash (even msysgit bash will do)
Once you have all this, just `cd` to that clone and run `src/gl-easy-install`
and follow the prompts! (Running it without any arguments shows you usage
plus other useful info).
#### typical example run
A typical run for me is:
src/gl-easy-install -q git my.git.server sitaram
`-q` stands for "quiet" mode -- very minimal output, no verbose descriptions
of what it is going to do, and no pauses unless absolutely needed. However,
if you're doing this for the first time or you appreciate knowing what it is
actually doing, I suggest you skip the `-q`.
#### advantages over the older install methods
* all ssh problems reduced to **just one pre-requisite**: enable ssh pubkey
(password-less) access to the server from your workstation first
* the script takes care of all the server side work
* when done:
* you get two different pubkeys (the original one for command line
access as before, plus a new one, created by the script, for gitolite
access)
* you can admin gitolite by commit+push a "gitolite-admin" repo, just
like gitosis (i.e., full "push to admin" power!)
#### disadvantages
* need a recent bash
#### upgrades
Upgrading gitolite is easy.
To upgrade, pull the latest "master" (or other) branch in your gitolite repo
clone, then run the same exact command you ran to do the install, except you
can leave out the last argument.
And you might want to add a `-q` to speed things up :-)
Note that this only upgrades the software. Unlike earlier versions, it does
**not** touch the `conf/gitolite.conf` file or the contents of `keydir` in any
way. I decided that it is not possible to **safely** let an upgrade do
something meaningful with them -- fiddling with existing config files (as
opposed to merely creating one which did not exist) is best left to a human.
#### other notes
* if you run `src/gl-easy-install` without the `-q` option, you will be
given a chance to edit `~/.gitolite.rc`. You can change any options (such
as paths, for instance), but be sure to keep the perl syntax -- you
*don't* have to know perl to do so, it's fairly easy to guess in this
limited case.
### system install / user setup
In this mode a system administrator installs gitolite using the server's
distro package mechanism (yum install, apt-get install, etc).
Once this is done, you as a user must run a command like this (unlike in the
"user install" mode, this is done directly on the server):
gl-setup yourname.pub
where yourname.pub is a copy of a public key from your workstation. The first
time you run this, it will create a "gitolite-admin" repo and populate it with
the right configuration for whoever has the corresponding private key to
clone and push it. In other words, that person is the administrator for this
particular gitolite instance.
If your system administrator upgrades gitolite itself, things will usually
just work without any change; you should not have to do anything special.
However, some new features may require additional settings in your
`~/.gitolite.rc` file.
Finally, in the rare case that you managed to lose your keys to the admin repo
and want to supply a new pubkey, you can use this command to replace any such
key. Could be useful in an emergency -- just get your new "yourname.pub" to
the server and run the same command as above.
**IMPORTANT**: there are two variables in the `~/.gitolite.rc` file:
`$GL_PACKAGE_CONF` and `$GL_PACKAGE_HOOKS`. If you remove or change either of
them, expect trouble :-)
### next steps
The last message produced by the easy install script should tell you how to
add users, repos, etc., and you will find more details in the [admin][admin]
document.
<a name="server_reqs"></a>
### appendix A: server and client requirements for user install
There are 3 machines *potentially* involved in installing and administering
gitolite.
#### server
This is where gitolite is eventually installed. You need a *normal* userid
(typically "git" but can be anything) on this machine; root access is *not*
needed, but it has to be some sort of Unix (not Windows).
You need the following software on it:
* git
* can be in a non-PATH location if you are unable to install it
normally; see the `$GIT_PATH` variable in the "rc" file
* perl
* default install is fine; no special modules are needed
* (a normal install of git also requires/installs perl, so you probably
have it already)
* openssh server
* (I guess any ssh server that can understand the `authorized_keys` file
format should work)
#### install workstation
Installing or upgrading the gitolite software itself is best done by running
the easy-install program from a gitolite clone.
This script is heavily dependent on bash, so you need a machine with a bash
shell. Even the bash that comes with msysgit is fine, if you don't have a
Linux box handy.
If you have neither Linux nor Windows+msysgit, you still have a few
alternatives:
* use a different userid on the same server (assuming it has bash)
* use the same userid on the same server (same assumption)
* manually simulate the script directly on the server (doable, but tedious)
#### admin workstation(s)
When you install gitolite, it creates a repository called "gitolite-admin" and
gives you permissions on it.
Administering gitolite (adding repos/users, assigning permissions, etc) is
done by cloning this repo, making changes to a file called
`conf/gitolite.conf`, adding users' pubkeys to `keydir/`, and pushing the
changes back to the server.
Which means all this can be done from *any* machine. You'll normally do it
from the same machine you used to install gitolite, but it doesn't have to be
the same one, as long as your pubkey has been added and permissions given to
allow you to push to the gitolite-admin repo.
<a name="uninstall"></a>
### appendix B: uninstalling gitolite
Sometimes you might find gitolite is overkill -- you have only one user
(yourself) pushing maybe. Or maybe gitolite is just not enough -- you want a
web-based front end that users can use to manage their keys themselves, etc.,
in which case you'd probably switch to [github][g1], [girocco][g2],
[indefero][g3] or [gitorious][g4]. Either way, you'd like to uninstall
gitolite.
[g1]: http://github.com
[g2]: http://repo.or.cz/w/girocco.git
[g3]: http://www.indefero.net/
[g4]: http://gitorious.com/
Uninstalling gitolite is fairly easy. Just log on to the server and do the
following (assuming `$REPO_BASE` in the rc file was left at its default of
`~/repositories`; if not, adjust accordingly):
* edit `~/.ssh/authorized_keys` and delete the `# gitolite start` and `#
gitolite end` markers and all the lines between them. This will prevent
any of your users from attempting a push while you are doing this.
If you are the only user, and/or *need* one or more of those keys to
continue to access this account (like if one of them is your laptop or
your home desktop etc.) then instead of deleting the line you can just
delete everything upto but not including the words "ssh-rsa" or "ssh-dss".
* Now remove (or move aside or rename to something else if you're paranoid)
the following files and directories.
~/.gitolite
~/.gitolite.rc
~/repositories/gitolite-admin.git
* Then remove all the `update` hooks that git installs on each repository.
The easiest way is:
find ~/repositories -wholename "*.git/hooks/update" | xargs rm -f
but you can do it manually if you want to be careful.
* Finally, any remote users that still have access must update their clone's
remote URLs (edit `.git/config` in the repo) to prefix `repositories/`
before the actual path used, in order for the remote to still work. This
is because you'll now be accessing it through plain ssh, which means you
have to give it the full path.
### appendix C: NOTE TO PACKAGE MAINTAINERS
Here's how you'd package gitolite. In the following description, location "X"
can be, say, `/usr/share/gitolite/conf` or some such, and similarly location
"Y" can be perhaps `/usr/share/gitolite/hooks`. It's upto your distro
policies where they are.
**Step 1**: Clone the gitolite repo and run the make command inside the clone
git clone git://github.com/sitaramc/gitolite.git
cd gitolite
make pu.tar # or "make master.tar" or "make v1.2.tar" etc
Then you explode the tar file in some temporary location.
*Alternatively, you can `git checkout` the tag or branch you want, and run
this command in the clone directly*:
git describe --tags --long > conf/VERSION
**Step 2**: Now make the following changes (no trailing slashes in the
location values please):
* `src/gl-setup` should have the following line:
GL_PACKAGE_CONF="X"
* `conf/example.gitolite.rc` should have the following lines:
$GL_PACKAGE_CONF="X";
$GL_PACKAGE_HOOKS="Y";
* delete `src/gl-easy-install`; that script is meant for a totally different
mode of installation and does *not* play well in this mode :-)
**Step 3**: Move (or arrange to move) the files to their proper locations as
given below:
* everything in "src" goes somewhere on the PATH
* everything in "conf" goes to location "X"
* everything in "hooks" goes to location "Y"
**Step 4**: There is no step 4. Unless you count telling your users to run
`gl-setup` as a step :)
On the initial install (urpmi, yum install, or apt-get install), you could
also choose to setup a userid called "gitolite", and run "gl-setup" as that
user; however I do not know how you would come up with the initial pubkey that
is needed. Anyway, the point is that the "gitolite" user is no more special
than any other in terms of hosting gitolite. Any user can host gitolite on
his userid by just running "gl-setup".
When you upgrade, just overwrite all the files; it'll all just work. In fact,
other than the initial "gl-setup" run, the only time a gitolite hosting user
has to actually do anything is to edit their own `~/.gitolite.rc` file if they
want to enable or disable specific features.

90
doc/1-migrate.mkd Normal file
View file

@ -0,0 +1,90 @@
# migrating from gitosis to gitolite
[TODO: make the migration tool fix up gitweb and daemon control also...]
Migrating from gitosis to gitolite is pretty easy, because the basic design is
the same.
Here's how we migrated my work repos:
1. login as the `git` user on the server, and get a bash shell prompt
2. **disable gitosis** by renaming `/usr/bin/gitosis-serve` to something
else. This will prevent users from pushing anything while you do the
backup, migration, etc.
3. **edit** `~/.ssh/authorized_keys` and **carefully** remove all the lines
containing "gitosis-serve", as well as the marker line that says
"auto-generated by gitosis, DO NOT REMOVE", then save the file. If the
file did not have any other keys and is now empty, don't worry -- save it
anyway because gitolite expects the file to be present (even if it is
empty).
4. For added safety, **delete** the post-update hook that gitosis-admin
installed
rm ~/repositories/gitosis-admin.git/hooks/post-update
or at least rename it to `.sample` like all the other hooks hanging
around, or edit it and comment out the line that calls `gitosis-run-hook
post-update`.
If you do not do this, an accidental push to the gitosis-admin repo will
mess up your `~/.ssh/authorized_keys` file
5. take a **backup** of the `~/repositories` directory
Now, log off the server and get back to the client:
[inst]: http://github.com/sitaramc/gitolite/blob/pu/doc/0-INSTALL.mkd
1. follow instructions to install gitolite; see the [install document][inst].
Make sure that you **don't** change the default path for `$REPO_BASE` if
you edit the config file!
This will give you a gitolite config that has the required entries for the
"gitolite-admin" repo.
2. **convert** your gitosis config file and append it to your gitolite config
file. Substitute the path for your gitosis-admin clone in `$GSAC` below,
and similarly the path for your gito**lite**-admin clone in `$GLAC`
src/gl-conf-convert < $GSAC/gitosis.conf >> $GLAC/gitolite.conf
Be sure to check the file to make sure it converted correctly
3. **copy** the keys from gitosis's keydir (same meanings for GSAC and GLAC)
cp $GSAC/keydir/* $GLAC/keydir
4. **Important: expand any multi-key files you may have**. [Here][mk]'s an
explanation of what multi-keys are, how gitosis does them and how gitolite
does it differently.
You can split the keys manually, or use the following code (just
copy-paste it into your xterm after "cd"-ing to your gitolite-admin repo
clone):
wc -l keydir/*.pub | grep -v total | grep -v -w 1 | while read a b
do
i=1
cat $b|while read l
do
echo "$l" > ${b%.pub}@$i.pub
(( i++ ))
done
mv $b $b.done
done
This will split each multi-key file (say "sitaram.pub") into individual
files called "sitaram@1.pub", "sitaram@2.pub", etc., and rename the
original to "sitaram.pub.done" so gitolite won't pick it up.
At this point you can rename the split parts more appropriately, like
"sitaram@laptop.pub" and "sitaram@desktop.pub" or whatever. *Please check
the files to make sure this worked properly*
5. Check all your changes to your gitolite-admin clone, commit, and push
[mk]: http://github.com/sitaramc/gitolite/blob/pu/doc/3-faq-tips-etc.mkd#multikeys

135
doc/2-admin.mkd Normal file
View file

@ -0,0 +1,135 @@
# administering and running gitolite
*Note*: some of the paths in this document use variable names. Just refer to
`~/.gitolite.rc` for the correct values for *your* installation.
In this document:
* administer
* adding users and repos
* moving pre-existing repos into gitolite
* specifying gitweb and daemon access
* custom hooks
* custom git config
### administer
First of all, ***do NOT add new repos manually***, unless you know how to add
the required hook as well. Without the hook, branch-level access control will
not work for that repo, which sorta defeats the idea of using gitolite :-)
Please read on to see how to do this correctly.
#### adding users and repos
* ask each user who will get access to send you a public key. See other
sources (for example [here][genpub]) for how to do this
[genpub]: http://sitaramc.github.com/0-installing/2-access-gitolite.html#generating_a_public_key
* rename each public key according to the user's name, with a `.pub`
extension, like `sitaram.pub` or `john-smith.pub`. You can also use
periods and underscores
* copy all these `*.pub` files to `keydir` in your gitolite-admin repo clone
* edit the config file (`conf/gitolite.conf` in your admin repo clone). See
`conf/example.conf` in the gitolite source for details on what goes in
that file, syntax, etc. Just add new repos as needed, and add new users
and give them permissions as required. The users names should be exactly
the same as their keyfile names, but without the `.pub` extension
* when done, commit your changes and push
#### moving pre-existing repos into gitolite
One simple way to add a pre-existing repo to gitolite is to let gitolite
create it as a brand new repo as in the previous section, and then, from an
existing clone, "push --all" to the new one.
However, if you have many existing repos to add, this can be time-consuming
and error-prone. Here's how to take a bunch of existing repos and add them to
gitolite:
* make sure they're *bare* repos ;-)
* log on to the server and copy the repos to `$REPO_BASE` (which defaults to
`~/repositories`), making sure that the directory names end in ".git".
* back on your workstation, add each repo (without the `.git` suffix) to
`conf/gitolite.conf` in your gitolite-admin repo clone. Then add, commit,
push.
#### specifying gitweb and daemon access
This is a feature that I personally do not use (corporate environments don't
like unauthenticated access of any kind to any repo!), but someone wanted it,
so here goes.
To make a repo or repo group accessible via "git daemon", just give read
permission to the special user "daemon". See the [faq, tips, etc][ss]
document for easy ways to specify access for multiple repositories.
[ss]: http://github.com/sitaramc/gitolite/blob/pu/doc/3-faq-tips-etc.mkd#gwd
There's a special user called "gitweb" also, which works the same way.
However, setting a description for the project also enables gitweb permissions
so you may as well use that method and kill two birds with one stone, like so:
gitolite = "fast, secure, access control for git in a corporate environment"
You can also specify an owner for gitweb to show, if you like:
gitolite "Sitaram Chamarty" = "fast, secure, access control for git in a corporate environment"
Note that gitolite does **not** install or configure gitweb/daemon -- that is
a one-time setup you must do separately. All this does is:
* for daemon, create the file `git-daemon-export-ok` in the repository
* for gitweb, add the repo (plus owner name, if given) to the list of
projects to be served by gitweb (see the config file variable
`$PROJECTS_LIST`, which should have the same value you specified for
`$projects_list` when setting up gitweb)
* put the description, if given, in `$repo/description`
The "compile" script will keep these files consistent with the config settings
-- this includes removing such settings/files if you remove "read" permissions
for the special usernames or remove the description line.
#### custom hooks
If you want to put in your own, custom, hooks every time a new repo is created
by gitolite, put a **tested** hook script in `hooks/common` of your gitolite
clone before running easy-install. As distributed, there are only two files
there, but everything (*everything*) in that directory will get copied to the
`hooks/` subdirectory of every *new* repo created.
In order to push a new or updated hook script to *existing* repos as well,
just run easy install once again; it'll do it to existing repos also.
**VERY IMPORTANT SECURITY NOTE: the `update` hook in `hooks/common` is what
implements all the branch-level permissions in gitolite. If you fiddle with
the hooks directory, please make sure you do not mess with this file
accidentally, or all your fancy per-branch permissions will stop working.**
#### custom git config
The custom hooks feature is a blunt instrument -- all repos get the hook you
specified and will run it. In order to make it a little more fine-grained,
you could set your hooks to only work if a certain "gitconfig" variable was
set. Which means we now need a way to specify "git config" settings on a per
repository basis.
Thanks to Teemu (teemu dot matilainen at iki dot fi), gitolite now does this
very easily. For security reasons, this can only be done from the master
config file (i.e., if you're using delegation, the delegated admins cannot
specify git config settings).
Please see `conf/example.conf` for syntax. Note that this only supports the
basic forms of the "git config" command:
git config section.key value # value may be an empty string
git config --unset-all section.key
It does not (currently) support other options like `--add`, the `value_regex`,
etc.

686
doc/3-faq-tips-etc.mkd Normal file
View file

@ -0,0 +1,686 @@
# assorted faqs, tips, and notes on gitolite
In this document:
* common errors and mistakes
* git version dependency
* other errors, warnings, notes...
* ssh-copy-id
* cloning an empty repo
* `@all` syntax for repos
* umask setting
* getting a tar file from a clone
* features
* syntax and normal usage
* simpler syntax
* one user, many keys
* security, access control, and auditing
* two levels of access rights checking
* better logging
* "exclude" (or "deny") rules
* file/dir NAME based restrictions
* delegating parts of the config file
* convenience features
* what repos do I have access to?
* error checking the config file
* including config lines from other files
* support for git installed outside default PATH
* "personal" branches
* custom hooks and custom git config
* helping with gitweb
* easier to specify gitweb "description" and gitweb/daemon access
* easier to link gitweb authorisation with gitolite
* advanced features
* repos named with wildcards
* access control for external commands
* design choices
* keeping the parser and the access control separate
## common errors and mistakes
* adding `repositories/` at the start of the repo name in the `git clone`.
This error is typically made by the *admin* himself -- because he knows
what `$REPO_BASE` is set to and thinks he has to provide that prefix on
the client side also :-) In fact gitolite prepends `$REPO_BASE`
internally, so you shouldn't also do the same thing!
* being able to clone but getting errors on push. Most likely caused by a
combination of:
* you already have shell access to the server, not just "gitolite"
access, *and*
* you cloned using `git clone git@server:repositories/repo.git` (notice
there's an extra "repositories/" in there?)
In other words, you used a key that completely bypassed gitolite and went
straight to the shell to do the clone.
Please see doc/6-ssh-troubleshooting.mkd for what all this means.
## git version dependency
Here's a workaround for a version dependency that the normal flow of gitolite
has.
When you edit your config file to create a new repo, and push the changes to
the server, gitolite creates an empty, bare repo for you. Normally, you're
expected to clone this on the client side, and start working -- make your
first commit(s), then push, etc.
However, cloning an empty repo requires a server side git version that is at
least 1.6.2. Gitolite detects this when creating a repo, and warns you.
The workaround is to use the older (gitosis-style) method on the client:
create an empty repo locally, make a commit or two, set an "origin" remote,
and then push. Something like:
mkdir my-new-project
cd my-new-project
git init
git commit --allow-empty -m 'Initial repository'
# or, if your client side git is too old for --allow-empty, just make some
# files, "git add" them, then "git commit"
git remote add origin git@gitolite-server:my-new-project.git
git push origin master:master
Once this is done, the repo is available for cloning by anyone else in the
normal way, since it's not empty anymore.
## other errors, warnings, notes...
### ssh-copy-id
don't have `ssh-copy-id`? This is broadly what that command does, if you want
to replicate it manually. The input is your pubkey, typically
`~/.ssh/id_rsa.pub` from your client/workstation.
* it copies it to the server as some file
* it appends that file to `~/.ssh/authorized_keys` on the server
(creating it if it doesn't already exist)
* it then makes sure that all these files/directories have go-w perms
set (assuming user is "git"):
/home/git/.ssh/authorized_keys
/home/git/.ssh
/home/git
[Actually, `sshd` requires that even directories *above* `~` (`/`, `/home`,
typically) also must be `go-w`, but that needs root. And typically
they're already set that way anyway. (Or if they're not, you've got
bigger problems than gitolite install not working!)]
### cloning an empty repo
Cloning an empty repo is only possible with clients greater than 1.6.2. So at
least one of your clients needs to have a recent git. Once at least one
commit has been made, older clients can also use it
When you clone an empty repo, git seems to complain about `fatal: The remote
end hung up unexpectedly`. However, you can ignore this, since it doesn't
seem to hurt anything. [Update 2009-09-14; this has been fixed in git
1.6.4.3]
### `@all` syntax for repos
There *is* a way to use the `@all` syntax for repos also, as described in
`conf/example.conf`. However, there is an important difference between this
and the old `@all` (for users):
* `@all` for repos is immediately expanded, when found, into the currently
known list of repos. "Currently" means upto this point in the config
file, and "known" means having some user with some permissions associated
with the repo!
* This means that if you really want *all* repos, you'd better put this para
at the **end** of the config file!
### umask setting
Gitweb not able to read your repos? You can change the umask for newly
created repos to something more relaxed -- see the `~/.gitolite.rc` file
## getting a tar file from a clone
You can clone the repo from github or indefero, then execute a make command to
extract a tar file of the branch you want. Please use the make command, not a
plain "git archive", because the Makefile adds a file called
`.GITOLITE-VERSION` that will help you identify which version you are using.
git clone git://github.com/sitaramc/gitolite.git
# (OR)
git clone git://sitaramc.indefero.net/sitaramc/gitolite.git
cd gitolite
make master.tar
# or maybe "make pu.tar"
<a name="features"></a>
## features
Apart from the big ones listed in the top level README, and subjective ones
like "better config file format", gitolite has evolved to have many useful
fearures than the original goal of "gitosis + branch-level access control".
### syntax and normal usage
<a name="simpler_syntax"></a>
#### simpler syntax
The basic syntax is simpler and cleaner but it goes beyond that: **you can
specify access in bits and pieces**, even if they overlap.
Some access needs are best grouped by repo, some by username, and some by
both. So just do all of them, and gitolite will combine all the access lists!
Here's an example:
# define groups of people
@bosses = phb1 phb2 phb3
@devs = dev1 dev2 dev3
@interns = int1 int2 int3
# define groups of projects
@open = git gitolite linux rakudo
@closed = c1 c2 c3
@topsecret = ts1 ts2 ts3
# all bosses have read access to all projects
repo @open @closed @topsecret
R = @bosses
# everyone has read access to "open" projects
repo @open
R = @bosses @devs @interns
[...or any other combination you want...]
# later in the file:
# specify access for individual repos (like RW, RW+, etc)
repo c1
[...]
[...etc...]
If you notice that `@bosses` are given read access to `@open` via both rules,
do not worry that this causes some duplication or inefficiency. It doesn't
:-)
See the "specify gitweb/daemon access" section below for one more example.
<a name="multikeys"></a>
#### one user, many keys
I have a laptop and a desktop I need to access the server from. I have
different private keys on them, but as far as gitolite is concerned both of
them should be treated as "sitaram". How does this work?
In gitosis, the admin creates a single "sitaram.pub" containing one line for
each of my pubkeys. In gitolite, we keep them separate: "sitaram@laptop.pub"
and "sitaram@desktop.pub". The part before the "@" is the username, so
gitolite knows these two keys belong to the same person.
Note that you don't say "sitaram@laptop" and so on in the **config** file --
as far as the config file is concerned there's just **one** user called
"sitaram" -- so you only say "sitaram" there.
I think this is easier to maintain if you have to delete or change one of
those keys.
However, now that `sitaramc@gmail.com` is also a valid username, we need to
distinguish between `sitaramc@gmail.com.pub` and `sitaramc@desktop.pub`. We
do that by requiring that the multi-key suffix you use (like "desktop" and
"laptop") should not have a `"."` in it. If it does, it looks like an email
address. The following table lists sample pubkey filenames and the
corresponding derived usernames (which is what goes into the
`conf/gitolite.conf` file):
* old style multikeys; not mistaken for emails because there is no "." in
hostname part
sitaramc.pub sitaramc
sitaramc@laptop.pub sitaramc
sitaramc@desktop.pub sitaramc
* new style, email keys; there is a "." in hostname part; so it's an email
address
sitaramc@gmail.com.pub sitaramc@gmail.com
* multikeys *with* email address
sitaramc@gmail.com@laptop.pub sitaramc@gmail.com
sitaramc@gmail.com@desktop.pub sitaramc@gmail.com
### security, access control, and auditing
<a name="two_levels"></a>
#### two levels of access rights checking
Gitolite has two levels of access checks. The **first check** is what I will
call the **pre-git** level (this is the only check that gitosis has). At this
stage, the `gl-auth-command` has been invoked by `sshd`, and it knows just
three things:
* who,
* what repository, and
* what type of access (R or W)
Note that at this point no git program has entered the picture, and we have no
way of knowing what **ref** (branch, tag, etc) he is trying to update, even if
it is a "write" operation.
For a "read" operation to pass this check, the username (or `@all`) must have
read permission (i.e., R, RW, or RW+) on at least one branch of the repo.
For a "write" operation, there is an additional restriction: lines specifying
only `R` (read access) don't count. *The user must have write access to
**some** ref in the repo in order to pass this stage!*
The **second check** is via a git `update hook`. This check only happens for
write operations. By this time we know what "ref" he is trying to update, as
well as the old and the new SHAs of that ref (by which we can also deduce
whether it's a rewind or not). This is where the "per-branch" permissions
come into play.
Each refex that allows `W` access (or `+` if this is a rewind) for *this*
user, on *this* repo, is matched against the actual refname being updated. If
any of the refexes match, the push succeeds. If none of them match, it fails.
Gitolite also allows "exclude" or "deny" rules. See later in this document
for details.
#### better logging
If you have been too liberal with the permission to rewind, it has built-in
logging as an emergency fallback if someone goes too far, or for audit
purposes [`*`]. The logfile names and location are configurable, and can
include the year/month/day etc in the filename for easy archival or further
processing. The log file even tells you which pattern in the config file
matched to allow that specific access to proceed.
> [`*`] setting `core.logAllRefUpdates true` does provide a safety net
> against over-zealous rewinds, but it does not tell you "who". And
> strangely, management does not seem to share the view that "blame" is just
> a synonym for "annotate" ;-)]
The log lines look like this:
2009-09-19.10:24:37 + b4e76569659939 4fb16f2a88d8b5 myrepo refs/heads/master user2 refs/heads/master
The "+" at the start indicates a non-fast forward update, in this case from
b4e76569659939 to 4fb16f2a88d8b5. So b4e76569659939 is the one to restore!
Can it get easier?
The other parts of the log line are the name of the repo, the refname being
updated, the user updating it, and the refex pattern (from the config file)
that matched, in case you need to debug the config file itself.
#### "exclude" (or "deny") rules
Here is an illustrative explanation of "deny" rules. However, please be sure
to read the "DENY/EXCLUDE RULES" section in `conf/example.conf` for important
notes/caveats before using "deny" rules.
Take a look at the following snippet, which *seems* to say that "bruce" can
write versioned tags (anything containing `refs/tags/v[0-9]`), but the other
staffers can't:
@staff = bruce whitfield martin
[... and later ...]
RW refs/tags/v[0-9] = bruce
RW refs/tags = @staff
But that's not how the matching works. As long as any refex matches the
refname being updated, it's a "yes". Since the second refex (which says
"anything containing `refs/tags`") is a superset of the first one, it lets
anyone on `@staff` create versioned tags, not just Bruce.
One way to fix this is to allow "excludes" -- some changes in syntax, combined
with a rigorous, ordered, interpretation would do it.
Let's recap the **existing semantics**:
> the first matching refex that has the permission you're looking for (`W`
> or `+`), results in success. A fallthrough results in failure
Here are the **new semantics**, with changes from the "main" one in bold:
> the first matching refex that has the permission you're looking for (`W`
> or `+`) **or a minus (`-`)**, results in success **or failure,
> respectively**. A fallthrough **also** results in failure
So the example we started with becomes, if you use "deny" rules:
RW refs/tags/v[0-9] = bruce
- refs/tags/v[0-9] = @staff
RW refs/tags = @staff
And here's how it works:
* for non-version tags, only the 3rd rule matches, so anyone on staff can
push them
* for version tags by bruce, the first rule matches so he can push them
* for version tags by staffers *other than bruce*, the second rule matches
before the third one, and it has a `-` as the permission, so the push
fails
#### file/dir NAME based restrictions
In addition to branch-name based restrictions, gitolite also allows you to
restrict what files or directories can be involved in changes being pushed.
This basically uses `git diff --name-only` to obtain the list of files being
changed, treating each filename as a "ref" to be matched.
Please see `conf/example.conf` for syntax and examples.
#### delegating parts of the config file
You can now split up the config file and delegate the authority to specify
access control for their own pieces. See
[doc/5-delegation.mkd](http://github.com/sitaramc/gitolite/blob/pu/doc/5-delegation.mkd)
for details.
### convenience features
<a name="myrights"></a>
#### what repos do I have access to?
Sometimes there are too many repos, maybe even named similarly, or with the
potential for typos, confusion about hyphens/underscores or upper/lower case,
etc. You'd just like a simple way to know what repos you have access to.
Easy! Just use ssh and try to log in as if you were attempting to get a
shell:
$ ssh gitolite info
PTY allocation request failed on channel 0
hello sitaram, the gitolite version here is v0.6-17-g94ed189
you have the following permissions:
R W Anu-WSD
R ROtest
R W SecureBrowse
R W entrans
R W git-notes
R W gitolite
R W gitolite-admin
R W indic_web_input
R W proxy
@ @ testing
R W vkc
Note that until this version, we used to put out an ugly `need
SSH_ORIGINAL_COMMAND` error, just like gitosis used to. All we did is put
that code path to better use :-)
#### error checking the config file
gitosis does not do any. I just found out that if you mis-spell `members` as
`member`, gitosis will silently ignore it, and leave you wondering why access
was denied.
Gitolite "compiles" the config file first and keyword typos *are* caught so
you know right away.
#### including config lines from other files
See the entry under "INCLUDE SOME OTHER FILE" in `conf/example.conf`.
#### support for git installed outside default PATH
The normal solution is to add to the system default PATH somehow, either by
munging `/etc/profile` or by enabling `PermitUserEnvironment` in
`/etc/ssh/sshd_config` and then setting the PATH in `~/.ssh/.environment`.
All these are security risks because they allow a lot more than just you and
your git install :-)
And if you don't have root, you can't do this anyway.
The only solution till now has been to ask every client to set the config
parameters `remote.<name>.receivepack` and `remote.<name>.uploadpack`. But
telling *every* client to do so is a pain...
Gitolite lets you specify the directory in which git binaries are to be found,
via a new variable (`$GIT_PATH`) in the "rc" file. If this variable is
non-empty, it will be appended to the PATH environment variable before
attempting to run git stuff.
Very easy, very simple, and completely transparent to the users :-)
#### "personal" branches
"personal" branches are great for corporate environments, where
unauthenticated pull/clone is a no-no. Since a dev workstation cannot do
authentication, even work shared just between 2 devs has to go *via* the
server. This causes the same branch name clutter as in a centralised VCS,
plus setting up permissions for this becomes a chore for the admin.
gitolite lets you define a "personal" or "scratch" namespace prefix for
each developer (e.g., `refs/personal/<devname>/*`), with full
permissions for that dev and read-only for everyone else. And you get
this without adding a single line to the access config file -- pretty
much fire and forget as far as the admin is concerned, even if there is
constant churn in the project teams.
Not bad for something that took just *one* line of code to implement.
And that's one clean, readable, line, by the way ;-)
The admin would set `$PERSONAL_BRANCH_PREFIX` in the rc file and communicate
this to all users. It could be something like `refs/heads/personal`, which
means all such branches will show up in `git branch` lookups and `git clone`
will fetch them. Or he could use, say, `refs/personal`, which means it won't
show up in any normal "branch-y" commands and stuff, and generally be much
less noisy.
**Note that a user who has NO write access cannot have personal branches**; if
you read the section (above) on "two levels of access rights checking" you'll
understand why.
For instance, in the following example, `user3` cannot push to any
`refs/heads/personal/user3/*` branches because the first level check stops him
cold:
# assume $PERSONAL = 'refs/heads/personal' in ~/.gitolite.rc
repo myrepo
RW+ master = sitaram
RW+ release = qa_guy
RW = user1 user2
R = user3
If we relax that check, *any* access becomes *write* access. Yes it will be
caught later, by the hook, but it's good practice to catch things in multiple
places.
If you want `user3` to have his own personal branch, but without write access
to any of the "real" branches (like "master", "release", etc.), just use a
dummy branch. Choose a name that will never exist in practice, or even if
someone creates it, we don't care. For example, this will get him past the
first check:
RW dummy = user3
Just don't *show* the user this config file; it might sound insulting :-)
#### custom hooks and custom git config
You can specify hooks that you want to propagate to all repos, as well as
per-repo "gitconfig" settings. Please see `doc/2-admin.mkd` and
`conf/example.conf` for details.
<a name="gitweb"></a>
### helping with gitweb
Although gitweb is a completely separate program, gitolite can do quite a
lot to help you manage gitweb access as well; once the initial setup is
complete, you can do it all from within the gitolite config file!
#### easier to specify gitweb "description" and gitweb/daemon access
To enable access to a repo via gitweb *and* create a "description" for it to
show up on the webpage, just add a line like this, anywhere in the config
file:
reponame = "one line of description"
You can also specify an "owner":
reponame "owner name" = "one line of description"
To enable access to one or more repos via git daemon, just give "read"
permissions to the special username `daemon`.
There is also a special user called `gitweb` to specify gitweb access; useful
if you don't care about specifying individual descriptions for each repo and
just want to quickly enable gitweb access to one or more repos.
Remember gitolite lets you specify the access control specs in bits and
pieces, so you can keep all the daemon/gitweb access in one place, even if
each repo has more specific branch-level access config specified elsewhere.
Here's an example, using really short reponames because I'm lazy:
# maybe near the top of the file, for ease of access:
@only_web = r1 r2 r3
@only_daemon = r4 r5 r6
@web_and_daemon = r7 r8 r9
repo @only_web
R = gitweb
repo @only_daemon
R = daemon
repo @web_and_daemon
R = gitweb
R = daemon
# ...maybe much later in the file:
repo r1
# normal developer access lists for r1 and its branches/tags in the
# usual way
repo r2
# ...and so on...
<a name="gitwebauth"></a>
#### easier to link gitweb authorisation with gitolite
Over and above whether a repo is even *shown* by gitweb, you may want to
further restrict people, allowing them to view *only* those repos for which
they have been given read access by gitolite.
This requires that:
* you have to have some sort of HTTP auth on your web server (out of my
scope, sorry!)
* the HTTP auth should use the same username (like "sitaram") as used in the
gitolite config (for the corresponding user)
Normally a superuser sets up passwords for users using the "htpasswd" command,
but this is an administrative chore.
Robin Smidsrød had the *great* idea that, since each user already has pubkey
access to `git@server`, this gives us a very neat way of using gitolite to let
the users *manage their own HTTP passwords*. Here's how:
* setup apache so that the htaccess file it looks for is owned by the "git"
user
* in the `~/.gitolite.rc` file, look for the variable `$HTPASSWD_FILE` and
point it to this file
* tell your users to type in `ssh git@server htpasswd` to set or change
their HTTP passwords
Here's the rest of how it hangs together.
Gitweb allows you to specify a subroutine to decide on access. We use that
feature and tie it to gitolite. Sample code (untested by me, but others do
use it, munged from something I saw [here][leho]) is given below.
Note the **utter simplicity** of the actual check (just 1 line!). This is an
unexpected piece of luck coming from the decision to keep the config parse
separate from the actual access control. The config parser puts a pure perl
hash in that file named below as `$gl_conf_compiled`, so all the parsing is
already done and we just use it!
# completely untested... but the basic idea should work fine
# change these as needed
# projectroot should be the same as gitolite's REPO_BASE, but converted to
# an absolute path
$projectroot = '/home/git/repositories/';
my $gl_conf_compiled = '/home/git/.gitolite/conf/gitolite.conf-compiled.pm';
# I am told this gives us the HTTP auth username
my $username = $cgi->remote_user;
# ----------
# parse the config file; updates %repos hash
our %repos;
die "parse $gl_conf_compiled failed: " . ($! or $@) unless do $gl_conf_compiled;
# this is gitweb's mechanism; it calls whatever sub is pointed at by this
# variable to decide access yes/no. Gitweb calls it with one argument
# containing the full path of the repo being accessed
$export_auth_hook = sub {
my $reponame = shift;
# take the full path provided, strip the beginning...
$reponame =~ s/\Q$projectroot\E\/?//;
# ...and the end, to get the repo name as it is specified in gitolite conf
$reponame =~ s/\.git$//;
return exists $repos{$reponame}{R}{$username}
|| exists $repos{$reponame}{R}{'@all'};
};
[leho]: http://leho.kraav.com/news/2009/10/27/using-apache-authentication-with-gitweb-gitosis-repository-access-control/
### advanced features
#### repos named with wildcards
Please see `doc/4-wildcard-repositories.mkd` for all the details.
#### access control for external commands
Gitolite now has a mechanism for allowing access control for arbitrary
external commands, as long as they are invoked via ssh and present a
server-side command that contains enough information to make an access control
decision. The first (and only, so far) such command implemented is rsync.
Note that this is incompatible with giving people shell access as described in
`doc/6-ssh-troubleshooting.mkd` -- people who have shell access are not
subject to this mechanism (it wouldn't make sense to try and control someone
who has shell access anyway).
Please see the config files (both of them) for examples and usage.
## design choices
### keeping the parser and the access control separate
There are two programs concerned with access control:
* `gl-auth-command`, the program that is run via `~/.ssh/authorized_keys`;
this decides whether git should even be allowed to run (basic R/W/no
access). (This one cannot decide on the branch-level access; it is not
known at this point what branch is being accessed)
* the update-hook on each repo, which decides the per-branch permissions
I have chosen to keep the relatively complex task of parsing the config file
out of them to keep them simpler (and faster). So any changes to the config
have to be first "compiled", and the access control programs use this
"compiled" version of the config. (The compile step also refreshes
`~/.ssh/authorized_keys`).
If you choose the "easy install" method, all this is quite transparent to you
anyway. If you cannot use the easy install and must install manually, I have
clear instructions on how to set it up.

View file

@ -0,0 +1,216 @@
# repositories named with wildcards
***IMPORTANT NOTE***:
This feature may be somewhat "brittle" in terms of security. Creating
repositories based on wild cards, giving "ownership" to the specific user who
created it, allowing him/her to hand out R and RW permissions to other users
to collaborate, all these are possible. And any of these could have a bug in
it. I haven't found any yet, but that doesn't mean there aren't any.
Also, there are some limitations. For example, you cannot specify gitconfig
values for a wildcard repo; it only works for actual repos.
There may be other such missing features. Sometimes it's just not possible to
make it work. Or it may be cumbersome enough that unless there are *no*
workarounds I may not have the time to code it right away.
----
In this document:
* wildcard repos
* wildcard repos with creater name in them
* wildcard repos without creater name in them
* side-note: line-anchored regexes
* contrast with refexes
* handing out rights to wildcard-matched repos
* setting a gitweb description for a wildcard-matched repo
* reporting
* other issues and discussion
This document is mostly "by example".
----
### Wildcard repos
Which of these alternatives you choose depends on your needs, and the social
aspects of your environment. The first one is a little more rigid, making it
harder to make mistakes, and the second is more flexible and trusting.
#### Wildcard repos with creater name in them
Here's an example snippet:
@prof = u1
@TAs = u2 u3
@students = u4 u5 u6
repo assignments/CREATER/a[0-9][0-9]
C = @students
RW+ = CREATER
RW = WRITERS @TAs
R = READERS @prof
For now, ignore the special usernames READERS and WRITERS, and just create a
new repo, as user "u4" (a student):
$ git clone git@server:assignments/u4/a12
Initialized empty Git repository in /home/sitaram/t/a12/.git/
Initialized empty Git repository in /home/gitolite/repositories/assignments/u4/a12.git/
warning: You appear to have cloned an empty repository.
Notice the *two* empty repo inits, and the order in which they occur ;-) Now
make some changes and push, and after that, that specific repo
(`assignments/u4/a12`) behaves as if the access control looked like this:
# effective config
repo assignments/u4/a12
RW+ = u4
RW = WRITERS @TAs
R = READERS @prof
#### Wildcard repos without creater name in them
Here's how the same example would look if you did not want the CREATER's name
to be part of the actual repo name.
repo assignments/a[0-9][0-9]
C = @students
RW+ = CREATER
RW = WRITERS @TAs
R = READERS @prof
We haven't changed anything except the repo name pattern. This means that the
first student that creates, say, `assignments/a12` becomes the owner.
Mistakes (such as claiming a12 instead of a13) need to be rectified by an
admin logging on to the back end, though it's not too difficult.
You could also repace the C line like this:
C = @TAs
and have a TA create the repos in advance.
In either case, they could then use the `setperms` feature to specify which
users are "READERS" and which are "WRITERS". See later for details.
### Side-note: Line-anchored regexes
A regex like
repo assignments/S[0-9]+/A[0-9]+
would match `assignments/S02/A37`. It will not match `assignments/S02/ABC`,
or `assignments/S02/a37`, obviously.
But you may be surprised to find that it does not match even
`assignments/S02/A37/B99`. This is because internally, gitolite
*line-anchors* the given regex; so that regex actually becomes
`^assignments/S[0-9]+/A[0-9]+$` -- notice the line beginning and ending
metacharacters.
#### Contrast with refexes
Just for interest, note that this is in contrast to the refexes for the normal
"branch" permissions, as described in `conf/example.conf` and elsewhere.
Those "refexes" are *not* anchored; a pattern like `refs/heads/master`
actually matches `foo/refs/heads/master01/bar` as well, even if no one will
actually push such a branch! You can anchor it if you really care, by using
`master$` instead of `master`, but anchoring is *not* the default for
refexes.]
### Handing out rights to wildcard-matched repos
In the examples above, we saw two special "user" names: READERS and WRITERS.
The permissions they have are controlled by the config file, but ***who is
part of this list*** is controlled by the person who created the repository.
The use case is that, although our toy example has only 3 students, in reality
there will be a few dozen, but each assignment will be worked on only by a
handful from among those. This allows the creater to take ad hoc sets of
users from among the actual users in the system, and place them into one of
two categories (whose permissions are, in this example, R and RW
respectively). In theory you could do the same thing by creating lots of
little "assignment-NN" groups in the config file but that may be a little too
cumbersome for non-secret environments.
Create a small text file that contains the permissions you desire:
$ cat > myperms
R u5
RW u6
(hit ctrl-d here)
...and use the new "setperms" command to set permissions for your repo:
$ ssh git@server setperms assignments/u4/a12 < myperms
New perms are:
R u5
RW u6
'setperms' will helpfully print what the new permissions are but you can also
use 'getperms' to check:
$ ssh git@server getperms assignments/u4/a12
R u5
RW u6
The following points are important:
* note the syntax of the commands; it's not a "git" command, and there's no
`:` like in a repo URL. The first space-separated word is R or RW, and
the rest are simple usernames.
* whoever you specify as "R" will match the special user READERS. "RW" will
match WRITERS.
### setting a gitweb description for a wildcard-matched repo
Similar to the getperm/setperm commands, there are the getdesc/setdesc
commands, thanks to Teemu.
### reporting
Remember the cool stuff you see when you just do `ssh git@server` (grep for
"myrights" in `doc/3-faq-tips-etc.mkd` if you forgot, or go [here][mr]).
[mr]: http://github.com/sitaramc/gitolite/blob/pu/doc/3-faq-tips-etc.mkd#myrights
This still works, except the format is a little more compressed to accommodate
a new column (at the start) for "C" permissions, which indicate that you are
allowed to *create* repos matching that pattern.
In addition, there is also the "expand" command, which takes any regex pattern
and returns you a list of all wildcard-created repos that you have access to
which fit that pattern.
### other issues and discussion
* *what if the repo name being pushed matches more than one pattern*?
I think it would be very hard to reason about access if we were to do
something like combine all the access rights in all the matching patterns.
No matter how you do it, and how carefully you document it, there'll be
someone who is surprised by the result.
And in security, that's a ***Bad Thing***.
So we don't combine permissions. At runtime, we die if we find more than
one match. Let 'em go holler at the admin for creating multiple matching
repo patterns :-)
This can make some repos inaccessible if the patterns changed *after* they
were created. The administrator should be careful not to do this. Most
of the time, it won't be difficult; the fixed prefix will usually be
different anyway so there won't be overlaps.
----
Enjoy, and please use with care. This is pretty powerful stuff. As they say:
if you break it, you get to keep both pieces :)
[jwzq]: http://regex.info/blog/2006-09-15/247
[av]: http://en.wikipedia.org/wiki/Autovivification

99
doc/5-delegation.mkd Normal file
View file

@ -0,0 +1,99 @@
# delegating access control responsibilities
[Thanks to jeromeag for forcing me to think through this...]
### lots of repos, lots of users
Gitolite tries to make it easy to manage access to lots of users and repos,
exploiting commonalities wherever possible. (The example in [this
section][ss] should give you an idea). As you can see, it lets you specify
bits and pieces of the access control separately -- i.e., *all* the access
specs for a certain repo need not be together; they can be scattered, which
makes it easier to manage the sort of slice and dice needed in that example.
[ss]: http://github.com/sitaramc/gitolite/blob/pu/doc/3-faq-tips-etc.mkd#simpler_syntax
But eventually the config file will become too big. If you let only one
person have control, he could become a bottleneck. If you give it to multiple
people, they might make mistakes or stomp on each others' work accidentally.
The best way is to divide up the config file and give parts of it to different
people.
Ideally, we would delegate authority for *groups* of repos, not individual
repos, otherwise it doesn't scale. It would also be nice if we could prevent
an admin from creating access rules for *any* repo in the system -- i.e., set
limits on what repos he can control. This would be a nice "security" feature.
Delegation offers a way to do all that. Note that delegated admins cannot
create or remove users, not can they define new repos. They can only define
access control rules for a set of repos they have been given authority for.
----
It's easier to show how it all works with an example instead of long
descriptions.
### splitting up the set of repos into groups
To start with, recall that gitolite allows you to specify **groups** (of users
or repos, same syntax). So the basic idea is that the main config file
(`conf/gitolite.conf` in your admin repo clone) will specify some repo groups:
# group your projects/repos however you want
@webbrowser_repos = firefox lynx
@webserver_repos = apache nginx
@malware_repos = conficker storm
# any other config as usual, including access control lines for any of the
# above projects or groups
### delegating ownership of groups of repos
Once the repos are grouped, give each person charge of one or more groups.
For example, Alice may be in charge of all web browser development projects,
Bob takes care of web servers, and Mallory, as [tradition][abe] dictates, is
in charge of malware ;-)
[abe]: http://en.wikipedia.org/wiki/Alice_and_Bob#List_of_characters
You do this by adding branches to the `gitolite-admin` repo:
# the admin repo access was probably like this to start with:
repo gitolite-admin
RW+ = sitaram
# now add these lines to the config for the admin repo
RW = alice bob mallory
RW+ NAME/ = sitaram
RW NAME/conf/fragments/webbrowser_repos = alice
RW NAME/conf/fragments/webserver_repos = bob
RW NAME/conf/fragments/malware_repos = mallory
This uses gitolite's ability to restrict pushes by file/dir name being changed
-- the syntax you see above ensures that, while "sitaram" does not have any
NAME based restrictions, the other 3 users do. See `conf/example.conf` for
syntax and notes.
As you can see, **for each repo group** you want to delegate authority over,
there's a rule for a **corresponding file** in `conf/fragments` in the
`gitolite-admin` repo. If you have write access to that file, you are allowed
to define rules for repos in that repo group.
In other words, we use gitolite's file/dir NAME-based permissions to "enforce"
the separation between the delegated configs!
Here's how to use this in practice:
* Alice clones the `gitolite-admin` repo, and adds a file called
`conf/fragments/webbrowser_repos.conf`
* she writes in this file any access control rules for the "firefox" and
"lynx" repos. She should not write access rules for any other project --
they will be ignored
* Alice then commits and pushes to the `gitolite-admin` repo
Naturally, a successful push invokes the post-update hook that the admin repo
has, which eventually runs the compile script. The **net effect** is as if
you appended the contents of all the "fragment" files, in alphabetical order,
to the bottom of the main file.

View file

@ -0,0 +1,417 @@
# ssh troubleshooting
In this document:
* basic ssh troubleshooting
* passphrases versus passwords
* ssh-agent problems
* basic ssh troubleshooting for the main admin
* basic ssh troubleshooting for a normal user
* details
* files on the server
* files on client
* why two keys on client
* more complex ssh setups
* two gitolite servers to manage?
* giving shell access to gitolite users
----
This document should help you troubleshoot ssh-related problems in accessing
gitolite *after* the install has completed successfully.
In addition, I **strongly** recommend reading [this document][glb] -- it's a
very detailed look at how gitolite uses ssh's features on the server side.
Most people don't know ssh as well as they *think* they do; even if you don't
have any problems right now, it's worth skimming over.
In addition to both these documents, there's now a program called
`sshkeys-lint` that you can run on your client. Run it without arguments to
get help on how to run it and what inputs it needs.
Please also note that ssh problems don't always look like ssh problems. One
common example: when the remote says the repo you're trying to access "does
not appear to be a git repository", and yet you are sure it exists, you
haven't mis-spelled it, etc. Another example is being able to access
repositories using the full unix path (typically like
`git@server:repositories/reponame.git`, assuming default `$REPO_BASE` setting,
instead of specifying only the part below `$REPO_BASE`, i.e.,
`git@server:reponame.git`).
[Both these errors indicate that you managed to bypass gitolite completely and
are using your shell access -- instead of running via
`/some/path/gl-auth-command <your_username>` it is just going to bash and
working from there!]
<a name="basic"></a>
### basic ssh troubleshooting
[glb]: http://sitaramc.github.com/0-installing/9-gitolite-basics.html#IMPORTANT_overview_of_ssh
I assume the gitolite server is called "server" and the user hosting all the
gitolite repos is "git". I will also be using "sitaram" as the *gitolite
username* of the admin.
Unless specifically mentioned, all these commands are run on the user's or
admin's workstation, not on the server.
#### passphrases versus passwords
When you create an ssh keypair, you have the option of protecting it with a
passphrase. When you subsequently use that keypair to access a remote host,
your *local* ssh client needs to unlock the corresponding private key, and ssh
will probably ask for the passphrase you set when you created the keypair.
Do not confuse or mistake this prompt (`Enter passphrase for key
'/home/sitaram/.ssh/id_rsa':`) for a password prompt from the remote server!
You have two choices to avoid this prompt every time you try to access the
remote. The first is to create keypairs *without* a passphrase (just hit
enter when prompted for one). **Be sure to add a passphrase later, once
everything is working, using `ssh-keygen -p`**.
The second is to use `ssh-agent` (or `keychain`, which in turn uses
`ssh-agent`) or something like that to manage your keys. Other than the next
section, further discussion of this is out of scope of this document.
#### ssh-agent problems
1. Run `ssh-add -l`. If this responds with either "The agent has no
identities." or "Could not open a connection to your authentication
agent.", skip this section.
2. However, if it lists some keys, like this:
2048 fc:c1:48:1e:06:31:97:a4:8b:fc:37:b2:76:14:c7:53 /home/sitaram/.ssh/id_rsa (RSA)
2048 d2:e0:7f:fa:1a:89:22:41:bb:06:d9:ff:a7:27:36:5c /home/sitaram/.ssh/sitaram (RSA)
then run `ls ~/.ssh` and make sure that all the keypairs you have there
are represented in the `ssh-add -l` output.
3. If you find any keypairs in `~/.ssh` that are not represented in the
`ssh-add -l` output, add them. For instance, if `ssh-add -l` showed me
only the `id_rsa` key, but I also had a `sitaram` (and `sitaram.pub`)
keypair, I'd run `ssh-add ~/.ssh/sitaram` to add it.
This is because ssh-agent has a quirk: if `ssh-add -l` shows *any* keys at
all, ssh will only use those keys. Even if you explicitly specify an unlisted
key using `ssh -i` or an `identityfile` directive in the config file, it won't
use it.
#### basic ssh troubleshooting for the main admin
You're the "main admin" if you're trying to access gitolite from the same
workstation and user account where you ran the "easy install" command. You
should have two keypairs in your `~/.ssh` directory. The pair called `id_rsa`
(and `id_rsa.pub`) was probably the first one you created, and you used this
to get passwordless (pubkey based) access to the server (which was a
pre-requisite for running the easy install command).
The second keypair has the same name as the last argument in the easy install
command you ran (in my case, `sitaram` and `sitaram.pub`). It was probably
created by the easy install script, and is the key used for gitolite access.
In addition, you should have a "gitolite" paragraph in your `~/.ssh/config`,
looking something like this:
host gitolite
user git
hostname server
identityfile ~/.ssh/sitaram
If any of these are not true, you did something funky in your install; email
me or hop onto #git and hope for the best ;-)
Otherwise, run these checks:
1. `ssh git@server` should get you a command line.
If it asks you for a password, then your `id_rsa` keypair changed after
you ran the easy install, or someone fiddled with the
`~/.ssh/authorized_keys` file on the server.
If it prints [gitolite version and access info][myrights], you managed to
overwrite the `id_rsa` keypair with the `sitaram` keypair, or something
equally weird.
2. `ssh gitolite info` should print some [gitolite version and access
info][myrights]. If you get the output of the GNU info command instead,
you probably reused your `id_rsa` keypair as your `sitaram` keypair, or
overwrote the `sitaram` keypair with the `id_rsa` keypair.
There are many ways to fix this, depending on where and what the damage is.
The most generic way (and therefore time-taking) is to re-install gitolite
from scratch:
* make a backup of your gitolite-admin repo clone somewhere (basically your
"keydir/*.pub" and your "conf/gitolite.conf"). If necessary get these
files from the server's `~/.gitolite` directory.
* log on to the server somehow (using some other account, using a password,
su-ing in, etc) and delete `~/.ssh/authorized_keys`. Rename or move aside
`~/.gitolite` so that also looks like it is missing.
* back on your workstation, make sure you have 2 keypairs (`id_rsa` and
`sitaram`, along with corresponding `.pub` files). Create them if needed.
Also make sure they are *different* and not a copy of each other :-)
* install gitolite normally:
* run `ssh-copy-id -i ~/.ssh/id_rsa git@server` to get passwordless
access to the server. (Mac users may have to do this step manually)
* make sure `ssh git@server pwd` prints the `$HOME` of `git@server`
**without** asking for a password. Do not proceed till this works.
* run easy install again, (in my case: `cd gitolite-source;
src/gl-easy-install -q git server sitaram`)
* go to your gitolite-admin repo clone, and copy `conf/gitolite.conf` and
`keydir/*.pub` from your backup to this directory
* copy (be sure to overwrite!) `~/.ssh/sitaram.pub` also to keydir
* now `git add keydir; git commit; git push -f`
That's a long sequence but it should work.
#### basic ssh troubleshooting for a normal user
For a normal user, life is much simpler. They should have only one pubkey,
which was previously sent to the gitolite admin to add into the admin repo's
`keydir` as "user.pub", and then "user" given permissions to some repo.
`ssh git@server info` should get you [gitolite version and access
info][myrights]. If it asks you for a password, your pubkey was not sent to
the server properly. Check with your admin.
[myrights]: http://github.com/sitaramc/gitolite/blob/pu/doc/3-faq-tips-etc.mkd#myrights
If it gets you the GNU info command output, you have shell access. This means
you had command line access to the server *before* you were added as a
gitolite user. If you send that same key to your gitolite admin to include in
the admin repo, it won't work. For reasons why, see below.
<a name="details"></a>
### details
Here's how it all hangs together.
#### files on the server
* the authkeys file; this contains one line containing the pubkey of each
user who is permitted to login without a password.
Pubkey lines that give shell access look like this:
ssh-rsa AAAAB3NzaC[snip]uPjrUiAUew== /home/sitaram/.ssh/id_rsa
On a typical server there will be only one or two of these lines.
Note that the last bit (`/home/sitaram/.ssh/id_rsa`) is purely a *comment*
field and can be anything. Also, the actual lines are much longer, about
400 characters; I snipped 'em in the middle, as you can see.
In contrast, pubkey lines that give access to git repos hosted by gitolite
look like this:
command="[some path]src/gl-auth-command sitaram",[some restrictions] ssh-rsa AAAAB3NzaC[snip]s18OnB42oQ== sitaram@sita-lt
You will have many more of these lines -- one for every pubkey file in
`keydir/` of your gitolite-admin repo, with the corresponding username in
place of "sitaram" in the example above.
The "command=" at the beginning ensures that when someone with the
corresponding private key logs in, they don't get a shell. Instead, the
`gl-auth-command` program is run, and (in this example) is given the
argument `sitaram`. This is how gitolite is invoked, (and is told the
user logging in is "sitaram").
#### files on client
* default keypair; used to get shell access to servers. You would have
copied this pubkey to the gitolite server in order to log in without a
password. (On Linux systems you may have used `ssh-copy-id` to do that).
You would have done this *before* you ran the easy install script, because
otherwise easy install won't run!
~/.ssh/id_rsa
~/.ssh/id_rsa.pub
* gitolite keypair; the "sitaram" in this is the 3rd argument to the
`src/gl-easy-install` command you ran; the easy install script does the
rest
~/.ssh/sitaram
~/.ssh/sitaram.pub
* config file; this file has an entry for gitolite access:
~/.ssh/config
To understand why we need that, let's step back a bit. Normally, you
might expect to access gitolite repos like this:
ssh://git@server/reponame.git
But this won't work, because this ends up using the *default* keypair
(normally), which gives you a command line. Which means it won't invoke
the `gl-auth-command` program at all, and so none of gitolite's access
control will work.
<a name="altkey"></a>
You need to force ssh to use the *other* keypair when performing a git
operation. With normal ssh, that would be
ssh -i ~/.ssh/sitaram git@server
but git does not support putting an alternate keypair in the URL.
Luckily, ssh has a very convenient way of capturing all the connection
information (username, hostname, port number (if it's not the default 22),
and keypair to be used) in one "paragraph" of `~/.ssh/config`. This is
what the para looks like for us (the easy install script puts it there the
first time):
host gitolite
user git
hostname server
identityfile ~/.ssh/sitaram
(The "gitolite" can be anything you want of course; it's like a group name
for all the stuff below it). This ensures that typing
ssh gitolite
is equivalent to
ssh -i ~/.ssh/sitaram git@server
and therefore this:
git clone gitolite:reponame.git
now works as expected, invoking the special keypair instead of the default
one.
<a name="twokeys"></a>
#### why two keys on client
Why do I (the admin) need two **different** keypairs?
There are two types of access the admin will make to the server: a normal
login, to get a shell prompt, and gitolite access (clone/fetch/push etc). The
first access needs an authkeys line *without* any "command=" restrictions,
while the second requires a line *with* such a restriction.
And we can't use the same key for both because there is no way to disambiguate
them; the ssh server will always (*always*) pick the first one in sequence
when the key is offered by the ssh client.
So the next question is usually "I have other ways to get a shell on that
account, so why do I need a key for shell access at all?"
The answer to this is that the "easy install" script, being written for the
most general case, needs shell access via ssh to do its stuff.
If you really, really, want to get rid of the extra key, here's a transcript
that should have enough info to get you going (but it helps to know ssh well):
* on "sitaram" user, on my workstation
cd ~/.ssh
cp id_rsa sitaram
cp id_rsa.pub sitaram.pub
cd ~/gitolite-clone
src/gl-easy-install -q git my.git.server sitaram
that last command produces something like the following:
you are upgrading from (unknown) to v0.80-6-gdde8c4e
setting up keypair...
...reusing /home/sitaram/.ssh/sitaram.pub...
creating gitolite para in ~/.ssh/config...
finding/creating gitolite rc...
installing/upgrading...
Pseudo-terminal will not be allocated because stdin is not a terminal.
[master (root-commit) e717a89] start
2 files changed, 11 insertions(+), 0 deletions(-)
create mode 100644 conf/gitolite.conf
create mode 100644 keydir/sitaram.pub
cloning gitolite-admin repo...
Initialized empty Git repository in /home/sitaram/gitolite-admin/.git/
fatal: 'gitolite-admin.git' does not appear to be a git repository
fatal: The remote end hung up unexpectedly
notice that the final step (the clone of the newly created gitolite-admin
repo) failed, as expected
* now log on to the git hosting account (`git@my.git.server` in this
example), edit `~/.ssh/authorized_keys`, and delete the line with the
first occurrence of your key (this should be *before* the `# gitolite
start` line)
* now go back to your workstation and
git clone git@my.git.server:gitolite-admin
That should do it.
<a name="complex"></a>
### more complex ssh setups
What do you need to know in order to create more complex ssh setups (for
instance if you have *two* gitolite servers you are administering)?
#### two gitolite servers to manage?
* they can have the same key; no harm there (example, sitaram.pub)
* instead of just one ssh/config para, you now have two (assuming that the
remote user on both machines is called "git"):
host gitolite
user git
hostname server
identityfile ~/.ssh/sitaram
host gitolite2
user git
hostname server2
identityfile ~/.ssh/sitaram
* now access one server's repos as `gitolite:reponame.git` and the other
server's repos as `gitolite2:reponame.git`.
<a name="shell"></a>
### giving shell access to gitolite users
We've managed (thanks to an idea from Jesse Keating) to make it possible for a
single key to allow both gitolite access *and* shell access.
This is done by:
* (**on the server**) listing all such users in a variable called
`$SHELL_USERS` in the `~/.gitolite.rc` file. For example:
$SHELL_USERS = "alice bob";
(Note the syntax: a space separated list of users in one string variable).
* (**on your client**) make at least a dummy change to your clone of the
gitolite-admin repo and push it.
**IMPORTANT UPGRADE NOTE**: a previous implementation of this feature worked
by adding people to a special group (`@SHELL`) in the *config* file. This
meant that anyone with gitolite-admin repo write access could add himself to
the `@SHELL` group and push, thus obtaining shell.
This is not a problem for most setups, but if someone wants to separate these
two privileges (the right to push the admin repo and the right to get a shell)
then it does pose a problem. Since the "rc" file can only be edited by
someone who already has shell access, we now use that instead, even though
this forces a change in the syntax.
To migrate from the old scheme to the new one, add a new variable
`$SHELL_USERS` to `~/.gitolite.rc` on the server with the appropriate names in
it. **It is best to do this directly on the server *before* upgrading to this
version.** (After the upgrade is done and tested you can remove the `@SHELL`
lines from the gitolite config file).

View file

@ -0,0 +1,211 @@
# gitolite install transcript
This is a *complete* transcript of a full gitolite install, *from scratch*,
using brand new userids ("sita" on the client, "git" on the server). Please
note that you can use existing userids also, it is not necessary to use
dedicated user IDs for this. Also, you don't have to use some *other* server
for all this, both server and client can be "localhost" if you like.
Please note that this entire transcript can be summarised as:
* create users on client and server (optional)
* get pubkey access to server from client (`ssh-copy-id` or manual eqvt)
* run one command ***on client*** (`gl-easy-install`)
...and only that last step is actually gitolite. In fact, the bulk of the
transcript is **non**-gitolite stuff :)
----
### create userids on server and client (optional)
Client side: add user, give him a password
sita-lt:~ # useradd sita
sita-lt:~ # passwd sita
Changing password for user sita.
New UNIX password:
Retype new UNIX password:
passwd: all authentication tokens updated successfully.
Server side: (log on to server, then) add user, give it a password
sita-lt:~ # ssh sitaram@server
sitaram@server's password:
Last login: Fri Dec 18 20:25:06 2009
-bash-3.2$ su -
Password:
sita-sv:~ # useradd git
sita-sv:~ # passwd git
Changing password for user git.
New UNIX password:
Retype new UNIX password:
passwd: all authentication tokens updated successfully.
Server side: allow ssh access to "git" user
This is done by editing the sshd config file and adding "git" to the
"AllowUsers" list (the grep command is just confirming the change we made,
because I'm not showing the actual "vi" session):
sita-sv:~ # vim /etc/ssh/sshd_config
sita-sv:~ # grep -i allowusers /etc/ssh/sshd_config
AllowUsers sitaram git
sita-sv:~ # service sshd restart
Stopping sshd: [ OK ]
Starting sshd: [ OK ]
----
### get pubkey access from client to server
This involves creating a keypair for yourself (using `ssh-keygen`), and
copying the public part of that keypair to the `~/.ssh/authorized_keys` file
on the server (using `ssh-copy-id`, if you're on Linux, or the manual method
described in the `ssh-copy-id` section in `doc/3-faq-tips-etc.mkd`).
sita-lt:~ $ su - sita
Password:
sita@sita-lt:~ $ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/sita/.ssh/id_rsa):
Created directory '/home/sita/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/sita/.ssh/id_rsa.
Your public key has been saved in /home/sita/.ssh/id_rsa.pub.
The key fingerprint is:
8a:e0:60:1b:04:58:68:50:a4:d7:d0:3a:a5:2d:bf:0a sita@sita-lt.atc.tcs.com
The key's randomart image is:
+--[ RSA 2048]----+
|===. |
|+o oo |
|o..=. |
|..= . |
|.o.+ S |
|.oo... . |
|E.. ... |
| . . |
| .. |
+-----------------+
sita@sita-lt:~ $ ssh-copy-id -i ~/.ssh/id_rsa git@server
git@server's password:
/usr/bin/xauth: creating new authority file /home/git/.Xauthority
Now try logging into the machine, with "ssh 'git@server'", and check in:
.ssh/authorized_keys
to make sure we haven't added extra keys that you weren't expecting.
Double check to make sure you can log on to `git@server` without a password:
sita@sita-lt:~ $ ssh git@server pwd
/home/git
----
### get gitolite source
sita@sita-lt:~ $ git clone git://github.com/sitaramc/gitolite gitolite-source
Initialized empty Git repository in /home/sita/gitolite-source/.git/
remote: Counting objects: 1157, done.
remote: Compressing objects: 100% (584/584), done.
remote: Total 1157 (delta 756), reused 912 (delta 562)
Receiving objects: 100% (1157/1157), 270.08 KiB | 61 KiB/s, done.
Resolving deltas: 100% (756/756), done.
### install gitolite
Note that gitolite is installed from the *client*. The `easy-install` script
runs on the client but installs gitolite on the server!
sita@sita-lt:~ $ cd gitolite-source/src
<font color="red"> **This is the only gitolite specific command in a typical
install sequence**. </font> Run it without any arguments to see a usage
message. Run it without the `-q` to get a more verbose, pause-at-every-step,
install mode that allows you to change the defaults etc.
sita@sita-lt:src $ ./gl-easy-install -q git server sitaram
you are upgrading (or installing first-time) to v0.95-38-gb0ce84d
setting up keypair...
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/sita/.ssh/sitaram.
Your public key has been saved in /home/sita/.ssh/sitaram.pub.
The key fingerprint is:
2a:8e:88:42:36:7e:71:e8:cc:ff:4c:54:64:8e:cf:19 sita@sita-lt.atc.tcs.com
The key's randomart image is:
+--[ RSA 2048]----+
| o |
| = |
| . E |
| + o |
| . .S+ |
| + o ... |
|+ = + .. |
|oo B .o |
|+ o o..o |
+-----------------+
creating gitolite para in ~/.ssh/config...
finding/creating gitolite rc...
installing/upgrading...
Initialized empty Git repository in /home/git/repositories/gitolite-admin.git/
Initialized empty Git repository in /home/git/repositories/testing.git/
Pseudo-terminal will not be allocated because stdin is not a terminal.
fatal: No HEAD commit to compare with (yet)
[master (root-commit) 2f40d4b] start
2 files changed, 13 insertions(+), 0 deletions(-)
create mode 100644 conf/gitolite.conf
create mode 100644 keydir/sitaram.pub
cloning gitolite-admin repo...
Initialized empty Git repository in /home/sita/gitolite-admin/.git/
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), done.
---------------------------------------------------------------
done!
Reminder:
*Your* URL for cloning any repo on this server will be
gitolite:reponame.git
*Other* users you set up will have to use
git@server:reponame.git
If this is your first time installing gitolite, please also:
tail -31 ./gl-easy-install
for next steps.
----
### examine what you have
sita@sita-lt:src $ cd ~/gitolite-admin/
sita@sita-lt:gitolite-admin $ git --no-pager log --stat
commit 2f40d4bb80d424dc39aae5d0973f8c1b2e395666
Author: git <git@sita-lt.atc.tcs.com>
Date: Thu Dec 24 21:39:15 2009 +0530
start
conf/gitolite.conf | 12 ++++++++++++
keydir/sitaram.pub | 1 +
2 files changed, 13 insertions(+), 0 deletions(-)
And that's really all. Add keys to keydir here, edit conf/gitolite.conf as
needed, then add, commit, and push the changes to the server. Try out that
`tail -31 ./gl-easy-install` too :)

48
doc/CHANGELOG Normal file
View file

@ -0,0 +1,48 @@
Major changes to gitolite, master branch only, most recent first, no dates but
the tags can help you position stuff approximately
- v1.3
- easier to move repos into gitolite
- pattern for expand is no longer anchored
- v1.2
- distro packaging support -- easy to install systemwide now
- v1.1
- contrib directory added
- expand now lists non-wildcard repos also
- refs also have groups now
- allow admins to get "info" for other users
- wildrepos merged
- getdesc and setdesc for wildrepos
- htpasswd subcommand
- access control for rsync
- v1.0
- sshkeys-lint program added, doc/6 revamped
- @SHELL in config changed to $SHELL_USERS in rc
- "include" mechanism
- delegation now uses NAME/ instead of branches
- PATH/ changed to NAME/
- @SHELL in config
- use of @all for repos also (see doc for caveat)
- config entries for repos
- deny rules (no more "rebel" branch!)
- PATH/
- specify gitweb owner
- v0.95
- easy install can run from msysgit also
- v0.90
- allow admin defined hooks
- specify gitweb desc
- v0.85
- emergency addkey program
- v0.80

View file

@ -1,12 +1,12 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
@ -56,7 +56,7 @@ patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
@ -255,7 +255,7 @@ make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
@ -276,3 +276,64 @@ TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

3
doc/TODO Normal file
View file

@ -0,0 +1,3 @@
* make a proper test suite
* change the "rc" file to use "gitconfig" instead...

158
doc/progit-article.mkd Normal file
View file

@ -0,0 +1,158 @@
## Gitolite ##
Git has started to become very popular in corporate environments, which tend to have some additional requirements in terms of access control. Gitolite was created to help with those requirements.
Gitolite allows you to specify permissions not just by repository (like Gitosis does), but also by branch or tag names within each repository. That is, you can specify that certain people (or groups of people) can only push certain "refs" (branches or tags) but not others.
### Installing ###
Installing Gitolite is very easy, even if you don't read the extensive documentation that comes with it. You need an account on a Unix server of some kind; various Linux flavours, and Solaris 10, have been tested. You do not need root access, assuming git, perl, and an openssh compatible ssh server are already installed. In the examples below, we will use the `gitolite` account on a host called `gitserver`.
Curiously, Gitolite is installed by running a script *on the workstation*, so your workstation must have a bash shell available. Even the bash that comes with msysgit will do, in case you're wondering.
You start by obtaining public key based access to your server, so that you can log in from your workstation to the server without getting a password prompt. The following method works on Linux; for other workstation OSs you may have to do this manually. We assume you already had a key pair generated using `ssh-keygen`.
$ ssh-copy-id -i ~/.ssh/id_rsa gitolite@gitserver
This will ask you for the password to the gitolite account, and then set up public key access. This is **essential** for the install script, so check to make sure you can run a command without getting a password prompt:
$ ssh gitolite@gitserver pwd
/home/gitolite
Next, you clone Gitolite from the project's main site and run the "easy install" script (the third argument is your name as you would like it to appear in the resulting gitolite-admin repository):
$ git clone git://github.com/sitaramc/gitolite
$ cd gitolite/src
$ ./gl-easy-install -q gitolite gitserver sitaram
And you're done! Gitolite has now been installed on the server, and you now have a brand new repository called `gitolite-admin` in the home directory of your workstation. You administer your gitolite setup by making changes to this repository and pushing (just like Gitosis).
[By the way, *upgrading* gitolite is also done the same way. Also, if you're interested, run the script without any arguments to get a usage message.]
That last command does produce a fair amount of output, which might be interesting to read. Also, the first time you run this, a new keypair is created; you will have to choose a passphrase or hit enter for none. Why a second keypair is needed, and how it is used, is explained in the "ssh troubleshooting" document that comes with Gitolite. (Hey the documentation has to be good for *something*!)
### Customising the Install ###
While the default, quick, install works for most people, there are some ways to customise the install if you need to. If you omit the `-q` argument, you get a "verbose" mode install -- detailed information on what the install is doing at each step. The verbose mode also allows you to change certain server-side parameters, such as the location of the actual repositories, by editing an "rc" file that the server uses. This "rc" file is liberally commented so you should be able to make any changes you need quite easily, save it, and continue. This file also contains various settings that you can change to enable or disable some of gitolite's advanced features.
### Config File and Access Control Rules ###
So once the install is done, you switch to the `gitolite-admin` repository (placed in your HOME directory) and poke around to see what you got:
$ cd ~/gitolite-admin/
$ ls
conf/ keydir/
$ find conf keydir -type f
conf/gitolite.conf
keydir/sitaram.pub
$ cat conf/gitolite.conf
#gitolite conf
# please see conf/example.conf for details on syntax and features
repo gitolite-admin
RW+ = sitaram
repo testing
RW+ = @all
Notice that "sitaram" (the last argument in the `gl-easy-install` command you gave earlier) has read-write permissions on the `gitolite-admin` repository as well as a public key file of the same name.
The config file syntax for Gitolite is *quite* different from Gitosis. Again, this is liberally documented in `conf/example.conf`, so we'll only mention some highlights here.
You can group users or repos for convenience. The group names are just like macros; when defining them, it doesn't even matter whether they are projects or users; that distinction is only made when you *use* the "macro".
@oss_repos = linux perl rakudo git gitolite
@secret_repos = fenestra pear
@admins = scott # Adams, not Chacon, sorry :)
@interns = ashok # get the spelling right, Scott!
@engineers = sitaram dilbert wally alice
@staff = @admins @engineers @interns
You can control permissions at the "ref" level. In the following example, interns can only push the "int" branch. Engineers can push any branch whose name starts with "eng-", and tags that start with "rc" followed by a digit. And the admins can do anything (including rewind) to any ref.
repo @oss_repos
RW int$ = @interns
RW eng- = @engineers
RW refs/tags/rc[0-9] = @engineers
RW+ = @admins
The expression after the `RW` or `RW+` is a regular expression (regex) that the refname (ref) being pushed is matched against. So we call it a "refex"! Of course, a refex can be far more powerful than shown here, so don't overdo it if you're not comfortable with perl regexes.
Also, as you probably guessed, Gitolite prefixes `refs/heads/` as a syntactic convenience if the refex does not begin with `refs/`.
An important feature of the config file's syntax is that all the rules for a repository need not be in one place. You can keep all the common stuff together, like the rules for all `oss_repos` shown above, then add specific rules for specific cases later on, like so:
repo gitolite
RW+ = sitaram
That rule will just get added to the ruleset for the `gitolite` repository.
At this point you might be wondering how the access control rules are actually applied, so let's go over that briefly.
There are two levels of access control in gitolite. The first is at the repository level; if you have read (or write) access to *any* ref in the repository, then you have read (or write) access to the repository. This is the only access control that Gitosis had.
The second level, applicable only to "write" access, is by branch or tag within a repository. The username, the access being attempted (`W` or `+`), and the refname being updated are known. The access rules are checked in order of appearance in the config file, looking for a match for this combination (but remember that the refname is regex-matched, not merely string-matched). If a match is found, the push succeeds. A fallthrough results in access being denied.
### Advanced Access Control with "deny" rules ###
So far, we've only seen permissions to be one or `R`, `RW`, or `RW+`. However, gitolite allows another permission: `-`, standing for "deny". This gives you a lot more power, at the expense of some complexity, because now fallthrough is not the *only* way for access to be denied, so the *order of the rules now matters*!
Let us say, in the situation above, we want engineers to be able to rewind any branch *except* master and integ. Here's how to do that:
RW master integ = @engineers
- master integ = @engineers
RW+ = @engineers
Again, you simply follow the rules top down until you hit a match for your access mode, or a deny. Non-rewind push to master or integ is allowed by the first rule. A rewind push to those refs does not match the first rule, drops down to the second, and is therefore denied. Any push (rewind or non-rewind) to refs other than master or integ won't match the first two rules anyway, and the third rule allows it.
### Restricting pushes by files changed ###
In addition to restricting what branches a user can push changes to, you can also restrict what files they are allowed to touch. For example, perhaps the Makefile (or some other program) is really not supposed to be changed by just anyone, because a lot of things depend on it or would break if the changes are not done *just right*. You can tell gitolite:
repo foo
RW = @junior_devs @senior_devs
RW NAME/ = @senior_devs
- NAME/Makefile = @junior_devs
RW NAME/ = @junior_devs
This powerful feature is documented in `conf/example.conf`.
### Personal Branches ###
Gitolite also has a feature called "personal branches" (or rather, "personal branch namespace") that can be very useful in a corporate environment.
A lot of code exchange in the git world happens by "please pull" requests. In a corporate environment, however, unauthenticated access is a no-no, and a developer workstation cannot do authentication, so you have to push to the central server and ask someone to pull from there.
This would normally cause the same branch name clutter as in a centralised VCS, plus setting up permissions for this becomes a chore for the admin.
Gitolite lets you define a "personal" or "scratch" namespace prefix for each developer (for example, `refs/personal/<devname>/*`), with full permissions for that dev only, and read access for everyone else. Just choose a verbose install and set the `$PERSONAL` variable in the "rc" file to `refs/personal`. That's all; it's pretty much fire and forget as far as the admin is concerned, even if there is constant churn in the project team composition.
### "Wildcard" repositories ###
Gitolite allows you to specify repositories with wildcards (actually perl regexes), like, for example `assignments/s[0-9][0-9]/a[0-9][0-9]`, to pick a random example. This is a *very* powerful feature, which has to be enabled by setting `$GL_WILDREPOS = 1;` in the rc file. It allows you to assign a new permission mode ("C") which allows users to create repositories based on such wild cards, automatically assigns ownership to the specific user who created it, allows him/her to hand out R and RW permissions to other users to collaborate, etc. This feature is documented in `doc/4-wildcard-repositories.mkd`.
### Other Features ###
We'll round off this discussion with a bunch of other features, all of which are described in great detail in the "faqs, tips, etc" document.
**Logging**: Gitolite logs all successful accesses. If you were somewhat relaxed about giving people rewind permissions (`RW+`) and some kid blew away "master", the log file is a life saver, in terms of easily and quickly finding the SHA that got hosed.
**Git outside normal PATH**: One extremely useful convenience feature in gitolite is support for git installed outside the normal `$PATH` (this is more common than you think; some corporate environments or even some hosting providers refuse to install things system-wide and you end up putting them in your own directories). Normally, you are forced to make the *client-side* git aware of this non-standard location of the git binaries in some way. With gitolite, just choose a verbose install and set `$GIT_PATH` in the "rc" files. No client-side changes are required after that :-)
**Access rights reporting**: Another convenient feature is what happens when you try and just ssh to the server. Older versions of gitolite used to complain about the `SSH_ORIGINAL_COMMAND` environment variable being empty (see the ssh documentation if interested). Now Gitolite comes up with something like this:
hello sitaram, the gitolite version here is v0.90-9-g91e1e9f
you have the following permissions:
R anu-wsd
R entrans
R W git-notes
R W gitolite
R W gitolite-admin
R indic_web_input
R shreelipi_converter
**Delegation**: For really large installations, you can delegate responsibility for groups of repositories to various people and have them manage those pieces independently. This reduces the load on the main admin, and makes him less of a bottleneck. This feature has its own documentation file in the `doc/` directory.
**Gitweb support**: Gitolite supports gitweb in several ways. You can specify which repos are visible via gitweb. You can set the "owner" and "description" for gitweb from the gitolite config file. Gitweb has a mechanism for you to implement access control based on HTTP authentication, so you can make it use the "compiled" config file that gitolite produces, which means the same access control rules (for read access) apply for gitweb and gitolite.

View file

113
hooks/common/update Executable file
View file

@ -0,0 +1,113 @@
#!/usr/bin/perl
use strict;
use warnings;
# === update ===
# this is gitolite's update hook
# part of the gitolite (GL) suite
# how run: via git, being copied as .git/hooks/update in every repo
# when: every push
# input:
# - see man githooks for STDIN
# - uses the compiled config file to get permissions info
# output: based on permissions etc., exit 0 or 1
# security:
# - none
# robustness:
# other notes:
# ----------------------------------------------------------------------------
# common definitions
# ----------------------------------------------------------------------------
our ($GL_CONF_COMPILED, $PERSONAL);
our %repos;
# we should already have the GL_RC env var set when we enter this hook
die "parse $ENV{GL_RC} failed: " . ($! or $@) unless do $ENV{GL_RC};
# then "do" the compiled config file, whose name we now know. Before doing
# that we setup the creater etc from environment variables so that the parse
# interpolates them. We've minimised the duplication but this *does*
# duplicate a bit of parse_acl from gitolite.pm; we don't want to include that
# file here just for that little bit
{
our $creater = $ENV{GL_CREATER};
our $readers = $ENV{GL_READERS};
our $writers = $ENV{GL_WRITERS};
die "parse $GL_CONF_COMPILED failed: " . ($! or $@) unless do $GL_CONF_COMPILED;
$repos{$ENV{GL_REPO}} = $repos{$ENV{GL_REPOPATT}} if ( $ENV{GL_REPOPATT} );
}
my $reported_repo = $ENV{GL_REPO} . ( $ENV{GL_REPOPATT} ? " ($ENV{GL_REPOPATT})" : "" );
# we've started to need some common subs in what used to be a small, cute,
# little script that barely spanned a few lines :(
require "$ENV{GL_BINDIR}/gitolite.pm";
# ----------------------------------------------------------------------------
# start...
# ----------------------------------------------------------------------------
my $ref = shift;
my $oldsha = shift;
my $newsha = shift;
my $merge_base = '0' x 40;
# compute a merge-base if both SHAs are non-0, else leave it as '0'x40
# (i.e., for branch create or delete, merge_base == '0'x40)
chomp($merge_base = `git merge-base $oldsha $newsha`)
unless $oldsha eq '0' x 40
or $newsha eq '0' x 40;
# what are you trying to do? (is it 'W' or '+'?)
my $perm = 'W';
# rewriting a tag is considered a rewind, in terms of permissions
$perm = '+' if $ref =~ m(refs/tags/) and $oldsha ne ('0' x 40);
# non-ff push to ref
# notice that ref delete looks like a rewind, as it should
$perm = '+' if $oldsha ne $merge_base;
my @allowed_refs;
# personal stuff -- right at the start in the new regime, I guess!
push @allowed_refs, { "$PERSONAL/$ENV{GL_USER}/" => "RW+" } if $PERSONAL;
# we want specific perms to override @all, so they come first
push @allowed_refs, @ { $repos{$ENV{GL_REPO}}{$ENV{GL_USER}} || [] };
push @allowed_refs, @ { $repos{$ENV{GL_REPO}}{'@all'} || [] };
# prepare the list of refs to be checked
# previously, we just checked $ref -- the ref being updated, which is passed
# to us by git (see man githooks). Now we also have to treat each NAME being
# updated as a potential "ref" and check that, if NAME-based restrictions have
# been specified
my @refs = ($ref); # the first ref to check is the real one
if (exists $repos{$ENV{GL_REPO}}{NAME_LIMITS}) {
# this is special to git -- the hash of an empty tree
my $empty='4b825dc642cb6eb9a060e54bf8d69288fbee4904';
# well they're not really "trees" but $empty is indeed the empty tree so
# we can just pretend $oldsha/$newsha are also trees, and anyway 'git
# diff' only wants trees
my $oldtree = $oldsha eq '0' x 40 ? $empty : $oldsha;
my $newtree = $newsha eq '0' x 40 ? $empty : $newsha;
push @refs, map { chomp; s/^/NAME\//; $_; } `git diff --name-only $oldtree $newtree`;
}
# and in this version, we have many "refs" to check. The one we print in the
# log is the *first* one (which is a *real* ref, like refs/heads/master),
# while all the rest (if they exist) are like NAME/something. So we do the
# first one separately to capture it, then run the rest (if any)
my $log_refex = check_ref(\@allowed_refs, $ENV{GL_REPO}, (shift @refs), $perm);
&check_ref (\@allowed_refs, $ENV{GL_REPO}, $_ , $perm) for @refs;
# if we returned at all, all the checks succeeded, so we log the action and exit 0
&log_it("$ENV{GL_TS} $perm\t" .
substr($oldsha, 0, 14) . "\t" . substr($newsha, 0, 14) .
"\t$reported_repo\t$ref\t$ENV{GL_USER}\t$log_refex\n");
exit 0;

View file

@ -0,0 +1,8 @@
#!/bin/sh
# checkout the master branch to $GL_ADMINDIR
# (the GL_ADMINDIR env var would have been set by gl-auth-command)
GIT_WORK_TREE=$GL_ADMINDIR git checkout -f master
cd $GL_ADMINDIR
$GL_BINDIR/gl-compile-conf

77
install
View file

@ -1,77 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
# Clearly you don't need a program to make one measly symlink, but the git
# describe command involved in generating the VERSION string is a bit fiddly.
use Getopt::Long;
use FindBin;
# meant to be run from the root of the gitolite tree, one level above 'src'
BEGIN { $ENV{GL_BINDIR} = $FindBin::RealBin . "/src"; }
BEGIN { $ENV{GL_LIBDIR} = "$ENV{GL_BINDIR}/lib"; }
use lib $ENV{GL_LIBDIR};
use Gitolite::Common;
=for usage
Usage (from gitolite clone directory):
./install
to run gitolite using an absolute or relative path, for example
'src/gitolite' or '/full/path/to/this/dir/src/gitolite'
./install -ln [<dir>]
to symlink just the gitolite executable to some <dir> that is in
$PATH. <dir> defaults to $HOME/bin if <dir> not specified. <dir> is
assumed to exist; gitolite will not create it.
Please provide a full path, not a relative path.
./install -to <dir>
to copy the entire 'src' directory to <dir>. If <dir> is not in
$PATH, use the full path to run gitolite commands.
Please provide a full path, not a relative path.
Simplest use, if $HOME/bin exists and is in $PATH, is:
git clone git://github.com/sitaramc/gitolite
gitolite/install -ln
# now run setup
gitolite setup -pk /path/to/YourName.pub
=cut
my ( $to, $ln, $help, $quiet );
GetOptions(
'to=s' => \$to,
'ln:s' => \$ln,
'help|h' => \$help,
'quiet|q' => \$quiet,
);
usage() if $to and $ln or $help;
$ln = "$ENV{HOME}/bin" if defined($ln) and not $ln;
for my $d ($ln, $to) {
if ($d and not -d $d) {
print STDERR "FATAL: '$d' does not exist.\n";
usage();
}
}
chdir($ENV{GL_BINDIR});
my $version = `git describe --tags --long --dirty=-dt`;
if ($to) {
_mkdir($to);
system("cp -a * $to");
_print( "$to/VERSION", $version );
} elsif ($ln) {
ln_sf( $ENV{GL_BINDIR}, "gitolite", $ln );
_print( "VERSION", $version );
} else {
say "use the following full path for gitolite:";
say "\t$ENV{GL_BINDIR}/gitolite";
_print( "VERSION", $version );
}

View file

@ -1,51 +0,0 @@
#!/bin/sh
# gitolite VREF to count number of changed/new files in a push
# see gitolite docs for what the first 7 arguments mean
# inputs:
# arg-8 is a number
# arg-9 is optional, and can be "NEWFILES"
# outputs (STDOUT)
# arg-7 if the number of changed (or new, if arg-9 supplied) files is > arg-8
# otherwise nothing
# exit status:
# always 0
die() { echo "$@" >&2; exit 1; }
[ -z "$8" ] && die "not meant to be run manually"
newsha=$3
oldtree=$4
newtree=$5
refex=$7
max=$8
nf=
[ "$9" = "NEWFILES" ] && nf='--diff-filter=A'
# NO_SIGNOFF implies NEWFILES
[ "$9" = "NO_SIGNOFF" ] && nf='--diff-filter=A'
# count files against all the other commits in the system not just $oldsha
# (why? consider what is $oldtree when you create a new branch, or what is
# $oldsha when you update an old feature branch from master and then push it
count=`git log --name-only $nf --format=%n $newtree --not --all | grep . | sort -u | perl -ne '}{print "$."'`
[ $count -gt $max ] && {
# count has been exceeded. If $9 was NO_SIGNOFF there's still a chance
# for redemption -- if the top commit has a proper signed-off by line
[ "$9" = "NO_SIGNOFF" ] && {
author_email=$(git log --format=%ae -1 $newsha)
git cat-file -p $newsha |
egrep -i >/dev/null "^ *$count +new +files +signed-off by: *$author_email *$" && exit 0
echo $refex top commit message should include the text \'$count new files signed-off by: $author_email\'
exit 0
}
echo -n $refex "(too many "
[ -n "$nf" ] && echo -n "new " || echo -n "changed "
echo "files in this push)"
}
exit 0

View file

@ -1,66 +0,0 @@
#!/usr/bin/perl
# gitolite VREF to check if all *new* commits have author == pusher
# THIS IS NOT READY TO USE AS IS
# ------------------------------
# you MUST change the 'email_ok()' sub to suit *YOUR* site's
# gitolite username -> author email mapping!
# See bottom of the program for important philosophical notes.
use strict;
use warnings;
# mapping between gitolite userid and correct email address is encapsulated in
# this subroutine; change as you like
sub email_ok {
my ($author_email) = shift;
my $expected_email = "$ENV{GL_USER}\@atc.tcs.com";
return $author_email eq $expected_email;
}
my ( $ref, $old, $new ) = @ARGV;
for my $rev (`git log --format="%ae\t%h\t%s" $new --not --all`) {
chomp($rev);
my ( $author_email, $hash, $subject ) = split /\t/, $rev;
# again, we use the trick that a vref can just choose to die instead of
# passing back a vref, having it checked, etc., if it's more convenient
die "$ENV{GL_USER}, you can't push $hash authored by $author_email\n" . "\t(subject of commit was $subject)\n"
unless email_ok($author_email);
}
exit 0;
__END__
The following discussion is for people who want to enforce this check on ALL
their developers (i.e., not just the newbies).
Doing this breaks the "D" in "DVCS", forcing all your developers to work to a
centralised model as far as pushes are concerned. It prevents amending
someone else's commit and pushing (this includes rebasing, cherry-picking, and
so on, which are all impossible now). It also makes *any* off-line
collabaration between two developers useless, because neither of them can push
the result to the server.
PHBs should note that validating the committer ID is NOT the same as reviewing
the code and running QA/tests on it. If you're not reviewing/QA-ing the code,
it's probably worthless anyway. Conversely, if you *are* going to review the
code and run QA/tests anyway, then you don't really need to validate the
author email!
In a DVCS, if you *pushed* a series of commits, you have -- in some sense --
signed off on them. The most formal way to "sign" a series is to tack on and
push a gpg-signed tag, although most people don't go that far. Gitolite's log
files are designed to preserve that accountability to *some* extent, though;
see contrib/adc/who-pushed for an admin defined command that quickly and
easily tells you who *pushed* a particular commit.
Anyway, the point is that the only purpose of this script is to
* pander to someone who still has not grokked *D*VCS
OR
* tick off an item in some stupid PHB's checklist

View file

@ -1,45 +0,0 @@
#!/bin/sh
# gitolite VREF to find autogenerated files
# *completely* site specific; use it as an illustration of what can be done
# with gitolite VREFs if you wish
# see gitolite docs for what the first 7 arguments mean
# inputs:
# arg-8 is currently only one possible value: AUTOGENERATED
# outputs (STDOUT)
# arg-7 if any files changed in the push look like they were autogenerated
# otherwise nothing
# exit status:
# always 0
die() { echo "$@" >&2; exit 1; }
[ -z "$8" ] && die "not meant to be run manually"
newsha=$3
oldtree=$4
newtree=$5
refex=$7
option=$8
[ "$option" = "AUTOGENERATED" ] && {
# currently we only look for ".java" programs with the string "Generated
# by the protocol buffer compiler. DO NOT EDIT" in them.
git log --name-only $nf --format=%n $newtree --not --all |
grep . |
sort -u |
grep '\.java$' |
while read fn
do
git show "$newtree:$fn" | egrep >/dev/null \
'Generated by the protocol buffer compiler. +DO NOT EDIT' ||
continue
echo $refex
exit 0
done
}

View file

@ -1,40 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
# gitolite VREF to check max size of new binary files
# see gitolite docs for what the first 7 arguments mean
# inputs:
# arg-8 is a number
# outputs (STDOUT)
# arg-7 if any new binary files exist that are greater in size than arg-8
# *and* there is no "signed-off by" line for such a file in the top commit
# message.
#
# Otherwise nothing
# exit status:
# always 0
die "not meant to be run manually" unless $ARGV[7];
my ( $newsha, $oldtree, $newtree, $refex, $max ) = @ARGV[ 2, 3, 4, 6, 7 ];
# / (.*) +\| Bin 0 -> (\d+) bytes/
chomp( my $author_email = `git log --format=%ae -1 $newsha` );
my $msg = `git cat-file -p $newsha`;
$msg =~ s/\t/ /g; # makes our regexes simpler
for my $newbin (`git diff --stat=999,999 $oldtree $newtree | grep Bin.0.-`) {
next unless $newbin =~ /^ (.*) +\| +Bin 0 -> (\d+) bytes/;
my ( $f, $s ) = ( $1, $2 );
next if $s <= $max;
next if $msg =~ /^ *$f +signed-off by: *$author_email *$/mi;
print "$refex $f is larger than $max";
}
exit 0

View file

@ -1,49 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
# gitolite VREF to check if there are any merge commits in the current push.
# THIS IS DEMO CODE; please read all comments below as well as
# doc/vref.mkd before trying to use this.
# usage in conf/gitolite.conf goes like this:
# - VREF/MERGE_CHECK/master = @all
# # reject only if the merge commit is being pushed to the master branch
# - VREF/MERGE_CHECK = @all
# # reject merge commits to any branch
my $ref = $ARGV[0];
my $oldsha = $ARGV[1];
my $newsha = $ARGV[2];
my $refex = $ARGV[6];
# The following code duplicates some code from parse_conf_line() and some from
# check_ref(). This duplication is the only thing that is preventing me from
# removing the "M" permission code from 'core' gitolite and using this
# instead. However, it does demonstrate how you would do this if you had to
# create any other similar features, for example someone wanted "no non-merge
# first-parent", which is far too specific for me to add to 'core'.
# -- begin duplication --
my $branch_refex = $ARGV[7] || '';
if ($branch_refex) {
$branch_refex =~ m(^refs/) or $branch_refex =~ s(^)(refs/heads/);
} else {
$branch_refex = 'refs/.*';
}
exit 0 unless $ref =~ /^$branch_refex/;
# -- end duplication --
# we can't run this check for tag creation or new branch creation, because
# 'git log' does not deal well with $oldsha = '0' x 40.
if ( $oldsha eq "0" x 40 or $newsha eq "0" x 40 ) {
print STDERR "ref create/delete ignored for purposes of merge-check\n";
exit 0;
}
my $ret = `git rev-list -n 1 --merges $oldsha..$newsha`;
print "$refex FATAL: merge commits not allowed\n" if $ret =~ /./;
exit 0;

View file

@ -1,80 +0,0 @@
#!/bin/sh
# gitolite VREF to count votes before allowing pushes to certain branches.
# This approximates gerrit's voting (but it is SHA based; I believe Gerrit is
# more "changeset" based). Here's how it works:
# - A normal developer "bob" proposes changes to master by pushing a commit to
# "pers/bob/master", then informs the voting members by email.
# - Some or all of the voting members fetch and examine the commit. If they
# approve, they "vote" for the commit like so. For example, say voting
# member "alice" fetched bob's proposed commit into "bob-master" on her
# clone, then tested or reviewed it. She would approve it by running:
# git push origin bob-master:votes/alice/master
# - Once enough votes have been tallied (hopefully there is normal team
# communication that says "hey I approved your commit", or it can be checked
# by 'git ls-remote origin' anyway), Bob, or any developer, can push the
# same commit (same SHA) to master and the push will succeed.
# - Finally, a "trusted" developer can push a commit to master without
# worrying about the voting restriction at all.
# The config for this example would look like this:
# repo foo
# # allow personal branches (to submit proposed changes)
# RW+ pers/USER/ = @devs
# - pers/ = @all
#
# # allow only voters to vote
# RW+ votes/USER/ = @voters
# - votes/ = @all
#
# # normal access rules go here; should allow *someone* to push master
# RW+ = @devs
#
# # 2 votes required to push master, but trusted devs don't have this restriction
# RW+ VREF/VOTES/2/master = @trusted-devs
# - VREF/VOTES/2/master = @devs
# Note: "2 votes required to push master" means at least 2 refs matching
# "votes/*/master" have the same SHA as the one currently being pushed.
# ----------------------------------------------------------------------
# see gitolite docs for what the first 7 arguments mean
# inputs:
# arg-8 is a number; see below
# arg-9 is a simple branch name (i.e., "master", etc). Currently this code
# does NOT do vote counting for branch names with more than one component
# (like foo/bar).
# outputs (STDOUT)
# nothing
# exit status:
# always 0
die() { echo "$@" >&2; exit 1; }
[ -z "$8" ] && die "not meant to be run manually"
ref=$1
newsha=$3
refex=$7
votes_needed=$8
branch=$9
# nothing to do if the branch being pushed is not "master" (using our example)
[ "$ref" = "refs/heads/$branch" ] || exit 0
# find how many votes have come in
votes=`git for-each-ref refs/heads/votes/*/$branch | grep -c $newsha`
# send back a vref if we don't have the minimum votes needed. For trusted
# developers this will invoke the RW+ rule and pass anyway, but for others it
# will invoke the "-" rule and fail.
[ $votes -ge $votes_needed ] || echo $refex "require at least $votes_needed votes to push $branch"
exit 0

View file

@ -1,36 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Common;
# gitolite VREF to lock and unlock (binary) files. Requires companion command
# 'lock' to be enabled; see doc/locking.mkd for details.
# ----------------------------------------------------------------------
# see gitolite docs for what the first 7 arguments mean
die "not meant to be run manually" unless $ARGV[6];
my $ff = "$ENV{GL_REPO_BASE}/$ENV{GL_REPO}.git/gl-locks";
exit 0 unless -f $ff;
our %locks;
my $t = slurp($ff);
eval $t;
_die "do '$ff' failed with '$@', contact your administrator" if $@;
my ( $oldtree, $newtree, $refex ) = @ARGV[ 3, 4, 6 ];
for my $file (`git diff --name-only $oldtree $newtree` ) {
chomp($file);
if ($locks{$file} and $locks{$file}{USER} ne $ENV{GL_USER}) {
print "$refex '$file' locked by '$locks{$file}{USER}'";
last;
}
}
exit 0

View file

@ -1,41 +0,0 @@
#!/bin/sh
# push updated branches back to the "main" repo.
# This must be run as the *last* VREF, though it doesn't matter what
# permission you give to it
die() { echo "$@" >&2; exit 1; }
repo=$GL_REPO
user=$GL_USER
ref=$1 # we're running like an update hook
old=$2
new=$3
# never send any STDOUT back, to avoid looking like a ref. If we fail, git
# will catch it by our exit code
exec >&2
main=`git config --file $GL_REPO_BASE/$repo.git/config --get gitolite.partialCopyOf`;
[ -z "$main" ] && exit 0
rand=$$
export GL_BYPASS_ACCESS_CHECKS=1
if [ "$new" = "0000000000000000000000000000000000000000" ]
then
# special case for deleting a ref (this is why it is important to put this
# VREF as the last one; if we got this far he is allowed to delete it)
git push -f $GL_REPO_BASE/$main.git :$ref || die "FATAL: failed to delete $ref"
exit 0
fi
git push -f $GL_REPO_BASE/$main.git $new:refs/partial/br-$rand || die "FATAL: failed to send $new"
cd $GL_REPO_BASE/$main.git
git update-ref -d refs/partial/br-$rand
git update-ref $ref $new $old || die "FATAL: update-ref for $ref failed"
exit 0

View file

@ -1,75 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
my $rule = $ARGV[7];
die "\n\nFATAL: GL_REFEX_EXPR_ doesn't exist\n(your admin probably forgot the rc file change needed for this to work)\n\n"
unless exists $ENV{"GL_REFEX_EXPR_" . $rule};
my $res = $ENV{"GL_REFEX_EXPR_" . $rule} || 0;
print "$ARGV[6] ($res)\n" if $res;
exit 0;
__END__
Documentation for the refex-expression evaluation feature
First, make sure you have both the VREF and the trigger scripts
(src/VREF/refex-expr and src/lib/Gitolite/Triggers/RefexExpr.pm)
Next, add this to the ACCESS_2 list in the rc file:
'RefexExpr::access_2',
For the rest, we'll use this example:
* user u1 can push foo to some other branch, and anything else to the master
branch, but not foo to the master branch
* user u2 is allowed to push either 'doc/' or 'src/' but not both
Here's the conf file extract:
repo testing
RW+ master = u1 # line 1
RW+ = @all # line 2
RW+ VREF/NAME/foo = u1
RW+ VREF/NAME/doc/ = u2
RW+ VREF/NAME/src/ = u2
# set up 2 refex expressions, named e1, e2
option refex-expr.e1 = master and VREF/NAME/foo
option refex-expr.e2 = VREF/NAME/doc/ and VREF/NAME/src/
# now deny users if the corresponding expression is true
- VREF/refex-expr/e1 = u1
- VREF/refex-expr/e2 = u2
Here are some IMPORTANT notes:
* You MUST place VREF/refex-expr rules at the end. (Only 'partial-copy', if
you use it, must come later).
* You MUST explicitly permit the refexes used in your refex expressions. If
you have more generic rules, the specific ones must come first.
For example, without line 1, the refex recorded for user u1 will come from
line 2, (so it will be 'refs/.*'), and 'master' in the refex expressions
will never have a true value.
* (corollary) make sure you use the exact same refex in the expression as
you did on the original rule line. E.g., a missing slash at the end will
mess things up.
* You can use any logical expression using refexes as operands and using
these operators:
and not xor or
Parens are not allowed.
If a refex has passed, it will have a 'true' value, else it will be false.
The result of the evaluation, after these substitutions, will be the
result of the refex-expr VREF.

View file

@ -1,131 +0,0 @@
#!/bin/sh
# ----------------------------------------------------------------------
# ADMINISTRATOR NOTES:
# ----------------------------------------------------------------------
# - set TRASH_CAN in the rc if you don't like the default. It should be
# relative to GL_REPO_BASE or an absolute value. It should also be on the
# same filesystem as GL_REPO_BASE, otherwise the 'mv' will take too long.
# - you could set TRASH_SUFFIX also but I recomend you leave it as it is
# - run a cron job to delete old repos based on age (the TRASH_SUFFIX has a
# timestamp); your choice how/how often you do that
# - you can completely disable the 'rm' command by setting an rc variable
# called D_DISABLE_RM to "1".
# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
# Usage: ssh git@host D <subcommand> <argument>
#
# The whimsically named "D" command deletes repos ("D" is a counterpart to the
# "C" permission which lets you create repos. Which also means that, just
# like "C", it only works for wild repos).
#
# There are two kinds of deletions: 'rm' removes a repo completely, while
# 'trash' moves it to a trashcan which can be recovered later (upto a time
# limit that your admin will tell you).
#
# The 'rm', 'lock', and 'unlock' subcommands:
# Initially, all repos are "locked" against 'rm'. The correct sequence is
# ssh git@host D unlock repo
# ssh git@host D rm repo
# Since the initial condition is always locked, the "lock" command is
# rarely used but it is there if you want it.
#
# The 'trash', 'list-trash', and 'restore' subcommands:
# You can 'trash' a repo, which moves it to a special place:
# ssh git@host D trash repo
# You can then 'list-trash'
# ssh git@host D list-trash
# which prints something like
# repo/2012-04-11_05:58:51
# allowing you to restore by saying
# ssh git@host D restore repo/2012-04-11_05:58:51
die() { echo "$@" >&2; exit 1; }
usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; }
[ -z "$1" ] && usage
[ "$1" = "-h" ] && usage
[ "$1" != "list-trash" ] && [ -z "$2" ] && usage
[ -z "$GL_USER" ] && die GL_USER not set
# ----------------------------------------------------------------------
cmd=$1
repo=$2
# ----------------------------------------------------------------------
RB=`gitolite query-rc GL_REPO_BASE`; cd $RB
TRASH_CAN=`gitolite query-rc TRASH_CAN`; tcan=Trash; TRASH_CAN=${TRASH_CAN:-$tcan}
TRASH_SUFFIX=`gitolite query-rc TRASH_SUFFIX`; tsuf=`date +%Y-%m-%d_%H:%M:%S`; TRASH_SUFFIX=${TRASH_SUFFIX:-$tsuf}
# ----------------------------------------------------------------------
owner_or_die() {
gitolite creator "$repo" $GL_USER || die You are not authorised
}
# ----------------------------------------------------------------------
if [ "$cmd" = "rm" ]
then
gitolite query-rc -q D_DISABLE_RM && die "sorry, 'unlock' and 'rm' are disabled"
owner_or_die
[ -f $repo.git/gl-rm-ok ] || die "'$repo' is locked!"
rm -rf $repo.git
echo "'$repo' is now gone!"
elif [ "$cmd" = "lock" ]
then
owner_or_die
rm -f $repo.git/gl-rm-ok
echo "'$repo' is now locked"
elif [ "$cmd" = "unlock" ]
then
gitolite query-rc -q D_DISABLE_RM && die "sorry, 'unlock' and 'rm' are disabled"
owner_or_die
touch $repo.git/gl-rm-ok
echo "'$repo' is now unlocked"
elif [ "$cmd" = "trash" ]
then
owner_or_die
mkdir -p $TRASH_CAN/$repo 2>/dev/null || die "failed creating directory in trashcan"
[ -d $TRASH_CAN/$repo/$TRASH_SUFFIX ] && die "try again in a few seconds..."
mv $repo.git $TRASH_CAN/$repo/$TRASH_SUFFIX
echo "'$repo' moved to trashcan"
elif [ "$cmd" = "list-trash" ]
then
cd $TRASH_CAN 2>/dev/null || exit 0
find . -name gl-creator | sort | while read t
do
owner=
owner=`cat "$t"`
[ "$owner" = "$GL_USER" ] && dirname $t
done | cut -c3-
elif [ "$cmd" = "restore" ]
then
owner=
owner=`cat $TRASH_CAN/$repo/gl-creator 2>/dev/null`
[ "$owner" = "$GL_USER" ] || die "'$repo' is not yours!"
cd $TRASH_CAN
realrepo=`dirname $repo`
[ -d $RB/$realrepo.git ] && die "'$realrepo' already exists"
mv $repo $RB/$realrepo.git
echo "'$repo' restored to '$realrepo'"
else
die "unknown subcommand '$cmd'"
fi

View file

@ -1,74 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
=for usage
Usage: gitolite access [-q] <repo> <user> <perm> <ref>
Print access rights for arguments given. The string printed has the word
DENIED in it if access was denied. With '-q', returns only an exit code
(shell truth, not perl truth -- 0 is success).
- repo: mandatory
- user: mandatory
- perm: defauts to '+'. Valid values: R, W, +, C, D, M
- ref: defauts to 'any'. See notes below
Notes:
- ref: Any fully qualified ref ('refs/heads/master', not 'master') is fine.
The 'any' ref is special -- it ignores deny rules (see docs for what this
means and exceptions).
For each case where access is not denied, one line is printed like this:
reponame<tab>username<tab>access rights
This is orders of magnitude faster than running the command multiple times;
you'll notice if you have more than a hundred or so repos.
Advanced uses: see src/triggers/post-compile/update-git-daemon-access-list for
a good example.
=cut
usage() if not @ARGV or $ARGV[0] eq '-h';
my $quiet = 0;
if ( $ARGV[0] eq '-q' ) { $quiet = 1; shift @ARGV; }
my ( $repo, $user, $aa, $ref ) = @ARGV;
$aa ||= '+';
$ref ||= 'any';
_die "invalid perm" if not( $aa and $aa =~ /^(R|W|\+|C|D|M|\^C)$/ );
_die "invalid ref name" if not( $ref and $ref =~ $REPONAME_PATT );
my $ret = '';
if ( $repo ne '%' and $user ne '%' ) {
# single repo, single user; no STDIN
$ret = access( $repo, $user, $aa, $ref );
if ( $ret =~ /DENIED/ ) {
print "$ret\n" unless $quiet;
exit 1;
}
print "$ret\n" unless $quiet;
exit 0;
}
$repo = '' if $repo eq '%';
$user = '' if $user eq '%';
_die "'-q' doesn't go with using a pipe" if $quiet;
@ARGV = ();
while (<>) {
my @in = split;
my $r = $repo || shift @in;
my $u = $user || shift @in;
$ret = access( $r, $u, $aa, $ref );
print "$r\t$u\t$ret\n";
}

View file

@ -1,15 +0,0 @@
#!/bin/bash
# Usage: ssh git@host create <repo>
#
# Create wild repo.
die() { echo "$@" >&2; exit 1; }
usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; }
[ -z "$1" ] && usage
[ -z "$2" ] || usage
[ "$1" = "-h" ] && usage
[ -z "$GL_USER" ] && die GL_USER not set
# ----------------------------------------------------------------------
exec $GL_BINDIR/commands/perms -c "$@" < /dev/null

View file

@ -1,40 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
=for usage
Usage: gitolite creator [-n] <reponame> [<username>]
Print the creator name for the repo. A '-n' suppresses the newline.
When an optional username is supplied, it checks if the user is the creator of
the repo and returns an exit code (shell truth, 0 for success) instead of
printing anything, which makes it possible to do this in shell:
if gitolite creator someRepo someUser
then
...
=cut
usage() if not @ARGV or $ARGV[0] eq '-h';
my $nl = "\n";
if ( $ARGV[0] eq '-n' ) {
$nl = '';
shift;
}
my $repo = shift;
my $user = shift || '';
my $creator = '';
$creator = creator($repo) if not repo_missing($repo);
if ($user) {
exit 0 if $creator eq $user;
exit 1;
}
return ( $creator eq $user ) if $user;
print "$creator$nl";

View file

@ -1,43 +0,0 @@
#!/bin/sh
# Usage: ssh git@host desc <repo>
# ssh git@host desc <repo> <description string>
#
# Show or set description for user-created ("wild") repo.
die() { echo "$@" >&2; exit 1; }
usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; }
[ -z "$1" ] && usage
[ "$1" = "-h" ] && usage
[ -z "$GL_USER" ] && die GL_USER not set
# ----------------------------------------------------------------------
repo=$1; shift
# this shell script takes arguments that are completely under the user's
# control, so make sure you quote those suckers!
# kernel.org needs 'desc' to be available to people who have "RW" or above,
# not just the "creator". In fact they need it for non-wild repos so there
# *is* no creator.
if gitolite query-rc -q WRITER_CAN_UPDATE_DESC
then
gitolite access -q "$repo" $GL_USER W any || die You are not authorised
else
gitolite creator "$repo" $GL_USER || die You are not authorised
fi
# if it passes, $repo is a valid repo name so it is known to contain only sane
# characters. This is because 'gitolite creator' return true only if there
# *is* a repo of that name and it has a gl-creator file that contains the same
# text as $GL_USER.
descfile=`gitolite query-rc GL_REPO_BASE`/"$repo".git/description
if [ -z "$1" ]
then
[ -r "$descfile" ] && cat "$descfile"
exit 0
fi
echo "$*" > "$descfile"

View file

@ -1,59 +0,0 @@
#!/bin/sh
# Usage: ssh git@host fork <repo1> <repo2>
#
# Forks repo1 to repo2. You must have read permissions on repo1, and create
# ("C") permissions for repo2, which of course must not exist.
#
# A fork is functionally the same as cloning repo1 to a client and pushing it
# to a new repo2. It's just a little more efficient, not just in network
# traffic but because it uses git clone's "-l" option to share the object
# store also, so it is likely to be almost instantaneous, regardless of how
# big the repo actually is.
die() { echo "$@" >&2; exit 1; }
usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; }
[ -z "$1" ] && usage
[ "$1" = "-h" ] && usage
[ -z "$GL_USER" ] && die GL_USER not set
# ----------------------------------------------------------------------
from=$1; shift
to=$1; shift
[ -z "$to" ] && usage
gitolite access -q "$from" $GL_USER R any || die "'$from' does not exist or you are not allowed to read it"
gitolite access -q "$to" $GL_USER ^C any || die "'$to' already exists or you are not allowed to create it"
# ----------------------------------------------------------------------
# IMPORTANT NOTE: checking whether someone can create a repo is done as above.
# However, make sure that the env var GL_USER is set, and that too to the same
# value as arg-2 of the access command), otherwise it won't work.
# Ideally, you'll leave such code to me. There's a reason ^C is not listed in
# the help message for 'gitolite access'.
# ----------------------------------------------------------------------
# clone $from to $to
git clone --bare -l $GL_REPO_BASE/$from.git $GL_REPO_BASE/$to.git
[ $? -ne 0 ] && exit 1
echo "$from forked to $to" >&2
# fix up creator, default role permissions (gl-perms), and hooks
cd $GL_REPO_BASE/$to.git
echo $GL_USER > gl-creator
touch gl-perms
if gitolite query-rc -q DEFAULT_ROLE_PERMS
then
gitolite query-rc DEFAULT_ROLE_PERMS > gl-perms
fi
ln -sf `gitolite query-rc GL_ADMIN_BASE`/hooks/common/* hooks
# record where you came from
echo "$from" > gl-forked-from
# trigger post_create
gitolite trigger POST_CREATE $to $GL_USER fork

View file

@ -1,86 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
=for usage
Usage: gitolite git-config [-n] [-q] [-r] <repo> <key|pattern>
Print git config keys and values for the given repo. The key is either a full
key, or, if '-r' is supplied, a regex that is applied to all available keys.
-q exit code only (shell truth; 0 is success)
-n suppress trailing newline when used as key (not pattern)
-r treat key as regex pattern (unanchored)
Examples:
gitolite git-config repo gitweb.owner
gitolite git-config -q repo gitweb.owner
gitolite git-config -r repo gitweb
When the key is treated as a pattern, prints:
reponame<tab>key<tab>value<newline>
Otherwise the output is just the value.
Finally, see the advanced use section of 'gitolite access -h' -- you can do
something similar here also:
gitolite list-phy-repos | gitolite git-config -r % gitweb\\. | cut -f1 > ~/projects.list
=cut
usage() if not @ARGV;
my ( $help, $nonl, $quiet, $regex ) = (0) x 4;
GetOptions(
'n' => \$nonl,
'q' => \$quiet,
'r' => \$regex,
'h' => \$help,
) or usage();
my ( $repo, $key ) = @ARGV;
usage() unless $key;
my $ret = '';
if ( $repo ne '%' and $key ne '%' ) {
# single repo, single key; no STDIN
$key = "^\Q$key\E\$" unless $regex;
$ret = git_config( $repo, $key );
# if the key is not a regex, it should match at most one item
_die "found more than one entry for '$key'" if not $regex and scalar( keys %$ret ) > 1;
# unlike access, there's nothing to print if we don't find any matching keys
exit 1 unless %$ret;
if ($regex) {
map { print "$repo\t$_\t" . $ret->{$_} . "\n" } sort keys %$ret unless $quiet;
} else {
map { print $ret->{$_} . ( $nonl ? "" : "\n" ) } sort keys %$ret unless $quiet;
}
exit 0;
}
$repo = '' if $repo eq '%';
$key = '' if $key eq '%';
_die "'-q' doesn't go with using a pipe" if $quiet;
@ARGV = ();
while (<>) {
my @in = split;
my $r = $repo || shift @in;
my $k = $key || shift @in;
$k = "^\Q$k\E\$" unless $regex;
$ret = git_config( $r, $k );
next unless %$ret;
map { print "$r\t$_\t" . $ret->{$_} . "\n" } sort keys %$ret;
}

View file

@ -1,42 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
=for usage
Usage: ssh git@host help # via ssh
gitolite help # directly on server command line
Prints a list of custom commands available at this gitolite installation.
Each command has its own help, accessed by passing it '-h' again.
=cut
usage() if @ARGV;
my $user = $ENV{GL_USER} || '';
print "hello" . ( $user ? " $user" : "" ) . ", this is gitolite3 " . version() . " on git " . substr( `git --version`, 12 ) . "\n";
print "list of " . ( $user ? "remote" : "gitolite" ) . " commands available:\n\n";
my %list = (list_x( $ENV{GL_BINDIR}), list_x($rc{LOCAL_CODE} || ''));
for (sort keys %list) {
print "\t$list{$_}" if $ENV{D};
print "\t$_\n" if not $user or $rc{COMMANDS}{$_};
}
print "\n";
exit 0;
# ------------------------------------------------------------------------------
sub list_x {
my $d = shift;
return unless $d;
return unless -d "$d/commands";
_chdir "$d/commands";
return map { $_ => $d } grep { -x $_ } map { chomp; s(^./)(); $_ } `find . -type f -o -type l|sort`;
}

View file

@ -1,44 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
=for usage
Usage: ssh git@host htpasswd
Sets your htpasswd, assuming your admin has enabled it.
(Admins: You need to add HTPASSWD_FILE to the rc file, pointing to an
existing, writable, but possibly an initially empty, file, as well as adding
an entry for 'htpasswd' to the COMMANDS hash).
=cut
# usage and sanity checks
usage() if @ARGV and $ARGV[0] eq '-h';
$ENV{GL_USER} or _die "GL_USER not set";
my $htpasswd_file = $rc{HTPASSWD_FILE} || '';
die "htpasswd not enabled\n" unless $htpasswd_file;
die "$htpasswd_file doesn't exist or is not writable\n" unless -w $htpasswd_file;
# prompt
$|++;
print <<EOFhtp;
Please type in your new htpasswd at the prompt. You only have to type it once.
NOTE THAT THE PASSWORD WILL BE ECHOED, so please make sure no one is
shoulder-surfing, and make sure you clear your screen as well as scrollback
history after you're done (or close your terminal instance).
EOFhtp
print "new htpasswd: ";
# get the password and run htpasswd
my $password = <>;
$password =~ s/[\n\r]*$//;
die "empty passwords are not allowed\n" unless $password;
my $res = system("htpasswd", "-mb", $htpasswd_file, $ENV{GL_USER}, $password);
die "htpasswd command seems to have failed with return code: $res.\n" if $res;

View file

@ -1,107 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
=for args
Usage: gitolite info [-lc] [-ld] [<repo name pattern>]
List all existing repos you can access, as well as repo name patterns you can
create repos from (if any).
'-lc' lists creators as an additional field at the end.
'-ld' lists description as an additional field at the end.
The optional pattern is an unanchored regex that will limit the repos
searched, in both cases. It might speed up things a little if you have more
than a few thousand repos.
=cut
# these two are globals
my ( $lc, $ld, $patt ) = args();
print_version();
print_patterns(); # repos he can create for himself
print_phy_repos(); # repos already created
print "\n$rc{SITE_INFO}\n" if $rc{SITE_INFO};
# ----------------------------------------------------------------------
sub args {
my ( $lc, $ld, $patt ) = ( '', '', '' );
my $help = '';
GetOptions(
'lc' => \$lc,
'ld' => \$ld,
'h' => \$help,
) or usage();
usage() if @ARGV > 1 or $help;
$patt = shift @ARGV || '.';
return ( $lc, $ld, $patt );
}
sub print_version {
chomp( my $hn = `hostname -s 2>/dev/null || hostname` );
my $gv = substr( `git --version`, 12 );
$ENV{GL_USER} or _die "GL_USER not set";
print "hello $ENV{GL_USER}, this is " . ($ENV{USER} || "httpd") . "\@$hn running gitolite3 " . version() . " on git $gv\n";
}
sub print_patterns {
my ( $repos, @aa );
# find repo patterns only, call them with ^C flag included
@$repos = grep { !/$REPONAME_PATT/ } @{ lister_dispatch('list-repos')->() };
@aa = qw(R W ^C);
listem( $repos, '', '', @aa );
# but squelch the 'lc' and 'ld' flags for these
}
sub print_phy_repos {
my ( $repos, @aa );
# now get the actual repos and get R or W only
_chdir( $rc{GL_REPO_BASE} );
$repos = list_phy_repos(1);
@aa = qw(R W);
listem( $repos, $lc, $ld, @aa );
}
sub listem {
my ( $repos, $lc, $ld, @aa ) = @_;
my $creator = '';
for my $repo (@$repos) {
next unless $repo =~ /$patt/;
my $perm = '';
$creator = creator($repo) if $lc;
my $desc = '';
for my $d ("$ENV{GL_REPO_BASE}/$repo.git/description") {
next unless $ld and -r $d;
$desc = slurp($d);
chomp($desc);
}
for my $aa (@aa) {
my $ret = access( $repo, $ENV{GL_USER}, $aa, 'any' );
$perm .= ( $ret =~ /DENIED/ ? " " : " $aa" );
}
$perm =~ s/\^//;
next unless $perm =~ /\S/;
print "$perm\t$repo";
print "\t$creator" if $lc;
print "\t$desc" if $ld;
print "\n";
}
}

View file

@ -1,55 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Common;
use Gitolite::Conf::Load;
=for usage
Usage: gitolite list-dangling-repos
List all existing repos that no one can access remotely any more. They could
be normal repos that were taken out of "repo" statements in the conf file, or
wildcard repos whose matching "wild" pattern was taken out or changed so it no
longer matches.
I would advise caution if you use this as a basis for deleting repos from the
file system. A bug in this program could cause you to lose important data!
=cut
usage() if @ARGV and $ARGV[0] eq '-h';
# get the two lists we need. %repos is the list of repos in "repo" statements
# in the conf file. %phy_repos is the list of actual repos on disk. Our job
# is to cull %phy_repos of all keys that have a matching key in %repos, where
# "matching" means "string equal" or "regex match".
my %repos = map { chomp; $_ => 1 } `gitolite list-repos`;
for my $r ( grep /^@/, keys %repos ) {
map { chomp; $repos{$_} = 1; } `gitolite list-members $r`;
}
my %phy_repos = map { chomp; $_ => 1 } `gitolite list-phy-repos`;
# Remove exact matches. But for repo names like "gtk+", you could have
# collapsed this into the next step (the regex match).
for my $pr (keys %phy_repos) {
next unless exists $repos{$pr};
delete $repos{$pr};
delete $phy_repos{$pr};
}
# Remove regex matches.
for my $pr (keys %phy_repos) {
my $matched = 0;
my $pr2 = Gitolite::Conf::Load::generic_name($pr);
for my $r (keys %repos) {
if ($pr =~ /^$r$/ or $pr2 =~ /^$r$/) {
$matched = 1;
next;
}
}
delete $phy_repos{$pr} if $matched;
}
# what's left in %phy_repos are dangling repos.
print join("\n", sort keys %phy_repos), "\n";

View file

@ -1,124 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
# gitolite command to lock and unlock (binary) files and deal with locks.
=for usage
Usage: ssh git@host lock -l <repo> <file> # lock a file
ssh git@host lock -u <repo> <file> # unlock a file
ssh git@host lock --break <repo> <file> # break someone else's lock
ssh git@host lock -ls <repo> # list locked files for repo
See doc/locking.mkd for other details.
=cut
usage() if not @ARGV or $ARGV[0] eq '-h';
$ENV{GL_USER} or _die "GL_USER not set";
my $op = '';
$op = 'lock' if $ARGV[0] eq '-l';
$op = 'unlock' if $ARGV[0] eq '-u';
$op = 'break' if $ARGV[0] eq '--break';
$op = 'list' if $ARGV[0] eq '-ls';
usage() if not $op;
shift;
my $repo = shift;
_die "You are not authorised" if access( $repo, $ENV{GL_USER}, 'W', 'any' ) =~ /DENIED/;
_die "You are not authorised" if $op eq 'break' and access( $repo, $ENV{GL_USER}, '+', 'any' ) =~ /DENIED/;
my $file = shift || '';
usage() if $op ne 'list' and not $file;
_chdir( $ENV{GL_REPO_BASE} );
_chdir("$repo.git");
my $ff = "gl-locks";
if ( $op eq 'lock' ) {
f_lock( $repo, $file );
} elsif ( $op eq 'unlock' ) {
f_unlock( $repo, $file );
} elsif ( $op eq 'break' ) {
f_break( $repo, $file );
} elsif ( $op eq 'list' ) {
f_list($repo);
}
# ----------------------------------------------------------------------
# everything below assumes we have already chdir'd to "$repo.git". Also, $ff
# is used as a global.
sub f_lock {
my ( $repo, $file ) = @_;
my %locks = get_locks();
_die "'$file' locked by '$locks{$file}{USER}' since " . localtime( $locks{$file}{TIME} ) if $locks{$file}{USER};
$locks{$file}{USER} = $ENV{GL_USER};
$locks{$file}{TIME} = time;
put_locks(%locks);
}
sub f_unlock {
my ( $repo, $file ) = @_;
my %locks = get_locks();
_die "'$file' not locked by '$ENV{GL_USER}'" if ( $locks{$file}{USER} || '' ) ne $ENV{GL_USER};
delete $locks{$file};
put_locks(%locks);
}
sub f_break {
my ( $repo, $file ) = @_;
my %locks = get_locks();
_die "'$file' was not locked" unless $locks{$file};
push @{ $locks{BREAKS} }, time . " $ENV{GL_USER} $locks{$file}{USER} $locks{$file}{TIME} $file";
delete $locks{$file};
put_locks(%locks);
}
sub f_list {
my $repo = shift;
my %locks = get_locks();
print "\n# locks held:\n\n";
map { print "$locks{$_}{USER}\t$_\t(" . scalar(localtime($locks{$_}{TIME})) . ")\n" } grep { $_ ne 'BREAKS' } sort keys %locks;
print "\n# locks broken:\n\n";
for my $b ( @{ $locks{BREAKS} } ) {
my ( $when, $who, $whose, $how_old, $what ) = split ' ', $b;
print "$who\t$what\t(" . scalar( localtime($when) ) . ")\t(locked by $whose at " . scalar( localtime($how_old) ) . ")\n";
}
}
sub get_locks {
if ( -f $ff ) {
our %locks;
my $t = slurp($ff);
eval $t;
_die "do '$ff' failed with '$@', contact your administrator" if $@;
return %locks;
}
return ();
}
sub put_locks {
my %locks = @_;
use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
my $dumped_data = Data::Dumper->Dump( [ \%locks ], [qw(*locks)] );
_print( $ff, $dumped_data );
}

View file

@ -1,77 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
my $tid;
BEGIN {
$tid = $ENV{GL_TID} || 0;
delete $ENV{GL_TID};
}
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
=for usage
Usage 1: gitolite mirror push <slave> <repo>
Usage 2: ssh git@master-server mirror push <slave> <repo>
Forces a push of one repo to one slave.
Usage 1 is directly on the master server. Nothing is checked; if the slave
accepts it, the push happens, even if the slave is not in any slaves
option. This is how you do delayed or lagged pushes to servers that do not
need real-time updates or have bandwidth/connectivity issues.
Usage 2 can be initiated by *any* user who has *any* gitolite access to the
master server, but it checks that the slave is in one of the slaves options
before doing the push.
=cut
usage() if not @ARGV or $ARGV[0] eq '-h';
_die "HOSTNAME not set" if not $rc{HOSTNAME};
my ( $cmd, $host, $repo ) = @ARGV;
usage() if not $repo;
if ( $cmd eq 'push' ) {
valid_slave( $host, $repo ) if exists $ENV{GL_USER};
# will die if host not in slaves for repo
trace( 1, "TID=$tid host=$host repo=$repo", "gitolite mirror push started" );
_chdir( $rc{GL_REPO_BASE} );
_chdir("$repo.git");
if (-f "gl-creator") {
# try to propagate the wild repo, including creator name and gl-perms
my $creator = `cat gl-creator`; chomp($creator);
trace(1, `cat gl-perms 2>/dev/null | ssh $host CREATOR=$creator perms -c \\'$repo\\' 2>/dev/null`);
}
my $errors = 0;
for (`git push --mirror $host:$repo 2>&1`) {
$errors = 1 if $?;
print STDERR "$_" if -t STDERR or exists $ENV{GL_USER};
chomp;
if (/FATAL/) {
$errors = 1;
gl_log( 'mirror', $_ );
} else {
trace( 1, "mirror: $_" );
}
}
exit $errors;
}
sub valid_slave {
my ( $host, $repo ) = @_;
_die "invalid repo '$repo'" unless $repo =~ $REPONAME_PATT;
my $ref = git_config( $repo, "^gitolite-options\\.mirror\\.slaves.*" );
my %list = map { $_ => 1 } map { split } values %$ref;
_die "'$host' not a valid slave for '$repo'" unless $list{$host};
}

View file

@ -1,127 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
=for usage
Usage: ssh git@host perms -l <repo>
ssh git@host perms <repo> - <rolename> <username>
ssh git@host perms <repo> + <rolename> <username>
List or set permissions for user-created ("wild") repo. The first usage shown
will list the current contents of the permissions file. The other two will
change permissions, adding or removing a user from a role.
Examples:
ssh git@host perms foo + READERS user1
ssh git@host perms foo + READERS user2
ssh git@host perms foo + READERS user3
----
There is also a batch mode useful for scripting and bulk loading. Do not
combine this with the +/- mode above. This mode also accepts an optional "-c"
flag to create the repo if it does not already exist (assuming $GL_USER has
permissions to create it).
Examples:
cat copy-of-backed-up-gl-perms | ssh git@host perms <repo>
cat copy-of-backed-up-gl-perms | ssh git@host perms -c <repo>
=cut
usage() if not @ARGV or $ARGV[0] eq '-h';
$ENV{GL_USER} or _die "GL_USER not set";
my $list = 0;
if ( $ARGV[0] eq '-l' ) {
$list++;
shift;
getperms(@ARGV); # doesn't return
}
my $generic_error = "repo does not exist, or you are not authorised";
# auto-create the repo if -c passed and repo doesn't exist
if ( $ARGV[0] eq '-c' ) {
shift;
my $repo = $ARGV[0] or usage();
_die "invalid repo '$repo'" unless $repo =~ $REPONAME_PATT;
if (not -d "$rc{GL_REPO_BASE}/$repo.git") {
my $ret = access( $repo, $ENV{GL_USER}, '^C', 'any' );
_die $generic_error if $ret =~ /DENIED/;
require Gitolite::Conf::Store;
Gitolite::Conf::Store->import;
new_wild_repo( $repo, $ENV{GL_USER}, 'perms-c' );
gl_log( 'create', $repo, $ENV{GL_USER}, 'perms-c' );
}
}
my $repo = shift;
setperms(@ARGV);
_system( "gitolite", "trigger", "POST_CREATE", $repo, $ENV{GL_USER}, 'perms' );
# ----------------------------------------------------------------------
sub getperms {
my $repo = shift;
_die $generic_error if repo_missing($repo) or creator($repo) ne $ENV{GL_USER};
my $pf = "$rc{GL_REPO_BASE}/$repo.git/gl-perms";
print slurp($pf) if -f $pf;
exit 0;
}
sub setperms {
_die $generic_error if repo_missing($repo) or creator($repo) ne $ENV{GL_USER};
my $pf = "$rc{GL_REPO_BASE}/$repo.git/gl-perms";
if ( not @_ ) {
# legacy mode; pipe data in
print STDERR "'batch' mode started, waiting for input (run with '-h' for details).\n";
print STDERR "Please hit Ctrl-C if you did not intend to do this.\n";
@ARGV = ();
my @a;
for (<>) {
_die "Invalid role '$1'; check the rc file" if /(\S+)/ and not $rc{ROLES}{$1};
push @a, $_;
}
print STDERR "\n"; # make sure Ctrl-C gets caught
_print( $pf, @a );
return;
}
_die "Invalid syntax. Please re-run with '-h' for detailed usage" if @_ != 3;
my ( $op, $role, $user ) = @_;
_die "Invalid syntax. Please re-run with '-h' for detailed usage" if $op ne '+' and $op ne '-';
_die "Invalid role '$role'; check the rc file" if not $rc{ROLES}{$role};
_die "Invalid user '$user'" if not $user =~ $USERNAME_PATT;
my $text = '';
my @text = slurp($pf) if -f $pf;
my $present = grep { $_ eq "$role $user\n" } @text;
if ( $op eq '-' ) {
if ( not $present ) {
_warn "'$role $user' was not present in file";
} else {
@text = grep { $_ ne "$role $user\n" } @text;
_print( $pf, @text );
}
} else {
if ($present) {
_warn "'$role $user' already present in file";
} else {
push @text, "$role $user\n";
@text = sort @text;
_print( $pf, @text );
}
}
}

View file

@ -1,8 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
print glrc('default-text');

View file

@ -1,5 +0,0 @@
#!/bin/sh
export GL_BYPASS_ACCESS_CHECKS=1
git push "$@"

View file

@ -1,149 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Easy;
=for admins
BUNDLE SUPPORT
(1) For each repo in gitolite.conf for which you want bundle support (or
'@all', if you wish), add the following line:
option bundle = 1
Or you can say:
option bundle.ttl = <number>
A bundle file that is more than <number> seconds old (default value
86400, i.e., 1 day) is recreated on the next bundle request. Increase
this if your repo is not terribly active.
Note: a bundle file is also deleted and recreated if it contains a ref
that was then either deleted or rewound in the repo. This is checked
on every invocation.
(2) Add 'rsync' to the COMMANDS list in the rc file
GENERIC RSYNC SUPPORT
TBD
=cut
=for usage
rsync helper for gitolite
BUNDLE SUPPORT
Admins: see src/commands/rsync for setup instructions
Users:
rsync -P git@host:repo.bundle .
# downloads a file called "<basename of repo>.bundle"; repeat as
# needed till the whole thing is downloaded
git clone repo.bundle repo
cd repo
git remote set-url origin git@host:repo
git fetch origin # and maybe git pull, etc. to freshen the clone
GENERIC RSYNC SUPPORT
TBD
=cut
usage() if not @ARGV or $ARGV[0] eq '-h';
# rsync driver program. Several things can be done later, but for now it
# drives just the 'bundle' transfer.
if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /^rsync --server --sender (-[-\w=.]+ )+\. (\S+)\.bundle$/ ) {
my $repo = $2;
$repo =~ s/\.git$//;
# all errors have the same message to avoid leaking info
can_read($repo) or _die "you are not authorised";
my %config = config( $repo, "gitolite-options.bundle" ) or _die "you are not authorised";
my $ttl = $config{'gitolite-options.bundle.ttl'} || 86400; # in seconds (default 1 day)
my $bundle = bundle_create( $repo, $ttl );
$ENV{SSH_ORIGINAL_COMMAND} =~ s( \S+\.bundle)( $bundle);
trace( 1, "rsync bundle", $ENV{SSH_ORIGINAL_COMMAND} );
Gitolite::Common::_system( split ' ', $ENV{SSH_ORIGINAL_COMMAND} );
exit 0;
}
_warn "invalid rsync command '$ENV{SSH_ORIGINAL_COMMAND}'";
usage();
# ----------------------------------------------------------------------
# helpers
# ----------------------------------------------------------------------
sub bundle_create {
my ( $repo, $ttl ) = @_;
my $bundle = "$repo.bundle";
$bundle =~ s(.*/)();
my $recreate = 0;
my ( %b, %r );
if ( -f $bundle ) {
%b = map { chomp; reverse split; } `git ls-remote --heads --tags $bundle`;
%r = map { chomp; reverse split; } `git ls-remote --heads --tags .`;
for my $ref ( sort keys %b ) {
my $mtime = ( stat $bundle )[9];
if ( time() - $mtime > $ttl ) {
trace( 1, "bundle too old" );
$recreate++;
last;
}
if ( not $r{$ref} ) {
trace( 1, "ref '$ref' deleted in repo" );
$recreate++;
last;
}
if ( $r{$ref} eq $b{$ref} ) {
# same on both sides; ignore
delete $r{$ref};
delete $b{$ref};
next;
}
`git rev-list --count --left-right $b{$ref}...$r{$ref}` =~ /^(\d+)\s+(\d+)$/ or _die "git too old";
if ($1) {
trace( 1, "ref '$ref' rewound in repo" );
$recreate++;
last;
}
}
} else {
trace( 1, "no bundle found" );
$recreate++;
}
return $bundle if not $recreate;
trace( 1, "creating bundle for '$repo'" );
-f $bundle and ( unlink $bundle or die "a horrible death" );
system("git bundle create $bundle --branches --tags >&2");
return $bundle;
}
sub trace {
Gitolite::Common::trace(@_);
}

View file

@ -1,192 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
# complete rewrite of the sshkeys-lint program. Usage has changed, see
# usage() function or run without arguments.
use Getopt::Long;
my $admin = 0;
my $quiet = 0;
my $help = 0;
GetOptions( 'admin|a=s' => \$admin, 'quiet|q' => \$quiet, 'help|h' => \$help );
use Data::Dumper;
$Data::Dumper::Deepcopy = 1;
$|++;
my $in_gl_section = 0;
my $warnings = 0;
sub dbg {
use Data::Dumper;
for my $i (@_) {
print STDERR "DBG: " . Dumper($i);
}
}
sub msg {
my $warning = shift;
return if $quiet and not $warning;
$warnings++ if $warning;
print "sshkeys-lint: " . ( $warning ? "WARNING: " : "" ) . $_ for @_;
}
usage() if $help;
our @pubkeyfiles = @ARGV; @ARGV = ();
my $kd = "$ENV{HOME}/.gitolite/keydir";
if ( not @pubkeyfiles ) {
chomp( @pubkeyfiles = `find $kd -type f -name "*.pub" | sort` );
}
if ( -t STDIN ) {
@ARGV = ("$ENV{HOME}/.ssh/authorized_keys");
}
# ------------------------------------------------------------------------
my @authkeys;
my %seen_fprints;
my %pkf_by_fp;
msg 0, "==== checking authkeys file:\n";
fill_authkeys(); # uses up STDIN
if ($admin) {
my $fp = fprint("$admin.pub");
my $fpu = ( $fp && $seen_fprints{$fp}{user} || 'no access' );
# dbg("fpu = $fpu, admin=$admin");
#<<<
die "\t\t*** FATAL ***\n" .
"$admin.pub maps to $fpu, not $admin.\n" .
"You will not be able to access gitolite with this key.\n" .
"Look for the 'ssh troubleshooting' link in http://sitaramc.github.com/gitolite/.\n"
if $fpu ne "user $admin";
#>>>
}
msg 0, "==== checking pubkeys:\n" if @pubkeyfiles;
for my $pkf (@pubkeyfiles) {
# get the short name for the pubkey file
( my $pkfsn = $pkf ) =~ s(^$kd/)();
my $fp = fprint($pkf);
next unless $fp;
msg 1, "$pkfsn appears to be a COPY of $pkf_by_fp{$fp}\n" if $pkf_by_fp{$fp};
$pkf_by_fp{$fp} ||= $pkfsn;
my $fpu = ( $seen_fprints{$fp}{user} || 'no access' );
msg 0, "$pkfsn maps to $fpu\n";
}
if ($warnings) {
print "\n$warnings warnings found\n";
}
exit $warnings;
# ------------------------------------------------------------------------
sub fill_authkeys {
while (<>) {
my $seq = $.;
next if ak_comment($_); # also sets/clears $in_gl_section global
my $fp = fprint($_);
my $user = user($_);
check( $seq, $fp, $user );
$authkeys[$seq]{fprint} = $fp;
$authkeys[$seq]{ustatus} = $user;
}
}
sub check {
my ( $seq, $fp, $user ) = @_;
msg 1, "line $seq, $user key found *outside* gitolite section!\n"
if $user =~ /^user / and not $in_gl_section;
msg 1, "line $seq, $user key found *inside* gitolite section!\n"
if $user !~ /^user / and $in_gl_section;
if ( $seen_fprints{$fp} ) {
#<<<
msg 1, "authkeys line $seq ($user) will be ignored by sshd; " .
"same key found on line " .
$seen_fprints{$fp}{seq} . " (" .
$seen_fprints{$fp}{user} . ")\n";
return;
#>>>
}
$seen_fprints{$fp}{seq} = $seq;
$seen_fprints{$fp}{user} = $user;
}
sub user {
my $user = '';
$user ||= "user $1" if /^command=.*gitolite-shell (.*?)"/;
$user ||= "unknown command" if /^command/;
$user ||= "shell access" if /^ssh-(rsa|dss)/;
return $user;
}
sub ak_comment {
local $_ = shift;
$in_gl_section = 1 if /^# gitolite start/;
$in_gl_section = 0 if /^# gitolite end/;
die "gitosis? what's that?\n" if /^#.*gitosis/;
return /^\s*(#|$)/;
}
sub fprint {
local $_ = shift;
my ( $fh, $tempfn, $in );
if (/ssh-(dss|rsa) /) {
# an actual key was passed. Since ssh-keygen requires an actual file,
# make a temp file to take the data and pass on to ssh-keygen
s/^.* (ssh-dss|ssh-rsa)/$1/;
use File::Temp qw(tempfile);
( $fh, $tempfn ) = tempfile();
$in = $tempfn;
print $fh $_;
close $fh;
} else {
# a filename was passed
$in = $_;
}
# dbg("in = $in");
-f $in or die "file not found: $in\n";
open( $fh, "ssh-keygen -l -f $in |" ) or die "could not fork: $!\n";
my $fp = <$fh>;
# dbg("fp = $fp");
close $fh;
unlink $tempfn if $tempfn;
warn "$fp\n" unless $fp =~ /([0-9a-f][0-9a-f](:[0-9a-f][0-9a-f])+)/;
return $1;
}
# ------------------------------------------------------------------------
sub usage {
print <<EOF;
Usage: gitolite sshkeys-lint [-q] [optional list of pubkey filenames]
(optionally, STDIN can be a pipe or redirected from a file; see below)
Look for potential problems in ssh keys.
sshkeys-lint expects:
- the contents of an authorized_keys file via STDIN, otherwise it uses
\$HOME/.ssh/authorized_keys
- one or more pubkey filenames as arguments, otherwise it uses all the keys
found (recursively) in \$HOME/.gitolite/keydir
The '-q' option will print only warnings instead of all mappings.
Note that this runs ssh-keygen -l for each line in the authkeys file and each
pubkey in the argument list, so be wary of running it on something huge. This
is meant for troubleshooting.
EOF
exit 1;
}

View file

@ -1,280 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
=for usage
Usage for this command is not that simple. Please read the full documentation
in doc/sskm.mkd or online at http://sitaramc.github.com/gitolite/sskm.html.
=cut
usage() if @ARGV and $ARGV[0] eq '-h';
my $rb = $rc{GL_REPO_BASE};
my $ab = $rc{GL_ADMIN_BASE};
# get to the keydir
_chdir("$ab/keydir");
# save arguments for later
my $operation = shift || 'list';
my $keyid = shift || '';
# keyid must fit a very specific pattern
$keyid and $keyid !~ /^@[-0-9a-z_]+$/i and die "invalid keyid $keyid\n";
# get the actual userid and keytype
my $gl_user = $ENV{GL_USER};
my $keytype = '';
$keytype = $1 if $gl_user =~ s/^zzz-marked-for-(...)-//;
print STDERR "hello $gl_user, you are currently using "
. (
$keytype
? "a key in the 'marked for $keytype' state\n"
: "a normal (\"active\") key\n"
);
# ----
# first collect the keys
my ( @pubkeys, @marked_for_add, @marked_for_del );
# get the list of pubkey files for this user, including pubkeys marked for
# add/delete
for my $pubkey (`find . -type f -name "*.pub" | sort`) {
chomp($pubkey);
$pubkey =~ s(^./)(); # artifact of the find command
my $user = $pubkey;
$user =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub
$user =~ s/(\@[^.]+)?\.pub$//; # baz.pub, baz@home.pub -> baz
next unless $user eq $gl_user or $user =~ /^zzz-marked-for-...-$gl_user/;
if ( $user =~ m(^zzz-marked-for-add-) ) {
push @marked_for_add, $pubkey;
} elsif ( $user =~ m(^zzz-marked-for-del-) ) {
push @marked_for_del, $pubkey;
} else {
push @pubkeys, $pubkey;
}
}
# ----
# list mode; just do it and exit
sub print_keylist {
my ( $message, @list ) = @_;
return unless @list;
print "== $message ==\n";
my $count = 1;
for (@list) {
my $fp = fingerprint($_);
s/zzz-marked(\/|-for-...-)//g;
print $count++ . ": $fp : $_\n";
}
}
if ( $operation eq 'list' ) {
print "you have the following keys:\n";
print_keylist( "active keys", @pubkeys );
print_keylist( "keys marked for addition/replacement", @marked_for_add );
print_keylist( "keys marked for deletion", @marked_for_del );
print "\n\n";
exit;
}
# ----
# please see docs for details on how a user interacts with this
if ( $keytype eq '' ) {
# user logging in with a normal key
die "valid operations: add, del, undo-add, confirm-del\n" unless $operation =~ /^(add|del|confirm-del|undo-add)$/;
if ( $operation eq 'add' ) {
print STDERR "please supply the new key on STDIN. (I recommend you
don't try to do this interactively, but use a pipe)\n";
kf_add( $gl_user, $keyid, safe_stdin() );
} elsif ( $operation eq 'del' ) {
kf_del( $gl_user, $keyid );
} elsif ( $operation eq 'confirm-del' ) {
die "you dont have any keys marked for deletion\n" unless @marked_for_del;
kf_confirm_del( $gl_user, $keyid );
} elsif ( $operation eq 'undo-add' ) {
die "you dont have any keys marked for addition\n" unless @marked_for_add;
kf_undo_add( $gl_user, $keyid );
}
} elsif ( $keytype eq 'del' ) {
# user is using a key that was marked for deletion. The only possible use
# for this is that she changed her mind for some reason (maybe she marked
# the wrong key for deletion) or is not able to get her client-side sshd
# to stop using this key
die "valid operations: undo-del\n" unless $operation eq 'undo-del';
# reinstate the key
kf_undo_del( $gl_user, $keyid );
} elsif ( $keytype eq 'add' ) {
die "valid operations: confirm-add\n" unless $operation eq 'confirm-add';
# user is trying to validate a key that has been previously marked for
# addition. This isn't interactive, but it *could* be... if someone asked
kf_confirm_add( $gl_user, $keyid );
}
exit;
# ----
# make a temp clone and switch to it
our $TEMPDIR;
BEGIN { $TEMPDIR = `mktemp -d -t tmp.XXXXXXXXXX`; }
END { `/bin/rm -rf $TEMPDIR`; }
sub cd_temp_clone {
chomp($TEMPDIR);
hushed_git( "clone", "$rb/gitolite-admin.git", "$TEMPDIR" );
chdir($TEMPDIR);
my $hostname = `hostname`; chomp($hostname);
hushed_git( "config", "--get", "user.email" ) and hushed_git( "config", "user.email", $ENV{USER} . "@" . $hostname );
hushed_git( "config", "--get", "user.name" ) and hushed_git( "config", "user.name", "$ENV{USER} on $hostname" );
}
sub fingerprint {
my $fp = `ssh-keygen -l -f $_[0]`;
die "does not seem to be a valid pubkey\n" unless $fp =~ /(([0-9a-f]+:)+[0-9a-f]+ )/i;
return $1;
}
sub safe_stdin {
# read one line from STDIN
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;
}
sub hushed_git {
local (*STDOUT) = \*STDOUT;
local (*STDERR) = \*STDERR;
open( STDOUT, ">", "/dev/null" );
open( STDERR, ">", "/dev/null" );
system( "git", @_ );
}
sub highlander {
# there can be only one
my ( $keyid, $die_if_empty, @a ) = @_;
# too many?
if ( @a > 1 ) {
print STDERR "
more than one key satisfies this condition, and I can't deal with that!
The keys are:
";
print STDERR "\t" . join( "\n\t", @a ), "\n\n";
exit 1;
}
# too few?
die "no keys with " . ( $keyid || "empty" ) . " keyid found\n" if $die_if_empty and not @a;
return @a;
}
sub kf_add {
my ( $gl_user, $keyid, $keymaterial ) = @_;
# add a new "marked for addition" key for $gl_user.
cd_temp_clone();
chdir("keydir");
mkdir("zzz-marked");
_print( "zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub", $keymaterial );
hushed_git( "add", "." ) and die "git add failed\n";
my $fp = fingerprint("zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub");
hushed_git( "commit", "-m", "sskm: add $gl_user$keyid ($fp)" ) and die "git commit failed\n";
system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}
sub kf_confirm_add {
my ( $gl_user, $keyid ) = @_;
# find entries in both @pubkeys and @marked_for_add whose basename matches $gl_user$keyid
my @pk = highlander( $keyid, 0, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys );
my @mfa = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add );
cd_temp_clone();
chdir("keydir");
my $fp = fingerprint( $mfa[0] );
if ( $pk[0] ) {
hushed_git( "mv", "-f", $mfa[0], $pk[0] );
hushed_git( "commit", "-m", "sskm: confirm-add (replace) $pk[0] ($fp)" ) and die "git commit failed\n";
} else {
hushed_git( "mv", "-f", $mfa[0], "$gl_user$keyid.pub" );
hushed_git( "commit", "-m", "sskm: confirm-add $gl_user$keyid ($fp)" ) and die "git commit failed\n";
}
system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}
sub kf_undo_add {
# XXX some code at start is shared with kf_confirm_add
my ( $gl_user, $keyid ) = @_;
my @mfa = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add );
cd_temp_clone();
chdir("keydir");
my $fp = fingerprint( $mfa[0] );
hushed_git( "rm", $mfa[0] );
hushed_git( "commit", "-m", "sskm: undo-add $gl_user$keyid ($fp)" ) and die "git commit failed\n";
system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}
sub kf_del {
my ( $gl_user, $keyid ) = @_;
cd_temp_clone();
chdir("keydir");
mkdir("zzz-marked");
my @pk = highlander( $keyid, 1, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys );
my $fp = fingerprint( $pk[0] );
hushed_git( "mv", $pk[0], "zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub" ) and die "git mv failed\n";
hushed_git( "commit", "-m", "sskm: del $pk[0] ($fp)" ) and die "git commit failed\n";
system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}
sub kf_confirm_del {
my ( $gl_user, $keyid ) = @_;
my @mfd = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del );
cd_temp_clone();
chdir("keydir");
my $fp = fingerprint( $mfd[0] );
hushed_git( "rm", $mfd[0] );
hushed_git( "commit", "-m", "sskm: confirm-del $gl_user$keyid ($fp)" ) and die "git commit failed\n";
system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}
sub kf_undo_del {
my ( $gl_user, $keyid ) = @_;
my @mfd = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del );
print STDERR "
You're undeleting a key that is currently marked for deletion.
Hit ENTER to undelete this key
Hit Ctrl-C to cancel the undelete
Please see documentation for caveats on the undelete process as well as how to
actually delete it.
";
<>; # yeay... always wanted to do that -- throw away user input!
cd_temp_clone();
chdir("keydir");
my $fp = fingerprint( $mfd[0] );
hushed_git( "mv", "-f", $mfd[0], "$gl_user$keyid.pub" );
hushed_git( "commit", "-m", "sskm: undo-del $gl_user$keyid ($fp)" ) and die "git commit failed\n";
system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}

View file

@ -1,24 +0,0 @@
#!/bin/sh
# Usage: ssh git@host sudo <user> <command> <arguments>
#
# Let super-user run commands as any other user. "Super-user" is defined as
# "have write access to the gitolite-admin repo".
die() { echo "$@" >&2; exit 1; }
usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; }
[ -z "$2" ] && usage
[ "$1" = "-h" ] && usage
[ -z "$GL_USER" ] && die GL_USER not set
gitolite access -q gitolite-admin $GL_USER W any || die "You are not authorised"
user="$1"; shift
cmd="$1"; shift
# switch user
GL_USER="$user"
# figure out if the command is allowed from a remote user
gitolite query-rc -q COMMANDS $cmd || die "Command '$cmd' not allowed"
gitolite $cmd "$@"

View file

@ -1,17 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
my $svnserve = $rc{SVNSERVE} || '';
$svnserve ||= "/usr/bin/svnserve -r /var/svn/ -t --tunnel-user=%u";
my $cmd = $ENV{SSH_ORIGINAL_COMMAND};
die "expecting 'svnserve -t', got '$cmd'\n" unless $cmd eq 'svnserve -t';
$svnserve =~ s/%u/$ENV{GL_USER}/g;
exec $svnserve;
die "svnserve exec failed\n";

View file

@ -1,31 +0,0 @@
#!/bin/sh
# Usage: ssh git@host symbolic-ref <repo> <arguments to git-symbolic-ref>
#
# allow 'git symbolic-ref' over a gitolite connection
# Security: remember all arguments to commands must match a very conservative
# pattern. Once that is assured, the symbolic-ref command has no security
# related side-effects, so we don't check arguments at all.
# Note: because of the restriction on allowed characters in arguments, you
# can't supply an arbitrary string to the '-m' option. The simplest
# work-around is-to-just-use-join-up-words-like-this if you feel the need to
# supply a "reason" string. In any case this is useless by default; you'd
# have to have core.logAllRefUpdates set for it to have any meaning.
die() { echo "$@" >&2; exit 1; }
usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; }
[ -z "$1" ] && usage
[ "$1" = "-h" ] && usage
[ -z "$GL_USER" ] && die GL_USER not set
# ----------------------------------------------------------------------
repo=$1; shift
repo=${repo%.git}
gitolite access -q "$repo" $GL_USER W any || die You are not authorised
# change head
cd $GL_REPO_BASE/$repo.git
git symbolic-ref "$@"

View file

@ -1,57 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Easy;
=for usage
Usage: ssh git@host who-pushed <repo> <SHA>
Determine who pushed the given commit. The first few hex digits of the SHA
should suffice.
Each line of the output contains the following fields: timestamp, a
transaction ID, username, refname, and the old and new SHAs for the ref.
We assume the logfile names have been left as default, or if changed, in such
a way that they come up oldest first when sorted.
The program searches ALL the log files, in reverse sorted order (i.e., newest
first). This means it could take a long time if your log directory is large
and contains lots of old log files. Patches to limit the search to an
optional date range are welcome.
Note on the "transaction ID" field: if looking at the log file doesn't help
you figure out what its purpose is, please just ignore it.
=cut
usage() if not @ARGV or @ARGV < 2 or $ARGV[0] eq '-h';
usage() if $ARGV[1] !~ /^[0-9a-f]+$/i;
my $repo = shift;
my $sha = shift; $sha =~ tr/A-F/a-f/;
$ENV{GL_USER} and ( can_read($repo) or die "no read permissions on '$repo'" );
# ----------------------------------------------------------------------
my $repodir = "$ENV{GL_REPO_BASE}/$repo.git";
chdir $repodir or die "repo '$repo' missing";
(my $logdir = $ENV{GL_LOGFILE}) =~ s(/[^/]+$)();
for my $logfile ( reverse glob("$logdir/*") ) {
@ARGV = ($logfile);
for my $line ( reverse grep { m(\tupdate\t($repo|$repodir)\t) } <> ) {
chomp($line);
my @fields = split /\t/, $line;
my ($ts, $pid, $who, $ref, $d_old, $new) = @fields[ 0, 1, 4, 6, 7, 8];
# d_old is what you display
my $old = $d_old;
$old = "" if $d_old eq ("0" x 40);
$old = "$old.." if $old;
system("git rev-list $old$new 2>/dev/null | grep ^$sha >/dev/null && echo '$ts $pid $who $ref $d_old $new'");
}
}

View file

@ -1,56 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Easy;
=for usage
Usage: gitolite writable <reponame>|@all on|off
Disable/re-enable pushes to all repos or named repo. Useful to run
non-git-aware backups and so on.
'on' enables, 'off' disables, writes (pushes) to the named repo or all repos.
With 'off', any subsequent text is taken to be the message to be shown to
users when their pushes get rejected. If it is not supplied, it will take it
from STDIN; this allows longer messages.
=cut
usage() if not @ARGV or @ARGV < 2 or $ARGV[0] eq '-h';
usage() if $ARGV[1] ne 'on' and $ARGV[1] ne 'off';
my $repo = shift;
my $on = ( shift eq 'on' );
if ( $repo eq '@all' ) {
_die "you are not authorized" if $ENV{GL_USER} and not is_admin();
} else {
_die "you are not authorized" if $ENV{GL_USER} and not owns($repo);
}
my $msg = join( " ", @ARGV );
# try STDIN only if no msg found in args *and* it's an 'off' command
if ( not $msg and not $on ) {
say2 "...please type the message to be shown to users:";
$msg = join( "", <> );
}
my $sf = ".gitolite.down";
my $rb = $ENV{GL_REPO_BASE};
if ( $repo eq '@all' ) {
target( $ENV{HOME} );
} else {
target("$rb/$repo.git");
}
sub target {
my $repodir = shift;
if ($on) {
unlink "$repodir/$sf";
} else {
_print( "$repodir/$sf", $msg );
}
}

View file

@ -1,102 +0,0 @@
#!/usr/bin/perl
# all gitolite CLI tools run as sub-commands of this command
# ----------------------------------------------------------------------
=for args
Usage: gitolite [sub-command] [options]
The following built-in subcommands are available; they should all respond to
'-h' if you want further details on each:
setup 1st run: initial setup; all runs: hook fixups
compile compile gitolite.conf
query-rc get values of rc variables
list-groups list all group names in conf
list-users list all users/user groups in conf
list-repos list all repos/repo groups in conf
list-phy-repos list all repos actually on disk
list-memberships list all groups a name is a member of
list-members list all members of a group
Warnings:
- list-users is disk bound and could take a while on sites with 1000s of repos
- list-memberships does not check if the name is known; unknown names come
back with 2 answers: the name itself and '@all'
In addition, running 'gitolite help' should give you a list of custom commands
available. They may or may not respond to '-h', depending on how they were
written.
=cut
# ----------------------------------------------------------------------
use FindBin;
BEGIN { $ENV{GL_BINDIR} = $FindBin::RealBin; }
BEGIN { $ENV{GL_LIBDIR} = "$ENV{GL_BINDIR}/lib"; }
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
use strict;
use warnings;
# ----------------------------------------------------------------------
my ( $command, @args ) = @ARGV;
gl_log( 'cli', 'gitolite', @ARGV ) if -d $rc{GL_ADMIN_BASE} and $$ == ( $ENV{GL_TID} || 0 );
args();
# the first two commands need options via @ARGV, as they have their own
# GetOptions calls and older perls don't have 'GetOptionsFromArray'
if ( $command eq 'setup' ) {
shift @ARGV;
require Gitolite::Setup;
Gitolite::Setup->import;
setup();
} elsif ( $command eq 'query-rc' ) {
shift @ARGV;
query_rc(); # doesn't return
# the rest don't need @ARGV per se
} elsif ( $command eq 'compile' ) {
require Gitolite::Conf;
Gitolite::Conf->import;
compile(@args);
} elsif ( $command eq 'trigger' ) {
trigger(@args);
} elsif ( my $c = _which("commands/$command", 'x' ) ) {
trace( 2, "attempting gitolite command $c" );
_system( $c, @args );
exit 0;
} elsif ( $command eq 'list-phy-repos' ) {
_chdir( $rc{GL_REPO_BASE} );
print "$_\n" for ( @{ list_phy_repos(@args) } );
} elsif ( $command =~ /^list-/ ) {
trace( 2, "attempting lister command $command" );
require Gitolite::Conf::Load;
Gitolite::Conf::Load->import;
my $fn = lister_dispatch($command);
print "$_\n" for ( @{ $fn->(@args) } );
} else {
_die "unknown gitolite sub-command";
}
gl_log('END') if $$ == $ENV{GL_TID};
sub args {
usage() if not $command or $command eq '-h';
}
# ----------------------------------------------------------------------

View file

@ -1,239 +0,0 @@
#!/usr/bin/perl
# gitolite shell, invoked from ~/.ssh/authorized_keys
# ----------------------------------------------------------------------
use FindBin;
BEGIN { $ENV{GL_BINDIR} = $FindBin::RealBin; }
BEGIN { $ENV{GL_LIBDIR} = "$ENV{GL_BINDIR}/lib"; }
use lib $ENV{GL_LIBDIR};
# set HOME
BEGIN { $ENV{HOME} = $ENV{GITOLITE_HTTP_HOME} if $ENV{GITOLITE_HTTP_HOME}; }
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
use strict;
use warnings;
# the main() sub expects ssh-ish things; set them up...
my $id = '';
if ( exists $ENV{G3T_USER} ) {
$id = in_file(); # file:// masquerading as ssh:// for easy testing
} elsif ( exists $ENV{SSH_CONNECTION} ) {
$id = in_ssh();
} elsif ( exists $ENV{REQUEST_URI} ) {
$id = in_http();
} else {
_die "who the *heck* are you?";
}
# sanity...
my $soc = $ENV{SSH_ORIGINAL_COMMAND};
$soc =~ s/[\n\r]+/<<newline>>/g;
_die "I don't like newlines in the command: '$soc'\n" if $ENV{SSH_ORIGINAL_COMMAND} ne $soc;
# the INPUT trigger massages @ARGV and $ENV{SSH_ORIGINAL_COMMAND} as needed
trigger('INPUT');
main($id);
gl_log('END') if $$ == $ENV{GL_TID};
exit 0;
# ----------------------------------------------------------------------
sub in_file {
gl_log( 'file', "ARGV=" . join( ",", @ARGV ), "SOC=$ENV{SSH_ORIGINAL_COMMAND}" );
if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /git-\w+-pack/ ) {
print STDERR "TRACE: gsh(", join( ")(", @ARGV ), ")\n";
print STDERR "TRACE: gsh(SOC=$ENV{SSH_ORIGINAL_COMMAND})\n";
}
return 'file';
}
sub in_http {
http_setup_die_handler();
_die "GITOLITE_HTTP_HOME not set" unless $ENV{GITOLITE_HTTP_HOME};
_die "fallback to DAV not supported" if $ENV{REQUEST_METHOD} eq 'PROPFIND';
# fake out SSH_ORIGINAL_COMMAND and SSH_CONNECTION when called via http,
# so the rest of the code stays the same (except the exec at the end).
http_simulate_ssh_connection();
$ENV{REMOTE_USER} ||= $rc{HTTP_ANON_USER};
@ARGV = ( $ENV{REMOTE_USER} );
return 'http';
}
sub in_ssh {
my $ip;
( $ip = $ENV{SSH_CONNECTION} || '(no-IP)' ) =~ s/ .*//;
gl_log( 'ssh', "ARGV=" . join( ",", @ARGV ), "SOC=" . ( $ENV{SSH_ORIGINAL_COMMAND} || '' ), "FROM=$ip" );
$ENV{SSH_ORIGINAL_COMMAND} ||= '';
return $ip;
}
# ----------------------------------------------------------------------
# call this once you are sure arg-1 is the username and SSH_ORIGINAL_COMMAND
# has been setup (even if it's not actually coming via ssh).
sub main {
my $id = shift;
umask $rc{UMASK};
# set up the user
my $user = $ENV{GL_USER} = shift @ARGV;
# set up the repo and the attempted access
my ( $verb, $repo ) = parse_soc(); # returns only for git commands
sanity($repo);
$ENV{GL_REPO} = $repo;
my $aa = ( $verb =~ 'upload' ? 'R' : 'W' );
# auto-create?
if ( repo_missing($repo) and access( $repo, $user, '^C', 'any' ) !~ /DENIED/ ) {
require Gitolite::Conf::Store;
Gitolite::Conf::Store->import;
new_wild_repo( $repo, $user, $aa );
gl_log( 'create', $repo, $user, $aa );
}
# a ref of 'any' signifies that this is a pre-git check, where we don't
# yet know the ref that will be eventually pushed (and even that won't
# apply if it's a read operation). See the matching code in access() for
# more information.
unless ( $ENV{GL_BYPASS_ACCESS_CHECKS} ) {
my $ret = access( $repo, $user, $aa, 'any' );
trace( 1, "access($repo, $user, $aa, 'any')", "-> $ret" );
trigger( 'ACCESS_1', $repo, $user, $aa, 'any', $ret );
_die $ret . "\n(or you mis-spelled the reponame)" if $ret =~ /DENIED/;
gl_log( "pre_git", $repo, $user, $aa, 'any', "-> $ret" );
}
trigger( 'PRE_GIT', $repo, $user, $aa, 'any', $verb );
if ($ENV{REQUEST_URI}) {
_system( "git", "http-backend" );
} else {
my $repodir = "'$rc{GL_REPO_BASE}/$repo.git'";
_system( "git", "shell", "-c", "$verb $repodir" );
}
trigger( 'POST_GIT', $repo, $user, $aa, 'any', $verb );
}
# ----------------------------------------------------------------------
sub parse_soc {
my $soc = $ENV{SSH_ORIGINAL_COMMAND};
$soc ||= 'info';
my $git_commands = "git-upload-pack|git-receive-pack|git-upload-archive";
if ( $soc =~ m(^($git_commands) '/?(.*?)(?:\.git(\d)?)?'$) ) {
my ( $verb, $repo, $trace_level ) = ( $1, $2, $3 );
$ENV{D} = $trace_level if $trace_level;
_die "invalid repo name: '$repo'" if $repo !~ $REPONAME_PATT;
trace( 2, "git command", $soc );
return ( $verb, $repo );
}
# after this we should not return; caller expects us to handle it all here
# and exit out
_die "suspicious characters loitering about '$soc'" if $soc !~ $REMOTE_COMMAND_PATT;
my @words = split ' ', $soc;
if ( $rc{COMMANDS}{ $words[0] } ) {
trace( 2, "gitolite command", $soc );
_system( "gitolite", @words );
exit 0;
}
_die "unknown git/gitolite command: '$soc'";
}
sub sanity {
my $repo = shift;
_die "'$repo' contains bad characters" if $repo !~ $REPONAME_PATT;
_die "'$repo' ends with a '/'" if $repo =~ m(/$);
_die "'$repo' contains '..'" if $repo =~ m(\.\.);
}
# ----------------------------------------------------------------------
# helper functions for "in_http"
sub http_setup_die_handler {
$SIG{__DIE__} = sub {
my $service = ( $ENV{SSH_ORIGINAL_COMMAND} =~ /git-receive-pack/ ? 'git-receive-pack' : 'git-upload-pack' );
my $message = shift; chomp($message);
print STDERR "$message\n";
# format the service response, then the message. With initial
# help from Ilari and then a more detailed email from Shawn...
$service = "# service=$service\n"; $message = "ERR $message\n";
$service = sprintf( "%04X", length($service) + 4 ) . "$service"; # no CRLF on this one
$message = sprintf( "%04X", length($message) + 4 ) . "$message";
http_print_headers();
print $service;
print "0000"; # flush-pkt, apparently
print $message;
print STDERR $service;
print STDERR $message;
exit 0; # if it's ok for die_webcgi in git.git/http-backend.c, it's ok for me ;-)
}
}
sub http_simulate_ssh_connection {
# these patterns indicate normal git usage; see "services[]" in
# http-backend.c for how I got that. Also note that "info" is overloaded;
# git uses "info/refs...", while gitolite uses "info" or "info?...". So
# there's a "/" after info in the list below
if ( $ENV{PATH_INFO} =~ m(^/(.*)/(HEAD$|info/refs$|objects/|git-(?:upload|receive)-pack$)) ) {
my $repo = $1;
my $verb = ( $ENV{REQUEST_URI} =~ /git-receive-pack/ ) ? 'git-receive-pack' : 'git-upload-pack';
$ENV{SSH_ORIGINAL_COMMAND} = "$verb '$repo'";
} else {
# this is one of our custom commands; could be anything really,
# because of the adc feature
my ($verb) = ( $ENV{PATH_INFO} =~ m(^/(\S+)) );
my $args = $ENV{QUERY_STRING};
$args =~ s/\+/ /g;
$args =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
$ENV{SSH_ORIGINAL_COMMAND} = $verb;
$ENV{SSH_ORIGINAL_COMMAND} .= " $args" if $args;
http_print_headers(); # in preparation for the eventual output!
}
$ENV{SSH_CONNECTION} = "$ENV{REMOTE_ADDR} $ENV{REMOTE_PORT} $ENV{SERVER_ADDR} $ENV{SERVER_PORT}";
}
my $http_headers_printed = 0;
sub http_print_headers {
my ( $code, $text ) = @_;
return if $http_headers_printed++;
$code ||= 200;
$text ||= "OK - gitolite";
$|++;
print "Status: $code $text\r\n";
print "Expires: Fri, 01 Jan 1980 00:00:00 GMT\r\n";
print "Pragma: no-cache\r\n";
print "Cache-Control: no-cache, max-age=0, must-revalidate\r\n";
print "Content-Type: text/plain\r\n";
print "\r\n";
}

472
src/gitolite.pm Normal file
View file

@ -0,0 +1,472 @@
use strict;
# this file is commonly used using "require". It is not required to use "use"
# (because it doesn't live in a different package)
# warning: preceding para requires 4th attribute of a programmer after
# laziness, impatience, and hubris: sense of humour :-)
# WARNING
# -------
# the name of this file will change as soon as its function/feature set
# stabilises enough ;-)
# right now all it does is
# - define a function that tells you where to find the rc file
# - define a function that creates a new repo and give it our update hook
# ----------------------------------------------------------------------------
# common definitions
# ----------------------------------------------------------------------------
our $ABRT = "\n\t\t***** ABORTING *****\n ";
our $WARN = "\n\t\t***** WARNING *****\n ";
# commands we're expecting
our $R_COMMANDS=qr/^(git[ -]upload-pack|git[ -]upload-archive)$/;
our $W_COMMANDS=qr/^git[ -]receive-pack$/;
# note that REPONAME_PATT allows "/", while USERNAME_PATT does not
# also, the reason REPONAME_PATT is a superset of USERNAME_PATT is (duh!)
# because in this version, a repo can have "CREATER" in the name (see docs)
our $REPONAME_PATT=qr(^\@?[0-9a-zA-Z][0-9a-zA-Z._\@/+-]*$); # very simple pattern
our $USERNAME_PATT=qr(^\@?[0-9a-zA-Z][0-9a-zA-Z._\@+-]*$); # very simple pattern
# same as REPONAME, plus some common regex metas
our $REPOPATT_PATT=qr(^\@?[0-9a-zA-Z][\\^.$|()[\]*+?{}0-9a-zA-Z._\@/-]*$);
# these come from the RC file
our ($REPO_UMASK, $GL_WILDREPOS, $GL_PACKAGE_CONF, $GL_PACKAGE_HOOKS);
our %repos;
# ----------------------------------------------------------------------------
# convenience subs
# ----------------------------------------------------------------------------
sub wrap_chdir {
chdir($_[0]) or die "$ABRT chdir $_[0] failed: $! at ", (caller)[1], " line ", (caller)[2], "\n";
}
sub wrap_open {
open (my $fh, $_[0], $_[1]) or die "$ABRT open $_[1] failed: $! at ", (caller)[1], " line ", (caller)[2], "\n" .
( $_[2] || '' ); # suffix custom error message if given
return $fh;
}
sub log_it {
open my $log_fh, ">>", $ENV{GL_LOG} or die "open log failed: $!\n";
print $log_fh @_;
close $log_fh or die "close log failed: $!\n";
}
# check one ref
sub check_ref {
# normally, the $ref will be whatever ref the commit is trying to update
# (like refs/heads/master or whatever). At least one of the refexes that
# pertain to this user must match this ref **and** the corresponding
# permission must also match the action (W or +) being attempted. If none
# of them match, the access is denied.
# Notice that the function DIES!!! Any future changes that require more
# work to be done *after* this, even on failure, can start using return
# codes etc., but for now we're happy to just die.
my ($allowed_refs, $repo, $ref, $perm) = @_;
for my $ar (@{$allowed_refs}) {
my $refex = (keys %$ar)[0];
# refex? sure -- a regex to match a ref against :)
next unless $ref =~ /^$refex/;
die "$perm $ref $ENV{GL_USER} DENIED by $refex\n" if $ar->{$refex} eq '-';
# as far as *this* ref is concerned we're ok
return $refex if ($ar->{$refex} =~ /\Q$perm/);
}
die "$perm $ref $repo $ENV{GL_USER} DENIED by fallthru\n";
}
# ln -sf :-)
sub ln_sf
{
my($srcdir, $glob, $dstdir) = @_;
for my $hook ( glob("$srcdir/$glob") ) {
$hook =~ s/$srcdir\///;
unlink "$dstdir/$hook";
symlink "$srcdir/$hook", "$dstdir/$hook" or die "could not symlink $hook\n";
}
}
# ----------------------------------------------------------------------------
# where is the rc file hiding?
# ----------------------------------------------------------------------------
sub where_is_rc
{
# till now, the rc file was in one fixed place: .gitolite.rc in $HOME of
# the user hosting the gitolite repos. This was fine, because gitolite is
# all about empowering non-root users :-)
# then we wanted to make a debian package out of it (thank you, Rhonda!)
# which means (a) it's going to be installed by root anyway and (b) any
# config files have to be in /etc/<something>
# the only way to resolve this in a backward compat way is to look for the
# $HOME one, and if you don't find it look for the /etc one
# this common routine does that, setting an env var for the first one it
# finds
return if $ENV{GL_RC};
for my $glrc ( $ENV{HOME} . "/.gitolite.rc", "/etc/gitolite/gitolite.rc" ) {
if (-f $glrc) {
$ENV{GL_RC} = $glrc;
return;
}
}
}
# ----------------------------------------------------------------------------
# create a new repository
# ----------------------------------------------------------------------------
# NOTE: this sub will change your cwd; caller beware!
sub new_repo
{
my ($repo, $hooks_dir, $creater) = @_;
umask($REPO_UMASK);
die "wildrepos disabled, can't set creater $creater on new repo $repo\n"
if $creater and not $GL_WILDREPOS;
system("mkdir", "-p", "$repo.git") and die "$ABRT mkdir $repo.git failed: $!\n";
# erm, note that's "and die" not "or die" as is normal in perl
wrap_chdir("$repo.git");
system("git --bare init >&2");
if ($creater) {
system("echo $creater > gl-creater");
system("git", "config", "gitweb.owner", $creater);
}
# propagate our own, plus any local admin-defined, hooks
ln_sf($hooks_dir, "*", "hooks");
# in case of package install, GL_ADMINDIR is no longer the top cop;
# override with the package hooks
ln_sf("$GL_PACKAGE_HOOKS/common", "*", "hooks") if $GL_PACKAGE_HOOKS;
chmod 0755, "hooks/update";
}
# ----------------------------------------------------------------------------
# metaphysics (like, "is there a god?", "who created me?", etc)
# ----------------------------------------------------------------------------
# "who created this repo", "am I on the R list", and "am I on the RW list"?
sub repo_rights
{
my ($repo_base_abs, $repo, $user) = @_;
# creater
my $c = '';
if ( -f "$repo_base_abs/$repo.git/gl-creater") {
my $fh = wrap_open("<", "$repo_base_abs/$repo.git/gl-creater");
chomp($c = <$fh>);
}
# $user's R and W rights
my ($r, $w); $r = ''; $w = '';
if ($user and -f "$repo_base_abs/$repo.git/gl-perms") {
my $fh = wrap_open("<", "$repo_base_abs/$repo.git/gl-perms");
my $perms = join ("", <$fh>);
if ($perms) {
$r = $user if $perms =~ /^\s*R(?=\s).*\s$user(\s|$)/m;
$w = $user if $perms =~ /^\s*RW(?=\s).*\s$user(\s|$)/m;
}
}
return ($c, $r, $w);
}
# ----------------------------------------------------------------------------
# getperms and setperms
# ----------------------------------------------------------------------------
sub get_set_perms
{
my($repo_base_abs, $repo, $verb, $user) = @_;
my ($creater, $dummy, $dummy2) = &repo_rights($repo_base_abs, $repo, "");
die "$repo doesnt exist or is not yours\n" unless $user eq $creater;
wrap_chdir("$repo_base_abs");
wrap_chdir("$repo.git");
if ($verb eq 'getperms') {
system("cat", "gl-perms") if -f "gl-perms";
} else {
system("cat > gl-perms");
print "New perms are:\n";
system("cat", "gl-perms");
}
}
# ----------------------------------------------------------------------------
# getdesc and setdesc
# ----------------------------------------------------------------------------
sub get_set_desc
{
my($repo_base_abs, $repo, $verb, $user) = @_;
my ($creater, $dummy, $dummy2) = &repo_rights($repo_base_abs, $repo, "");
die "$repo doesnt exist or is not yours\n" unless $user eq $creater;
wrap_chdir("$repo_base_abs");
wrap_chdir("$repo.git");
if ($verb eq 'getdesc') {
system("cat", "description") if -f "description";
} else {
system("cat > description");
print "New description is:\n";
system("cat", "description");
}
}
# ----------------------------------------------------------------------------
# parse the compiled acl
# ----------------------------------------------------------------------------
sub parse_acl
{
# IMPLEMENTATION NOTE: a wee bit of this is duplicated in the update hook;
# please update that also if the interface or the env vars change
my ($GL_CONF_COMPILED, $repo, $c, $r, $w) = @_;
$c = $r = $w = "NOBODY" unless $GL_WILDREPOS;
# void $r if same as $w (otherwise "readers" overrides "writers"; this is
# the same problem that needed a sort sub for the Dumper in the compile
# script, but in this case it's limited to just $readers and $writers)
$r = "NOBODY" if $r eq $w;
# set up the variables for a parse to interpolate stuff from the dumped
# hash (remember the selective conversion of single to double quotes?).
# if they're not passed in, then we look for an env var of that name, else
# we default to "NOBODY" (we hope there isn't a real user called NOBODY!)
# And in any case, we set those env vars so level 2 can redo the last
# parse without any special code
our $creater = $ENV{GL_CREATER} = $c || $ENV{GL_CREATER} || "NOBODY";
our $readers = $ENV{GL_READERS} = $r || $ENV{GL_READERS} || "NOBODY";
our $writers = $ENV{GL_WRITERS} = $w || $ENV{GL_WRITERS} || "NOBODY";
die "parse $GL_CONF_COMPILED failed: " . ($! or $@) unless do $GL_CONF_COMPILED;
# basic access reporting doesn't send $repo, and doesn't need to; you just
# want the config dumped as is, really
return unless $repo;
return $ENV{GL_REPOPATT} = "" if $repos{$repo};
# didn't find it, but wild is off? too bad, die!!! muahahaha
die "$repo not found in compiled config\n" unless $GL_WILDREPOS;
# didn't find $repo in %repos, so it must be a wildcard-match case
my @matched = grep { $repo =~ /^$_$/ } sort keys %repos;
die "$repo has no matches\n" unless @matched;
die "$repo has multiple matches\n@matched\n" if @matched > 1;
# found exactly one pattern that matched, copy its ACL
$repos{$repo} = $repos{$matched[0]};
# and return the pattern
return $ENV{GL_REPOPATT} = $matched[0];
}
# ----------------------------------------------------------------------------
# print a report of $user's basic permissions
# ----------------------------------------------------------------------------
# basic means wildcards will be shown as wildcards; this is pretty much what
# got parsed by the compile script
sub report_basic
{
my($GL_ADMINDIR, $GL_CONF_COMPILED, $user) = @_;
&parse_acl($GL_CONF_COMPILED, "", "CREATER", "READERS", "WRITERS");
# send back some useful info if no command was given
print "hello $user, the gitolite version here is ";
system("cat", ($GL_PACKAGE_CONF || "$GL_ADMINDIR/conf") . "/VERSION");
print "\ryou have the following permissions:\r\n";
for my $r (sort keys %repos) {
my $perm .= ( $repos{$r}{C}{'@all'} ? ' @' : ( $repos{$r}{C}{$user} ? ' C' : ' ' ) );
$perm .= ( $repos{$r}{R}{'@all'} ? ' @' : ( $repos{$r}{R}{$user} ? ' R' : ' ' ) );
$perm .= ( $repos{$r}{W}{'@all'} ? ' @' : ( $repos{$r}{W}{$user} ? ' W' : ' ' ) );
print "$perm\t$r\r\n" if $perm =~ /\S/;
}
}
# ----------------------------------------------------------------------------
# print a report of $user's basic permissions
# ----------------------------------------------------------------------------
sub expand_wild
{
my($GL_CONF_COMPILED, $repo_base_abs, $repo, $user) = @_;
# this is for convenience; he can copy-paste the output of the basic
# access report instead of having to manually change CREATER to his name
$repo =~ s/\bCREAT[EO]R\b/$user/g;
# get the list of repo patterns
&parse_acl($GL_CONF_COMPILED, "", "NOBODY", "NOBODY", "NOBODY");
my %normal_repos = %repos;
# display matching repos (from *all* the repos in the system) that $user
# has at least "R" access to
chdir("$repo_base_abs") or die "chdir $repo_base_abs failed: $!\n";
for my $actual_repo (`find . -type d -name "*.git"|sort`) {
chomp ($actual_repo);
$actual_repo =~ s/^\.\///;
$actual_repo =~ s/\.git$//;
# actual_repo has to match the pattern being expanded
next unless $actual_repo =~ /$repo/;
# if actual_repo is present "as is" in the config, those
# permissions will override anything inherited from a
# wildcard that also happens to match
my $creater;
if ($normal_repos{$actual_repo}) {
%repos = %normal_repos;
$creater = '<gitolite>';
} else {
# find the creater and subsitute in repos
my ($read, $write);
($creater, $read, $write) = &repo_rights($repo_base_abs, $actual_repo, $user);
# get access list with this
&parse_acl($GL_CONF_COMPILED, $actual_repo, $creater, $read || "NOBODY", $write || "NOBODY");
$creater = "($creater)";
}
my $perm = ' ';
$perm .= ( $repos{$actual_repo}{R}{'@all'} ? ' @' : ( $repos{$actual_repo}{R}{$user} ? ' R' : ' ' ) );
$perm .= ( $repos{$actual_repo}{W}{'@all'} ? ' @' : ( $repos{$actual_repo}{W}{$user} ? ' W' : ' ' ) );
next if $perm eq ' ';
print "$perm\t$creater\t$actual_repo\n";
}
}
# ----------------------------------------------------------------------------
# S P E C I A L C O M M A N D S
# ----------------------------------------------------------------------------
sub special_cmd
{
my ($GL_ADMINDIR, $GL_CONF_COMPILED, $shell_allowed, $RSYNC_BASE, $HTPASSWD_FILE) = @_;
my $cmd = $ENV{SSH_ORIGINAL_COMMAND};
my $user = $ENV{GL_USER};
# check each special command we know about and call it if enabled
if ($cmd eq 'info') {
&report_basic($GL_ADMINDIR, $GL_CONF_COMPILED, $user);
print "you also have shell access\r\n" if $shell_allowed;
} elsif ($cmd =~ /^info\s+(.+)$/) {
my @otherusers = split ' ', $1;
&parse_acl($GL_CONF_COMPILED);
die "you can't ask for others' permissions\n" unless $repos{'gitolite-admin'}{'R'}{$user};
for my $otheruser (@otherusers) {
warn("ignoring illegal username $otheruser\n"), next unless $otheruser =~ $USERNAME_PATT;
&report_basic($GL_ADMINDIR, $GL_CONF_COMPILED, $otheruser);
}
} elsif ($HTPASSWD_FILE and $cmd eq 'htpasswd') {
&ext_cmd_htpasswd($HTPASSWD_FILE);
} elsif ($RSYNC_BASE and $cmd =~ /^rsync /) {
&ext_cmd_rsync($GL_CONF_COMPILED, $RSYNC_BASE, $cmd);
} else {
# if the user is allowed a shell, just run the command
exec $ENV{SHELL}, "-c", $cmd if $shell_allowed;
die "bad command: $cmd\n";
}
}
# ----------------------------------------------------------------------------
# generic check access routine
# ----------------------------------------------------------------------------
sub check_access
{
my ($GL_CONF_COMPILED, $repo, $path, $perm) = @_;
my $ref = "NAME/$path";
&parse_acl($GL_CONF_COMPILED);
# until I do some major refactoring (which will bloat the update hook a
# bit, sadly), this code duplicates stuff in the current update hook.
my @allowed_refs;
# we want specific perms to override @all, so they come first
push @allowed_refs, @ { $repos{$repo}{$ENV{GL_USER}} || [] };
push @allowed_refs, @ { $repos{$repo}{'@all'} || [] };
for my $ar (@allowed_refs) {
my $refex = (keys %$ar)[0];
next unless $ref =~ /^$refex/;
die "$perm $ref $ENV{GL_USER} DENIED by $refex\n" if $ar->{$refex} eq '-';
return if ($ar->{$refex} =~ /\Q$perm/);
}
die "$perm $ref $ENV{GL_REPO} $ENV{GL_USER} DENIED by fallthru\n";
}
# ----------------------------------------------------------------------------
# external command helper: rsync
# ----------------------------------------------------------------------------
sub ext_cmd_rsync
{
my ($GL_CONF_COMPILED, $RSYNC_BASE, $cmd) = @_;
# test the command patterns; reject if they don't fit. Rsync sends
# commands that looks like one of these to the server (the first one is
# for a read, the second for a write)
# rsync --server --sender -some.flags . some/path
# rsync --server -some.flags . some/path
die "bad rsync command: $cmd"
unless $cmd =~ /^rsync --server( --sender)? -[\w.]+(?: --(?:delete|partial))* \. (\S+)$/;
my $perm = "W";
$perm = "R" if $1;
my $path = $2;
die "I dont like some of the characters in $path\n" unless $path =~ $REPOPATT_PATT;
# XXX make a better pattern for this if people complain ;-)
die "I dont like absolute paths in $cmd\n" if $path =~ /^\//;
die "I dont like '..' paths in $cmd\n" if $path =~ /\.\./;
# ok now check if we're permitted to execute a $perm action on $path
# (taken as a refex) using rsync.
&check_access($GL_CONF_COMPILED, 'EXTCMD/rsync', $path, $perm);
# that should "die" if there's a problem
wrap_chdir($RSYNC_BASE);
&log_it("$ENV{GL_TS}\t$ENV{SSH_ORIGINAL_COMMAND}\t$ENV{USER}\n");
exec $ENV{SHELL}, "-c", $ENV{SSH_ORIGINAL_COMMAND};
}
# ----------------------------------------------------------------------------
# external command helper: htpasswd
# ----------------------------------------------------------------------------
sub ext_cmd_htpasswd
{
my $HTPASSWD_FILE = shift;
die "$HTPASSWD_FILE doesn't exist or is not writable\n" unless -w $HTPASSWD_FILE;
$|++;
print <<EOFhtp;
Please type in your new htpasswd at the prompt. You only have to type it once.
NOTE THAT THE PASSWORD WILL BE ECHOED, so please make sure no one is
shoulder-surfing, and make sure you clear your screen as well as scrollback
history after you're done (or close your terminal instance).
EOFhtp
print "new htpasswd:";
my $password = <>;
$password =~ s/[\n\r]*$//;
die "empty passwords are not allowed\n" unless $password;
my $rc = system("htpasswd", "-b", $HTPASSWD_FILE, $ENV{GL_USER}, $password);
die "htpasswd command seems to have failed with $rc return code...\n" if $rc;
}
1;

200
src/gl-auth-command Executable file
View file

@ -0,0 +1,200 @@
#!/usr/bin/perl
use strict;
use warnings;
# === auth-command ===
# the command that GL users actually run
# part of the gitolite (GL) suite
# how run: via sshd, being listed in "command=" in ssh authkeys
# when: every login by a GL user
# input: $1 is GL username, plus $SSH_ORIGINAL_COMMAND
# output:
# security:
# - currently, we just make some basic checks, copied from gitosis
# robustness:
# other notes:
# ----------------------------------------------------------------------------
# common definitions
# ----------------------------------------------------------------------------
# these are set by the "rc" file
our ($GL_LOGT, $GL_CONF_COMPILED, $REPO_BASE, $GIT_PATH, $REPO_UMASK, $GL_ADMINDIR, $RSYNC_BASE, $HTPASSWD_FILE, $GL_WILDREPOS);
# and these are set by gitolite.pm
our ($R_COMMANDS, $W_COMMANDS, $REPONAME_PATT, $REPOPATT_PATT);
our %repos;
# the common setup module is in the same directory as this running program is
my $bindir = $0;
$bindir =~ s/\/[^\/]+$//;
$bindir = "$ENV{PWD}/$bindir" unless $bindir =~ /^\//;
require "$bindir/gitolite.pm";
# ask where the rc file is, get it, and "do" it
&where_is_rc();
die "parse $ENV{GL_RC} failed: " . ($! or $@) unless do $ENV{GL_RC};
# we need to pass GL_ADMINDIR and the bindir to the child hooks
$ENV{GL_ADMINDIR} = $GL_ADMINDIR;
$ENV{GL_BINDIR} = $bindir;
# add a custom path for git binaries, if specified
$ENV{PATH} .= ":$GIT_PATH" if $GIT_PATH;
# set the umask before creating any files
umask($REPO_UMASK);
my $repo_base_abs = ( $REPO_BASE =~ m(^/) ? $REPO_BASE : "$ENV{HOME}/$REPO_BASE" );
# ----------------------------------------------------------------------------
# start...
# ----------------------------------------------------------------------------
# if the first argument is a "-s", this user is allowed to get a shell using
# this key
my $shell_allowed = 0;
if ($ARGV[0] eq '-s') {
$shell_allowed = 1;
shift;
}
# first, fix the biggest gripe I have with gitosis, a 1-line change
my $user=$ENV{GL_USER}=shift; # there; now that's available everywhere!
# ----------------------------------------------------------------------------
# logging, timestamp env vars
# ----------------------------------------------------------------------------
# timestamp
my ($s, $min, $h, $d, $m, $y) = (localtime)[0..5];
$y += 1900; $m++; # usual adjustments
for ($s, $min, $h, $d, $m) {
$_ = "0$_" if $_ < 10;
}
$ENV{GL_TS} = "$y-$m-$d.$h:$min:$s";
# substitute template parameters and set the logfile name
$GL_LOGT =~ s/%y/$y/g;
$GL_LOGT =~ s/%m/$m/g;
$GL_LOGT =~ s/%d/$d/g;
$ENV{GL_LOG} = $GL_LOGT;
# ----------------------------------------------------------------------------
# sanity checks on SSH_ORIGINAL_COMMAND
# ----------------------------------------------------------------------------
# no SSH_ORIGINAL_COMMAND given...
unless ($ENV{SSH_ORIGINAL_COMMAND}) {
# if the user is allowed to use a shell, give him one
if ($shell_allowed) {
my $shell = $ENV{SHELL};
$shell =~ s/.*\//-/; # change "/bin/bash" to "-bash"
exec { $ENV{SHELL} } $shell;
}
# otherwise, pretend he typed in "info" and carry on...
$ENV{SSH_ORIGINAL_COMMAND} = 'info';
}
# ----------------------------------------------------------------------------
# get and set perms for actual repo created by wildcard-autoviv
# ----------------------------------------------------------------------------
my $CUSTOM_COMMANDS=qr/^\s*(expand|(get|set)(perms|desc))\b/;
# note that all the subs called here chdir somewhere else and do not come
# back; they all blithely take advantage of the fact that processing custom
# commands is sort of a dead end for normal (git) processing
if ($ENV{SSH_ORIGINAL_COMMAND} =~ $CUSTOM_COMMANDS) {
die "wildrepos disabled, sorry\n" unless $GL_WILDREPOS;
my $cmd = $ENV{SSH_ORIGINAL_COMMAND};
my ($verb, $repo) = ($cmd =~ /^\s*(\S+)(?:\s+'?\/?(.*?)(?:\.git)?'?)?$/);
# deal with "no argument" cases
$verb eq 'expand' ? $repo = '^' : die "$verb needs an argument\n" unless $repo;
if ($repo =~ $REPONAME_PATT and $verb =~ /getperms|setperms/) {
# with an actual reponame, you can "getperms" or "setperms"
get_set_perms($repo_base_abs, $repo, $verb, $user);
}
elsif ($repo =~ $REPONAME_PATT and $verb =~ /(get|set)desc/) {
# with an actual reponame, you can "getdesc" or "setdesc"
get_set_desc($repo_base_abs, $repo, $verb, $user);
}
elsif ($verb eq 'expand') {
# with a wildcard, you can "expand" it to see what repos actually match
die "$repo has invalid characters" unless "x$repo" =~ $REPOPATT_PATT;
expand_wild($GL_CONF_COMPILED, $repo_base_abs, $repo, $user);
} else {
die "$cmd doesn't make sense to me\n";
}
exit 0;
}
# ----------------------------------------------------------------------------
# non-git commands
# ----------------------------------------------------------------------------
# if the command does NOT fit the pattern of a normal git command, send it off
# somewhere else...
# side notes on detecting a normal git command: the pattern we check allows
# old style as well as new style ("git-subcommand arg" or "git subcommand
# arg"), just like gitosis does, although I'm not sure how necessary that is.
# Currently, this is how git sends across the command (including the single
# quotes):
# git-receive-pack 'reponame.git'
my ($verb, $repo) = ($ENV{SSH_ORIGINAL_COMMAND} =~ /^\s*(git\s+\S+|\S+)\s+'\/?(.*?)(?:\.git)?'/);
unless ( $verb and ( $verb =~ $R_COMMANDS or $verb =~ $W_COMMANDS ) and $repo and $repo =~ $REPONAME_PATT ) {
# ok, it's not a normal git command; call the special command helper
&special_cmd ($GL_ADMINDIR, $GL_CONF_COMPILED, $shell_allowed, $RSYNC_BASE, $HTPASSWD_FILE);
exit;
}
die "$repo ends with a slash; I don't like that\n" if $repo =~ /\/$/;
die "$repo has two consecutive periods; I don't like that\n" if $repo =~ /\.\./;
# reponame
$ENV{GL_REPO}=$repo;
# ----------------------------------------------------------------------------
# the real git commands (git-receive-pack, etc...)
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# first level permissions check
# ----------------------------------------------------------------------------
if ( -d "$repo_base_abs/$repo.git" ) {
# existing repo
my ($creater, $user_R, $user_W) = &repo_rights($repo_base_abs, $repo, $user);
&parse_acl($GL_CONF_COMPILED, $repo, $creater, $user_R, $user_W);
} else {
&parse_acl($GL_CONF_COMPILED, $repo, $user, $user, $user);
# auto-vivify new repo if you have C access (and wildrepos is on)
if ( $GL_WILDREPOS and $repos{$repo}{C}{$user} || $repos{$repo}{C}{'@all'} ) {
wrap_chdir("$repo_base_abs");
new_repo($repo, "$GL_ADMINDIR/hooks/common", $user);
wrap_chdir($ENV{HOME});
}
}
# we know the user and repo; we just need to know what perm he's trying
my $perm = ($verb =~ $R_COMMANDS ? 'R' : 'W');
die "$perm access for $repo DENIED to $user\n"
unless $repos{$repo}{$perm}{$user}
or $repos{$repo}{$perm}{'@all'};
# ----------------------------------------------------------------------------
# over to git now
# ----------------------------------------------------------------------------
&log_it("$ENV{GL_TS}\t$ENV{SSH_ORIGINAL_COMMAND}\t$user\n");
$repo = "'$REPO_BASE/$repo.git'";
exec("git", "shell", "-c", "$verb $repo");

560
src/gl-compile-conf Executable file
View file

@ -0,0 +1,560 @@
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Sortkeys = sub { return [ reverse sort keys %{$_[0]} ]; };
# this is to make sure that $creater etc go to the end of the dumped hash.
# Without this, a setup that has something like
# @team = u1 u2 u3
# repo priv/CREATER/.+
# RW+ = CREATER
# RW = @team
# has a problem. The RW overrides the RW+ when the dumped hash is read in
# (simply going by sequence), so creater's special privs are lost
# === add-auth-keys ===
# part of the gitolite (GL) suite
# (1) - "compiles" ~/.ssh/authorized_keys from the list of pub-keys
# (2) - also "compiles" the user-friendly GL conf file into something easier
# to parse. We're doing this because both the gl-auth-command and the
# (gl-)update hook need this, and it seems easier to do this than
# replicate the parsing code in both those places. As a bonus, it's
# probably more efficient.
# (3) - finally does what I have resisted doing all along -- handle gitweb and
# git-daemon access. It won't *setup* gitweb/daemon for you -- you have
# to that yourself. What this does is make sure that "repo.git"
# contains the file "git-daemon-export-ok" (for daemon case) and the
# line "repo.git" exists in the "projects.list" file (for gitweb case).
# how run: manual, by GL admin
# when:
# - anytime a pubkey is added/deleted
# - anytime gitolite.conf is changed
# input:
# - GL_CONF (default: ~/.gitolite/conf/gitolite.conf)
# - GL_KEYDIR (default: ~/.gitolite/keydir)
# output:
# - ~/.ssh/authorized_keys (dictated by sshd)
# - GL_CONF_COMPILED (default: ~/.gitolite/conf/gitolite.conf-compiled.pm)
# security:
# - touches a very critical system file that manages the restrictions on
# incoming users. Be sure to audit AUTH_COMMAND and AUTH_OPTIONS (see
# below) on any change to this script
# - no security checks within program. The GL admin runs this manually
# warnings:
# - if the "start" line exists, but the "end" line does not, you lose the
# rest of the existing authkey file. In general, "don't do that (TM)",
# but we do have a "vim -d" popping up so you can see the changes being
# made, just in case...
# ----------------------------------------------------------------------------
# common definitions
# ----------------------------------------------------------------------------
# setup quiet mode if asked; please do not use this when running manually
open STDOUT, ">", "/dev/null" if (@ARGV and shift eq '-q');
# these are set by the "rc" file
our ($GL_ADMINDIR, $GL_CONF, $GL_KEYDIR, $GL_CONF_COMPILED, $REPO_BASE, $REPO_UMASK, $PROJECTS_LIST, $GIT_PATH, $SHELL_USERS, $GL_WILDREPOS, $GL_GITCONFIG_KEYS, $GL_PACKAGE_HOOKS);
# and these are set by gitolite.pm
our ($REPONAME_PATT, $REPOPATT_PATT, $USERNAME_PATT, $AUTH_COMMAND, $AUTH_OPTIONS, $ABRT, $WARN);
# the common setup module is in the same directory as this running program is
my $bindir = $0;
$bindir =~ s/\/[^\/]+$//;
$bindir = "$ENV{PWD}/$bindir" unless $bindir =~ /^\//;
require "$bindir/gitolite.pm";
# ask where the rc file is, get it, and "do" it
&where_is_rc();
die "$ABRT parse $ENV{GL_RC} failed: " . ($! or $@) unless do $ENV{GL_RC};
# add a custom path for git binaries, if specified
$ENV{PATH} .= ":$GIT_PATH" if $GIT_PATH;
# ----------------------------------------------------------------------------
# definitions specific to this program
# ----------------------------------------------------------------------------
# command and options for authorized_keys
$AUTH_COMMAND="$bindir/gl-auth-command";
$AUTH_OPTIONS="no-port-forwarding,no-X11-forwarding,no-agent-forwarding";
# note, for most users there's also a "no-pty" added to this, see later
# groups can now represent user groups or repo groups.
# $groups{group}{member} = "master" (or name of fragment file in which the
# group is defined).
our %groups = ();
# %repos has two functions.
# $repos{repo}{R|W}{user} = 1 if user has R (or W) permissions for at least
# one branch in repo. This is used by the "level 1 check" (see faq). There's
# also the new "C" (create a repo) permission now
# $repos{repo}{user} is a list of {ref, perms} pairs. This is used by the
# level 2 check. In order to allow "exclude" rules, the order of rules now
# matters, so what used to be entirely "hash of hash of hash" now has a list
# in between :)
my %repos = ();
# <sigh>... having been forced to use a list as described above, we lose some
# efficiency due to the possibility of the same {ref, perms} pair showing up
# multiple times for the same repo+user. So...
my %rurp_seen = ();
# catch usernames<->pubkeys mismatches; search for "lint" below
my %user_list = ();
# repo configurations
my %repo_config = ();
# gitweb descriptions and owners; plain text, keyed by "$repo.git"
my %desc = ();
my %owner = ();
# set the umask before creating any files
umask($REPO_UMASK);
# ----------------------------------------------------------------------------
# subroutines
# ----------------------------------------------------------------------------
sub expand_list
{
my @list = @_;
my @new_list = ();
for my $item (@list)
{
if ($item =~ /^@/) # nested group
{
die "$ABRT undefined group $item\n" unless $groups{$item};
# add those names to the list
push @new_list, sort keys %{ $groups{$item} };
}
else
{
push @new_list, $item;
}
}
return @new_list;
}
# ----------------------------------------------------------------------------
# "compile" GL conf
# ----------------------------------------------------------------------------
sub parse_conf_file
{
my ($conffile, $fragment) = @_;
# the second arg, $fragment, is passed in as "master" when parsing the
# main config, and the fragment name when parsing a fragment. In the
# latter case, the parser uses that information to ignore (and warn about)
# any repos in the fragment that are not members of the "repo group" of
# the same name.
my %ignored = ();
my $conf_fh = wrap_open( "<", $conffile );
# the syntax is fairly simple, so we parse it inline
my @repos;
while (<$conf_fh>)
{
# kill comments, but take care of "#" inside *simple* strings
s/^((".*?"|[^#"])*)#.*/$1/;
# normalise whitespace; keeps later regexes very simple
s/=/ = /;
s/\s+/ /g;
s/^ //;
s/ $//;
# and blank lines
next unless /\S/;
# user or repo groups
if (/^(@\S+) = (.*)/)
{
# store the members of each group as hash key. Keep track of when
# the group was *first* created by using $fragment as the *value*
do { $groups{$1}{$_} ||= $fragment } for ( expand_list( split(' ', $2) ) );
die "$ABRT bad group $1\n" unless $1 =~ $REPONAME_PATT;
}
# repo(s)
elsif (/^repo (.*)/)
{
# grab the list and expand any @stuff in it
@repos = split ' ', $1;
if (@repos == 1 and $repos[0] eq '@all') {
@repos = keys %repos;
} else {
@repos = expand_list ( @repos );
do { die "$ABRT bad reponame $_\n" unless ($GL_WILDREPOS ? $_ =~ $REPOPATT_PATT : $_ =~ $REPONAME_PATT) } for @repos;
}
s/\bCREAT[EO]R\b/\$creater/g for @repos;
}
# actual permission line
elsif (/^(-|C|R|RW|RW\+) (.* )?= (.+)/)
{
my $perms = $1;
my @refs; @refs = split(' ', $2) if $2;
@refs = expand_list ( @refs );
my @users = split ' ', $3;
die "wildrepos disabled, cant use 'C' in config\n" if $perms eq 'C' and not $GL_WILDREPOS;
# if no ref is given, this PERM applies to all refs
@refs = qw(refs/.*) unless @refs;
# deprecation warning
map { warn "WARNING: old syntax 'PATH/' found; please use new syntax 'NAME/'\n" if s(^PATH/)(NAME/) } @refs;
# fully qualify refs that dont start with "refs/" or "NAME/";
# prefix them with "refs/heads/"
@refs = map { m(^(refs|NAME)/) or s(^)(refs/heads/); $_ } @refs;
# expand the user list, unless it is just "@all"
@users = expand_list ( @users )
unless (@users == 1 and $users[0] eq '@all');
do { die "$ABRT bad username $_\n" unless $_ =~ $USERNAME_PATT } for @users;
s/\bCREAT[EO]R\b/\$creater/g for @users;
s/\bREADERS\b/\$readers/g for @users;
s/\bWRITERS\b/\$writers/g for @users;
# ok, we can finally populate the %repos hash
for my $repo (@repos) # each repo in the current stanza
{
# if we're processing a delegated config file (not the master
# config), we need to prevent attempts by that admin to obtain
# rights on stuff outside his domain
# trying to set access for $repo (='foo')...
if (
# processing the master config, not a fragment
( $fragment eq 'master' ) or
# fragment is also called 'foo' (you're allowed to have a
# fragment that is only concerned with one repo)
( $fragment eq $repo ) or
# fragment is called "bar" and "@bar = foo" has been
# defined in the master config
( ($groups{"\@$fragment"}{$repo} || '') eq 'master' )
) {
# all these are fine
} else {
# this is a little more complex
# fragment is called "bar", one or more "@bar = regex"
# have been specified in master, and "foo" matches some
# such "regex"
my @matched = grep { $repo =~ /^$_$/ }
grep { $groups{"\@$fragment"}{$_} eq 'master' }
sort keys %{ $groups{"\@$fragment"} };
if (@matched < 1) {
$ignored{$fragment}{$repo} = 1;
next;
}
}
for my $user (@users)
{
$user_list{$user}++; # only to catch lint, see later
# for 1st level check (see faq/tips doc)
$repos{$repo}{C}{$user} = 1, next if $perms eq 'C';
$repos{$repo}{R}{$user} = 1 if $perms =~ /R/;
$repos{$repo}{W}{$user} = 1 if $perms =~ /W/;
# for 2nd level check, store each "ref, perms" pair in order
for my $ref (@refs)
{
# checking NAME based restrictions is expensive for
# the update hook (see the changes to src/hooks/update
# in this commit for why) so we would *very* much like
# to avoid doing it for the large majority of repos
# that do *not* use NAME limits. Setting a flag that
# can be checked right away will help us do that
$repos{$repo}{NAME_LIMITS} = 1 if $ref =~ /^NAME\//;
push @{ $repos{$repo}{$user} }, { $ref => $perms }
unless $rurp_seen{$repo}{$user}{$ref}{$perms}++;
}
}
}
}
# configuration
elsif (/^config (.+) = ?(.*)/)
{
my ($key, $value) = ($1, $2);
my @validkeys = split (' ', ($GL_GITCONFIG_KEYS || ''));
my @matched = grep { $key =~ /^$_$/ } @validkeys;
die "$ABRT git config $key not allowed\n" if (@matched < 1);
for my $repo (@repos) # each repo in the current stanza
{
$repo_config{$repo}{$key} = $value;
}
}
# include
elsif (/^include "(.+)"/)
{
my $file = $1;
$file = "$GL_ADMINDIR/conf/$file" unless $file =~ /^\//;
die "$WARN $fragment attempting to include configuration\n" if $fragment ne 'master';
die "$ABRT included file not found: '$file'\n" unless -f $file;
parse_conf_file($file, $fragment);
}
# very simple syntax for the gitweb description of repo; one of:
# reponame = "some description string"
# reponame "owner name" = "some description string"
elsif (/^(\S+)(?: "(.*?)")? = "(.*)"$/)
{
my ($repo, $owner, $desc) = ($1, $2, $3);
die "$ABRT bad repo name $repo\n" unless $repo =~ $REPONAME_PATT;
die "$WARN $fragment attempting to set description for $repo\n" if
$fragment ne 'master' and $fragment ne $repo and ($groups{"\@$fragment"}{$repo} || '') ne 'master';
$desc{"$repo.git"} = $desc;
$owner{"$repo.git"} = $owner || '';
}
else
{
die "$ABRT can't make head or tail of '$_'\n";
}
}
for my $ig (sort keys %ignored)
{
warn "\n\t\t***** WARNING *****\n" .
"\t$ig.conf attempting to set access for " .
join (", ", sort keys %{ $ignored{$ig} }) . "\n";
}
}
# parse the main config file
parse_conf_file($GL_CONF, 'master');
# parse any delegated fragments
wrap_chdir($GL_ADMINDIR);
for my $fragment_file (glob("conf/fragments/*.conf"))
{
# we already check (elsewhere) that a fragment called "foo" will not try
# to specify access control for a repo whose name is not "foo" or is not
# part of a group called "foo" created by master
# meanwhile, I found a possible attack where the admin for group B creates
# a "convenience" group of (a subset of) his users, and then the admin for
# repo group A (alphabetically before B) adds himself to that same group
# in his own fragment.
# as a result, admin_A now has access to group B repos :(
# so now we lock the groups hash to the value it had after parsing
# "master", and localise any changes to it by this fragment so that they
# don't propagate to the next fragment. Thus, each fragment now has only
# those groups that are defined in "master" and itself
local %groups = %groups;
my $fragment = $fragment_file;
$fragment =~ s/^conf\/fragments\/(.*).conf$/$1/;
parse_conf_file($fragment_file, $fragment);
}
my $compiled_fh = wrap_open( ">", $GL_CONF_COMPILED );
my $dumped_data = Data::Dumper->Dump([\%repos], [qw(*repos)]);
# the dump uses single quotes, but we convert any strings containing $creater,
# $readers, $writers, to double quoted strings. A wee bit sneaky, but not too
# much...
$dumped_data =~ s/'(?=[^']*\$(?:creater|readers|writers))(.*?)'/"$1"/g;
print $compiled_fh $dumped_data;
close $compiled_fh or die "$ABRT close compiled-conf failed: $!\n";
# ----------------------------------------------------------------------------
# any new repos to be created?
# ----------------------------------------------------------------------------
# modern gits allow cloning from an empty repo, so we just create it. Gitosis
# did not have that luxury, so it was forced to detect the first push and
# create it then
# but it turns out not everyone has "modern" gits :)
my $git_version = `git --version`;
my ($gv_maj, $gv_min, $gv_patchrel) = ($git_version =~ m/git version (\d+)\.(\d+)\.(\d+)/);
die "$ABRT I can't understand $git_version\n" unless ($gv_maj >= 1);
$git_version = $gv_maj*10000 + $gv_min*100 + $gv_patchrel; # now it's "normalised"
# repo-base needs to be an absolute path for this loop to work right
# so if it was not already absolute, prefix $HOME.
my $repo_base_abs = ( $REPO_BASE =~ m(^/) ? $REPO_BASE : "$ENV{HOME}/$REPO_BASE" );
wrap_chdir("$repo_base_abs");
for my $repo (sort keys %repos) {
next unless $repo =~ $REPONAME_PATT;
next if $repo =~ m(^EXTCMD/); # these are not real repos
unless (-d "$repo.git") {
print STDERR "creating $repo...\n";
new_repo($repo, "$GL_ADMINDIR/hooks/common");
# new_repo would have chdir'd us away; come back
wrap_chdir("$repo_base_abs");
}
# when repos are copied over from elsewhere, one had to run easy install
# once again to make the new (OS-copied) repo contain the proper update
# hook. Perhaps we can make this easier now, and eliminate the easy
# install, with a quick check (and a new, empty, "hook" as a sentinel)
unless (-l "$repo.git/hooks/gitolite-hooked") {
ln_sf("$GL_ADMINDIR/hooks/common", "*", "$repo.git/hooks");
# in case of package install, GL_ADMINDIR is no longer the top cop;
# override with the package hooks
ln_sf("$GL_PACKAGE_HOOKS/common", "*", "$repo.git/hooks") if $GL_PACKAGE_HOOKS;
}
}
warn "\n\t\t***** WARNING *****\n" .
"\tyour git version is older than 1.6.2\n" .
"\tgitolite will work but you MUST read the section on\n" .
"\t\"git version dependency\" in doc/3-faq-tips-etc.mkd\n"
if $git_version < 10602; # that's 1.6.2 to you
# ----------------------------------------------------------------------------
# update repo configurations
# ----------------------------------------------------------------------------
for my $repo (keys %repo_config) {
wrap_chdir("$repo_base_abs/$repo.git");
while ( my ($key, $value) = each(%{ $repo_config{$repo} }) ) {
if ($value) {
$value =~ s/^"(.*)"$/$1/;
system("git", "config", $key, $value);
} else {
system("git", "config", "--unset-all", $key);
}
}
}
# ----------------------------------------------------------------------------
# handle gitweb and daemon
# ----------------------------------------------------------------------------
# How you specify gitweb and daemon access is quite different from gitosis. I
# just assume you'll never have any *real* users called "gitweb" or "daemon"
# :-) These are now "pseduo users" -- giving them "R" access to a repo is all
# you have to do
wrap_chdir("$repo_base_abs");
# daemons first...
for my $repo (sort keys %repos) {
next unless $repo =~ $REPONAME_PATT;
my $export_ok = "$repo.git/git-daemon-export-ok";
if ($repos{$repo}{'R'}{'daemon'}) {
system("touch $export_ok");
} else {
unlink($export_ok);
}
}
my %projlist = ();
# ...then gitwebs
for my $repo (sort keys %repos) {
next unless $repo =~ $REPONAME_PATT;
my $desc_file = "$repo.git/description";
# note: having a description also counts as enabling gitweb
if ($repos{$repo}{'R'}{'gitweb'} or $desc{"$repo.git"}) {
$projlist{"$repo.git"} = 1;
# add the description file; no messages to user or error checking :)
$desc{"$repo.git"} and open(DESC, ">", $desc_file) and print DESC $desc{"$repo.git"} . "\n" and close DESC;
if ($owner{"$repo.git"}) {
# set the repository owner
system("git", "--git-dir=$repo.git", "config", "gitweb.owner", $owner{"$repo.git"});
} else {
# remove the repository owner setting
system("git --git-dir=$repo.git config --unset-all gitweb.owner 2>/dev/null");
}
} else {
# delete the description file; no messages to user or error checking :)
unlink $desc_file;
# remove the repository owner setting
system("git --git-dir=$repo.git config --unset-all gitweb.owner 2>/dev/null");
}
# unless there are other gitweb.* keys set, remove the section to keep the
# config file clean
my $keys = `git --git-dir=$repo.git config --get-regexp '^gitweb\\.' 2>/dev/null`;
if (length($keys) == 0) {
system("git --git-dir=$repo.git config --remove-section gitweb 2>/dev/null");
}
}
# update the project list
my $projlist_fh = wrap_open( ">", $PROJECTS_LIST);
for my $proj (sort keys %projlist) {
print $projlist_fh "$proj\n";
}
close $projlist_fh;
# ----------------------------------------------------------------------------
# "compile" ssh authorized_keys
# ----------------------------------------------------------------------------
my $authkeys_fh = wrap_open( "<", $ENV{HOME} . "/.ssh/authorized_keys",
"\tFor security reasons, gitolite will not *create* this file if it does\n" .
"\tnot already exist. Please see the \"admin\" document for details\n");
my $newkeys_fh = wrap_open( ">", $ENV{HOME} . "/.ssh/new_authkeys" );
# save existing authkeys minus the GL-added stuff
while (<$authkeys_fh>)
{
print $newkeys_fh $_ unless (/^# gito(sis-)?lite start/../^# gito(sis-)?lite end/);
}
# add our "start" line, each key on its own line (prefixed by command and
# options, in the standard ssh authorized_keys format), then the "end" line.
print $newkeys_fh "# gitolite start\n";
wrap_chdir($GL_KEYDIR);
for my $pubkey (glob("*"))
{
# lint check 1
unless ($pubkey =~ /\.pub$/)
{
print STDERR "WARNING: pubkey files should end with \".pub\", ignoring $pubkey\n";
next;
}
my $user = $pubkey; $user =~ s/(\@[^.]+)?\.pub$//;
# lint check 2
print STDERR "WARNING: pubkey $pubkey exists but user $user not in config\n"
unless $user_list{$user};
$user_list{$user} = 'has pubkey';
# apparently some pubkeys don't end in a newline...
my $pubkey_content = `cat $pubkey`;
$pubkey_content =~ s/\s*$/\n/;
# don't trust files with multiple lines (i.e., something after a newline)
if ($pubkey_content =~ /\n./)
{
print STDERR "WARNING: a pubkey file can only have one line (key); ignoring $pubkey\n";
next;
}
if ($SHELL_USERS and $SHELL_USERS =~ /(^|\s)$user(\s|$)/) {
print $newkeys_fh "command=\"$AUTH_COMMAND -s $user\",$AUTH_OPTIONS ";
} else {
print $newkeys_fh "command=\"$AUTH_COMMAND $user\",$AUTH_OPTIONS,no-pty ";
}
print $newkeys_fh $pubkey_content;
}
# lint check 3; a little more severe than the first two I guess...
for my $user (sort keys %user_list)
{
next if $user =~ /^(gitweb|daemon|\@all|\$creater|\$readers|\$writers)$/ or $user_list{$user} eq 'has pubkey';
print STDERR "$WARN user $user in config, but has no pubkey!\n";
}
print $newkeys_fh "# gitolite end\n";
close $newkeys_fh or die "$ABRT close newkeys failed: $!\n";
# all done; overwrite the file (use cat to avoid perm changes)
system("cat $ENV{HOME}/.ssh/authorized_keys > $ENV{HOME}/.ssh/old_authkeys");
system("cat $ENV{HOME}/.ssh/new_authkeys > $ENV{HOME}/.ssh/authorized_keys")
and die "couldn't write authkeys file\n";
system("rm $ENV{HOME}/.ssh/new_authkeys");

101
src/gl-conf-convert Executable file
View file

@ -0,0 +1,101 @@
#!/usr/bin/perl -w
use strict;
use warnings;
# migrate gitosis.conf to gitolite.conf format
# not very smart, but there shouldn't be any errors for simple configurations.
# the biggest thing you'll find is probably some comments rearranged or
# something, due to the "flush" thing below
# for stuff it can't handle, it'll ignore the trivial ones (like gitweb and
# daemon), and put in an obviously syntax error-ed line for "repositories" and
# "map" statements.
my @repos;
my @RO_repos;
my @comments;
my @users;
my $groupname;
# a gitosis.conf stanza ends when a new "[group name]" line shows up, so you
# can't write as you go; you have to accumulate and flush
sub flush {
die "repos but no users?\n" if (not @users and (@repos or @RO_repos));
# just a groupname
if (@users and not (@repos or @RO_repos)) {
print "\@$groupname = ", join(" ", @users), "\n";
}
# RW repos
if (@repos)
{
print "repo ", join(" ", @repos), "\n";
print " RW = ", join(" ", @users), "\n";
}
# RO repos
if (@RO_repos)
{
print "repo ", join(" ", @RO_repos), "\n";
print " R = ", join(" ", @users), "\n";
}
# comments; yes there'll be some reordering, sorry!
print @comments if @comments;
# empty out for next round
@users = ();
@repos = ();
@RO_repos = ();
@comments = ();
}
while (<>)
{
# pure comment lines or blank lines
if (/^\s*#/ or /^\s*$/) {
push @comments, $_;
next;
}
# not supported
if (/^repositories *=/ or /^map /) {
print STDERR "not supported: $_";
s/^/NOT SUPPORTED: /;
print;
next;
}
chomp;
# normalise whitespace to help later regexes
s/\s+/ /g;
s/ ?= ?/ = /;
s/^ //;
s/ $//;
# the chaff...
next if /^\[(gitosis|repo)\]$/
or /^(gitweb|daemon|loglevel|description|owner) =/;
# the wheat...
if (/^members = (.*)/) {
push @users, split(' ', $1);
next;
}
if (/^write?able = (.*)/) {
push @repos, split(' ', $1);
next;
}
if (/^readonly = (.*)/) {
push @RO_repos, split(' ', $1);
next;
}
# new group starts
if (/^\[group (.*?) ?\]/) {
flush();
$groupname = $1;
}
}
flush();

620
src/gl-easy-install Executable file
View file

@ -0,0 +1,620 @@
#!/bin/bash
# easy install for gitolite
# you run this on the client side, and it takes care of all the server side
# work. You don't have to do anything on the server side directly
# to do a manual install (since I have tested this only on Linux), open this
# script in a nice, syntax coloring, text editor and follow the instructions
# prefixed by the word "MANUAL" in the comments below :-)
# run without any arguments for "usage" info
# important setting: bail on any errors (else we have to check every single
# command!)
set -e
# ----------------------------------------------------------------------
# bootstrap and main
# ----------------------------------------------------------------------
if [[ $1 != boot/strap ]]
then
# did someone tell you you can't call functions before they're defined in
# bash? Don't believe everything you hear ;-)
. $0 boot/strap
main "$@"
cleanup
exit 0
fi
# ----------------------------------------------------------------------
# no direct executable statements after this; only functions
# ----------------------------------------------------------------------
main() {
basic_sanity "$@"
setup_tempdir
version_info "$@"
[[ -n $admin_name ]] && setup_local_ssh
copy_gl # src, conf, etc
run_install
[[ $upgrade == 0 ]] && initial_conf_key
# MANUAL: cd to $GL_ADMINDIR and run "src/gl-compile-conf"
ssh -p $port $user@$host "cd $GL_ADMINDIR; \$PWD/src/gl-compile-conf $quiet"
setup_pta
clone_it
}
# ----------------------------------------------------------------------
# setup temp files
# ----------------------------------------------------------------------
setup_tempdir() {
export tmpgli=tmp-gl-install
trap cleanup 0
mkdir -p $tmpgli
}
cleanup() {
rm -rf $tmpgli
}
# ----------------------------------------------------------------------
# service functions
# ----------------------------------------------------------------------
die() { echo "$@"; echo; echo "run $0 without any arguments for help and tips"; cleanup; exit 1; }
prompt() {
# receives two arguments. A short piece of text to be displayed, without
# pausing, in "quiet" mode, and a much longer one to be displayed, *with*
# a pause, in normal (verbose) mode
[[ $quiet == -q ]] && [[ -n $1 ]] && {
eval "echo \"$1\""
return
}
shift
echo
echo
echo ------------------------------------------------------------------------
eval "echo \"$1\""
echo
read -p '...press enter to continue or Ctrl-C to bail out'
}
usage() {
cat <<EOFU
Usage: $0 [-q] user host [port] admin_name # install
$0 [-q] user host [port] # upgrade
- (optional) "-q" as first arg sets "quiet" mode: no verbose descriptions of
what is going on, no pauses unless absolutely necessary
- "user" is the username on the server where you will be installing gitolite
- "host" is that server's hostname (or IP address)
- "port" is the ssh server port on "host"; optional, defaults to 22
- "admin_name" is *your* name as it should appear in the eventual gitolite
config file. For upgrades (ie., gitolite is already installed on the
server), this argument is not needed, and will be *ignored* if provided.
Example usage: $0 git my.git.server sitaram
Notes:
- "user" and "admin_name" must be simple names -- no special characters etc
please (only alphanumerics, dot, hyphen, underscore)
- traditionally, the "user" is "git", but it can be anything you want
- "admin_name" should be your name, for clarity, or whoever will be the
gitolite admin
Pre-requisites:
- you must already have pubkey based access to user@host. If you currently
only have password access, use "ssh-copy-id" or something equivalent (or
copy the key manually). Somehow (doesn't matter how), get to the point
where you can type "ssh user@host" and get a command line.
**DO NOT RUN THIS PROGRAM UNTIL THAT WORKS**
EOFU
exit 1;
}
# ----------------------------------------------------------------------
# basic sanity / argument checks
# ----------------------------------------------------------------------
basic_sanity() {
# MANUAL: this *must* be run as "src/gl-easy-install", not by cd-ing to
# src and then running "./gl-easy-install"
bindir=${0%/*}
# switch to parent of bindir; we assume the conf files are all there
cd "$bindir"; cd ..
# are we in quiet mode?
quiet=
[[ "$1" == "-q" ]] && {
quiet=-q
shift
}
# MANUAL: (info) we'll use "git" as the user, "server" as the host, and
# "sitaram" as the admin_name in example commands shown below, if any
[[ -z $2 ]] && usage
user=$1
host=$2
port=22
admin_name=$3
# but if the 3rd arg is a number, that's a port number, and the 4th arg is
# the admin_name
if echo $3 | perl -lne 'exit 1 unless /^[0-9]+$/'
then
port=$3
admin_name=$4
fi
echo $user | perl -lne 'exit 1 if /[^a-zA-Z0-9._-]/' ||
die "user '$user' invalid"
[[ "$user" == "root" ]] && die I refuse to install to root
echo $admin_name | perl -lne 'exit 1 if /[^a-zA-Z0-9._-]/' ||
die "admin_name '$admin_name' invalid"
# MANUAL: make sure you're in the gitolite directory, at the top level.
# The following files should all be visible:
ls hooks/gitolite-admin/post-update \
hooks/common/update \
src/gitolite.pm \
src/gl-install \
src/gl-auth-command \
src/gl-compile-conf \
conf/example.conf \
conf/example.gitolite.rc >/dev/null ||
die "cant find at least some files in gitolite sources/config; aborting"
# MANUAL: make sure you have password-less (pubkey) auth on the server.
# That is, running "ssh git@server" should log in straight away, without
# asking for a password
ssh -p $port -o PasswordAuthentication=no $user@$host true ||
die "pubkey access didn't work; please set it up using 'ssh-copy-id' or something"
}
# ----------------------------------------------------------------------
# version info
# ----------------------------------------------------------------------
version_info() {
# MANUAL: if needed, make a note of the version you are upgrading from, and to
# record which version is being sent across; we assume it's HEAD
git describe --tags --long HEAD 2>/dev/null > conf/VERSION || echo '(unknown)' > conf/VERSION
# what was the old version there?
export upgrade_details="you are upgrading \
$(ssh -p $port $user@$host cat gitolite-install/conf/VERSION 2>/dev/null || echo '(or installing first-time)' ) \
to $(cat conf/VERSION)"
prompt "$upgrade_details" "$v_upgrade_details"
}
# ----------------------------------------------------------------------
# new keypair, ssh-config para; only on "install" (not upgrade)
# ----------------------------------------------------------------------
setup_local_ssh() {
# MANUAL: create a new key for you as a "gitolite user" (as opposed to you
# as the "gitolite admin" who needs to login to the server and get a
# command line). For example, "ssh-keygen -t rsa ~/.ssh/sitaram"; this
# would create two files in ~/.ssh (sitaram and sitaram.pub)
prompt "setting up keypair..." "$v_setting_up_keypair"
if [[ -f "$HOME/.ssh/$admin_name.pub" ]]
then
prompt "" "$v_reuse_pubkey"
else
ssh-keygen -t rsa -f "$HOME/.ssh/$admin_name" || die "ssh-keygen failed for some reason..."
fi
# MANUAL: copy the pubkey created to the server, say to /tmp. This would
# be "scp ~/.ssh/sitaram.pub git@server:/tmp" (the script does this at a
# later stage, you do it now for convenience). Note: only the pubkey
# (sitaram.pub). Do NOT copy the ~/.ssh/sitaram file -- that is a private
# key!
# MANUAL: if you're running ssh-agent (see if you have an environment
# variable called SSH_AGENT_PID in your "env"), you should add this new
# key. The command is "ssh-add ~/.ssh/sitaram"
if ssh-add -l &>/dev/null
then
prompt " ...adding key to agent..." "$v_ssh_add"
ssh-add "$HOME/.ssh/$admin_name"
fi
# MANUAL: you now need to add some lines to the end of your ~/.ssh/config
# file. If the file doesn't exist, create it. Make sure the file is
# "chmod 644".
# The lines to be included look like this:
# host gitolite
# user git
# hostname server
# port 22
# identityfile ~/.ssh/sitaram
echo "host gitolite
user $user
hostname $host
port $port
identityfile ~/.ssh/$admin_name" > $tmpgli/.gl-stanza
if grep 'host *gitolite' "$HOME/.ssh/config" &>/dev/null
then
prompt "found gitolite para in ~/.ssh/config; assuming it is correct..." "$v_found_para"
else
prompt "creating gitolite para in ~/.ssh/config..." "$v_creating_para"
cat $tmpgli/.gl-stanza >> "$HOME/.ssh/config"
# if the file didn't exist at all, it might have the wrong permissions
chmod 644 "$HOME/.ssh/config"
fi
}
# ----------------------------------------------------------------------
# server side
# ----------------------------------------------------------------------
copy_gl() {
# MANUAL: copy the gitolite directories "src", "conf", and "doc" to the
# server, to a directory called (for example) "gitolite-install". You may
# have to create the directory first.
ssh -p $port $user@$host mkdir -p gitolite-install
scp $quiet -P $port -r src conf doc hooks $user@$host:gitolite-install/
# MANUAL: now log on to the server (ssh git@server) and get a command
# line. This step is for your convenience; the script does it all from
# the client side but that may be too much typing for manual use ;-)
# MANUAL: cd to the "gitolite-install" directory where the sources are.
# Then copy conf/example.gitolite.rc as ~/.gitolite.rc and edit it if you
# wish to change any paths. Make a note of the GL_ADMINDIR and REPO_BASE
# paths; you will need them later
prompt "finding/creating gitolite rc..." "$v_edit_glrc"
# lets try and get the file from there first
if scp -P $port $user@$host:.gitolite.rc $tmpgli &>/dev/null
then
prompt " ...trying to reuse existing rc" \
"Oh hey... you already had a '.gitolite.rc' file on the server.
Let's see if we can use that instead of the default one..."
< $tmpgli/.gitolite.rc perl -ne 'print "$1\n" if /^\s*(\$\w+) *=/' | sort > $tmpgli/glrc.old
< conf/example.gitolite.rc perl -ne 'print "$1\n" if /^\s*(\$\w+) *=/' | sort > $tmpgli/glrc.new
# msysgit doesn't have "comm". diff is not ideal for our purposes
# because we only care about differences in one direction, but we'll
# have to make do...
set +e
diff -u $tmpgli/glrc.old $tmpgli/glrc.new | grep '^+.*\$' > $tmpgli/glrc.comm13
set -e
if [[ ! -s $tmpgli/glrc.comm13 ]]
then
[[ $quiet == -q ]] || ${VISUAL:-${EDITOR:-vi}} $tmpgli/.gitolite.rc
else
echo new variables found in rc file:
cat $tmpgli/glrc.comm13
echo
# MANUAL: if you're upgrading, read the instructions below and
# manually make sure your final ~/.gitolite.rc has both your existing
# customisations as well as any new variables that the new version of
# gitolite has introduced
prompt "" "$v_upgrade_glrc"
${VISUAL:-${EDITOR:-vi}} conf/example.gitolite.rc $tmpgli/.gitolite.rc
fi
else
cp conf/example.gitolite.rc $tmpgli/.gitolite.rc
[[ $quiet == -q ]] || ${VISUAL:-${EDITOR:-vi}} $tmpgli/.gitolite.rc
fi
# copy the rc across
scp $quiet -P $port $tmpgli/.gitolite.rc $user@$host:
}
run_install() {
prompt "installing/upgrading..." "$v_ignore_stuff"
# extract the GL_ADMINDIR, REPO_BASE and GIT_PATH locations
GL_ADMINDIR=$(ssh -p $port $user@$host "perl -e 'do \".gitolite.rc\"; print \$GL_ADMINDIR'")
REPO_BASE=$( ssh -p $port $user@$host "perl -e 'do \".gitolite.rc\"; print \$REPO_BASE'")
GIT_PATH=$( ssh -p $port $user@$host "perl -e 'do \".gitolite.rc\"; print \$GIT_PATH'")
# determine if this is an upgrade; we decide based on whether a file
# called $GL_ADMINDIR/conf/gitolite.conf exists on the remote side. We
# can't do this till we know the correct value for GL_ADMINDIR
upgrade=0
if ssh -p $port $user@$host cat $GL_ADMINDIR/conf/gitolite.conf &> /dev/null
then
upgrade=1
ssh -p $port $user@$host cat $GL_ADMINDIR/conf/gitolite.conf 2> /dev/null | grep '@SHELL' &&
prompt "" "$v_at_shell_bwi"
[[ -n $admin_name ]] && echo -e "\n *** WARNING ***: looks like an upgrade... ignoring argument '$admin_name'"
else
[[ -z $admin_name ]] && die " *** ERROR ***: doesn't look like an upgrade, so I need a name for the admin"
fi
# MANUAL: still in the "gitolite-install" directory? Good. Run
# "src/gl-install"
ssh -p $port $user@$host "cd gitolite-install; src/gl-install $quiet"
# MANUAL: if you're upgrading, run "src/gl-compile-conf" and you're done!
# -- ignore the rest of this file for the purposes of an upgrade
}
# ----------------------------------------------------------------------
# from here on it's install only
# ----------------------------------------------------------------------
# MANUAL: setup the initial config file. Edit $GL_ADMINDIR/conf/gitolite.conf
# and add at least the following lines to it:
# repo gitolite-admin
# RW+ = sitaram
initial_conf_key() {
echo "#gitolite conf
# please see conf/example.conf for details on syntax and features
repo gitolite-admin
RW+ = $admin_name
repo testing
RW+ = @all
" > $tmpgli/gitolite.conf
# send the config and the key to the remote
scp $quiet -P $port $tmpgli/gitolite.conf $user@$host:$GL_ADMINDIR/conf/
scp $quiet -P $port "$HOME/.ssh/$admin_name.pub" $user@$host:$GL_ADMINDIR/keydir
}
# ----------------------------------------------------------------------
# hey lets go the whole hog on this; setup push-to-admin!
# ----------------------------------------------------------------------
setup_pta() {
# MANUAL: you have to now make the first commit in the admin repo. This
# is a little more complex, so read carefully and substitute the correct
# paths. What you have to do is:
# cd $REPO_BASE/gitolite-admin.git
# GIT_WORK_TREE=$GL_ADMINDIR git add conf/gitolite.conf keydir
# GIT_WORK_TREE=$GL_ADMINDIR git commit -am start
# Substitute $GL_ADMINDIR and $REPO_BASE appropriately. Note there is no
# space around the "=" in the second and third lines.
echo "cd $REPO_BASE/gitolite-admin.git
PATH=$PATH:$GIT_PATH
GIT_WORK_TREE=$GL_ADMINDIR git add conf/gitolite.conf keydir
GIT_WORK_TREE=$GL_ADMINDIR git diff --cached --quiet 2>/dev/null || GIT_WORK_TREE=$GL_ADMINDIR git commit -am start
" | ssh -p $port $user@$host
# MANUAL: now that the admin repo is created, you have to set the hooks
# properly. The install program does this. So cd back to the
# "gitolite-install" directory and run "src/gl-install"
ssh -p $port $user@$host "cd gitolite-install; src/gl-install $quiet"
# MANUAL: you're done! Log out of the server, come back to your
# workstation, and clone the admin repo using "git clone
# gitolite:gitolite-admin", or pull once again if you already have a
# clone
}
clone_it()
{
cleanup
cd "$HOME"
if [[ -d gitolite-admin ]]
then
echo $HOME/gitolite-admin exists, skipping clone step...
else
prompt "cloning gitolite-admin repo..." "$v_cloning"
git clone gitolite:gitolite-admin
fi
# MANUAL: be sure to read the message below; this applies to you too...
echo
echo
echo ---------------------------------------------------------------
eval "echo \"$v_done\""
}
# ----------------------------------------------------------------------
# prompt strings
# ----------------------------------------------------------------------
v_upgrade_details="
\$upgrade_details
Note: getting '(unknown)' for the 'from' version should only happen once.
Getting '(unknown)' for the 'to' version means you are probably installing
from a tar file dump, not a real clone. This is not an error but it's nice to
have those version numbers in case you need support. Try and install from a
clone
"
v_setting_up_keypair="
the next command will create a new keypair for your gitolite access
The pubkey will be \$HOME/.ssh/\$admin_name.pub. You will have to choose a
passphrase or hit enter for none. I recommend not having a passphrase for
now, *especially* if you do not have a passphrase for the key which you are
already using to get server access!
Add one using 'ssh-keygen -p' after all the setup is done and you've
successfully cloned and pushed the gitolite-admin repo. After that, install
'keychain' or something similar, and add the following command to your bashrc
(since this is a non-default key)
ssh-add "\\\$HOME/.ssh/\$admin_name"
This makes using passphrases very convenient.
"
v_reuse_pubkey="
Hmmm... pubkey \$HOME/.ssh/\$admin_name.pub exists; should I just (re-)use it?
IMPORTANT: once the install completes, *this* key can no longer be used to get
a command line on the server -- it will be used by gitolite, for git access
only. If that is a problem, please ABORT now.
doc/6-ssh-troubleshooting.mkd will explain what is happening here, if you need
more info.
"
v_ssh_add="
you're running ssh-agent. We'll try and do an ssh-add of the
private key we just created, otherwise this key won't get picked up. If
you specified a passphrase in the previous step, you'll get asked for one
now -- type in the same one.
"
v_found_para="
your \\\$HOME/.ssh/config already has settings for gitolite. I will assume
they're correct, but if they're not, please edit that file, delete that
paragraph (that line and the following few lines), Ctrl-C, and rerun.
In case you want to check right now (from another terminal) if they're
correct, here's what they are *supposed* to look like:
\$(cat \$tmpgli/.gl-stanza)
"
v_creating_para="
creating settings for your gitolite access in \$HOME/.ssh/config;
these are the lines that will be appended to your ~/.ssh/config:
\$(cat \$tmpgli/.gl-stanza)
"
v_edit_glrc="
the gitolite rc file needs to be edited by hand. The defaults are sensible,
so if you wish, you can just exit the editor.
Otherwise, make any changes you wish and save it. Read the comments to
understand what is what -- the rc file's documentation is inline.
Please remember this file will actually be copied to the server, and that all
the paths etc. represent paths on the server!
"
v_upgrade_glrc="
looks like you're upgrading, and there are some new rc variables that this
version is expecting that your old rc file doesn't have.
I'm going to run your \\\$EDITOR with two filenames. The first is the example
file from this gitolite version. It will have a block (code and comments) for
each of the variables shown above with a '+' sign.
The second is your current rc file, the destination. Copy those lines into
this file, preferably *with* the surrounding comments (for clarity) and save
it.
This is necessary; please dont skip this!
[It's upto you to figure out how your \\\$EDITOR handles 2 filename arguments,
switch between them, copy lines, etc ;-)]
"
v_ignore_stuff="
ignore any 'please edit this file' or 'run this command' type lines in the
next set of command outputs coming up. They're only relevant for a manual
install, not this one...
"
v_at_shell_bwi="
you are using the @SHELL feature in your gitolite config. This feature has
now changed in a backward incompatible way; see doc/6-ssh-troubleshooting.mkd
for information on migrating this to the new syntax.
DO NOT hit enter unless you have understood that information and properly
migrated your setup, or you are sure you have shell access to the server
through some other means than the $admin_name key.
"
v_done="
done!
Reminder:
*Your* URL for cloning any repo on this server will be
gitolite:reponame.git
*Other* users you set up will have to use
\$user@\$host:reponame.git
However, if your server uses a non-standard ssh port, they should use
ssh://\$user@\$host:\$port/reponame.git
If this is your first time installing gitolite, please also:
tail -31 \$0
for next steps.
"
v_cloning="
now we will clone the gitolite-admin repo to your workstation and see if it
all hangs together. We'll do this in your \\\$HOME for now, and you can move
it elsewhere later if you wish to.
"
tail="
NOTE: All the below stuff is on your *workstation*. You should not, normally,
have to do anything directly on your server to administer/use gitolite.
The admin repo is currently cloned at ~/gitolite-admin. You can reclone it
anywhere else if you wish. To administer gitolite, make changes to the config
file (conf/gitolite.conf) and/or the pubkeys (in subdirectory 'keydir') in any
clone, then git add, git commit, and git push.
ADDING REPOS: Do NOT add repos manually on the server. Edit the config file
to give *some* user access to the repo. When you push, an empty repo will be
created on the server.
ADDING USERS: copy their pubkey as keydir/<username>.pub, add it, commit and
push.
CONFIG FILE FORMAT: see comments in conf/example.conf in the gitolite source.
SSH MAGIC: Remember you (the admin) now have *two* keys to access the server
hosting your gitolite setup -- one to get you a command line, and one to get
you gitolite access; see doc/6-ssh-troubleshooting.mkd. If you're not using
keychain or some such software, you may have to run an 'ssh-add' command to
add that key each time you log in.
URLS: *Your* URL for cloning any repo on this server is different from the
url that the *other* users have to use. The easy install command should tell
you what these URLs look like, at the end of each successful run. Feel free
to re-run easy install again (using the same arguments) if you missed it.
UPGRADING GITOLITE: just pull a fresh clone from github, and run the same easy
install command as before, with the same arguments.
"

40
src/gl-emergency-addkey Executable file
View file

@ -0,0 +1,40 @@
#!/bin/bash
# what/why: re-establish gitolite admin access when admin key(s) lost
# where: on server (NOT client!)
# pre-req: shell access to the server (even with password is fine)
# pre-work: - make yourself a new keypair on your workstation
# - copy the pubkey and this script to the server
# usage: $0 admin_name client_host_shortname pubkeyfile
# notes: - admin_name should already have RW or RW+ access to the
# gitolite-admin repo
# - client_host_shortname is any simple word; see example below
# WARNING: ABSOLUTELY NO ARGUMENT CHECKING DONE
# WARNING: NEWER GITS ONLY ON SERVER SIDE (for now)
# example: $0 sitaram laptop /tmp/sitaram.pub
# result: a new keyfile named sitaram@laptop.pub would be added
# ENDHELP
[[ -z $1 ]] && { perl -pe "s(\\\$0)($0); last if /ENDHELP/" < $0; exit 1; }
set -e
cd
REPO_BASE=$( perl -e 'do ".gitolite.rc"; print $REPO_BASE' )
GL_ADMINDIR=$(perl -e 'do ".gitolite.rc"; print $GL_ADMINDIR')
cd; cd $GL_ADMINDIR/keydir; pwd
cp -v $3 $1@$2.pub
cd; cd $REPO_BASE/gitolite-admin.git; pwd
# XXX FIXME TODO -- fix this to work with older gits also
GIT_WORK_TREE=$GL_ADMINDIR git add keydir
GIT_WORK_TREE=$GL_ADMINDIR git commit -m "emergency add $1@$2.pub"
cd $GL_ADMINDIR
src/gl-compile-conf

107
src/gl-install Executable file
View file

@ -0,0 +1,107 @@
#!/usr/bin/perl
use strict;
use warnings;
our ($REPO_BASE, $GL_ADMINDIR, $GL_CONF, $GIT_PATH, $GL_PACKAGE_CONF, $GL_PACKAGE_HOOKS);
# setup quiet mode if asked; please do not use this when running manually
open STDOUT, ">", "/dev/null" if (@ARGV and shift eq '-q');
# wrapper around mkdir; it's not an error if the directory exists, but it is
# an error if it doesn't exist and we can't create it
sub wrap_mkdir
{
my $dir = shift;
if ( -d $dir ) {
print "$dir already exists\n";
return;
}
mkdir($dir) or die "mkdir $dir failed: $!\n";
print "created $dir\n";
}
# the common setup module is in the same directory as this running program is
my $bindir = $0;
$bindir =~ s/\/[^\/]+$//;
require "$bindir/gitolite.pm";
# ask where the rc file is, get it, and "do" it
&where_is_rc();
unless ($ENV{GL_RC}) {
# doesn't exist. Copy it across, tell user to edit it and come back
my $glrc = $ENV{HOME} . "/.gitolite.rc";
if ($GL_PACKAGE_CONF) {
system("cp $GL_PACKAGE_CONF/example.gitolite.rc $glrc");
} else {
system("cp $bindir/../conf/example.gitolite.rc $glrc");
}
print "created $glrc\n";
print "please edit it, change the paths if you wish to, and RERUN THIS SCRIPT\n";
exit;
}
# ok now the rc file exists; read it to get the other paths
die "parse $ENV{GL_RC} failed: " . ($! or $@) unless do $ENV{GL_RC};
# add a custom path for git binaries, if specified
$ENV{PATH} .= ":$GIT_PATH" if $GIT_PATH;
# mkdir $REPO_BASE, $GL_ADMINDIR if they don't already exist
my $repo_base_abs = ( $REPO_BASE =~ m(^/) ? $REPO_BASE : "$ENV{HOME}/$REPO_BASE" );
wrap_mkdir($repo_base_abs);
wrap_mkdir($GL_ADMINDIR);
# mkdir $GL_ADMINDIR's subdirs
for my $dir qw(conf doc keydir logs src hooks hooks/common hooks/gitolite-admin) {
# some of them will stay empty; too lazy to fix right now ;-)
wrap_mkdir("$GL_ADMINDIR/$dir");
}
# "src" and "doc" will be overwritten on each install, but not conf
if ($GL_PACKAGE_HOOKS) {
system("cp -R $GL_PACKAGE_HOOKS $GL_ADMINDIR");
} else {
system("cp -R $bindir/../src $bindir/../doc $bindir/../hooks $GL_ADMINDIR");
system("cp $bindir/../conf/VERSION $GL_ADMINDIR/conf");
}
unless (-f $GL_CONF or $GL_PACKAGE_CONF) {
print <<EOF;
please do the following:
1. create and edit $GL_CONF to contain something like this:
repo gitolite-admin
RW+ = yourname
2. copy "yourname.pub" to $GL_ADMINDIR/keydir
3. run this command
$GL_ADMINDIR/src/gl-compile-conf
EOF
}
# finally, hooks must be propagated to all the repos in case they changed
chdir("$repo_base_abs") or die "chdir $repo_base_abs failed: $!\n";
for my $repo (`find . -type d -name "*.git"`) {
chomp ($repo);
# propagate our own, plus any local admin-defined, hooks
ln_sf("$GL_ADMINDIR/hooks/common", "*", "$repo/hooks");
# in case of package install, GL_ADMINDIR is no longer the top cop;
# override with the package hooks
ln_sf("$GL_PACKAGE_HOOKS/common", "*", "$repo/hooks") if $GL_PACKAGE_HOOKS;
chmod 0755, "$repo/hooks/update";
}
# oh and one of those repos is a bit more special and has an extra hook :)
if ( -d "gitolite-admin.git/hooks" ) {
print "copying post-update hook to gitolite-admin repo...\n";
unlink "gitolite-admin.git/hooks/post-update";
symlink "$GL_ADMINDIR/hooks/gitolite-admin/post-update", "gitolite-admin.git/hooks/post-update"
or die "could not symlink post-update hook\n";
# ditto... (see previous block)
ln_sf("$GL_PACKAGE_HOOKS/gitolite-admin", "post-update", "gitolite-admin.git/hooks") if $GL_PACKAGE_HOOKS;
chmod 0755, "gitolite-admin.git/hooks/post-update";
}
# fixup program renames
for my $oldname qw(pta-hook.sh conf-convert.pl 00-easy-install.sh 99-emergency-addkey.sh install.pl update-hook.pl hooks/update ga-post-update-hook VERSION) {
unlink "$GL_ADMINDIR/src/$oldname";
unlink "$ENV{HOME}/gitolite-install/src/$oldname";
}

92
src/gl-setup Executable file
View file

@ -0,0 +1,92 @@
#!/bin/sh
GL_PACKAGE_CONF=/tmp/share/gitolite/conf
# must be the same as the value for the same variable in
# $GL_PACKAGE_CONF/example.gitolite.rc. Sorry about the catch-22 :)
# TODO need to fix for portability to ksh and so on
# TODO need to get the version in there somehow
# This program is meant to be completely non-interactive, suitable for running
# server-side from a "post RPM/DEB install" script, or manually by users.
# Please see the doc/0-user-setup.mkd for details.
# usage:
# $0 [foo.pub]
# The pubkey filename must end with ".pub" and is mandatory when you first run
# this command. Otherwise it is optional, and can be used to override a
# pubkey file if you happen to have lost all gitolite-access to the repos (but
# do have shell access via some other means)
die() { echo "$@"; echo death at line number ${BASH_LINENO[0]}; exit 1; }
pubkey_file=$1
admin_name=
if [[ -n $pubkey_file ]]
then
[[ $pubkey_file =~ .pub$ ]] || die "$pubkey_file must end in .pub"
[[ -f $pubkey_file ]] || die "cant find $pubkey_file"
admin_name=$(basename $pubkey_file .pub)
fi
if [[ -f ~/.gitolite.rc ]]
then
perl -ne 's/^\s+//; s/[\s=].*//; print if /^\$/;' < $GL_PACKAGE_CONF/example.gitolite.rc | sort > .newvars
perl -ne 's/^\s+//; s/[\s=].*//; print if /^\$/;' < ~/.gitolite.rc | sort > .oldvars
comm -23 .newvars .oldvars > .diffvars
if [[ -s .diffvars ]]
then
cp $GL_PACKAGE_CONF/example.gitolite.rc ~/.gitolite.rc.new
echo new version of the rc file saved in ~/.gitolite.rc.new
echo
echo please update ~/.gitolite.rc manually if you need features
echo controlled by any of the following variables:
echo ----
sed -e 's/^/ /' < .diffvars
echo ----
fi
rm -f .newvars .oldvars .diffvars
else
[[ -n $pubkey_file ]] || die "looks like first run -- I need a pubkey file"
cp $GL_PACKAGE_CONF/example.gitolite.rc ~/.gitolite.rc
fi
# setup ssh stuff. We break our normal rule that we will not fiddle with
# authkeys etc., because in this case it seems appropriate
cd
mkdir -p .ssh
touch .ssh/authorized_keys
chmod go-w . .ssh .ssh/authorized_keys
# now we get to gitolite itself
gl-install -q
GL_ADMINDIR=$(cd;perl -e 'do ".gitolite.rc"; print $GL_ADMINDIR')
REPO_BASE=$( cd;perl -e 'do ".gitolite.rc"; print $REPO_BASE' )
[[ -f $GL_ADMINDIR/conf/gitolite.conf ]] || {
cat <<EOF > $GL_ADMINDIR/conf/gitolite.conf
repo gitolite-admin
RW+ = $admin_name
repo testing
RW+ = @all
EOF
}
[[ -n $pubkey_file ]] && cp $pubkey_file $GL_ADMINDIR/keydir
touch $HOME/.ssh/authorized_keys
gl-compile-conf -q
# setup push-to-admin
od=$PWD
cd; cd $REPO_BASE/gitolite-admin.git
GIT_WORK_TREE=$GL_ADMINDIR git add conf/gitolite.conf keydir
GIT_WORK_TREE=$GL_ADMINDIR git diff --cached --quiet || GIT_WORK_TREE=$GL_ADMINDIR git commit -am start
cd $od
# now that the admin repo is created, you have to set the hooks properly; best
# do it by running install again
gl-install -q

View file

@ -1,324 +0,0 @@
package Gitolite::Common;
# common (non-gitolite-specific) functions
# ----------------------------------------------------------------------
#<<<
@EXPORT = qw(
print2 dbg _mkdir _open ln_sf tsh_rc sort_u
say _warn _chdir _print tsh_text list_phy_repos
say2 _die _system slurp tsh_lines
trace cleanup_conf_line tsh_try
usage tsh_run
gen_lfn
gl_log
dd
t_start
t_lap
);
#>>>
use Exporter 'import';
use File::Path qw(mkpath);
use Carp qw(carp cluck croak confess);
use strict;
use warnings;
# ----------------------------------------------------------------------
sub print2 {
local $/ = "\n";
print STDERR @_;
}
sub say {
local $/ = "\n";
print @_, "\n";
}
sub say2 {
local $/ = "\n";
print STDERR @_, "\n";
}
sub trace {
gl_log( "\t" . join( ",", @_[ 1 .. $#_ ] ) ) if $_[0] <= 1 and defined $Gitolite::Rc::rc{LOG_EXTRA};
return unless defined( $ENV{D} );
my $level = shift; return if $ENV{D} < $level;
my $args = ''; $args = join( ", ", @_ ) if @_;
my $sub = ( caller 1 )[3] || ''; $sub =~ s/.*://;
if ( not $sub ) {
$sub = (caller)[1];
$sub =~ s(.*/(.*))(($1));
}
$sub .= ' ' x ( 32 - length($sub) );
say2 "TRACE $level $sub", ( @_ ? shift : () );
say2( "TRACE $level " . ( " " x 32 ), $_ ) for @_;
}
sub dbg {
use Data::Dumper;
return unless defined( $ENV{D} );
for my $i (@_) {
print STDERR "DBG: " . Dumper($i);
}
}
sub dd {
local $ENV{D} = 1;
dbg(@_);
}
{
use Time::HiRes;
my %start_times;
sub t_start {
my $name = shift || 'default';
$start_times{$name} = [ Time::HiRes::gettimeofday() ];
}
sub t_lap {
my $name = shift || 'default';
return Time::HiRes::tv_interval( $start_times{$name} );
}
}
sub _warn {
gl_log( 'warn', @_ );
if ( $ENV{D} and $ENV{D} >= 3 ) {
cluck "WARNING: ", @_, "\n";
} elsif ( defined( $ENV{D} ) ) {
carp "WARNING: ", @_, "\n";
} else {
warn "WARNING: ", @_, "\n";
}
}
$SIG{__WARN__} = \&_warn;
sub _die {
gl_log( 'die', @_ );
if ( $ENV{D} and $ENV{D} >= 3 ) {
confess "FATAL: " . join( ",", @_ ) . "\n" if defined( $ENV{D} );
} elsif ( defined( $ENV{D} ) ) {
croak "FATAL: " . join( ",", @_ ) . "\n";
} else {
die "FATAL: " . join( ",", @_ ) . "\n";
}
}
$SIG{__DIE__} = \&_die;
sub usage {
_warn(shift) if @_;
my $script = (caller)[1];
my $function = ( ( ( caller(1) )[3] ) || ( ( caller(0) )[3] ) );
$function =~ s/.*:://;
my $code = slurp($script);
$code =~ /^=for $function\b(.*?)^=cut/sm;
say2( $1 ? $1 : "...no usage message in $script" );
exit 1;
}
sub _mkdir {
# it's not an error if the directory exists, but it is an error if it
# doesn't exist and we can't create it
my $dir = shift;
my $perm = shift; # optional
return if -d $dir;
mkpath($dir);
chmod $perm, $dir if $perm;
return 1;
}
sub _chdir {
chdir( $_[0] || $ENV{HOME} ) or _die "chdir $_[0] failed: $!\n";
}
sub _system {
# run system(), catch errors. Be verbose only if $ENV{D} exists. If not,
# exit with <rc of system()> if it applies, else just "exit 1".
trace( 1, 'system', @_ );
if ( system(@_) != 0 ) {
trace( 1, "system() failed", @_, "-> $?" );
if ( $? == -1 ) {
die "failed to execute: $!\n" if $ENV{D};
} elsif ( $? & 127 ) {
die "child died with signal " . ( $? & 127 ) . "\n" if $ENV{D};
} else {
die "child exited with value " . ( $? >> 8 ) . "\n" if $ENV{D};
exit( $? >> 8 );
}
exit 1;
}
}
sub _open {
open( my $fh, $_[0], $_[1] ) or _die "open $_[1] failed: $!\n";
return $fh;
}
sub _print {
my ( $file, @text ) = @_;
my $fh = _open( ">", "$file.$$" );
print $fh @text;
close($fh) or _die "close $file failed: $! at ", (caller)[1], " line ", (caller)[2], "\n";
my $oldmode = ( ( stat $file )[2] );
rename "$file.$$", $file;
chmod $oldmode, $file if $oldmode;
}
sub slurp {
return unless defined wantarray;
local $/ = undef unless wantarray;
my $fh = _open( "<", $_[0] );
return <$fh>;
}
sub dos2unix {
# WARNING: when calling this, make sure you supply a list context
s/\r\n/\n/g for @_;
return @_;
}
sub ln_sf {
trace( 3, @_ );
my ( $srcdir, $glob, $dstdir ) = @_;
for my $hook ( glob("$srcdir/$glob") ) {
$hook =~ s/$srcdir\///;
unlink "$dstdir/$hook";
symlink "$srcdir/$hook", "$dstdir/$hook" or croak "could not symlink $srcdir/$hook to $dstdir\n";
}
}
sub sort_u {
my %uniq;
my $listref = shift;
return [] unless @{$listref};
undef @uniq{ @{$listref} }; # expect a listref
my @sort_u = sort keys %uniq;
return \@sort_u;
}
sub cleanup_conf_line {
my $line = shift;
# kill comments, but take care of "#" inside *simple* strings
$line =~ s/^((".*?"|[^#"])*)#.*/$1/;
# normalise whitespace; keeps later regexes very simple
$line =~ s/=/ = /;
$line =~ s/\s+/ /g;
$line =~ s/^ //;
$line =~ s/ $//;
return $line;
}
{
my @phy_repos = ();
sub list_phy_repos {
# use cached value only if it exists *and* no arg was received (i.e.,
# receiving *any* arg invalidates cache)
return \@phy_repos if ( @phy_repos and not @_ );
for my $repo (`find . -name "*.git" -prune`) {
chomp($repo);
$repo =~ s(\./(.*)\.git$)($1);
push @phy_repos, $repo;
}
trace( 2, scalar(@phy_repos) . " physical repos found" );
return sort_u( \@phy_repos );
}
}
# generate a timestamp
sub gen_ts {
my ( $s, $min, $h, $d, $m, $y ) = (localtime)[ 0 .. 5 ];
$y += 1900; $m++; # usual adjustments
for ( $s, $min, $h, $d, $m ) {
$_ = "0$_" if $_ < 10;
}
my $ts = "$y-$m-$d.$h:$min:$s";
return $ts;
}
# generate a log file name
sub gen_lfn {
my ( $s, $min, $h, $d, $m, $y ) = (localtime)[ 0 .. 5 ];
$y += 1900; $m++; # usual adjustments
for ( $s, $min, $h, $d, $m ) {
$_ = "0$_" if $_ < 10;
}
my ($template) = shift;
# substitute template parameters and set the logfile name
$template =~ s/%y/$y/g;
$template =~ s/%m/$m/g;
$template =~ s/%d/$d/g;
return $template;
}
sub gl_log {
# the log filename and the timestamp come from the environment. If we get
# called even before they are set, we have no choice but to dump to STDERR
# (and probably call "logger").
# tab sep if there's more than one field
my $msg = join( "\t", @_ );
$msg =~ s/[\n\r]+/<<newline>>/g;
my $ts = gen_ts();
my $tid = $ENV{GL_TID} ||= $$;
my $fh;
logger_plus_stderr( "errors found before logging could be setup", "$msg" ) if not $ENV{GL_LOGFILE};
open my $lfh, ">>", $ENV{GL_LOGFILE}
or logger_plus_stderr( "errors found but logfile could not be created", "$ENV{GL_LOGFILE}: $!", "$msg" );
print $lfh "$ts\t$tid\t$msg\n";
close $lfh;
}
sub logger_plus_stderr {
open my $fh, "|-", "logger" or confess "it's really not my day is it...?\n";
for ( @_ ) {
print STDERR "FATAL: $_\n";
print $fh "FATAL: $_\n";
}
exit 1;
}
# ----------------------------------------------------------------------
# bare-minimum subset of 'Tsh' (see github.com/sitaramc/tsh)
{
my ( $rc, $text );
sub tsh_rc { return $rc || 0; }
sub tsh_text { return $text || ''; }
sub tsh_lines { return split /\n/, $text; }
sub tsh_try {
my $cmd = shift; die "try: expects only one argument" if @_;
$text = `( $cmd ) 2>&1; printf RC=\$?`;
if ( $text =~ s/RC=(\d+)$// ) {
$rc = $1;
trace( 3, $text );
return ( not $rc );
}
die "couldnt find RC= in result; this should not happen:\n$text\n\n...\n";
}
sub tsh_run {
open( my $fh, "-|", @_ ) or die "popen failed: $!";
local $/ = undef; $text = <$fh>;
close $fh; warn "pclose failed: $!" if $!;
$rc = ( $? >> 8 );
trace( 3, $text );
return $text;
}
}
1;

View file

@ -1,82 +0,0 @@
package Gitolite::Conf;
# explode/parse a conf file
# ----------------------------------------------------------------------
@EXPORT = qw(
compile
explode
parse
);
use Exporter 'import';
use Getopt::Long;
use Gitolite::Common;
use Gitolite::Rc;
use Gitolite::Conf::Sugar;
use Gitolite::Conf::Store;
use strict;
use warnings;
# ----------------------------------------------------------------------
sub compile {
_die "'gitolite compile' does not take any arguments" if @_;
_chdir( $rc{GL_ADMIN_BASE} );
_chdir("conf");
parse( sugar('gitolite.conf') );
# the order matters; new repos should be created first, to give store a
# place to put the individual gl-conf files
new_repos();
store();
for my $repo ( @{ $rc{NEW_REPOS_CREATED} } ) {
trigger( 'POST_CREATE', $repo );
}
}
sub parse {
my $lines = shift;
trace( 2, scalar(@$lines) . " lines incoming" );
for my $line (@$lines) {
# user or repo groups
if ( $line =~ /^(@\S+) = (.*)/ ) {
add_to_group( $1, split( ' ', $2 ) );
} elsif ( $line =~ /^repo (.*)/ ) {
set_repolist( split( ' ', $1 ) );
} elsif ( $line =~ /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/ ) {
my $perm = $1;
my @refs = parse_refs( $2 || '' );
my @users = parse_users($3);
for my $ref (@refs) {
for my $user (@users) {
add_rule( $perm, $ref, $user );
}
}
} elsif ( $line =~ /^config (.+) = ?(.*)/ ) {
my ( $key, $value ) = ( $1, $2 );
$value =~ s/^['"](.*)["']$/$1/;
my @validkeys = split( ' ', ( $rc{GIT_CONFIG_KEYS} || '' ) );
push @validkeys, "gitolite-options\\..*";
my @matched = grep { $key =~ /^$_$/ } @validkeys;
_die "git config '$key' not allowed\ncheck GIT_CONFIG_KEYS in the rc file" if ( @matched < 1 );
_die "bad value '$value'" if $value =~ $UNSAFE_PATT;
add_config( 1, $key, $value );
} elsif ( $line =~ /^subconf (\S+)$/ ) {
trace( 2, $line );
set_subconf($1);
} else {
_warn "?? $line";
}
}
parse_done();
}
1;

View file

@ -1,117 +0,0 @@
package Gitolite::Conf::Explode;
# include/subconf processor
# ----------------------------------------------------------------------
@EXPORT = qw(
explode
);
use Exporter 'import';
use Gitolite::Rc;
use Gitolite::Common;
use strict;
use warnings;
# ----------------------------------------------------------------------
# 'seen' for include/subconf files
my %included = ();
# 'seen' for group names on LHS
my %prefixed_groupname = ();
sub explode {
trace( 3, @_ );
my ( $file, $subconf, $out ) = @_;
# seed the 'seen' list if it's empty
$included{ device_inode("gitolite.conf") }++ unless %included;
my $fh = _open( "<", $file );
while (<$fh>) {
my $line = cleanup_conf_line($_);
next unless $line =~ /\S/;
# subst %HOSTNAME word if rc defines a hostname, else leave as is
$line =~ s/%HOSTNAME\b/$rc{HOSTNAME}/g if $rc{HOSTNAME};
$line = prefix_groupnames( $line, $subconf ) if $subconf ne 'master';
if ( $line =~ /^(include|subconf) (?:(\S+) )?(\S.+)$/ ) {
incsub( $1, $2, $3, $subconf, $out );
} else {
# normal line, send it to the callback function
push @{$out}, $line;
}
}
}
sub incsub {
my $is_subconf = ( +shift eq 'subconf' );
my ( $new_subconf, $include_glob, $current_subconf, $out ) = @_;
_die "subconf '$current_subconf' attempting to run 'subconf'\n" if $is_subconf and $current_subconf ne 'master';
_die "invalid include/subconf file/glob '$include_glob'"
unless $include_glob =~ /^"(.+)"$/
or $include_glob =~ /^'(.+)'$/;
$include_glob = $1;
trace( 2, $is_subconf, $include_glob );
for my $file ( glob($include_glob) ) {
_warn("included file not found: '$file'"), next unless -f $file;
_die "invalid include/subconf filename '$file'" unless $file =~ m(([^/]+).conf$);
my $basename = $1;
next if already_included($file);
if ($is_subconf) {
push @{$out}, "subconf " . ( $new_subconf || $basename );
explode( $file, ( $new_subconf || $basename ), $out );
push @{$out}, "subconf $current_subconf";
} else {
explode( $file, $current_subconf, $out );
}
}
}
sub prefix_groupnames {
my ( $line, $subconf ) = @_;
my $lhs = '';
# save 'foo' if it's an '@foo = list' line
$lhs = $1 if $line =~ /^@(\S+) = /;
# prefix all @groups in the line
$line =~ s/(^| )(@\S+)(?= |$)/ $1 . ($prefixed_groupname{$subconf}{$2} || $2) /ge;
# now prefix the LHS and store it if needed
if ($lhs) {
$line =~ s/^@\S+ = /"\@$subconf.$lhs = "/e;
$prefixed_groupname{$subconf}{"\@$lhs"} = "\@$subconf.$lhs";
trace( 3, "prefixed_groupname.$subconf.\@$lhs = \@$subconf.$lhs" );
}
return $line;
}
sub already_included {
my $file = shift;
my $file_id = device_inode($file);
return 0 unless $included{$file_id}++;
_warn("$file already included");
trace( 2, "$file already included" );
return 1;
}
sub device_inode {
my $file = shift;
trace( 3, $file, ( stat $file )[ 0, 1 ] );
return join( "/", ( stat $file )[ 0, 1 ] );
}
1;

View file

@ -1,595 +0,0 @@
package Gitolite::Conf::Load;
# load conf data from stored files
# ----------------------------------------------------------------------
@EXPORT = qw(
load
access
git_config
option
repo_missing
creator
vrefs
lister_dispatch
);
use Exporter 'import';
use Gitolite::Common;
use Gitolite::Rc;
use strict;
use warnings;
# ----------------------------------------------------------------------
# our variables, because they get loaded by a 'do'
our $data_version = '';
our %repos;
our %one_repo;
our %groups;
our %patterns;
our %configs;
our %one_config;
our %split_conf;
my $subconf = 'master';
my %listers = (
'list-groups' => \&list_groups,
'list-users' => \&list_users,
'list-repos' => \&list_repos,
'list-memberships' => \&list_memberships,
'list-members' => \&list_members,
);
# helps maintain the "cache" in both "load_common" and "load_1"
my $last_repo = '';
# ----------------------------------------------------------------------
{
my $loaded_repo = '';
sub load {
my $repo = shift or _die "load() needs a reponame";
trace( 3, "$repo" );
if ( $repo ne $loaded_repo ) {
load_common();
load_1($repo);
$loaded_repo = $repo;
}
}
}
sub access {
my ( $repo, $user, $aa, $ref ) = @_;
_die "invalid user '$user'" if not( $user and $user =~ $USERNAME_PATT );
sanity($repo);
my @rules;
my $deny_rules;
load($repo);
@rules = rules( $repo, $user );
$deny_rules = option( $repo, 'deny-rules' );
# sanity check the only piece the user can control
_die "invalid characters in ref or filename: '$ref'\n" unless $ref =~ $REF_OR_FILENAME_PATT;
# when a real repo doesn't exist, ^C is a pre-requisite for any other
# check to give valid results.
if ( $aa ne '^C' and $repo !~ /^\@/ and $repo =~ $REPONAME_PATT and repo_missing($repo) ) {
my $iret = access( $repo, $user, '^C', $ref );
$iret =~ s/\^C/$aa/;
return $iret if $iret =~ /DENIED/;
}
# similarly, ^C must be denied if the repo exists
if ( $aa eq '^C' and not repo_missing($repo) ) {
trace( 2, "DENIED by existence" );
return "$aa $ref $repo $user DENIED by existence";
}
trace( 2, scalar(@rules) . " rules found" );
for my $r (@rules) {
my $perm = $r->[1];
my $refex = $r->[2]; $refex =~ s(/USER/)(/$user/);
trace( 3, "perm=$perm, refex=$refex" );
# skip 'deny' rules if the ref is not (yet) known
next if $perm eq '-' and $ref eq 'any' and not $deny_rules;
# rule matches if ref matches or ref is any (see gitolite-shell)
next unless $ref =~ /^$refex/ or $ref eq 'any';
trace( 2, "DENIED by $refex" ) if $perm eq '-';
return "$aa $ref $repo $user DENIED by $refex" if $perm eq '-';
# $perm can be RW\+?(C|D|CD|DC)?M?. $aa can be W, +, C or D, or
# any of these followed by "M".
( my $aaq = $aa ) =~ s/\+/\\+/;
$aaq =~ s/M/.*M/;
# as far as *this* ref is concerned we're ok
return $refex if ( $perm =~ /$aaq/ );
}
trace( 2, "DENIED by fallthru" );
return "$aa $ref $repo $user DENIED by fallthru";
}
sub git_config {
my ( $repo, $key, $empty_values_OK ) = @_;
$key ||= '.';
if (repo_missing($repo)) {
load_common();
} else {
load($repo);
}
# read comments bottom up
my %ret =
# and take the second and third elements to make up your new hash
map { $_->[1] => $_->[2] }
# keep only the ones where the second element matches your key
grep { $_->[1] =~ qr($key) }
# sort this list of listrefs by the first element in each list ref'd to
sort { $a->[0] <=> $b->[0] }
# dereference it (into a list of listrefs)
map { @$_ }
# take the value of that entry
map { $configs{$_} }
# if it has an entry in %configs
grep { $configs{$_} }
# for each "repo" that represents us
memberships( 'repo', $repo );
# %configs looks like this (for each 'foo' that is in memberships())
# 'foo' => [ [ 6, 'foo.bar', 'repo' ], [ 7, 'foodbar', 'repoD' ], [ 8, 'foo.czar', 'jule' ] ],
# the first map gets you the value
# [ [ 6, 'foo.bar', 'repo' ], [ 7, 'foodbar', 'repoD' ], [ 8, 'foo.czar', 'jule' ] ],
# the deref gets you
# [ 6, 'foo.bar', 'repo' ], [ 7, 'foodbar', 'repoD' ], [ 8, 'foo.czar', 'jule' ]
# the sort rearranges it (in this case it's already sorted but anyway...)
# the grep gets you this, assuming the key is foo.bar (and "." is regex ".')
# [ 6, 'foo.bar', 'repo' ], [ 7, 'foodbar', 'repoD' ]
# and the final map does this:
# 'foo.bar'=>'repo' , 'foodbar'=>'repoD'
# now some of these will have an empty key; we need to delete them unless
# we're told empty values are OK
unless ($empty_values_OK) {
my($k, $v);
while (($k, $v) = each %ret) {
delete $ret{$k} if not $v;
}
}
my($k, $v);
my $creator = creator($repo);
while (($k, $v) = each %ret) {
$v =~ s/%GL_REPO/$repo/g;
$v =~ s/%GL_CREATOR/$creator/g if $creator;
$ret{$k} = $v;
}
trace( 3, map { ( "$_" => "-> $ret{$_}" ) } ( sort keys %ret ) );
return \%ret;
}
sub option {
my ( $repo, $option ) = @_;
$option = "gitolite-options.$option";
my $ret = git_config( $repo, "^\Q$option\E\$" );
return '' unless %$ret;
return $ret->{$option};
}
sub sanity {
my $repo = shift;
_die "invalid repo '$repo'" if not( $repo and $repo =~ $REPOPATT_PATT );
_die "'$repo' ends with a '/'" if $repo =~ m(/$);
_die "'$repo' contains '..'" if $repo =~ $REPONAME_PATT and $repo =~ m(\.\.);
}
sub repo_missing {
my $repo = shift;
sanity($repo);
return not -d "$rc{GL_REPO_BASE}/$repo.git";
}
# ----------------------------------------------------------------------
sub load_common {
_chdir( $rc{GL_ADMIN_BASE} );
# we take an unusual approach to caching this function!
# (requires that first call to load_common is before first call to load_1)
if ( $last_repo and $split_conf{$last_repo} ) {
delete $repos{$last_repo};
delete $configs{$last_repo};
return;
}
my $cc = "conf/gitolite.conf-compiled.pm";
_die "parse '$cc' failed: " . ( $! or $@ ) unless do $cc;
if ( data_version_mismatch() ) {
_system("gitolite setup");
_die "parse '$cc' failed: " . ( $! or $@ ) unless do $cc;
_die "data version update failed; this is serious" if data_version_mismatch();
}
}
sub load_1 {
my $repo = shift;
return if $repo =~ /^\@/;
trace( 3, $repo );
if ( repo_missing($repo) ) {
trace( 1, "repo '$repo' missing" ) if $repo =~ $REPONAME_PATT;
return;
}
_chdir("$rc{GL_REPO_BASE}/$repo.git");
if ( $repo eq $last_repo ) {
$repos{$repo} = $one_repo{$repo};
$configs{$repo} = $one_config{$repo} if $one_config{$repo};
return;
}
if ( -f "gl-conf" ) {
return if not $split_conf{$repo};
my $cc = "./gl-conf";
_die "parse '$cc' failed: " . ( $! or $@ ) unless do $cc;
$last_repo = $repo;
$repos{$repo} = $one_repo{$repo};
$configs{$repo} = $one_config{$repo} if $one_config{$repo};
} else {
_die "split conf set, gl-conf not present for '$repo'" if $split_conf{$repo};
}
}
{
my $lastrepo = '';
my $lastuser = '';
my @cached = ();
sub rules {
my ( $repo, $user ) = @_;
trace( 3, "repo=$repo, user=$user" );
return @cached if ( $lastrepo eq $repo and $lastuser eq $user and @cached );
my @rules = ();
my @repos = memberships( 'repo', $repo );
my @users = memberships( 'user', $user, $repo );
trace( 3, "memberships: " . scalar(@repos) . " repos and " . scalar(@users) . " users found" );
for my $r (@repos) {
for my $u (@users) {
push @rules, @{ $repos{$r}{$u} } if exists $repos{$r} and exists $repos{$r}{$u};
}
}
@rules = sort { $a->[0] <=> $b->[0] } @rules;
$lastrepo = $repo;
$lastuser = $user;
@cached = @rules;
# however if the repo was missing, invalidate the cache
$lastrepo = '' if repo_missing($repo);
return @rules;
}
sub vrefs {
my ( $repo, $user ) = @_;
# fill the cache if needed
rules( $repo, $user ) unless ( $lastrepo eq $repo and $lastuser eq $user and @cached );
my %seen;
my @vrefs = grep { /^VREF\// and not $seen{$_}++ } map { $_->[2] } @cached;
return @vrefs;
}
}
sub memberships {
trace( 3, @_ );
my ( $type, $base, $repo ) = @_;
$repo ||= '';
my @ret;
my $base2 = '';
@ret = ( $base, '@all' );
if ( $type eq 'repo' ) {
# first, if a repo, say, pub/sitaram/project, has a gl-creator file
# that says "sitaram", find memberships for pub/CREATOR/project also
$base2 = generic_name($base);
# second, you need to check in %repos also
for my $i ( keys %repos, keys %configs ) {
if ( $base eq $i or $base =~ /^$i$/ or $base2 and ( $base2 eq $i or $base2 =~ /^$i$/ ) ) {
push @ret, $i;
}
}
}
push @ret, @{ $groups{$base} } if exists $groups{$base};
push @ret, @{ $groups{$base2} } if $base2 and exists $groups{$base2};
for my $i ( keys %{ $patterns{groups} } ) {
if ( $base =~ /^$i$/ or $base2 and ( $base2 =~ /^$i$/ ) ) {
push @ret, @{ $groups{$i} };
}
}
push @ret, @{ ext_grouplist($base) } if $type eq 'user' and $rc{GROUPLIST_PGM};
if ( $type eq 'user' and $repo and not repo_missing($repo) ) {
# find the roles this user has when accessing this repo and add those
# in as groupnames he is a member of. You need the already existing
# memberships for this; see below this function for an example
push @ret, user_roles( $base, $repo, @ret );
}
@ret = @{ sort_u( \@ret ) };
trace( 3, sort @ret );
return @ret;
}
=for example
conf/gitolite.conf:
@g1 = u1
@g2 = u1
# now user is a member of both g1 and g2
gl-perms for repo being accessed:
READERS @g1
This should result in @READERS being added to the memberships that u1 has
(when accessing this repo). So we send the current list (@g1, @g2) to
user_roles(), otherwise it has to redo that logic.
=cut
sub data_version_mismatch {
return $data_version ne glrc('current-data-version');
}
sub user_roles {
my ( $user, $repo, @eg ) = @_;
# eg == existing groups (that user is already known to be a member of)
my %eg = map { $_ => 1 } @eg;
my %ret = ();
my $f = "$rc{GL_REPO_BASE}/$repo.git/gl-perms";
my @roles = ();
if ( -f $f ) {
my $fh = _open( "<", $f );
chomp( @roles = <$fh> );
}
push @roles, "CREATOR = " . creator($repo);
for (@roles) {
# READERS u3 u4 @g1
s/^\s+//; s/ +$//; s/=/ /; s/\s+/ /g; s/^\@//;
next if /^#/;
next unless /\S/;
my ( $role, @members ) = split;
# role = READERS, members = u3, u4, @g1
if ( $role ne 'CREATOR' and not $rc{ROLES}{$role} ) {
_warn "role '$role' not allowed, ignoring";
next;
}
for my $m (@members) {
if ( $m !~ $USERNAME_PATT ) {
_warn "ignoring '$m' in perms line";
next;
}
# if user eq u3/u4, or is a member of @g1, he has role READERS
$ret{ '@' . $role } = 1 if $m eq $user or $eg{$m};
}
}
return keys %ret;
}
sub generic_name {
my $base = shift;
my $base2 = '';
my $creator;
# get the creator name. For not-yet-born repos this is $ENV{GL_USER},
# which should be set in all cases that we care about, viz., where we are
# checking ^C permissions before new_wild_repo(), and the info command.
# In particular, 'gitolite access' can't be used to check ^C perms on wild
# repos that contain "CREATOR" if GL_USER is not set.
$creator = creator($base);
$base2 = $base;
$base2 =~ s(\b$creator\b)(CREATOR) if $creator;
$base2 = '' if $base2 eq $base; # if there was no change
return $base2;
}
sub creator {
my $repo = shift;
sanity($repo);
return ( $ENV{GL_USER} || '' ) if repo_missing($repo);
my $f = "$rc{GL_REPO_BASE}/$repo.git/gl-creator";
my $creator = '';
chomp( $creator = slurp($f) ) if -f $f;
return $creator;
}
{
my %cache = ();
sub ext_grouplist {
my $user = shift;
my $pgm = $rc{GROUPLIST_PGM};
return [] if not $pgm;
return $cache{$user} if $cache{$user};
my @extgroups = map { s/^@?/@/; $_; } split ' ', `$rc{GROUPLIST_PGM} $user`;
return ( $cache{$user} = \@extgroups );
}
}
# ----------------------------------------------------------------------
# api functions
# ----------------------------------------------------------------------
sub lister_dispatch {
my $command = shift;
my $fn = $listers{$command} or _die "unknown gitolite sub-command";
return $fn;
}
=for list_groups
Usage: gitolite list-groups
- lists all group names in conf
- no options, no flags
=cut
sub list_groups {
usage() if @_;
load_common();
my @g = ();
while ( my ( $k, $v ) = each(%groups) ) {
push @g, @{$v};
}
return ( sort_u( \@g ) );
}
=for list_users
Usage: gitolite list-users [<repo name pattern>]
List all users and groups explicitly named in a rule. User names not
mentioned in an access rule will not show up; you have to run 'list-members'
on each group name yourself to see them.
WARNING: may be slow if you have thousands of repos. The optional repo name
pattern is an unanchored regex; it can speed things up if you're interested
only in users of a matching set of repos. This is only an optimisation, not
an actual access list; you will still have to pipe it to 'gitolite access'
with appropriate arguments to get an actual access list.
=cut
sub list_users {
my $patt = shift || '.';
usage() if $patt eq '-h' or @_;
my $count = 0;
my $total = 0;
load_common();
my @u = map { keys %{$_} } values %repos;
$total = scalar( grep { /$patt/ } keys %split_conf );
warn "WARNING: you have $total repos to check; this could take some time!\n" if $total > 100;
for my $one ( grep { /$patt/ } keys %split_conf ) {
load_1($one);
$count++; print STDERR "$count / $total\r" if not( $count % 100 ) and timer(5);
push @u, map { keys %{$_} } values %one_repo;
}
print STDERR "\n" if $count >= 100;
return ( sort_u( \@u ) );
}
=for list_repos
Usage: gitolite list-repos
- lists all repos/repo groups in conf
- no options, no flags
=cut
sub list_repos {
usage() if @_;
load_common();
my @r = keys %repos;
push @r, keys %split_conf;
return ( sort_u( \@r ) );
}
=for list_memberships
Usage: gitolite list-memberships <name>
- list all groups a name is a member of
- takes one user/repo name
=cut
sub list_memberships {
usage() if @_ and $_[0] eq '-h' or not @_;
my $name = shift;
load_common();
my @m = memberships( '', $name );
return ( sort_u( \@m ) );
}
=for list_members
Usage: gitolite list-members <group name>
- list all members of a group
- takes one group name
=cut
sub list_members {
usage() if @_ and $_[0] eq '-h' or not @_;
my $name = shift;
load_common();
my @m = ();
while ( my ( $k, $v ) = each(%groups) ) {
for my $g ( @{$v} ) {
push @m, $k if $g eq $name;
}
}
return ( sort_u( \@m ) );
}
# ----------------------------------------------------------------------
{
my $start_time = 0;
sub timer {
unless ($start_time) {
$start_time = time();
return 0;
}
my $elapsed = shift;
return 0 if time() - $start_time < $elapsed;
$start_time = time();
return 1;
}
}
1;

View file

@ -1,368 +0,0 @@
package Gitolite::Conf::Store;
# receive parsed conf data and store it
# ----------------------------------------------------------------------
@EXPORT = qw(
add_to_group
set_repolist
parse_refs
parse_users
add_rule
add_config
set_subconf
expand_list
new_repos
new_repo
new_wild_repo
hook_repos
store
parse_done
);
use Exporter 'import';
use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
use Gitolite::Common;
use Gitolite::Rc;
use Gitolite::Hooks::Update;
use Gitolite::Hooks::PostUpdate;
use strict;
use warnings;
# ----------------------------------------------------------------------
my %repos;
my %groups;
my %configs;
my %split_conf;
my @repolist; # current repo list; reset on each 'repo ...' line
my $subconf = 'master';
my $nextseq = 0;
my %ignored;
# ----------------------------------------------------------------------
sub add_to_group {
my ( $lhs, @rhs ) = @_;
_die "bad group '$lhs'" unless $lhs =~ $REPONAME_PATT;
map { _die "bad expansion '$_'" unless $_ =~ $REPOPATT_PATT } @rhs;
# store the group association, but overload it to keep track of when
# the group was *first* created by using $subconf as the *value*
do { $groups{$lhs}{$_} ||= $subconf }
for ( expand_list(@rhs) );
# create the group hash even if empty
$groups{$lhs} = {} unless $groups{$lhs};
}
sub set_repolist {
@repolist = ();
# ...sanity checks
for (@_) {
if ( check_subconf_repo_disallowed( $subconf, $_ ) ) {
(my $repo = $_) =~ s/^\@$subconf\./locally modified \@/;
$ignored{$subconf}{$repo} = 1;
next;
}
_warn "explicit '.git' extension ignored for $_.git" if s/\.git$//;
_die "bad reponame '$_'" if $_ !~ $REPOPATT_PATT;
push @repolist, $_;
}
}
sub parse_refs {
my $refs = shift;
my @refs; @refs = split( ' ', $refs ) if $refs;
@refs = expand_list(@refs);
# if no ref is given, this PERM applies to all refs
@refs = qw(refs/.*) unless @refs;
# fully qualify refs that dont start with "refs/" or "VREF/";
# prefix them with "refs/heads/"
@refs = map { m(^(refs|VREF)/) or s(^)(refs/heads/); $_ } @refs;
return @refs;
}
sub parse_users {
my $users = shift;
my @users = split ' ', $users;
do { _die "bad username '$_'" unless $_ =~ $USERNAME_PATT }
for @users;
return @users;
}
sub add_rule {
my ( $perm, $ref, $user ) = @_;
_die "bad ref '$ref'" unless $ref =~ $REPOPATT_PATT;
_die "bad user '$user'" unless $user =~ $USERNAME_PATT;
$nextseq++;
for my $repo (@repolist) {
push @{ $repos{$repo}{$user} }, [ $nextseq, $perm, $ref ];
}
}
sub add_config {
my ( $n, $key, $value ) = @_;
$nextseq++;
for my $repo (@repolist) {
push @{ $configs{$repo} }, [ $nextseq, $key, $value ];
}
}
sub set_subconf {
$subconf = shift;
_die "bad subconf '$subconf'" unless $subconf =~ /^[-\w.]+$/;
}
# ----------------------------------------------------------------------
sub expand_list {
my @list = @_;
my @new_list = ();
for my $item (@list) {
if ( $item =~ /^@/ and $item ne '@all' ) # nested group
{
_die "undefined group '$item'" unless $groups{$item};
# add those names to the list
push @new_list, sort keys %{ $groups{$item} };
} else {
push @new_list, $item;
}
}
return @new_list;
}
sub new_repos {
trace(3);
_chdir( $rc{GL_REPO_BASE} );
# normal repos
my @repos = grep { $_ =~ $REPONAME_PATT and not /^@/ } sort keys %repos;
# add in members of repo groups
map { push @repos, keys %{ $groups{$_} } } grep { /^@/ and $_ ne '@all' } keys %repos;
for my $repo ( @{ sort_u( \@repos ) } ) {
next unless $repo =~ $REPONAME_PATT; # skip repo patterns
next if $repo =~ m(^\@|EXTCMD/); # skip groups and fake repos
# use gl-conf as a sentinel
hook_1($repo) if -d "$repo.git" and not -f "$repo.git/gl-conf";
if (not -d "$repo.git") {
push @{ $rc{NEW_REPOS_CREATED} }, $repo;
trigger( 'PRE_CREATE', $repo );
new_repo($repo);
}
}
}
sub new_repo {
my $repo = shift;
trace( 3, $repo );
_mkdir("$repo.git");
_chdir("$repo.git");
_system("git init --bare >&2");
_chdir( $rc{GL_REPO_BASE} );
hook_1($repo);
}
sub new_wild_repo {
my ( $repo, $user, $aa ) = @_;
_chdir( $rc{GL_REPO_BASE} );
trigger( 'PRE_CREATE', $repo, $user, $aa );
new_repo($repo);
_print( "$repo.git/gl-creator", $user );
_print( "$repo.git/gl-perms", ( $rc{DEFAULT_ROLE_PERMS} ? "$rc{DEFAULT_ROLE_PERMS}\n" : "" ) );
trigger( 'POST_CREATE', $repo, $user, $aa );
_chdir( $rc{GL_ADMIN_BASE} );
}
sub hook_repos {
trace(3);
# all repos, all hooks
_chdir( $rc{GL_REPO_BASE} );
for my $repo (`find . -name "*.git" -prune`) {
chomp($repo);
$repo =~ s/\.git$//;
$repo =~ s(^\./)();
hook_1($repo);
}
}
sub store {
trace(3);
# first write out the ones for the physical repos
_chdir( $rc{GL_REPO_BASE} );
my $phy_repos = list_phy_repos(1);
for my $repo ( @{$phy_repos} ) {
store_1($repo);
}
_chdir( $rc{GL_ADMIN_BASE} );
store_common();
}
sub parse_done {
for my $ig ( sort keys %ignored ) {
_warn "subconf '$ig' attempting to set access for " . join( ", ", sort keys %{ $ignored{$ig} } );
}
}
# ----------------------------------------------------------------------
sub check_subconf_repo_disallowed {
# trying to set access for $repo (='foo')...
my ( $subconf, $repo ) = @_;
trace( 2, $subconf, $repo );
# processing the master config, not a subconf
return 0 if $subconf eq 'master';
# subconf is also called 'foo' (you're allowed to have a
# subconf that is only concerned with one repo)
return 0 if $subconf eq $repo;
# same thing in big-config-land; foo is just @foo now
return 0 if ( "\@$subconf" eq $repo );
my @matched = grep { $repo =~ /^$_$/ }
grep { $groups{"\@$subconf"}{$_} eq 'master' }
sort keys %{ $groups{"\@$subconf"} };
return 0 if @matched > 0;
trace( 2, "-> disallowed" );
return 1;
}
sub store_1 {
# warning: writes and *deletes* it from %repos and %configs
my ($repo) = shift;
trace( 3, $repo );
return unless ( $repos{$repo} or $configs{$repo} ) and -d "$repo.git";
my ( %one_repo, %one_config );
open( my $compiled_fh, ">", "$repo.git/gl-conf" ) or return;
my $dumped_data = '';
if ($repos{$repo}) {
$one_repo{$repo} = $repos{$repo};
delete $repos{$repo};
$dumped_data = Data::Dumper->Dump( [ \%one_repo ], [qw(*one_repo)] );
}
if ( $configs{$repo} ) {
$one_config{$repo} = $configs{$repo};
delete $configs{$repo};
$dumped_data .= Data::Dumper->Dump( [ \%one_config ], [qw(*one_config)] );
}
print $compiled_fh $dumped_data;
close $compiled_fh;
$split_conf{$repo} = 1;
}
sub store_common {
trace(3);
my $cc = "conf/gitolite.conf-compiled.pm";
my $compiled_fh = _open( ">", "$cc.new" );
my %patterns = ();
my $data_version = glrc('current-data-version');
trace( 3, "data_version = $data_version" );
print $compiled_fh Data::Dumper->Dump( [$data_version], [qw(*data_version)] );
my $dumped_data = Data::Dumper->Dump( [ \%repos ], [qw(*repos)] );
$dumped_data .= Data::Dumper->Dump( [ \%configs ], [qw(*configs)] ) if %configs;
print $compiled_fh $dumped_data;
if (%groups) {
my %groups = %{ inside_out( \%groups ) };
$dumped_data = Data::Dumper->Dump( [ \%groups ], [qw(*groups)] );
print $compiled_fh $dumped_data;
# save patterns in %groups for faster handling of multiple repos, such
# as happens in the various POST_COMPILE scripts
for my $k (keys %groups) {
$patterns{groups}{$k} = 1 unless $k =~ $REPONAME_PATT;
}
}
print $compiled_fh Data::Dumper->Dump( [ \%patterns ], [qw(*patterns)] ) if %patterns;
print $compiled_fh Data::Dumper->Dump( [ \%split_conf ], [qw(*split_conf)] ) if %split_conf;
close $compiled_fh or _die "close compiled-conf failed: $!\n";
rename "$cc.new", $cc;
}
{
my $hook_reset = 0;
sub hook_1 {
my $repo = shift;
trace( 3, $repo );
# reset the gitolite supplied hooks, in case someone fiddled with
# them, but only once per run
if ( not $hook_reset ) {
_mkdir("$rc{GL_ADMIN_BASE}/hooks/common");
_mkdir("$rc{GL_ADMIN_BASE}/hooks/gitolite-admin");
_print( "$rc{GL_ADMIN_BASE}/hooks/common/update", update_hook() );
_print( "$rc{GL_ADMIN_BASE}/hooks/gitolite-admin/post-update", post_update_hook() );
chmod 0755, "$rc{GL_ADMIN_BASE}/hooks/common/update";
chmod 0755, "$rc{GL_ADMIN_BASE}/hooks/gitolite-admin/post-update";
$hook_reset++;
}
# propagate user-defined (custom) hooks to all repos
ln_sf( "$rc{LOCAL_CODE}/hooks/common", "*", "$repo.git/hooks" ) if $rc{LOCAL_CODE};
# override/propagate gitolite defined hooks for all repos
ln_sf( "$rc{GL_ADMIN_BASE}/hooks/common", "*", "$repo.git/hooks" );
# override/propagate gitolite defined hooks for the admin repo
ln_sf( "$rc{GL_ADMIN_BASE}/hooks/gitolite-admin", "*", "$repo.git/hooks" ) if $repo eq 'gitolite-admin';
}
}
sub inside_out {
my $href = shift;
# input conf: @aa = bb cc <newline> @bb = @aa dd
my %ret = ();
while ( my ( $k, $v ) = each( %{$href} ) ) {
# $k is '@aa', $v is a href
for my $k2 ( keys %{$v} ) {
# $k2 is bb, then cc
push @{ $ret{$k2} }, $k;
}
}
return \%ret;
# %groups = ( 'bb' => [ '@bb', '@aa' ], 'cc' => [ '@bb', '@aa' ], 'dd' => [ '@bb' ]);
}
1;

View file

@ -1,194 +0,0 @@
# and now for something completely different...
package SugarBox;
sub run_sugar_script {
my ( $ss, $lref ) = @_;
do $ss if -r $ss;
$lref = sugar_script($lref);
return $lref;
}
# ----------------------------------------------------------------------
package Gitolite::Conf::Sugar;
# syntactic sugar for the conf file, including site-local macros
# ----------------------------------------------------------------------
@EXPORT = qw(
sugar
);
use Exporter 'import';
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Explode;
use strict;
use warnings;
# ----------------------------------------------------------------------
sub sugar {
# gets a filename, returns a listref
my @lines = ();
explode( shift, 'master', \@lines );
my $lines;
$lines = \@lines;
# run through the sugar stack one by one
# first, user supplied sugar:
if ( exists $rc{SYNTACTIC_SUGAR} ) {
if ( ref( $rc{SYNTACTIC_SUGAR} ) ne 'ARRAY' ) {
_warn "bad syntax for specifying sugar scripts; see docs";
} else {
for my $s ( @{ $rc{SYNTACTIC_SUGAR} } ) {
# perl-ism; apart from keeping the full path separate from the
# simple name, this also protects %rc from change by implicit
# aliasing, which would happen if you touched $s itself
my $sfp = _which("syntactic-sugar/$s", 'r');
_warn("skipped sugar script '$s'"), next if not -r $sfp;
$lines = SugarBox::run_sugar_script( $sfp, $lines );
$lines = [ grep /\S/, map { cleanup_conf_line($_) } @$lines ];
}
}
}
# then our stuff:
$lines = rw_cdm($lines);
$lines = option($lines); # must come after rw_cdm
$lines = owner_desc($lines);
$lines = name_vref($lines);
$lines = role_names($lines);
return $lines;
}
sub rw_cdm {
my $lines = shift;
my @ret;
# repo foo <...> RWC = ...
# -> option CREATE_IS_C = 1
# (and similarly DELETE_IS_D and MERGE_CHECK)
# but only once per repo of course
my %seen = ();
for my $line (@$lines) {
push @ret, $line;
if ( $line =~ /^repo / ) {
%seen = ();
} elsif ( $line =~ /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/ ) {
my $perms = $1;
push @ret, "option DELETE_IS_D = 1" if $perms =~ /D/ and not $seen{D}++;
push @ret, "option CREATE_IS_C = 1" if $perms =~ /RW.*C/ and not $seen{C}++;
push @ret, "option MERGE_CHECK = 1" if $perms =~ /M/ and not $seen{M}++;
}
}
return \@ret;
}
sub option {
my $lines = shift;
my @ret;
# option foo = bar
# -> config gitolite-options.foo = bar
for my $line (@$lines) {
if ( $line =~ /^option (\S+) = (\S.*)/ ) {
push @ret, "config gitolite-options.$1 = $2";
} else {
push @ret, $line;
}
}
return \@ret;
}
sub owner_desc {
my $lines = shift;
my @ret;
# owner = "owner name"
# -> config gitweb.owner = owner name
# desc = "some long description"
# -> config gitweb.description = some long description
# category = "whatever..."
# -> config gitweb.category = whatever...
# older formats:
# repo = "some long description"
# repo "owner name" = "some long description"
# -> config gitweb.owner = owner name
# -> config gitweb.description = some long description
for my $line (@$lines) {
if ( $line =~ /^desc = (\S.*)/ ) {
push @ret, "config gitweb.description = $1";
} elsif ( $line =~ /^owner = (\S.*)/ ) {
push @ret, "config gitweb.owner = $1";
} elsif ( $line =~ /^category = (\S.*)/ ) {
push @ret, "config gitweb.category = $1";
} elsif ( $line =~ /^(\S+)(?: "(.*?)")? = "(.*)"$/ ) {
my ( $repo, $owner, $desc ) = ( $1, $2, $3 );
push @ret, "repo $repo";
push @ret, "config gitweb.description = $desc";
push @ret, "config gitweb.owner = $owner" if $owner;
} else {
push @ret, $line;
}
}
return \@ret;
}
sub name_vref {
my $lines = shift;
my @ret;
# <perm> NAME/foo = <user>
# -> <perm> VREF/NAME/foo = <user>
for my $line (@$lines) {
if ( $line =~ /^(-|R\S+) \S.* = \S.*/ ) {
$line =~ s( NAME/)( VREF/NAME/)g;
}
push @ret, $line;
}
return \@ret;
}
sub role_names {
my $lines = shift;
my @ret;
# <perm> [<ref>] = <user list containing CREATOR|READERS|WRITERS>
# -> same but with "@" prepended to rolenames
for my $line (@$lines) {
if ( $line =~ /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/ ) {
my ( $p, $r ) = ( $1, $2 );
my $u = '';
for ( split ' ', $3 ) {
$_ = "\@$_" if $_ eq 'CREATOR' or $rc{ROLES}{$_};
$u .= " $_";
}
$r ||= '';
# mind the spaces (or play safe and run cleanup_conf_line again)
push @ret, cleanup_conf_line("$p $r = $u");
} else {
push @ret, $line;
}
}
return \@ret;
}
1;

View file

@ -1,173 +0,0 @@
package Gitolite::Easy;
# easy access to gitolite from external perl programs
# ----------------------------------------------------------------------
# most/all functions in this module test $ENV{GL_USER}'s rights and
# permissions so it needs to be set.
# "use"-ing this module
# ----------------------------------------------------------------------
# Using this module from within a gitolite trigger or command is easy; you
# just need 'use lib $ENV{GL_LIBDIR};' before the 'use Gitolite::Easy;'.
#
# Using it from something completely outside gitolite requires a bit more
# work. First, run 'gitolite query-rc -a' to find the correct values for
# GL_BINDIR and GL_LIBDIR in your installation. Then use this code in your
# external program, using the paths you just found:
#
# BEGIN {
# $ENV{GL_BINDIR} = "/full/path/to/gitolite/src";
# $ENV{GL_LIBDIR} = "/full/path/to/gitolite/src/lib";
# }
# use lib $ENV{GL_LIBDIR};
# use Gitolite::Easy;
# API documentation
# ----------------------------------------------------------------------
# documentation for each function is at the top of the function.
# Documentation is NOT in pod format; just read the source with a nice syntax
# coloring text editor and you'll be happy enough. (I do not like POD; please
# don't send me patches for this aspect of the module).
#<<<
@EXPORT = qw(
is_admin
is_super_admin
in_group
owns
can_read
can_write
config
%rc
say
say2
_die
_warn
_print
usage
);
#>>>
use Exporter 'import';
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
use strict;
use warnings;
my $user;
# ----------------------------------------------------------------------
# is_admin()
# return true if $ENV{GL_USER} is set and has W perms to the admin repo
# shell equivalent
# if gitolite access -q gitolite-admin $GL_USER W; then ...
sub is_admin {
valid_user();
return not( access( 'gitolite-admin', $user, 'W', 'any' ) =~ /DENIED/ );
}
# is_super_admin()
# (useful only if you are using delegation)
# return true if $ENV{GL_USER} is set and has W perms to any file in the admin
# repo
# shell equivalent
# if gitolite access -q gitolite-admin $GL_USER W VREF/NAME/; then ...
sub is_super_admin {
valid_user();
return not( access( 'gitolite-admin', $user, 'W', 'VREF/NAME/' ) =~ /DENIED/ );
}
# in_group()
# return true if $ENV{GL_USER} is set and is in the given group
# shell equivalent
# if gitolite list-memberships $GL_USER | grep -x $GROUPNAME >/dev/null; then ...
sub in_group {
valid_user();
my $g = shift;
$g =~ s/^\@?/@/;
return grep { $_ eq $g } @{ Gitolite::Conf::Load::list_memberships($user) };
}
# owns()
# return true if $ENV{GL_USER} is set and is the creator of the given repo
# shell equivalent
# if gitolite creator $REPONAME $GL_USER; then ...
sub owns {
valid_user();
my $r = shift;
# prevent unnecessary disclosure of repo existence info
return 0 if repo_missing($r);
return ( creator($r) eq $user );
}
# can_read()
# return true if $ENV{GL_USER} is set and can read the given repo
# shell equivalent
# if gitolite access -q $REPONAME $GL_USER R; then ...
sub can_read {
valid_user();
my $r = shift;
return not( access( $r, $user, 'R', 'any' ) =~ /DENIED/ );
}
# can_write()
# return true if $ENV{GL_USER} is set and can write to the given repo.
# Optional second argument can be '+' to check that instead of 'W'. Optional
# third argument can be a full ref name instead of 'any'.
# shell equivalent
# if gitolite access -q $REPONAME $GL_USER W; then ...
sub can_write {
valid_user();
my ($r, $aa, $ref) = @_;
$aa ||= 'W';
$ref ||= 'any';
return not( access( $r, $user, $aa, $ref ) =~ /DENIED/ );
}
# config()
# given a repo and a key, return a hash containing all the git config
# variables for that repo where the section+key match the regex. If none are
# found, return an empty hash. If you don't want it as a regex, use \Q
# appropriately
# shell equivalent
# foo=$(gitolite git-config -r $REPONAME foo\\.bar)
sub config {
my $repo = shift;
my $key = shift;
return () if repo_missing($repo);
my $ret = git_config( $repo, $key );
return %$ret;
}
# ----------------------------------------------------------------------
sub valid_user {
_die "GL_USER not set" unless exists $ENV{GL_USER};
$user = $ENV{GL_USER};
}
1;

View file

@ -1,74 +0,0 @@
package Gitolite::Hooks::PostUpdate;
# everything to do with the post-update hook
# ----------------------------------------------------------------------
@EXPORT = qw(
post_update
post_update_hook
);
use Exporter 'import';
use Gitolite::Rc;
use Gitolite::Common;
use strict;
use warnings;
# ----------------------------------------------------------------------
sub post_update {
trace( 1, 'post-up', @ARGV );
# this is the *real* post_update hook for gitolite
tsh_try("git ls-tree --name-only master");
_die "no files/dirs called 'hooks' or 'logs' are allowed" if tsh_text() =~ /^(hooks|logs)$/m;
my $hooks_changed = 0;
{
local $ENV{GIT_WORK_TREE} = $rc{GL_ADMIN_BASE};
tsh_try("git diff --name-only master");
$hooks_changed++ if tsh_text() =~ m(/hooks/common/);
# the leading slash ensure that this hooks/common directory is below
# some top level directory, not *at* the top. That's LOCAL_CODE, and
# it's actual name could be anything but it doesn't matter to us.
tsh_try("git checkout -f --quiet master");
}
_system("gitolite compile");
_system("gitolite setup --hooks-only") if $hooks_changed;
_system("gitolite trigger POST_COMPILE");
exit 0;
}
{
my $text = '';
sub post_update_hook {
if ( not $text ) {
local $/ = undef;
$text = <DATA>;
}
return $text;
}
}
1;
__DATA__
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Hooks::PostUpdate;
# gitolite post-update hook (only for the admin repo)
# ----------------------------------------------------------------------
post_update(); # is not expected to return
exit 1; # so if it does, something is wrong

View file

@ -1,169 +0,0 @@
package Gitolite::Hooks::Update;
# everything to do with the update hook
# ----------------------------------------------------------------------
@EXPORT = qw(
update
update_hook
);
use Exporter 'import';
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
use strict;
use warnings;
# ----------------------------------------------------------------------
sub update {
# this is the *real* update hook for gitolite
bypass() if $ENV{GL_BYPASS_ACCESS_CHECKS};
my ( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ) = args(@ARGV);
trace( 1, 'update', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, @ARGV );
my $ret = access( $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref );
trigger( 'ACCESS_2', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref, $ret, $oldsha, $newsha );
_die $ret if $ret =~ /DENIED/;
check_vrefs( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa );
trace( 1, "-> $ret" );
gl_log( 'update', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, @ARGV );
exit 0;
}
sub bypass {
require Cwd;
Cwd->import;
gl_log( 'update', getcwd(), '(' . ( $ENV{USER} || '?' ) . ')', 'bypass', @ARGV );
exit 0;
}
sub check_vrefs {
my ( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ) = @_;
my $name_seen = 0;
my $n_vrefs = 0;
for my $vref ( vrefs( $ENV{GL_REPO}, $ENV{GL_USER} ) ) {
$n_vrefs++;
if ( $vref =~ m(^VREF/NAME/) ) {
# this one is special; we process it right here, and only once
next if $name_seen++;
for my $ref ( map { chomp; s(^)(VREF/NAME/); $_; } `git diff --name-only $oldtree $newtree` ) {
check_vref( $aa, $ref );
}
} else {
my ( $dummy, $pgm, @args ) = split '/', $vref;
$pgm = _which("VREF/$pgm", 'x');
$pgm or _die "'$vref': helper program missing or unexecutable";
open( my $fh, "-|", $pgm, @_, $vref, @args ) or _die "'$vref': can't spawn helper program: $!";
while (<$fh>) {
# print non-vref lines and skip processing (for example,
# normal STDOUT by a normal update hook)
unless (m(^VREF/)) {
print;
next;
}
my ( $ref, $deny_message ) = split( ' ', $_, 2 );
check_vref( $aa, $ref, $deny_message );
}
close($fh) or _die $!
? "Error closing sort pipe: $!"
: "$vref: helper program exit status $?";
}
}
return $n_vrefs;
}
sub check_vref {
my ( $aa, $ref, $deny_message ) = @_;
my $ret = access( $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref );
trace( 2, "access($ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref)", "-> $ret" );
trigger( 'ACCESS_2', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref, $ret );
_die "$ret" . ( $deny_message ? "\n$deny_message" : '' )
if $ret =~ /DENIED/ and $ret !~ /by fallthru/;
trace( 2, "remember, fallthru is success here!" ) if $ret =~ /by fallthru/;
}
{
my $text = '';
sub update_hook {
if ( not $text ) {
local $/ = undef;
$text = <DATA>;
}
return $text;
}
}
# ----------------------------------------------------------------------
sub args {
my ( $ref, $oldsha, $newsha ) = @_;
my ( $oldtree, $newtree, $aa );
# this is special to git -- the hash of an empty tree
my $empty = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
$oldtree = $oldsha eq '0' x 40 ? $empty : $oldsha;
$newtree = $newsha eq '0' x 40 ? $empty : $newsha;
my $merge_base = '0' x 40;
# for branch create or delete, merge_base stays at '0'x40
chomp( $merge_base = `git merge-base $oldsha $newsha` )
unless $oldsha eq '0' x 40
or $newsha eq '0' x 40;
$aa = 'W';
# tag rewrite
$aa = '+' if $ref =~ m(refs/tags/) and $oldsha ne ( '0' x 40 );
# non-ff push to ref (including ref delete)
$aa = '+' if $oldsha ne $merge_base;
$aa = 'D' if ( option( $ENV{GL_REPO}, 'DELETE_IS_D' ) ) and $newsha eq '0' x 40;
$aa = 'C' if ( option( $ENV{GL_REPO}, 'CREATE_IS_C' ) ) and $oldsha eq '0' x 40;
# and now "M" commits. All the other accesses (W, +, C, D) were mutually
# exclusive in some sense. Sure a W could be a C or a + could be a D but
# that's by design. A merge commit, however, could still be any of the
# others (except a "D").
# so we have to *append* 'M' to $aa (if the repo has MERGE_CHECK in
# effect and this push contains a merge inside)
if ( option( $ENV{GL_REPO}, 'MERGE_CHECK' ) ) {
if ( $oldsha eq '0' x 40 or $newsha eq '0' x 40 ) {
_warn "ref create/delete ignored for purposes of merge-check\n";
} else {
$aa .= 'M' if `git rev-list -n 1 --merges $oldsha..$newsha` =~ /./;
}
}
return ( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa );
}
1;
__DATA__
#!/usr/bin/perl
use strict;
use warnings;
use lib $ENV{GL_LIBDIR};
use Gitolite::Hooks::Update;
# gitolite update hook
# ----------------------------------------------------------------------
update(); # is not expected to return
exit 1; # so if it does, something is wrong

View file

@ -1,454 +0,0 @@
package Gitolite::Rc;
# everything to do with 'rc'. Also defines some 'constants'
# ----------------------------------------------------------------------
@EXPORT = qw(
%rc
glrc
query_rc
version
trigger
_which
$REMOTE_COMMAND_PATT
$REF_OR_FILENAME_PATT
$REPONAME_PATT
$REPOPATT_PATT
$USERNAME_PATT
$UNSAFE_PATT
);
use Exporter 'import';
use Getopt::Long;
use Gitolite::Common;
# ----------------------------------------------------------------------
our %rc;
# ----------------------------------------------------------------------
# pre-populate some important rc keys
# ----------------------------------------------------------------------
$rc{GL_BINDIR} = $ENV{GL_BINDIR};
$rc{GL_LIBDIR} = $ENV{GL_LIBDIR};
# these keys could be overridden by the rc file later
$rc{GL_REPO_BASE} = "$ENV{HOME}/repositories";
$rc{GL_ADMIN_BASE} = "$ENV{HOME}/.gitolite";
$rc{LOG_TEMPLATE} = "$ENV{HOME}/.gitolite/logs/gitolite-%y-%m.log";
# variables that should probably never be changed but someone will want to, I'll bet...
# ----------------------------------------------------------------------
#<<<
$REMOTE_COMMAND_PATT = qr(^[-0-9a-zA-Z._\@/+ :,\%=]*$);
$REF_OR_FILENAME_PATT = qr(^[0-9a-zA-Z][-0-9a-zA-Z._\@/+ :,]*$);
$REPONAME_PATT = qr(^\@?[0-9a-zA-Z][-0-9a-zA-Z._\@/+]*$);
$REPOPATT_PATT = qr(^\@?[[0-9a-zA-Z][-0-9a-zA-Z._\@/+\\^$|()[\]*?{},]*$);
$USERNAME_PATT = qr(^\@?[0-9a-zA-Z][-0-9a-zA-Z._\@+]*$);
$UNSAFE_PATT = qr([`~#\$\&()|;<>]);
#>>>
# ----------------------------------------------------------------------
# find the rc file and 'do' it
# ----------------------------------------------------------------------
my $current_data_version = "3.2";
my $rc = glrc('filename');
if (-r $rc and -s $rc) {
do $rc or die $@;
}
if ( defined($GL_ADMINDIR) ) {
say2 "";
say2 "FATAL: '$rc' seems to be for older gitolite; please see doc/g2migr.mkd\n" . "(online at http://sitaramc.github.com/gitolite/g2migr.html)";
exit 1;
}
# let values specified in rc file override our internal ones
# ----------------------------------------------------------------------
@rc{ keys %RC } = values %RC;
# add internal triggers
# ----------------------------------------------------------------------
# is the server/repo in a writable state (i.e., not down for maintenance etc)
unshift @{ $rc{ACCESS_1} }, 'Writable::access_1';
# (testing only) override the rc file silently
# ----------------------------------------------------------------------
# use an env var that is highly unlikely to appear in real life :)
do $ENV{G3T_RC} if exists $ENV{G3T_RC} and -r $ENV{G3T_RC};
# setup some perl/rc/env vars
# ----------------------------------------------------------------------
unshift @INC, "$rc{LOCAL_CODE}/lib" if $rc{LOCAL_CODE};
$ENV{PATH} = "$ENV{GL_BINDIR}:$ENV{PATH}" unless $ENV{PATH} =~ /^$ENV{GL_BINDIR}:/;
{
$rc{GL_TID} = $ENV{GL_TID} ||= $$;
# TID: loosely, transaction ID. The first PID at the entry point passes
# it down to all its children so you can track each access, across all the
# various commands it spawns and actions it generates.
$rc{GL_LOGFILE} = $ENV{GL_LOGFILE} ||= gen_lfn( $rc{LOG_TEMPLATE} );
}
# these two are meant to help externally written commands (see
# src/commands/writable for an example)
$ENV{GL_REPO_BASE} = $rc{GL_REPO_BASE};
$ENV{GL_ADMIN_BASE} = $rc{GL_ADMIN_BASE};
# ----------------------------------------------------------------------
use strict;
use warnings;
# ----------------------------------------------------------------------
my $glrc_default_text = '';
{
local $/ = undef;
$glrc_default_text = <DATA>;
}
sub glrc {
my $cmd = shift;
if ( $cmd eq 'default-filename' ) {
return "$ENV{HOME}/.gitolite.rc";
} elsif ( $cmd eq 'default-text' ) {
return $glrc_default_text if $glrc_default_text;
_die "rc file default text not set; this should not happen!";
} elsif ( $cmd eq 'filename' ) {
# where is the rc file?
# search $HOME first
return "$ENV{HOME}/.gitolite.rc" if -f "$ENV{HOME}/.gitolite.rc";
return '';
} elsif ( $cmd eq 'current-data-version' ) {
return $current_data_version;
} else {
_die "unknown argument to glrc: '$cmd'";
}
}
# exported functions
# ----------------------------------------------------------------------
my $all = 0;
my $nonl = 0;
my $quiet = 0;
sub query_rc {
my @vars = args();
no strict 'refs';
if ($all) {
for my $e ( sort keys %rc ) {
print "$e=" . ( defined( $rc{$e} ) ? $rc{$e} : 'undef' ) . "\n";
}
exit 0;
}
my $cv = \%rc; # current "value"
while (@vars) {
my $v = shift @vars;
# dig into the rc hash, using each var as a component
if (not ref($cv)) {
_warn "unused arguments...";
last;
} elsif (ref($cv) eq 'HASH') {
$cv = $cv->{$v} || '';
} elsif (ref($cv) eq 'ARRAY') {
$cv = $cv->[$v] || '';
} else {
_die "dont know what to do with " . ref($cv) . " item in the rc file";
}
}
# we've run out of arguments so $cv is what we have. If we're supposed to
# be quiet, we don't have to print anything so let's get that done first:
exit ( $cv ? 0 : 1 ) if $quiet; # shell truth
# print values (notice we ignore the '-n' option if it's a ref)
if (ref($cv) eq 'HASH') {
print join("\n", sort keys %$cv), "\n" if %$cv;
} elsif (ref($cv) eq 'ARRAY') {
print join("\n", @$cv), "\n" if @$cv;
} else {
print $cv . ( $nonl ? '' : "\n" ) if $cv;
}
exit ( $cv ? 0 : 1 ); # shell truth
}
sub version {
my $version = '';
$version = '(unknown)';
for ("$ENV{GL_BINDIR}/VERSION") {
$version = slurp($_) if -r $_;
}
chomp($version);
return $version;
}
sub trigger {
my $rc_section = shift;
if ( exists $rc{$rc_section} ) {
if ( ref( $rc{$rc_section} ) ne 'ARRAY' ) {
_die "'$rc_section' section in rc file is not a perl list";
} else {
for my $s ( @{ $rc{$rc_section} } ) {
my ( $pgm, @args ) = split ' ', $s;
if ( my ( $module, $sub ) = ( $pgm =~ /^(.*)::(\w+)$/ ) ) {
require Gitolite::Triggers;
trace( 1, 'trigger', $module, $sub, @args, $rc_section, @_ );
Gitolite::Triggers::run( $module, $sub, @args, $rc_section, @_ );
} else {
$pgm = _which("triggers/$pgm", 'x');
_warn("skipped command '$s'"), next if not $pgm;
trace( 2, "command: $s" );
_system( $pgm, @args, $rc_section, @_ ); # they better all return with 0 exit codes!
}
}
}
return;
}
trace( 2, "'$rc_section' not found in rc" );
}
sub _which {
# looks for a file in LOCAL_CODE or GL_BINDIR. Returns whichever exists
# (LOCAL_CODE preferred if defined) or 0 if not found.
my $file = shift;
my $mode = shift; # could be 'x' or 'r'
my @files = ("$rc{GL_BINDIR}/$file");
unshift @files, ("$rc{LOCAL_CODE}/$file") if $rc{LOCAL_CODE};
for my $f ( @files ) {
return $f if -x $f;
return $f if -r $f and $mode eq 'r';
}
return 0;
}
# ----------------------------------------------------------------------
=for args
Usage: gitolite query-rc -a
gitolite query-rc [-n] [-q] rc-variable
-a print all variables and values (first level only)
-n do not append a newline if variable is scalar
-q exit code only (shell truth; 0 is success)
Query the rc hash. Second and subsequent arguments dig deeper into the hash.
The examples are for the default configuration; yours may be different.
Single values:
gitolite query-rc GL_ADMIN_BASE # prints "/home/git/.gitolite" or similar
gitolite query-rc UMASK # prints "63" (that's 0077 in decimal!)
Hashes:
gitolite query-rc COMMANDS
# prints "desc", "help", "info", "perms", "writable", one per line
gitolite query-rc COMMANDS help # prints 1
gitolite query-rc -q COMMANDS help # prints nothing; exit code is 0
gitolite query-rc COMMANDS fork # prints nothing; exit code is 1
Arrays (somewhat less useful):
gitolite query-rc POST_GIT # prints nothing; exit code is 0
gitolite query-rc POST_COMPILE # prints 4 lines
gitolite query-rc POST_COMPILE 0 # prints the first of those 4 lines
Explore:
gitolite query-rc -a
# prints all first level variables and values, one per line. Any that are
# listed as HASH or ARRAY can be explored further in subsequent commands.
=cut
sub args {
my $help = 0;
GetOptions(
'all|a' => \$all,
'nonl|n' => \$nonl,
'quiet|q' => \$quiet,
'help|h' => \$help,
) or usage();
usage("'-a' cannot be combined with other arguments or options") if $all and ( @ARGV or $nonl or $quiet );
usage() if not $all and not @ARGV or $help;
return @ARGV;
}
1;
# ----------------------------------------------------------------------
__DATA__
# configuration variables for gitolite
# This file is in perl syntax. But you do NOT need to know perl to edit it --
# just mind the commas, use single quotes unless you know what you're doing,
# and make sure the brackets and braces stay matched up!
# (Tip: perl allows a comma after the last item in a list also!)
# HELP for commands (see COMMANDS list below) can be had by running the
# command with "-h" as the sole argument.
# HELP for all the other external programs (the syntactic sugar helpers and
# the various programs/functions in the 8 trigger lists), can be found in
# doc/non-core.mkd (http://sitaramc.github.com/gitolite/non-core.html) or in
# the corresponding source file itself.
%RC = (
# if you're using mirroring, you need a hostname. This is *one* simple
# word, not a full domain name. See documentation if in doubt
# HOSTNAME => 'darkstar',
UMASK => 0077,
# look in the "GIT-CONFIG" section in the README for what to do
GIT_CONFIG_KEYS => '',
# comment out if you don't need all the extra detail in the logfile
LOG_EXTRA => 1,
# settings used by external programs; uncomment and change as needed. You
# can add your own variables for use in your own external programs; take a
# look at the info and desc commands for perl and shell samples.
# used by the CpuTime trigger
# DISPLAY_CPU_TIME => 1,
# CPU_TIME_WARN_LIMIT => 0.1,
# used by the desc command
# WRITER_CAN_UPDATE_DESC => 1,
# used by the info command
# SITE_INFO => 'Please see http://blahblah/gitolite for more help',
# add more roles (like MANAGER, TESTER, ...) here.
# WARNING: if you make changes to this hash, you MUST run 'gitolite
# compile' afterward, and possibly also 'gitolite trigger POST_COMPILE'
ROLES =>
{
READERS => 1,
WRITERS => 1,
},
# uncomment (and change) this if you wish
# DEFAULT_ROLE_PERMS => 'READERS @all',
# comment out or uncomment as needed
# these are available to remote users
COMMANDS =>
{
'help' => 1,
'desc' => 1,
# 'fork' => 1,
'info' => 1,
# 'mirror' => 1,
'perms' => 1,
# 'sskm' => 1,
'writable' => 1,
# 'D' => 1,
},
# comment out or uncomment as needed
# these will run in sequence during the conf file parse
SYNTACTIC_SUGAR =>
[
# 'continuation-lines',
# 'keysubdirs-as-groups',
],
# comment out or uncomment as needed
# these will run in sequence to modify the input (arguments and environment)
INPUT =>
[
# 'CpuTime::input',
# 'Shell::input',
# 'Alias::input',
# 'Mirroring::input',
],
# comment out or uncomment as needed
# these will run in sequence just after the first access check is done
ACCESS_1 =>
[
],
# comment out or uncomment as needed
# these will run in sequence just before the actual git command is invoked
PRE_GIT =>
[
# 'renice 10',
# 'Mirroring::pre_git',
# 'partial-copy',
],
# comment out or uncomment as needed
# these will run in sequence just after the second access check is done
ACCESS_2 =>
[
],
# comment out or uncomment as needed
# these will run in sequence after the git command returns
POST_GIT =>
[
# 'Mirroring::post_git',
# 'CpuTime::post_git',
],
# comment out or uncomment as needed
# these will run in sequence before a new wild repo is created
PRE_CREATE =>
[
],
# comment out or uncomment as needed
# these will run in sequence after a new repo is created
POST_CREATE =>
[
'post-compile/update-git-configs',
'post-compile/update-gitweb-access-list',
'post-compile/update-git-daemon-access-list',
],
# comment out or uncomment as needed
# these will run in sequence after post-update
POST_COMPILE =>
[
'post-compile/ssh-authkeys',
'post-compile/update-git-configs',
'post-compile/update-gitweb-access-list',
'post-compile/update-git-daemon-access-list',
],
);
# ------------------------------------------------------------------------------
# per perl rules, this should be the last line in such a file:
1;
# Local variables:
# mode: perl
# End:
# vim: set syn=perl:

View file

@ -1,167 +0,0 @@
package Gitolite::Setup;
# implements 'gitolite setup'
# ----------------------------------------------------------------------
=for args
Usage: gitolite setup [<option>]
Setup gitolite, compile conf, run the POST_COMPILE trigger (see rc file) and
propagate hooks.
-a, --admin <name> admin name
-pk, --pubkey <file> pubkey file name
-ho, --hooks-only skip other steps and just propagate hooks
First run: either the pubkey or the admin name is *required*, depending on
whether you're using ssh mode or http mode.
Subsequent runs:
- Without options, 'gitolite setup' is a general "fix up everything" command
(for example, if you brought in repos from outside, or someone messed
around with the hooks, or you made an rc file change that affects access
rules, etc.)
- '-pk' can be used to replace the admin key; useful if you lost the admin's
private key but do have shell access to the server.
- '-ho' is mainly for scripting use. Do not combine with other options.
- '-a' is ignored
=cut
# ----------------------------------------------------------------------
@EXPORT = qw(
setup
);
use Exporter 'import';
use Getopt::Long;
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Store;
use strict;
use warnings;
# ----------------------------------------------------------------------
sub setup {
my ( $admin, $pubkey, $h_only, $argv ) = args();
unless ($h_only) {
setup_glrc();
setup_gladmin( $admin, $pubkey, $argv );
_system("gitolite compile");
_system("gitolite trigger POST_COMPILE");
}
hook_repos(); # all of them, just to be sure
}
# ----------------------------------------------------------------------
sub args {
my $admin = '';
my $pubkey = '';
my $h_only = 0;
my $help = 0;
my $argv = join( " ", @ARGV );
GetOptions(
'admin|a=s' => \$admin,
'pubkey|pk=s' => \$pubkey,
'hooks-only|ho' => \$h_only,
'help|h' => \$help,
) or usage();
usage() if $help or ( $pubkey and $admin );
usage() if $h_only and ($admin or $pubkey);
if ($pubkey) {
$pubkey =~ /\.pub$/ or _die "'$pubkey' name does not end in .pub";
$pubkey =~ /\@/ and _die "'$pubkey' name contains '\@'";
tsh_try("cat $pubkey") or _die "'$pubkey' not a readable file";
tsh_lines() == 1 or _die "'$pubkey' must have exactly one line";
tsh_try("ssh-keygen -l -f $pubkey") or _die "'$pubkey' does not seem to be a valid ssh pubkey file";
$admin = $pubkey;
$admin =~ s(.*/)();
$admin =~ s/\.pub$//;
}
return ( $admin || '', $pubkey || '', $h_only || 0, $argv );
}
sub setup_glrc {
_print( glrc('default-filename'), glrc('default-text') ) if not glrc('filename');
}
sub setup_gladmin {
my ( $admin, $pubkey, $argv ) = @_;
_die "'-pk' or '-a' required; see 'gitolite setup -h' for more"
if not $admin and not -f "$rc{GL_ADMIN_BASE}/conf/gitolite.conf";
# reminder: 'admin files' are in ~/.gitolite, 'admin repo' is
# $rc{GL_REPO_BASE}/gitolite-admin.git
# grab the pubkey content before we chdir() away
my $pubkey_content = '';
$pubkey_content = slurp($pubkey) if $pubkey;
# set up the admin files in admin-base
_mkdir( $rc{GL_ADMIN_BASE} );
_chdir( $rc{GL_ADMIN_BASE} );
_mkdir("conf");
_mkdir("logs");
my $conf;
{
local $/ = undef;
$conf = <DATA>;
}
$conf =~ s/%ADMIN/$admin/g;
_print( "conf/gitolite.conf", $conf ) if not -f "conf/gitolite.conf";
if ($pubkey) {
_mkdir("keydir");
_print( "keydir/$admin.pub", $pubkey_content );
}
# set up the admin repo in repo-base
_chdir();
_mkdir( $rc{GL_REPO_BASE} );
_chdir( $rc{GL_REPO_BASE} );
new_repo("gitolite-admin") if not -d "gitolite-admin.git";
# commit the admin files to the admin repo
$ENV{GIT_WORK_TREE} = $rc{GL_ADMIN_BASE};
_chdir("$rc{GL_REPO_BASE}/gitolite-admin.git");
_system("git add conf/gitolite.conf");
_system("git add keydir") if $pubkey;
tsh_try("git config --get user.email") or tsh_run( "git config user.email $ENV{USER}\@" . `hostname` );
tsh_try("git config --get user.name") or tsh_run( "git config user.name '$ENV{USER} on '" . `hostname` );
tsh_try("git diff --cached --quiet")
or tsh_try("git commit -am 'gitolite setup $argv'")
or _die "setup failed to commit to the admin repo";
delete $ENV{GIT_WORK_TREE};
}
1;
__DATA__
repo gitolite-admin
RW+ = %ADMIN
repo testing
RW+ = @all

View file

@ -1,121 +0,0 @@
package Gitolite::Test;
# functions for the test code to use
# ----------------------------------------------------------------------
#<<<
@EXPORT = qw(
try
put
text
lines
dump
confreset
confadd
cmp
md5sum
);
#>>>
use Exporter 'import';
use File::Path qw(mkpath);
use Carp qw(carp cluck croak confess);
use Digest::MD5 qw(md5_hex);
use Gitolite::Common;
BEGIN {
require Gitolite::Test::Tsh;
*{'try'} = \&Tsh::try;
*{'put'} = \&Tsh::put;
*{'text'} = \&Tsh::text;
*{'lines'} = \&Tsh::lines;
*{'cmp'} = \&Tsh::cmp;
}
use strict;
use warnings;
# ----------------------------------------------------------------------
# make sure the user is ready for it
if (not $ENV{GITOLITE_TEST} or $ENV{GITOLITE_TEST} ne 'y') {
print "Bail out! See t/README for information on how to run the tests.\n";
exit 255;
}
# required preamble for all tests
try "
DEF gsh = /TRACE: gsh.SOC=/
DEF reject = /hook declined to update/; /remote rejected.*hook declined/; /error: failed to push some refs to/
DEF AP_1 = cd ../gitolite-admin; ok or die cant find admin repo clone;
DEF AP_2 = AP_1; git add conf ; ok; git commit -m %1; ok; /master.* %1/
DEF ADMIN_PUSH = AP_2 %1; glt push admin origin; ok; gsh; /master -> master/
DEF CS_1 = pwd; //tmp/tsh_tempdir.*gitolite-admin/; git remote -v; ok; /file://gitolite-admin/
DEF CHECK_SETUP = CS_1; git log; ok; /fa7564c1b903ea3dce49314753f25b34b9e0cea0/
DEF CLONE = glt clone %1 file:///%2
DEF PUSH = glt push %1 origin
# clean install
mkdir -p $ENV{HOME}/bin
ln -sf $ENV{PWD}/t/glt ~/bin
./install -ln
cd; rm -vrf .gito* repositories
git config --global user.name \"gitolite tester\"
git config --global user.email \"tester\@example.com\"
# setup
gitolite setup -a admin
# clone admin repo
cd tsh_tempdir
glt clone admin --progress file://gitolite-admin
cd gitolite-admin
" or die "could not setup the test environment; errors:\n\n" . text() . "\n\n";
sub dump {
use Data::Dumper;
for my $i (@_) {
print STDERR "DBG: " . Dumper($i);
}
}
sub _confargs {
return @_ if ( $_[1] );
return 'gitolite.conf', $_[0];
}
sub confreset {
chdir("../gitolite-admin") or die "in `pwd`, could not cd ../g-a";
system( "rm", "-rf", "conf" );
mkdir("conf");
system("mv ~/repositories/gitolite-admin.git ~/repositories/.ga");
system("mv ~/repositories/testing.git ~/repositories/.te");
system("find ~/repositories -name '*.git' |xargs rm -rf");
system("mv ~/repositories/.ga ~/repositories/gitolite-admin.git");
system("mv ~/repositories/.te ~/repositories/testing.git ");
put "|cut -c9- > conf/gitolite.conf", '
repo gitolite-admin
RW+ = admin
repo testing
RW+ = @all
';
}
sub confadd {
chdir("../gitolite-admin") or die "in `pwd`, could not cd ../g-a";
my ( $file, $string ) = _confargs(@_);
put "|cat >> conf/$file", $string;
}
sub md5sum {
my $out = '';
for my $file (@_) {
$out .= md5_hex(slurp($file)) . " $file\n";
}
return $out;
}
1;

View file

@ -1,643 +0,0 @@
#!/usr/bin/perl
use 5.10.0;
# Tsh -- non interactive Testing SHell in perl
# TODO items:
# - allow an RC file to be used to add basic and extended commands
# - convert internal defaults to additions to the RC file
# - implement shell commands as you go
# - solve the "pass/fail" inconsistency between shell and perl
# - solve the pipes problem (use 'overload'?)
# ----------------------------------------------------------------------
# modules
package Tsh;
use Exporter 'import';
@EXPORT = qw(
try run cmp AUTOLOAD
rc error_count text lines error_list put
cd tsh_tempdir
$HOME $PWD $USER
);
@EXPORT_OK = qw();
use Env qw(@PATH HOME PWD USER TSH_VERBOSE);
# other candidates:
# GL_ADMINDIR GL_BINDIR GL_RC GL_REPO_BASE_ABS GL_REPO GL_USER
use strict;
use warnings;
use Text::Tabs; # only used for formatting the usage() message
use Text::ParseWords;
use File::Temp qw(tempdir);
END { chdir( $ENV{HOME} ); }
# we need this END handler *after* the 'use File::Temp' above. Without
# this, if $PWD at exit was $tempdir, you get errors like "cannot remove
# path when cwd is [...] at /usr/share/perl5/File/Temp.pm line 902".
use Data::Dumper;
# ----------------------------------------------------------------------
# globals
my $rc; # return code from backticked (external) programs
my $text; # STDOUT+STDERR of backticked (external) programs
my $lec; # the last external command (the rc and text are from this)
my $cmd; # the current command
my $testnum; # current test number, for info in TAP output
my $testname; # current test name, for error info to user
my $line; # current line number and text
my $err_count; # count of test failures
my @errors_in; # list of testnames that errored
my $tick; # timestamp for git commits
my %autoloaded;
my $tempdir = '';
# ----------------------------------------------------------------------
# setup
# unbuffer STDOUT and STDERR
select(STDERR); $|++;
select(STDOUT); $|++;
# set the timestamp (needed only under harness)
test_tick() if $ENV{HARNESS_ACTIVE};
# ----------------------------------------------------------------------
# this is for one-liner access from outside, using @ARGV, as in:
# perl -MTsh -e 'tsh()' 'tsh command list'
# or via STDIN
# perl -MTsh -e 'tsh()' < file-containing-tsh-commands
# NOTE: it **exits**!
sub tsh {
my @lines;
if (@ARGV) {
# simple, single argument which is a readable filename
if ( @ARGV == 1 and $ARGV[0] !~ /\s/ and -r $ARGV[0] ) {
# take the contents of the file
@lines = <>;
} else {
# more than one argument *or* not readable filename
# just take the arguments themselves as the command list
@lines = @ARGV;
@ARGV = ();
}
} else {
# no arguments given, take STDIN
usage() if -t;
@lines = <>;
}
# and process them
try(@lines);
# print error summary by default
if ( not defined $TSH_VERBOSE ) {
say STDERR "$err_count error(s)" if $err_count;
}
exit $err_count;
}
# these two get called with series of tsh commands, while the autoload,
# (later) handles single commands
sub try {
$line = $rc = $err_count = 0;
@errors_in = ();
# break up multiline arguments into separate lines
my @lines = map { split /\n/ } @_;
# and process them
rc_lines(@lines);
# bump err_count if the last command had a non-0 rc (that was apparently not checked).
$err_count++ if $rc;
# finish up...
dbg( 1, "$err_count error(s)" ) if $err_count;
return ( not $err_count );
}
# run() differs from try() in that
# - uses open(), not backticks
# - takes only one command, not tsh-things like ok, /patt/ etc
# - - if you pass it an array it uses the list form!
sub run {
open( my $fh, "-|", @_ ) or die "tell sitaram $!";
local $/ = undef; $text = <$fh>;
close $fh; warn "tell sitaram $!" if $!;
$rc = ( $? >> 8 );
return $text;
}
sub put {
my ( $file, $data ) = @_;
die "probable quoting error in arguments to put: $file\n" if $file =~ /^\s*['"]/;
my $mode = ">";
$mode = "|-" if $file =~ s/^\s*\|\s*//;
$rc = 0;
my $fh;
open( $fh, $mode, $file )
and print $fh $data
and close $fh
and return 1;
$rc = 1;
dbg( 1, "put $file: $!" );
return '';
}
# ----------------------------------------------------------------------
# TODO: AUTOLOAD and exportable convenience subs for common shell commands
sub cd {
my $dir = shift || '';
_cd($dir);
dbg( 1, "cd $dir: $!" ) if $rc;
return ( not $rc );
}
# this is classic AUTOLOAD, almost from the perlsub manpage. Although, if
# instead of `ls('bin');` you want to be able to say `ls 'bin';` you will need
# to predeclare ls, with `sub ls;`.
sub AUTOLOAD {
my $program = $Tsh::AUTOLOAD;
dbg( 4, "program = $program, arg=$_[0]" );
$program =~ s/.*:://;
$autoloaded{$program}++;
die "tsh's autoload support expects only one arg\n" if @_ > 1;
_sh("$program $_[0]");
return ( not $rc ); # perl truth
}
# ----------------------------------------------------------------------
# exportable service subs
sub rc {
return $rc || 0;
}
sub text {
return $text || '';
}
sub lines {
return split /\n/, $text;
}
sub error_count {
return $err_count;
}
sub error_list {
return (
wantarray
? @errors_in
: join( "\n", @errors_in )
);
}
sub tsh_tempdir {
# create tempdir if not already done
$tempdir = tempdir( "tsh_tempdir.XXXXXXXXXX", TMPDIR => 1, CLEANUP => 1 ) unless $tempdir;
# XXX TODO that 'UNLINK' doesn't work for Ctrl_C
return $tempdir;
}
# ----------------------------------------------------------------------
# internal (non-exportable) service subs
sub print_plan {
return unless $ENV{HARNESS_ACTIVE};
my $_ = shift;
say "1..$_";
}
sub rc_lines {
my @lines = @_;
while (@lines) {
my $_ = shift @lines;
chomp; $_ = trim_ws($_);
$line++;
# this also sets $testname
next if is_comment_or_empty($_);
dbg( 2, "L: $_" );
$line .= ": $_"; # save line for printing with 'FAIL:'
# a DEF has to be on a line by itself
if (/^DEF\s+([-.\w]+)\s*=\s*(\S.*)$/) {
def( $1, $2 );
next;
}
my @cmds = cmds($_);
# process each command
# (note: some of the commands may put stuff back into @lines)
while (@cmds) {
# this needs to be the 'global' one, since fail() prints it
$cmd = shift @cmds;
# is the current command a "testing" command?
my $testing_cmd = (
$cmd =~ m(^ok(?:\s+or\s+(.*))?$)
or $cmd =~ m(^!ok(?:\s+or\s+(.*))?$)
or $cmd =~ m(^/(.*?)/(?:\s+or\s+(.*))?$)
or $cmd =~ m(^!/(.*?)/(?:\s+or\s+(.*))?$)
);
# warn if the previous command failed but rc is not being checked
if ( $rc and not $testing_cmd ) {
dbg( 1, "rc: $rc from cmd prior to '$cmd'\n" );
# count this as a failure, for exit status purposes
$err_count++;
# and reset the rc, otherwise for example 'ls foo; tt; tt; tt'
# will tell you there are 3 errors!
$rc = 0;
push @errors_in, $testname if $testname;
}
# prepare to run the command
dbg( 3, "C: $cmd" );
if ( def($cmd) ) {
# expand macro and replace head of @cmds (unshift)
dbg( 2, "DEF: $cmd" );
unshift @cmds, cmds( def($cmd) );
} else {
parse($cmd);
}
# reset rc if checking is done
$rc = 0 if $testing_cmd;
# assumes you will (a) never have *both* 'ok' and '!ok' after
# an action command, and (b) one of them will come immediately
# after the action command, with /patt/ only after it.
}
}
}
sub def {
my ( $cmd, $list ) = @_;
state %def;
%def = read_rc_file() unless %def;
if ($list) {
# set mode
die "attempt to redefine macro $cmd\n" if $def{$cmd};
$def{$cmd} = $list;
return;
}
# get mode: split the $cmd at spaces, see if there is a definition
# available, substitute any %1, %2, etc., in it and send it back
my ( $c, @d ) = shellwords($cmd);
my $e; # the expanded value
if ( $e = $def{$c} ) { # starting value
for my $i ( 1 .. 9 ) {
last unless $e =~ /%$i/; # no more %N's (we assume sanity)
die "$def{$c} requires more arguments\n" unless @d;
my $f = shift @d; # get the next datum
$e =~ s/%$i/$f/g; # and substitute %N all over
}
return join( " ", $e, @d ); # join up any remaining data
}
return '';
}
sub _cd {
my $dir = shift || $HOME;
# a directory name of 'tsh_tempdir' is special
$dir = tsh_tempdir() if $dir eq 'tsh_tempdir';
$rc = 0;
chdir($dir) or $rc = 1;
}
sub _sh {
my $cmd = shift;
# TODO: switch to IPC::Open3 or something...?
dbg( 4, " running: ( $cmd ) 2>&1" );
$text = `( $cmd ) 2>&1; /bin/echo -n RC=\$?`;
$lec = $cmd;
dbg( 4, " results:\n$text" );
if ( $text =~ /RC=(\d+)$/ ) {
$rc = $1;
$text =~ s/RC=\d+$//;
} else {
die "couldnt find RC= in result; this should not happen:\n$text\n\n...\n";
}
}
sub _perl {
my $perl = shift;
local $_;
$_ = $text;
dbg( 4, " eval: $perl" );
my $evrc = eval $perl;
if ($@) {
$rc = 1; # shell truth
dbg( 1, $@ );
# leave $text unchanged
} else {
$rc = not $evrc;
# $rc is always shell truth, so we need to cover the case where
# there was no error but it still returned a perl false
$text = $_;
}
dbg( 4, " eval-rc=$evrc, results:\n$text" );
}
sub parse {
my $cmd = shift;
if ( $cmd =~ /^sh (.*)/ ) {
_sh($1);
} elsif ( $cmd =~ /^perl (.*)/ ) {
_perl($1);
} elsif ( $cmd eq 'tt' or $cmd eq 'test-tick' ) {
test_tick();
} elsif ( $cmd =~ /^plan ?(\d+)$/ ) {
print_plan($1);
} elsif ( $cmd =~ /^cd ?(\S*)$/ ) {
_cd($1);
} elsif ( $cmd =~ /^ENV (\w+)=['"]?(.+?)['"]?$/ ) {
$ENV{$1} = $2;
} elsif ( $cmd =~ /^(?:tc|test-commit)\s+(\S.*)$/ ) {
# this is the only "git special" really; the default expansions are
# just that -- defaults. But this one is hardwired!
dummy_commits($1);
} elsif ( $cmd =~ '^put(?:\s+(\S.*))?$' ) {
if ($1) {
put( $1, $text );
} else {
print $text if defined $text;
}
} elsif ( $cmd =~ m(^ok(?:\s+or\s+(.*))?$) ) {
$rc ? fail( "ok, rc=$rc from $lec", $1 || '' ) : ok();
} elsif ( $cmd =~ m(^!ok(?:\s+or\s+(.*))?$) ) {
$rc ? ok() : fail( "!ok, rc=0 from $lec", $1 || '' );
} elsif ( $cmd =~ m(^/(.*?)/(?:\s+or\s+(.*))?$) ) {
expect( $1, $2 );
} elsif ( $cmd =~ m(^!/(.*?)/(?:\s+or\s+(.*))?$) ) {
not_expect( $1, $2 );
} else {
_sh($cmd);
}
}
# currently unused
sub executable {
my $cmd = shift;
# path supplied
$cmd =~ m(/) and -x $cmd and return 1;
# barename; look up in $PATH
for my $p (@PATH) {
-x "$p/$cmd" and return 1;
}
return 0;
}
sub ok {
$testnum++;
say "ok ($testnum)" if $ENV{HARNESS_ACTIVE};
}
sub fail {
$testnum++;
say "not ok ($testnum)" if $ENV{HARNESS_ACTIVE};
my $die = 0;
my ( $msg1, $msg2 ) = @_;
if ($msg2) {
# if arg2 is non-empty, print it regardless of debug level
$die = 1 if $msg2 =~ s/^die //;
say STDERR "# $msg2";
}
local $TSH_VERBOSE = 1 if $ENV{TSH_ERREXIT};
dbg( 1, "FAIL: $msg1", $testname || '', "test number $testnum", "L: $line", "results:\n$text" );
# count the error and add the testname to the list if it is set
$err_count++;
push @errors_in, $testname if $testname;
return unless $die or $ENV{TSH_ERREXIT};
dbg( 1, "exiting at cmd $cmd\n" );
exit( $rc || 74 );
}
sub cmp {
# compare input string with second input string or text()
my $in = shift;
my $text = ( @_ ? +shift : text() );
if ( $text eq $in ) {
ok();
} else {
fail( 'cmp failed', '' );
dbg( 4, "\n\ntext = <<<$text>>>, in = <<<$in>>>\n\n" );
}
}
sub expect {
my ( $patt, $msg ) = @_;
$msg =~ s/^\s+// if $msg;
my $sm;
if ( $sm = sm($patt) ) {
dbg( 4, " M: $sm" );
ok();
} else {
fail( "/$patt/", $msg || '' );
}
}
sub not_expect {
my ( $patt, $msg ) = @_;
$msg =~ s/^\s+// if $msg;
my $sm;
if ( $sm = sm($patt) ) {
dbg( 4, " M: $sm" );
fail( "!/$patt/", $msg || '' );
} else {
ok();
}
}
sub sm {
# smart match? for now we just do regex match
my $patt = shift;
return ( $text =~ qr($patt) ? $& : "" );
}
sub trim_ws {
my $_ = shift;
s/^\s+//; s/\s+$//;
return $_;
}
sub is_comment_or_empty {
my $_ = shift;
chomp; $_ = trim_ws($_);
if (/^##\s(.*)/) {
$testname = $1;
say "# $1";
}
return ( /^#/ or /^$/ );
}
sub cmds {
my $_ = shift;
chomp; $_ = trim_ws($_);
# split on unescaped ';'s, then unescape the ';' in the results
my @cmds = map { s/\\;/;/g; $_ } split /(?<!\\);/;
@cmds = grep { $_ = trim_ws($_); /\S/; } @cmds;
return @cmds;
}
sub dbg {
return unless $TSH_VERBOSE;
my $level = shift;
return unless $TSH_VERBOSE >= $level;
my $all = join( "\n", grep( /./, @_ ) );
chomp($all);
$all =~ s/\n/\n\t/g;
say STDERR "# $all";
}
sub ddump {
for my $i (@_) {
print STDERR "DBG: " . Dumper($i);
}
}
sub usage {
# TODO
print "Please see documentation at:
https://github.com/sitaramc/tsh/blob/master/README.mkd
Meanwhile, here are your local 'macro' definitions:
";
my %m = read_rc_file();
my @m = map { "$_\t$m{$_}\n" } sort keys %m;
$tabstop = 16;
print join( "", expand(@m) );
exit 1;
}
# ----------------------------------------------------------------------
# git-specific internal service subs
sub dummy_commits {
for my $f ( split ' ', shift ) {
if ( $f eq 'tt' or $f eq 'test-tick' ) {
test_tick();
next;
}
my $ts = ( $tick ? gmtime( $tick + 19800 ) : gmtime() );
_sh("echo $f at $ts >> $f && git add $f && git commit -m '$f at $ts'");
}
}
sub test_tick {
unless ( $ENV{HARNESS_ACTIVE} ) {
sleep 1;
return;
}
$tick += 60 if $tick;
$tick ||= 1310000000;
$ENV{GIT_COMMITTER_DATE} = "$tick +0530";
$ENV{GIT_AUTHOR_DATE} = "$tick +0530";
}
# ----------------------------------------------------------------------
# the internal macros, for easy reference and reading
sub read_rc_file {
my $rcfile = "$HOME/.tshrc";
my $rctext;
if ( -r $rcfile ) {
local $/ = undef;
open( my $rcfh, "<", $rcfile ) or die "this should not happen: $!\n";
$rctext = <$rcfh>;
} else {
# this is the default "rc" content
$rctext = "
add = git add
branch = git branch
clone = git clone
checkout = git checkout
commit = git commit
fetch = git fetch
init = git init
push = git push
reset = git reset
tag = git tag
empty = git commit --allow-empty -m empty
push-om = git push origin master
reset-h = git reset --hard
reset-hu = git reset --hard \@{u}
"
}
# ignore everything except lines of the form "aa = bb cc dd"
my %commands = ( $rctext =~ /^\s*([-.\w]+)\s*=\s*(\S.*)$/gm );
return %commands;
}
1;

View file

@ -1,33 +0,0 @@
package Gitolite::Triggers;
# load and run triggered modules
# ----------------------------------------------------------------------
#<<<
@EXPORT = qw(
);
#>>>
use Exporter 'import';
use Gitolite::Rc;
use Gitolite::Common;
use strict;
use warnings;
# ----------------------------------------------------------------------
sub run {
my ( $module, $sub, @args ) = @_;
$module = "Gitolite::Triggers::$module" if $module !~ /^Gitolite::/;
eval "require $module";
_die "$@" if $@;
my $subref;
eval "\$subref = \\\&$module" . "::" . "$sub";
_die "module '$module' does not exist or does not have sub '$sub'" unless ref($subref) eq 'CODE';
$subref->(@args);
}
1;

View file

@ -1,81 +0,0 @@
package Gitolite::Triggers::Alias;
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
use strict;
use warnings;
# aliasing a repo to another
# ----------------------------------------------------------------------
=for usage
Why:
We had an existing repo "foo" that lots of people use. We wanted to
rename it to "foo/code", so that related repos "foo/upstream" and
"foo/docs" (both containing stuff we did not want to put in "foo") could
also be made and then the whole thing would be structured nicely.
At the same time we did not want to *force* all the users to change the
name. At least git operations should still work with the old name,
although it is OK for "info" and other "commands" to display/require the
proper name (i.e., the new name).
How:
* add a new variable REPO_ALIASES to the rc file, with entries like:
REPO_ALIASES =>
{
'foo' => 'foo/code',
}
* add the following line to the INPUT section in the rc file:
'Alias::input',
Notes:
* only git operations (clone/fetch/push) are alias aware. Nothing else in
gitolite, such as all the gitolite commands etc., are alias-aware and will
always use/require the proper repo name.
* 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.
* funnily enough, this even works with mirroring! That is, a master can
push a repo "foo" to a slave per its configuration, while the slave thinks
it is getting repo "bar" from the master per its configuration.
Just make sure to put the Alias::input line *before* the Mirroring::input
line in the rc file on the slave.
However, it will probably not work with redirected pushes unless you setup
the opposite alias ("bar" -> "foo") on master.
=cut
sub input {
my $git_commands = "git-upload-pack|git-receive-pack|git-upload-archive";
my $user = $ARGV[0] || '@all'; # user name is undocumented for now
if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /(?:$git_commands) '\/?(\S+)'$/ ) {
my $repo = $1;
( my $norm = $repo ) =~ s/\.git$//; # normalised repo name
my $target;
return unless $target = $rc{REPO_ALIASES}{$norm};
$target = $target->{$user} if ref($target) eq 'HASH';
return unless $target;
_warn "'$norm' is an alias for '$target'";
$ENV{SSH_ORIGINAL_COMMAND} =~ s/'\/?$repo'/'$target'/;
}
}
1;

View file

@ -1,24 +0,0 @@
package Gitolite::Triggers::AutoCreate;
use strict;
use warnings;
# perl trigger set for stuff to do with auto-creating repos
# ----------------------------------------------------------------------
# to deny auto-create on read access, add 'AutoCreate::deny_R' to the
# PRE_CREATE trigger list
sub deny_R {
die "autocreate denied\n" if $_[3] and $_[3] eq 'R';
return;
}
# to deny auto-create on read *and* write access, add 'AutoCreate::deny_RW' to
# the PRE_CREATE trigger list. This means you can only create repos using the
# 'create' command, (which needs to be enabled in the COMMANDS list).
sub deny_RW {
die "autocreate denied\n" if $_[3] and ( $_[3] eq 'R' or $_[3] eq 'W' );
return;
}
1;

View file

@ -1,52 +0,0 @@
package Gitolite::Triggers::CpuTime;
use Time::HiRes;
use Gitolite::Rc;
use Gitolite::Common;
use strict;
use warnings;
# cpu and elapsed times for gitolite+git operations
# ----------------------------------------------------------------------
# uncomment the appropriate lines in the rc file to enable this
# Ideally, you will (a) write your own code with a different filename so later
# gitolite upgrades won't overwrite your copy, (b) add appropriate variables
# to the rc file, and (c) change your rc file to call your program instead.
# ----------------------------------------------------------------------
my $start_time;
sub input {
_warn "something wrong with the invocation of CpuTime::input" if $ENV{GL_TID} ne $$;
$start_time = [ Time::HiRes::gettimeofday() ];
}
sub post_git {
_warn "something wrong with the invocation of CpuTime::post_git" if $ENV{GL_TID} ne $$;
my ( $trigger, $repo, $user, $aa, $ref, $verb ) = @_;
my ( $utime, $stime, $cutime, $cstime ) = times();
my $elapsed = Time::HiRes::tv_interval($start_time);
gl_log( 'times', $utime, $stime, $cutime, $cstime, $elapsed );
# now do whatever you want with the data; the following is just an example.
if ( my $limit = $rc{CPU_TIME_WARN_LIMIT} ) {
my $total = $utime + $cutime + $stime + $cstime;
# some code to send an email or whatever...
say2 "limit = $limit, actual = $total" if $total > $limit;
}
if ( $rc{DISPLAY_CPU_TIME} ) {
say2 "perf stats for $verb on repo '$repo':";
say2 " user CPU time: " . ( $utime + $cutime );
say2 " sys CPU time: " . ( $stime + $cstime );
say2 " elapsed time: " . $elapsed;
}
}
1;

View file

@ -1,232 +0,0 @@
package Gitolite::Triggers::Mirroring;
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
use strict;
use warnings;
my $git_commands = "git-upload-pack|git-receive-pack|git-upload-archive";
my $hn = $rc{HOSTNAME};
my ( $mode, $master, %slaves, %trusted_slaves );
# ----------------------------------------------------------------------
sub input {
unless ($ARGV[0] =~ /^server-(\S+)$/) {
_die "'$ARGV[0]' is not a valid server name" if $ENV{SSH_ORIGINAL_COMMAND} =~ /^USER=(\S+) SOC=(git-receive-pack '(\S+)')$/;
return;
}
# note: we treat %rc as our own internal "poor man's %ENV"
$rc{FROM_SERVER} = $1;
trace( 3, "from_server: $1" );
my $sender = $rc{FROM_SERVER} || '';
# custom peer-to-peer commands. At present the only one is 'perms -c',
# sent from a mirror command
if ($ENV{SSH_ORIGINAL_COMMAND} =~ /^CREATOR=(\S+) perms -c '(\S+)'$/) {
$ENV{GL_USER} = $1;
my $repo = $2;
details($repo);
_die "$hn: '$repo' is local" if $mode eq 'local';
_die "$hn: '$repo' is native" if $mode eq 'master';
_die "$hn: '$sender' is not the master for '$repo'" if $master ne $sender;
# this expects valid perms content on STDIN
_system("gitolite perms -c $repo");
# we're done. Yes, really...
exit 0;
}
if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /^USER=(\S+) SOC=(git-receive-pack '(\S+)')$/ ) {
# my ($user, $newsoc, $repo) = ($1, $2, $3);
$ENV{SSH_ORIGINAL_COMMAND} = $2;
@ARGV = ($1);
$rc{REDIRECTED_PUSH} = 1;
trace( 3, "redirected_push for user $1" );
} else {
# master -> slave push, no access checks needed
$ENV{GL_BYPASS_ACCESS_CHECKS} = 1;
}
}
# ----------------------------------------------------------------------
sub pre_git {
return unless $hn;
# nothing, and I mean NOTHING, happens if HOSTNAME is not set
trace( 1, "pre_git() on $hn" );
my ( $repo, $user, $aa ) = @_[ 1, 2, 3 ];
my $sender = $rc{FROM_SERVER} || '';
$user = '' if $sender and not exists $rc{REDIRECTED_PUSH};
# ------------------------------------------------------------------
# now you know the repo, get its mirroring details
details($repo);
# we don't deal with any reads. Note that for pre-git this check must
# happen *after* getting details, to give mode() a chance to die on "known
# unknown" repos (repos that are in the config, but mirror settings
# exclude this host from both the master and slave lists)
return if $aa eq 'R';
trace( 1, "mirror", "pre_git", $repo, "user=$user", "sender=$sender", "mode=$mode", ( $rc{REDIRECTED_PUSH} ? ("redirected") : () ) );
# ------------------------------------------------------------------
# case 1: we're master or slave, normal user pushing to us
if ( $user and not $rc{REDIRECTED_PUSH} ) {
trace( 3, "case 1, user push" );
return if $mode eq 'local' or $mode eq 'master';
if ( $trusted_slaves{$hn} ) {
trace( 3, "redirecting to $master" );
trace( 1, "redirect to $master" );
exec( "ssh", $master, "USER=$user", "SOC=$ENV{SSH_ORIGINAL_COMMAND}" );
} else {
_die "$hn: pushing '$repo' to slave '$hn' not allowed";
}
}
# ------------------------------------------------------------------
# case 2: we're slave, master pushing to us
if ( $sender and not $rc{REDIRECTED_PUSH} ) {
trace( 3, "case 2, master push" );
_die "$hn: '$repo' is local" if $mode eq 'local';
_die "$hn: '$repo' is native" if $mode eq 'master';
_die "$hn: '$sender' is not the master for '$repo'" if $master ne $sender;
return;
}
# ------------------------------------------------------------------
# case 3: we're master, slave sending a redirected push to us
if ( $sender and $rc{REDIRECTED_PUSH} ) {
trace( 3, "case 2, slave redirect" );
_die "$hn: '$repo' is local" if $mode eq 'local';
_die "$hn: '$repo' is not native" if $mode eq 'slave';
_die "$hn: '$sender' is not a valid slave for '$repo'" if not $slaves{$sender};
_die "$hn: redirection not allowed from '$sender'" if not $trusted_slaves{$sender};
return;
}
_die "$hn: should not reach this line";
}
# ----------------------------------------------------------------------
sub post_git {
return unless $hn;
# nothing, and I mean NOTHING, happens if HOSTNAME is not set
trace( 1, "post_git() on $hn" );
my ( $repo, $user, $aa ) = @_[ 1, 2, 3 ];
# we don't deal with any reads
return if $aa eq 'R';
my $sender = $rc{FROM_SERVER} || '';
$user = '' if $sender;
# ------------------------------------------------------------------
# now you know the repo, get its mirroring details
details($repo);
trace( 1, "mirror", "post_git", $repo, "user=$user", "sender=$sender", "mode=$mode", ( $rc{REDIRECTED_PUSH} ? ("redirected") : () ) );
# ------------------------------------------------------------------
# case 1: we're master or slave, normal user pushing to us
if ( $user and not $rc{REDIRECTED_PUSH} ) {
trace( 3, "case 1, user push" );
return if $mode eq 'local';
# slave was eliminated earlier anyway, so that leaves 'master'
# find all slaves and push to each of them
push_to_slaves($repo);
return;
}
# ------------------------------------------------------------------
# case 2: we're slave, master pushing to us
if ( $sender and not $rc{REDIRECTED_PUSH} ) {
trace( 3, "case 2, master push" );
# nothing to do
return;
}
# ------------------------------------------------------------------
# case 3: we're master, slave sending a redirected push to us
if ( $sender and $rc{REDIRECTED_PUSH} ) {
trace( 3, "case 2, slave redirect" );
# find all slaves and push to each of them
push_to_slaves($repo);
return;
}
}
{
my $lastrepo = '';
sub details {
my $repo = shift;
return if $lastrepo eq $repo;
$master = master($repo);
%slaves = slaves($repo);
$mode = mode($repo);
%trusted_slaves = trusted_slaves($repo);
trace( 3, $master, $mode, join( ",", sort keys %slaves ), join( ",", sort keys %trusted_slaves ) );
}
sub master {
return option( +shift, 'mirror.master' );
}
sub slaves {
my $ref = git_config( +shift, "^gitolite-options\\.mirror\\.slaves.*" );
my %out = map { $_ => 1 } map { split } values %$ref;
return %out;
}
sub trusted_slaves {
my $ref = git_config( +shift, "^gitolite-options\\.mirror\\.redirectOK.*" );
# the list of trusted slaves (where we accept redirected pushes from)
# is either explicitly given...
my @out = map { split } values %$ref;
my %out = map { $_ => 1 } @out;
# ...or it's all the slaves mentioned if the list is just a "all"
%out = %slaves if ( @out == 1 and $out[0] eq 'all' );
return %out;
}
sub mode {
my $repo = shift;
return 'local' if not $hn;
return 'master' if $master eq $hn;
return 'slave' if $slaves{$hn};
return 'local' if not $master and not %slaves;
_die "$hn: '$repo' is mirrored but not here";
}
}
sub push_to_slaves {
my $repo = shift;
my $u = $ENV{GL_USER};
delete $ENV{GL_USER}; # why? see src/commands/mirror
for my $s ( sort keys %slaves ) {
system("gitolite mirror push $s $repo &");
}
$ENV{GL_USER} = $u;
}
1;

View file

@ -1,80 +0,0 @@
package Gitolite::Triggers::RefexExpr;
use strict;
use warnings;
# track refexes passed and evaluate expressions on them
# ----------------------------------------------------------------------
# see instructions for use at the bottom of src/VREF/refex-expr
use Gitolite::Easy;
my %passed;
my %rules;
my $init_done = 0;
sub access_2 {
# get out quick for repos that don't have any rules
return if $init_done and not %rules;
# but we don't really know that the first time, heh!
if (not $init_done) {
my $repo = $_[1];
init($repo);
return unless %rules;
}
my $refex = $_[5];
return if $refex =~ /DENIED/;
$passed{$refex}++;
# evaluate the rules each time; it's not very expensive
for my $k (sort keys %rules) {
$ENV{"GL_REFEX_EXPR_" . $k} = eval_rule($rules{$k});
}
}
sub eval_rule {
my $rule = shift;
my $e;
$e = join " ", map { convert($_) } split ' ', $rule;
my $ret = eval $e;
_die "eval '$e' -> '$@'" if $@;
Gitolite::Common::trace(1, "RefexExpr", "'$rule' -> '$e' -> '$ret'");
return "'$rule' -> '$e'" if $ret;
}
my %constant;
%constant = map { $_ => $_ } qw(1 not and or xor + - ==);
$constant{'-lt'} = '<';
$constant{'-gt'} = '>';
$constant{'-eq'} = '==';
$constant{'-le'} = '<=';
$constant{'-ge'} = '>=';
$constant{'-ne'} = '!=';
sub convert {
my $i = shift;
return $i if $i =~ /^-?\d+$/;
return $constant{$i} || $passed{$i} || $passed{"refs/heads/$i"} || 0;
}
# called only once
sub init {
$init_done = 1;
my $repo = shift;
# find all the rule expressions
my %t = config($repo, "^gitolite-options\\.refex-expr\\.");
my ($k, $v);
# get rid of the cruft and store just the rule name as the key
while ( ($k, $v) = each %t) {
$k =~ s/^gitolite-options\.refex-expr\.//;
$rules{$k} = $v;
}
}
1;

View file

@ -1,56 +0,0 @@
package Gitolite::Triggers::RepoUmask;
use Gitolite::Rc;
use Gitolite::Common;
use Gitolite::Conf::Load;
use strict;
use warnings;
# setting a repo specific umask
# ----------------------------------------------------------------------
# this is for people who are too paranoid to trust e.g., gitweb's repo
# exclusion logic, but not paranoid enough to put it on a different server
=for usage
* In the rc file, add 'RepoUmask::pre_git' and 'RepoUmask::post_create' to
the corresponding trigger lists.
* For each repo that is to get a different umask than the default, add a
line like this:
option umask = 0027
=cut
# sadly option/config values are not available at pre_create time for normal
# repos. So we have to do a one-time fixup in a post_create trigger.
sub post_create {
my $repo = $_[1];
my $umask = option($repo, 'umask');
_chdir($rc{GL_REPO_BASE}); # because using option() moves us to ADMIN_BASE!
return unless $umask;
# unlike the one in the rc file, this is a string
$umask = oct($umask);
my $mode = "0" . sprintf("%o", $umask ^ 0777);
system("chmod -R $mode $repo.git >&2");
}
sub pre_git {
my $repo = $_[1];
my $umask = option($repo, 'umask');
_chdir($rc{GL_REPO_BASE}); # because using option() moves us to ADMIN_BASE!
return unless $umask;
# unlike the one in the rc file, this is a string
umask oct($umask);
}
1;

View file

@ -1,67 +0,0 @@
package Gitolite::Triggers::Shell;
# usage notes: this module must be loaded first in the INPUT trigger list. Or
# at least before Mirroring::input anyway.
# documentation is in the ssh troubleshooting and tips document, under the
# section "giving shell access to gitolite users"
use Gitolite::Rc;
use Gitolite::Common;
# fedora likes to do things that are a little off the beaten track, compared
# to typical gitolite usage:
# - every user has their own login
# - the forced command may not get the username as an argument. If it does
# not, the gitolite user name is $USER (the unix user name)
# - and finally, if the first argument to the forced command is '-s', and
# $SSH_ORIGINAL_COMMAND is empty or runs a non-git/gitolite command, then
# the user gets a shell
sub input {
my $shell_allowed = 0;
if ( @ARGV and $ARGV[0] eq '-s' ) {
shift @ARGV;
$shell_allowed++;
}
@ARGV = ( $ENV{USER} ) unless @ARGV;
return unless $shell_allowed;
# now determine if this was intended as a shell command or git/gitolite
# command
my $soc = $ENV{SSH_ORIGINAL_COMMAND};
# no command, just 'ssh alice@host'; doesn't return ('exec's out)
shell_out() if $shell_allowed and not $soc;
return if git_gitolite_command($soc);
gl_log( 'shell', $ENV{SHELL}, "-c", $soc );
exec $ENV{SHELL}, "-c", $soc;
}
sub shell_out {
my $shell = $ENV{SHELL};
$shell =~ s/.*\//-/; # change "/bin/bash" to "-bash"
gl_log( 'shell', $shell );
exec { $ENV{SHELL} } $shell;
}
# some duplication with gitolite-shell, factor it out later, if it works fine
# for fedora and they like it.
sub git_gitolite_command {
my $soc = shift;
my $git_commands = "git-upload-pack|git-receive-pack|git-upload-archive";
return 1 if $soc =~ /^($git_commands) /;
my @words = split ' ', $soc;
return 1 if $rc{COMMANDS}{ $words[0] };
return 0;
}
1;

View file

@ -1,17 +0,0 @@
package Gitolite::Triggers::Writable;
use Gitolite::Rc;
use Gitolite::Common;
sub access_1 {
my ( $repo, $aa, $result ) = @_[ 1, 3, 5 ];
return if $aa eq 'R' or $result =~ /DENIED/;
for my $f ( "$ENV{HOME}/.gitolite.down", "$rc{GL_REPO_BASE}/$repo.git/.gitolite.down" ) {
next unless -f $f;
_die slurp($f) if -s $f;
_die "sorry, writes are currently disabled (no more info available)\n";
}
}
1;

100
src/sshkeys-lint Executable file
View file

@ -0,0 +1,100 @@
#!/usr/bin/perl -w
use strict;
our (%users, %linenos);
&usage unless $ARGV[0] and -f $ARGV[0];
my @authlines = &filelines($ARGV[0]);
my $lineno = 0;
for (@authlines)
{
$lineno++;
if (/^# gitolite start/ .. /^# gitolite end/) {
warn "line $lineno: non-gitolite key found in gitolite section" if /ssh-rsa|ssh-dss/ and not /command=.*gl-auth-command/;
} else {
warn "line $lineno: gitolite key found outside gitolite section" if /command=.*gl-auth-command/;
}
next if /\# gitolite (start|end)/;
die "line $lineno: unrecognised line\n" unless /^(?:command=".*gl-auth-command (\S+?)"\S+ )?(?:ssh-rsa|ssh-dss) (\S+)/;
my ($user, $key) = ($1 || '', $2);
if ($linenos{$key}) {
warn "authkeys file line $lineno is repeat of line $linenos{$key}, will be ignored by server sshd\n";
next;
}
$linenos{$key} = $lineno;
$users{$key} = ($user ? "maps to gitolite user $user" : "gets you a command line");
}
print "\n";
# all *.pub in current dir should be exactly one line, starting with ssh-rsa
# or ssh-dss
my @pubkeys = glob("*.pub");
die "no *.pub files here\n" unless @pubkeys;
for my $pub (@pubkeys) {
my @lines = &filelines($pub);
die "$pub has more than one line\n" if @lines > 1;
die "$pub does not start with ssh-rsa or ssh-dss\n" unless $lines[0] =~ /^(?:ssh-rsa|ssh-dss) (\S+)/;
my $key = $1;
if ($users{$key}) {
print "$pub $users{$key}\n";
} else {
print "$pub has NO ACCESS to the server\n";
}
}
print <<INFO;
Git operations using a pubkey that gets you a command line will BYPASS
gitolite completely. This means:
- using "git clone git\@server:reponame" will get you the "does not appear to
be a git repository" message
- using "git clone git\@server:repositories/reponame" [assuming default value
of \$REPO_BASE) will work but subsequent push will fail
----
Now you know what pubkey gets you what access.
To see what key is *actually* being used when you run your commands, try "ssh
-v git\@server" or "ssh -v gitolite", and look for a line saying "Offering
public key". If there are more than one such lines, the last one is what
counts.
If at any time you are asked for a password (password, not passphrase; see
doc/6 for the difference, if needed), then none of this applies anyway.
INFO
sub filelines
{
my $f;
my $fn = shift;
open ($f, "<", $fn) or die "open $fn failed: $!\n";
return <$f>;
}
sub usage
{
print STDERR <<EOF;
On your *client*:
- copy the server's ~/.ssh/authorized_keys file to your *client*'s
/tmp/foo (maybe using "scp" or whatever)
- cd to the ~/.ssh directory (which contains all the pub keys this client
can use)
- run "$0 /tmp/foo"
Note: people who have so many keypairs they keep them in *sub*-directories of
~/.ssh [you know who you are ;-)] can figure it out themselves; you clearly
know enough about ssh not to need my help!
EOF
exit 1;
}

View file

@ -1,34 +0,0 @@
# vim: syn=perl:
# "sugar script" (syntactic sugar helper) for gitolite3
# Enabling this script in the rc file allows you to use back-slash escaped
# continuation lines, like in C or shell etc.
# This script also serves as an example "sugar script" if you want to write
# your own (and maybe send them to me). A "sugar script" in gitolite will be
# executed via a perl 'do' and is expected to contain one function called
# 'sugar_script'. This function should take a listref and return a listref.
# Each item in the list is one line. There are NO newlines; g3 kills them off
# fairly early in the process.
# If you're not familiar with perl please do not try this. Ask me to write
# you a sugar script instead.
sub sugar_script {
my $lines = shift;
my @out = ();
my $keep = '';
for my $l (@$lines) {
if ( $l =~ s/\\$// ) {
$keep .= $l;
} else {
$l = $keep . $l if $keep;
$keep = '';
push @out, $l;
}
}
return \@out;
}

View file

@ -1,32 +0,0 @@
# vim: syn=perl:
# "sugar script" (syntactic sugar helper) for gitolite3
# Enabling this script in the rc file allows you to use subdirectories in
# keydir as group names. The last component other than keydir itself will be
# taken as the group name.
sub sugar_script {
Gitolite::Common::trace( 2, "running 'keysubdirs-as-groups' sugar script..." );
my $lines = shift;
my @out = @{$lines};
unshift @out, groupnames();
return \@out;
}
sub groupnames {
my @out = ();
my %members = ();
for my $pk (`find ../keydir/ -name "*.pub"`) {
next unless $pk =~ m(.*/([^/]+)/([^/]+?)(?:@[^./]+)?\.pub$);
next if $1 eq 'keydir';
$members{$1} .= " $2";
}
for my $m ( sort keys %members ) {
push @out, "\@$m =" . $members{$m};
}
return @out;
}

View file

@ -1,74 +0,0 @@
# vim: syn=perl:
# "sugar script" (syntactic sugar helper) for gitolite3
# simple line-wise macro processor
# ----------------------------------------------------------------------
# see documentation at the end of this script
my %macro;
sub sugar_script {
my $lines = shift;
my @out = ();
my $l = join("\n", @$lines);
while ($l =~ s/^macro (\w+)\b(.*?)\nend//ms) {
$macro{$1} = $2;
}
$l =~ s/^((\w+)\b.*)/$macro{$2} ? expand($1) : $1/gem;
$lines = [split "\n", $l];
return $lines;
}
sub expand {
my $l = shift;
my ($word, @arg) = split ' ', $l;
my $v = $macro{$word};
$v =~ s/%(\d+)/$arg[$1-1] or die "macro '$word' needs $1 arguments at '$l'\n"/gem;
return $v;
}
__END__
Documentation is mostly by example.
Setup:
* the line
'macros',
should be added to the SYNTACTIC_SUGAR list in ~/.gitolite.rc
Notes on macro definition:
* the keywords 'macro' and 'end' should start on a new line
* the first word after 'macro' is the name of the macro, and the rest, until
the 'end', is the body
Notes on macro use:
* the macro name should be the first word on a line
* the rest of the line is used as arguments to the macro
Example:
if your conf contains:
macro foo repo aa-%1
RW = u1 %2
R = u2
end
foo 1 alice
foo 2 bob
this will effectively turn into
repo aa-1
RW = u1 alice
R = u2
repo aa-2
RW = u1 bob
R = u2

View file

@ -1,17 +0,0 @@
#!/bin/bash
# quick and dirty program to background any of the triggers programs that are
# taking too long. To use, just replace a line like
# 'post-compile/update-gitweb-access-list',
# with
# 'bg post-compile/update-gitweb-access-list',
# We dump output to a file in the log directory but please keep in mind this
# is not a "log" so much as a redirection of the entire output.
echo `date` $GL_TID "$0: $@" >> $GL_LOGFILE.bg
path=${0%/*}
script=$path/$1; shift
( ( $script "$@" < /dev/null >> $GL_LOGFILE.bg 2>&1 & ) )

View file

@ -1,35 +0,0 @@
#!/bin/sh
# this is a wee bit expensive in terms of forks etc., compared to doing it in
# perl, but I wanted to show how *easy* it actually is now. And really,
# you'll only notice if you access this repo like a hundred times a minute or
# something so don't sweat it.
# given a repo and a user, check if option('partialCopyOf') is set, and if so,
# fetch all allowed branches from there.
die() { echo "$@" >&2; exit 1; }
# make sure we're being called from the pre_git trigger
[ "$1" = "PRE_GIT" ] || die I must be called from PRE_GIT, not "$1"
shift
repo=$1
user=$2
main=`git config --file $GL_REPO_BASE/$repo.git/config --get gitolite.partialCopyOf`;
[ -z "$main" ] && exit 0
# "we", "our repo" => the partial copy
# "main", "pco" => the one which we are a "partial copy of"
cd $GL_REPO_BASE/$main.git
for ref in `git for-each-ref refs/heads '--format=%(refname)'`
do
cd $GL_REPO_BASE/$repo.git
gitolite access -q $repo $user R $ref &&
git fetch -f $GL_REPO_BASE/$main.git $ref:$ref
done
exit 0

View file

@ -1,146 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use File::Temp qw(tempfile);
use Getopt::Long;
use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;
$|++;
# best called via 'gitolite trigger POST_COMPILE'; other modes at your own
# risk, especially if the rc file specifies arguments for it. (That is also
# why it doesn't respond to "-h" like most gitolite commands do).
# option procesing
# ----------------------------------------------------------------------
# currently has one option:
# -kfn, --key-file-name adds the keyfilename as a second argument
my $kfn = '';
GetOptions( 'key-file-name|kfn' => \$kfn, );
tsh_try("sestatus");
my $selinux = ( tsh_text() =~ /enabled/ );
my $ab = $rc{GL_ADMIN_BASE};
trace( 2, "'keydir' not found in '$ab'; exiting" ), exit if not -d "$ab/keydir";
my $akdir = "$ENV{HOME}/.ssh";
my $akfile = "$ENV{HOME}/.ssh/authorized_keys";
my $glshell = $rc{GL_BINDIR} . "/gitolite-shell";
my $auth_options = auth_options();
sanity();
# ----------------------------------------------------------------------
_chdir($ab);
# old data
my $old_ak = slurp($akfile);
my @non_gl = grep { not /^# gito.*start/ .. /^# gito.*end/ } slurp($akfile);
chomp(@non_gl);
my %seen = map { $_ => 'a non-gitolite key' } ( fp(@non_gl) );
# pubkey files
chomp( my @pubkeys = `find keydir/ -type f -name "*.pub" | sort` );
my @gl_keys = ();
for my $f (@pubkeys) {
my $fp = fp($f);
if ( $seen{$fp} ) {
_warn "$f duplicates $seen{$fp}, sshd will ignore it";
} else {
$seen{$fp} = $f;
}
push @gl_keys, grep { /./ } optionise($f);
}
# dump it out
if (@gl_keys) {
my $out = join( "\n", @non_gl, "# gitolite start", @gl_keys, "# gitolite end" ) . "\n";
my $ak = slurp($akfile);
_die "'$akfile' changed between start and end of this program!" if $ak ne $old_ak;
_print( $akfile, $out );
}
# ----------------------------------------------------------------------
sub sanity {
_die "'$glshell' not found; this should NOT happen..." if not -f $glshell;
_die "'$glshell' found but not readable; this should NOT happen..." if not -r $glshell;
_die "'$glshell' found but not executable; this should NOT happen..." if not -x $glshell;
_warn "$akdir missing; creating a new one" if not -d $akdir;
_warn "$akfile missing; creating a new one" if not -f $akfile;
_mkdir( $akdir, 0700 ) if not -d $akfile;
if ( not -f $akfile ) {
_print( $akfile, "" );
chmod 0700, $akfile;
}
}
sub auth_options {
my $auth_options = $rc{AUTH_OPTIONS};
$auth_options ||= "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty";
return $auth_options;
}
sub fp {
# input: see below
# output: a (list of) FPs
my $in = shift || '';
if ( $in =~ /\.pub$/ ) {
# single pubkey file
_die "bad pubkey file '$in'" unless $in =~ $REPONAME_PATT;
return fp_file($in);
} elsif ( -f $in ) {
# an authkeys file
return map { fp_line($_) } grep { !/^#/ and /\S/ } slurp($in);
} else {
# one or more actual keys
return map { fp_line($_) } grep { !/^#/ and /\S/ } ( $in, @_ );
}
}
sub fp_file {
return $selinux++ if $selinux; # return a unique "fingerprint" to prevent noise
my $f = shift;
my $fp = `ssh-keygen -l -f '$f'`;
chomp($fp);
_die "fingerprinting failed for '$f'" unless $fp =~ /([0-9a-f][0-9a-f](:[0-9a-f][0-9a-f])+)/;
$fp = $1;
return $fp;
}
sub fp_line {
my ( $fh, $fn ) = tempfile();
print $fh shift;
close $fh;
my $fp = fp_file($fn);
unlink $fn;
return $fp;
}
sub optionise {
my $f = shift;
my $user = $f;
$user =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub
$user =~ s/(\@[^.]+)?\.pub$//; # baz.pub, baz@home.pub -> baz
my @line = slurp($f);
if ( @line != 1 ) {
_warn "$f does not contain exactly 1 line; ignoring";
return '';
}
chomp(@line);
return "command=\"$glshell $user" . ( $kfn ? " $f" : "" ) . "\",$auth_options $line[0]";
}

Some files were not shown because too many files have changed in this diff Show more