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}; # ---------------------------------------------------------------------- sub input { return unless $ARGV[0] =~ /^server-(\S+)$/; # note: we treat %rc as our own internal "poor man's %ENV" $rc{FROM_SERVER} = $1; trace(3, "from_server: $1"); 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; } } # ---------------------------------------------------------------------- my ($mode, $master, %slaves, %trusted_slaves); 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;