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 @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;