gitolite/src/gl-mirror-shell
2012-02-29 06:45:31 +05:30

171 lines
6.1 KiB
Perl
Executable file

#!/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);
}