diff options
Diffstat (limited to 'src/lib/Gitolite/Conf')
-rw-r--r-- | src/lib/Gitolite/Conf/Explode.pm | 118 | ||||
-rw-r--r-- | src/lib/Gitolite/Conf/Load.pm | 704 | ||||
-rw-r--r-- | src/lib/Gitolite/Conf/Store.pm | 411 | ||||
-rw-r--r-- | src/lib/Gitolite/Conf/Sugar.pm | 202 |
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; + |