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;