summaryrefslogtreecommitdiffstats
path: root/src/lib/Gitolite/Conf
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Gitolite/Conf')
-rw-r--r--src/lib/Gitolite/Conf/Explode.pm118
-rw-r--r--src/lib/Gitolite/Conf/Load.pm704
-rw-r--r--src/lib/Gitolite/Conf/Store.pm411
-rw-r--r--src/lib/Gitolite/Conf/Sugar.pm202
4 files changed, 1435 insertions, 0 deletions
diff --git a/src/lib/Gitolite/Conf/Explode.pm b/src/lib/Gitolite/Conf/Explode.pm
new file mode 100644
index 0000000..cf89620
--- /dev/null
+++ b/src/lib/Gitolite/Conf/Explode.pm
@@ -0,0 +1,118 @@
+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}, "# $file $.";
+ 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( 3, $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( 3, "$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;
+
diff --git a/src/lib/Gitolite/Conf/Load.pm b/src/lib/Gitolite/Conf/Load.pm
new file mode 100644
index 0000000..7dea259
--- /dev/null
+++ b/src/lib/Gitolite/Conf/Load.pm
@@ -0,0 +1,704 @@
+package Gitolite::Conf::Load;
+
+# load conf data from stored files
+# ----------------------------------------------------------------------
+
+@EXPORT = qw(
+ load
+
+ access
+ git_config
+ env_options
+
+ option
+ repo_missing
+ creator
+
+ vrefs
+ lister_dispatch
+);
+
+use Exporter 'import';
+use Cwd;
+
+use Gitolite::Rc;
+use Gitolite::Common;
+
+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 ) = @_;
+ trace( 2, $repo, $user, $aa, $ref );
+ _die "invalid user '$user'" if not( $user and $user =~ $USERNAME_PATT );
+ sanity($repo);
+ return "$aa any $repo $user DENIED by fallthru" unless update_hook_present($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 =~ m(^VREF/NAME/) or $ref =~ $REF_OR_FILENAME_PATT;
+ # apparently we can't always force sanity; at least what we *return*
+ # should be sane/safe. This pattern is based on REF_OR_FILENAME_PATT.
+ ( my $safe_ref = $ref ) =~ s([^-0-9a-zA-Z._\@/+ :,])(.)g;
+ trace( 3, "safe_ref", $safe_ref ) if $ref ne $safe_ref;
+
+ # 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 $safe_ref $repo $user DENIED by existence";
+ }
+
+ trace( 3, scalar(@rules) . " rules found" );
+
+ $rc{RULE_TRACE} = '';
+ for my $r (@rules) {
+ $rc{RULE_TRACE} .= " " . $r->[0] . " ";
+
+ my $perm = $r->[1];
+ my $refex = $r->[2]; $refex =~ s(/USER/)(/$user/);
+ trace( 3, "perm=$perm, refex=$refex" );
+
+ $rc{RULE_TRACE} .= "d";
+ # skip 'deny' rules if the ref is not (yet) known
+ next if $perm eq '-' and $ref eq 'any' and not $deny_rules;
+
+ $rc{RULE_TRACE} .= "r";
+ # rule matches if ref matches or ref is any (see gitolite-shell)
+ next unless $ref =~ /^$refex/ or $ref eq 'any';
+
+ $rc{RULE_TRACE} .= "D";
+ trace( 2, "DENIED by $refex" ) if $perm eq '-';
+ return "$aa $safe_ref $repo $user DENIED by $refex" if $perm eq '-';
+
+ # For repo creation, perm will be C and aa will be "^C". For branch
+ # access, $perm can be RW\+?(C|D|CD|DC)?M?, and $aa can be W, +, C or
+ # D, or any of these followed by "M".
+
+ # We need to turn $aa into a regex that can match a suitable $perm.
+ # This is trivially true for "^C", "W" and "D", but the others (+, C,
+ # M) need some tweaking.
+
+ # first, quote the '+':
+ ( my $aaq = $aa ) =~ s/\+/\\+/;
+ # if aa is just "C", the user is trying to create a *branch* (not a
+ # *repo*), so let's make the pattern clearer to reflect that.
+ $aaq = "RW.*C" if $aaq eq "C";
+ # if the aa is, say "WM", make this "W.*M" because the perm could be
+ # 'RW+M', 'RW+CDM' etc, and they are all valid:
+ $aaq =~ s/M/.*M/;
+
+ $rc{RULE_TRACE} .= "A";
+
+ # as far as *this* ref is concerned we're ok
+ return $refex if ( $perm =~ /$aaq/ );
+
+ $rc{RULE_TRACE} .= "p";
+ }
+ $rc{RULE_TRACE} .= " F";
+
+ trace( 2, "DENIED by fallthru" );
+ return "$aa $safe_ref $repo $user DENIED by fallthru";
+}
+
+# cache control
+if ($rc{CACHE}) {
+ require Gitolite::Cache;
+ Gitolite::Cache::cache_wrap('Gitolite::Conf::Load::access');
+}
+
+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;
+ }
+
+ map { trace( 3, "$_", "$ret{$_}" ) } ( sort keys %ret ) if $ENV{D};
+ return \%ret;
+}
+
+sub env_options {
+ return unless -f "$rc{GL_ADMIN_BASE}/conf/gitolite.conf-compiled.pm";
+ # prevent catch-22 during initial install
+
+ my $cwd = getcwd();
+
+ my $repo = shift;
+ map { delete $ENV{$_} } grep { /^GL_OPTION_/ } keys %ENV;
+ my $h = git_config( $repo, '^gitolite-options.ENV\.' );
+ while ( my ( $k, $v ) = each %$h ) {
+ next unless $k =~ /^gitolite-options.ENV\.(\w+)$/;
+ $ENV{ "GL_OPTION_" . $1 } = $v;
+ }
+
+ chdir($cwd);
+}
+
+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, $patt) = @_;
+ $patt ||= $REPOPATT_PATT;
+
+ _die "invalid repo '$repo'" if not( $repo and $repo =~ $patt );
+ _die "'$repo' ends with a '/'" if $repo =~ m(/$);
+ _die "'$repo' contains '..'" if $repo =~ $REPONAME_PATT and $repo =~ m(\.\.);
+ _die "'$repo' contains '.git/'" if $repo =~ $REPONAME_PATT and $repo =~ m(\.git/);
+ _die "'$repo' ends with '.git'" if $repo =~ m(\.git$);
+}
+
+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} and not $rc{ALLOW_ORPHAN_GL_CONF};
+
+ 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, $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;
+ }
+ }
+
+ # add in any group names explicitly given in (GIT_DIR)/gl-repo-groups
+ push @ret,
+ map { s/^\@?/\@/; $_ }
+ grep { ! /[^\w@-]/ }
+ split (' ', slurp("$ENV{GL_REPO_BASE}/$base.git/gl-repo-groups"))
+ if -f "$ENV{GL_REPO_BASE}/$base.git/gl-repo-groups";
+ }
+
+ push @ret, @{ $groups{$base} } if exists $groups{$base};
+ push @ret, @{ $groups{$base2} } if $base2 and exists $groups{$base2};
+ if ($type eq 'repo') {
+ # regexes can only be used for repos, not for users
+ 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.
+
+- you will have to run 'list-members' on each group name to expand it -- for
+ details and caveats on that please see its help message.
+- User names not mentioned in an access rule will not show up at all (for
+ example, if you have users who only have access via an '@all' rule).
+
+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.
+
+NOTE: If you're running in ssh mode, it may be simpler to parse the authorized
+keys file in ~/.ssh, like so:
+ perl -lne '/ ([a-z0-9]+)"/; print $1 if $1' < ~/.ssh/authorized_keys | sort -u
+If you're running in http mode, only your web server knows all the potential
+user names.
+=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 -u|-r <name>
+
+List all groups a name is a member of. One of the flags '-u' or '-r' is
+mandatory, to specify if the name is a user or a repo.
+
+For users, the output includes the result from GROUPLIST_PGM, if it is
+defined. For repos, the output includes any repo patterns that the repo name
+matches, as well as any groups that contain those patterns.
+=cut
+
+sub list_memberships {
+ require Getopt::Long;
+
+ my ( $user, $repo, $help );
+
+ Getopt::Long::GetOptionsFromArray(
+ \@_,
+ 'user|u=s' => \$user,
+ 'repo|r=s' => \$repo,
+ 'help|h' => \$help,
+ );
+ usage() if $help or ( not $user and not $repo );
+
+ load_common();
+ my @m;
+
+ if ( $user and $repo ) {
+ # unsupported/undocumented except via "in_role()" in Easy.pm
+ @m = memberships( 'user', $user, $repo );
+ } elsif ($user) {
+ @m = memberships( 'user', $user );
+ } elsif ($repo) {
+ @m = memberships( 'repo', $repo );
+ }
+
+ @m = grep { $_ ne '@all' and $_ ne ( $user || $repo ) } @m;
+ return ( sort_u( \@m ) );
+}
+
+=for list_members
+Usage: gitolite list-members <group name>
+
+ - list all members of a group
+ - takes one group name
+
+'@all' is not expandable in this context. Also, if you have GROUPLIST_PGM set
+in your rc file[1], gitolite cannot expand group names completely; only your
+external database can.
+
+[1]: http://gitolite.com/gitolite/conf.html#ldap
+
+=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;
+
diff --git a/src/lib/Gitolite/Conf/Store.pm b/src/lib/Gitolite/Conf/Store.pm
new file mode 100644
index 0000000..8757c89
--- /dev/null
+++ b/src/lib/Gitolite/Conf/Store.pm
@@ -0,0 +1,411 @@
+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::Rc;
+use Gitolite::Common;
+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 {
+ my @in = @_;
+ @repolist = ();
+ # ...sanity checks
+ while (@in) {
+ $_ = shift @in;
+ if ( check_subconf_repo_disallowed( $subconf, $_ ) ) {
+ if ( exists $groups{$_} ) {
+ # groupname disallowed; try individual members now
+ ( my $g = $_ ) =~ s/^\@$subconf\./\@/;
+ _warn "expanding '$g'; this *may* slow down compilation";
+ unshift @in, keys %{ $groups{$_} };
+ next;
+ }
+ $ignored{$subconf}{$_} = 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, $fname, $lnum ) = @_;
+ _warn "doesn't make sense to supply a ref ('$ref') for 'R' rule"
+ if $perm eq 'R' and $ref ne 'refs/.*';
+ _warn "possible undeclared group '$user'"
+ if $user =~ /^@/
+ and not $groups{$user}
+ and not $rc{GROUPLIST_PGM}
+ and not special_group($user);
+ _die "bad ref '$ref'" unless $ref =~ $REPOPATT_PATT;
+ _die "bad user '$user'" unless $user =~ $USERNAME_PATT;
+
+ $nextseq++;
+ store_rule_info( $nextseq, $fname, $lnum );
+ for my $repo (@repolist) {
+ push @{ $repos{$repo}{$user} }, [ $nextseq, $perm, $ref ];
+ }
+
+ sub special_group {
+ # ok perl doesn't really have lexical subs (at least not the older
+ # perls I want to support) but let's pretend...
+ my $g = shift;
+ $g =~ s/^\@//;
+ return 1 if $g eq 'all' or $g eq 'CREATOR';
+ return 1 if $rc{ROLES}{$g};
+ return 0;
+ }
+
+}
+
+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, sort keys %configs );
+ # 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; if it exists, all is well
+ next if -f "$repo.git/gl-conf";
+
+ if (-d "$repo.git") {
+ # directory exists but sentinel missing? Maybe a freshly imported repo?
+ hook_1($repo);
+ } else {
+ 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 );
+ 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} );
+ my $phy_repos = list_phy_repos(1);
+
+ for my $repo ( @{$phy_repos} ) {
+ hook_1($repo);
+ }
+}
+
+sub store {
+ trace(3);
+
+ # first write out the ones for the physical repos
+ _chdir( $rc{GL_REPO_BASE} );
+
+ # list of repos (union of keys of %repos plus %configs)
+ my %kr_kc;
+ @kr_kc{ keys %repos } = ();
+ @kr_kc{ keys %configs } = ();
+ for my $repo ( keys %kr_kc ) {
+ 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} } );
+ }
+
+ close_rule_info();
+}
+
+# ----------------------------------------------------------------------
+
+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 -d "$repo.git";
+
+ my ( %one_repo, %one_config );
+
+ 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( "$repo.git/gl-conf", $dumped_data );
+
+ $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' ]);
+}
+
+{
+ my $ri_fh = '';
+
+ sub store_rule_info {
+ $ri_fh = _open( ">", $rc{GL_ADMIN_BASE} . "/conf/rule_info" ) unless $ri_fh;
+ # $nextseq, $fname, $lnum
+ print $ri_fh join( "\t", @_ ) . "\n";
+ }
+
+ sub close_rule_info {
+ close $ri_fh or die "close rule_info file failed: $!";
+ }
+}
+
+1;
+
diff --git a/src/lib/Gitolite/Conf/Sugar.pm b/src/lib/Gitolite/Conf/Sugar.pm
new file mode 100644
index 0000000..5c743d3
--- /dev/null
+++ b/src/lib/Gitolite/Conf/Sugar.pm
@@ -0,0 +1,202 @@
+# 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);
+ $lines = skip_block($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) {
+ $line =~ s/option mirror\.slaves/option mirror.copies/;
+ 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...
+
+ 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";
+ } 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;
+}
+
+sub skip_block {
+ my $lines = shift;
+
+ my @out = ();
+ for (@$lines) {
+ my $skip = 0;
+ $skip = 1 if /^= *begin testconf$/;
+ $skip = 1 if /^= *begin template-data$/;
+ # add code for other types of blocks here as needed
+
+ next if $skip .. /^= *end$/;
+ push @out, $_;
+ }
+
+ return \@out;
+}
+
+1;
+