#!/usr/bin/perl # terminology: # native repo: a repo for which we are the master; pushes happen here # authkeys: shorthand for ~/.ssh/authorized_keys # this is invoked in one of two ways: # (1) locally, from a shell script or command line # (2) from a remote server, via authkeys, with one argument (the name of the # sending server), similar to what happens with normal users and the # 'gl-auth-command' program. SSH_ORIGINAL_COMMAND will then contain the # actual command that the remote sent. # # Currently, these commands are (a) 'info', (b) 'git-receive-pack' when a # mirror push is *received* by a slave, (c) 'request-push' sent by a slave # (possibly via an ADC) when the slave finds itself out of sync, (d) a # redirected push, from a user pushing to a slave, which is represented not by # a command per se but by starting with "USER=..." use strict; use warnings; # ---------------------------------------------------------------------------- # this section of code snarfed from gl-auth-command BEGIN { $0 =~ m|^(/)?(.*)/| and $ENV{GL_BINDIR} = ($1 || "$ENV{PWD}/") . $2; } use lib $ENV{GL_BINDIR}; use gitolite_rc; use gitolite_env; use gitolite; setup_environment(); die "fatal: GL_HOSTNAME not set in rc; mirroring disabled\n" unless $GL_HOSTNAME; # ---------------------------------------------------------------------------- die "please read the gitolite mirroring documentation; this program is too critical for you to just run it based on a 'usage' message.\n" if not @ARGV or $ARGV[0] eq '-h'; # ---------------------------------------------------------------------------- # deal with local invocations first # on the "master", run from a shell, for one specific repo, with an optional # list of slaves, like so: # gl-mirror-shell request-push some-repo [optional list of slaves/keys] if ( ($ARGV[0] || '') eq 'request-push' and not $ENV{SSH_ORIGINAL_COMMAND} ) { shift; my $repo = shift or die "fatal: missing reponame\n"; -d "$REPO_BASE/$repo.git" or die "fatal: no such repo?\n"; # this is the default argument if no slave list or key is supplied @ARGV = ('gitolite.mirror.slaves') unless @ARGV; my @slaves = (); my %seen = (); # each argument in @ARGV is either a slave name, or a gitolite mirroring # key to be replaced with its value, split into a list of slaves while (@ARGV) { $a = shift @ARGV; if ($a =~ /^gitolite\.mirror\.[\w.-]+$/) { my @values = split(' ', `git config --file $REPO_BASE/$repo.git/config --get $a` || ''); unshift @ARGV, @values; } else { push @slaves, $a unless $seen{$a}++; } } exit 1 unless @slaves; # we don't want to complain louder than that because the most common # use of this script on the master server is via cron, run against # *all* known repos without checking their individual key values print STDERR "info: mirror-push $repo ", join(" ", @slaves), "\n"; system("gl-mirror-push", $repo, @slaves); exit 0; } # ---------- # now the remote invocations; log it, then get the sender name my $sender = shift; $ENV{GL_USER} ||= "host:$sender"; # default SSH_ORIGINAL_COMMAND is 'info', as usual $ENV{SSH_ORIGINAL_COMMAND} ||= 'info'; # and it's too long to bloody type... my $soc = $ENV{SSH_ORIGINAL_COMMAND}; log_it(); # ---------- # our famous 'info' command if ($soc eq 'info') { print STDERR "Hello $sender, I am $GL_HOSTNAME\n"; exit; } # ---------- # when running on the "slave", we have to "receive" the `git push --mirror` # from a master. Check that the repo is indeed a slave and the sender is the # correct master before allowing the push. if ($soc =~ /^git-receive-pack '(\S+)'$/) { my $repo = $1; die "fatal: invalid characters in $repo\n" unless $repo =~ $REPONAME_PATT; my $mm = mirror_mode($repo); # reminder: we're not going through the slave-side gl-auth-command. This # is a server-to-server transaction, with an authenticated sender. # Authorisation consists of checking to make sure our config says this # sender is indeed the master for this repo die "$ABRT fatal: $GL_HOSTNAME <==//== $sender mirror-push rejected: $repo is $mm\n" unless $mm eq "slave of $sender"; print STDERR "$GL_HOSTNAME <=== ($repo) ==== $sender\n"; $ENV{GL_BYPASS_UPDATE_HOOK} = 1; $ENV{GL_REPO} = $repo; # replace the repo path with the full path and hand off to git-shell $soc =~ s(')('$ENV{GL_REPO_BASE_ABS}/); exec("git", "shell", "-c", $soc); } # ---------- # a slave may have found itself out of sync (perhaps the network was down at # the time of the last push to the master), and now wants to request a sync. # This is similar to the "local invocation" described above, but we check the # sender name against gitolite.mirror.slaves to prevent some random slave from # asking for a repo it should not be having! if ($soc =~ /^request-push (\S+)$/) { my $repo = $1; die "fatal: invalid characters in $repo\n" unless $repo =~ $REPONAME_PATT; die "$ABRT fatal: $GL_HOSTNAME ==//==> $sender refused: not in slave list\n" unless mirror_listslaves($repo) =~ /(^|\s)$sender(\s|$)/; print STDERR "$GL_HOSTNAME ==== ($repo) ===> $sender\n"; # just one sender, and we've checked that he is "on the list". Foreground... system("$ENV{GL_BINDIR}/gl-mirror-push", $repo, "-fg", $sender); exit; } # ---------- # experimental feature... # when running on the "master", receive a redirected push from a slave. This # is disabled by default and needs to be explicitly enabled on both the master # and the slave. SEE DOCUMENTATION FOR CAVEATS AND CAUTIONS. if ($soc =~ /^USER=(\S+) SOC=(git-receive-pack '(\S+)')$/) { my $user = $1; $ENV{SSH_ORIGINAL_COMMAND} = $2; my $repo = $3; die "fatal: invalid characters in $user\n" unless $user =~ $USERNAME_PATT; die "fatal: invalid characters in $repo\n" unless $repo =~ $REPONAME_PATT; die "$ABRT fatal: $GL_HOSTNAME <==//== $sender redirected push rejected\n" unless mirror_redirectOK($repo, $sender); print STDERR "$GL_HOSTNAME <=== $user ($repo) ==== $sender\n"; my $pgm = $0; $pgm =~ s([^/]+$)(gl-auth-command); exec($pgm, $user); }