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

# ----------------------------------------------------------------------------

# 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;
}

unless (@ARGV) { print STDERR "fatal: missing command\n"; exit 1; }

# ----------

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