471 lines
15 KiB
Perl
Executable file
471 lines
15 KiB
Perl
Executable file
#!/usr/bin/perl -w
|
|
|
|
# SECURITY: look for the word SECURITY below and decide...
|
|
|
|
# handle pull-requests and related stuff
|
|
|
|
# developer notes:
|
|
# - 'requestor' is too long, so I use "bob"; if you see the documentation
|
|
# you'll realise this isn't as crazy as you think :-)
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
die "ENV GL_RC not set\n" unless $ENV{GL_RC};
|
|
die "ENV GL_BINDIR not set\n" unless $ENV{GL_BINDIR};
|
|
|
|
sub usage {
|
|
print STDERR <<'EOF';
|
|
GENERAL USAGE: ssh git@server hub <hub-command> <args>
|
|
|
|
See docs for concepts; this usage message is only a refresher!
|
|
|
|
Requestor's commands (repo child):
|
|
request-pull child b1 [parent]
|
|
request-status child [parent] [request-number]
|
|
Parent repo owner's commands (repo parent):
|
|
list-requests parent
|
|
view-request parent request-number
|
|
view-log parent request-number <git log options>
|
|
view-diff parent request-number <git diff options>
|
|
reject parent request-number
|
|
fetch parent request-number
|
|
accept parent request-number
|
|
|
|
EOF
|
|
exit 1;
|
|
}
|
|
|
|
our $tempdir;
|
|
END {
|
|
wrap_chdir($ENV{GL_REPO_BASE_ABS});
|
|
system("rm", "-rf", "$tempdir.git") if $tempdir and $tempdir =~ /gl-internal-temp-repo/;
|
|
}
|
|
|
|
my %dispatch = (
|
|
rp => \&rp,
|
|
'request-pull' => \&rp,
|
|
rs => \&rs,
|
|
'request-status' => \&rs,
|
|
lr => \&lr,
|
|
'list-requests' => \&lr,
|
|
vr => \&vr,
|
|
'view-request' => \&vr,
|
|
vl => \&vl,
|
|
'view-log' => \&vl,
|
|
vd => \&vd,
|
|
'view-diff' => \&vd,
|
|
reject => \&reject,
|
|
fetch => \&fetch,
|
|
accept => \&accept,
|
|
);
|
|
|
|
my $cmd = shift || '';
|
|
usage() unless ($dispatch{$cmd});
|
|
|
|
unshift @INC, $ENV{GL_BINDIR};
|
|
require gitolite or die "parse gitolite.pm failed\n";
|
|
gitolite->import;
|
|
|
|
# find what is effectively GL_ADC_PATH, then get the config var we're interested in
|
|
use FindBin;
|
|
my $BASE_FETCH_URL = `. $FindBin::Bin/adc.common-functions; echo \$BASE_FETCH_URL`;
|
|
chomp($BASE_FETCH_URL);
|
|
my $GL_FORKED_FROM = `. $FindBin::Bin/adc.common-functions; echo \$GL_FORKED_FROM`;
|
|
chomp($GL_FORKED_FROM);
|
|
|
|
my @args = @ARGV; @ARGV = ();
|
|
$dispatch{$cmd}->(@args);
|
|
|
|
# -------------------- bob's commands
|
|
|
|
sub rp {
|
|
# request-pull child b1 [parent]
|
|
usage() unless @_ == 2 or @_ == 3;
|
|
|
|
# implicitly gives owner-parent read access to part of child, so requestor
|
|
# should already have read access to child (to prevent someone gaining
|
|
# access to child by faking a pull request against it!)
|
|
# XXX would it be better to ensure it is writable by Bob, because how/why
|
|
# would he make a pull request if he didn't just write to it?
|
|
my ($repo, $creator) = readable_repo(shift);
|
|
my $ref = valid_ref($repo, shift);
|
|
|
|
# the parent is either explicitly given, or the name of the parent
|
|
# recorded by the 'fork' ADC is used
|
|
my $repo_to = shift || parent_repo($repo);
|
|
# requestor need not have any access to parent; it is quite possible he
|
|
# gets this via git-daemon or something, so we just need to make sure it's
|
|
# a valid repo
|
|
$repo_to = valid_repo($repo_to);
|
|
|
|
# the 'cover letter' message comes from STDIN
|
|
my $cover = join("", <>);
|
|
|
|
# now create/update the pull request file
|
|
cd2repo($repo_to);
|
|
my %hub = get_hub();
|
|
$hub{$repo}{$ref}{BOB} = $ENV{GL_USER};
|
|
$hub{$repo}{$ref}{COVER} = $cover;
|
|
$hub{$repo}{$ref}{TIME} = time();
|
|
$hub{$repo}{$ref}{STATUS} = 'pending';
|
|
dump_hub(%hub);
|
|
}
|
|
|
|
sub rs {
|
|
# request-status child [parent] [request-number]
|
|
usage() unless @_ > 0 and @_ < 4; # 1 or 2 or 3
|
|
# same checks as in 'rp' above
|
|
my ($repo_from, $creator) = readable_repo(shift);
|
|
|
|
my $repo;
|
|
if ($_[0] and $_[0] !~ /^\d+$/) {
|
|
# next arg is not a number, so it should be 'parent'
|
|
$repo = shift;
|
|
} else {
|
|
$repo = parent_repo($repo_from);
|
|
}
|
|
$repo = valid_repo($repo);
|
|
|
|
my $rqno = 0;
|
|
$rqno = shift if ($_[0] and $_[0] =~ /^\d+$/);
|
|
|
|
# there should not be any arguments left over
|
|
usage() if @_;
|
|
|
|
unless ($rqno) {
|
|
cd2repo($repo);
|
|
my %hub_full = get_hub();
|
|
return unless $hub_full{$repo_from};
|
|
my %hub; $hub{$repo_from} = $hub_full{$repo_from};
|
|
|
|
list_hub('', %hub);
|
|
|
|
return;
|
|
}
|
|
|
|
my ($child, $ref, %hub) = get_request_N($repo, $rqno);
|
|
# this also does a chdir to $repo, by the way
|
|
|
|
my %hub1; $hub1{$child}{$ref} = $hub{$child}{$ref};
|
|
list_hub('', %hub1);
|
|
print "\nMessage:\n$hub1{$child}{$ref}{COVER}\n";
|
|
}
|
|
|
|
# -------------------- alice's commands
|
|
|
|
sub lr {
|
|
# list-requests parent [optional search strings]
|
|
usage() unless @_ >= 1;
|
|
|
|
# Alice must have write access to parent, otherwise she can't really
|
|
# accept a pull request eventually right?
|
|
my ($repo, $creator) = writable_repo(shift);
|
|
cd2repo($repo);
|
|
my %hub = get_hub();
|
|
return unless %hub;
|
|
|
|
# create the search pattern. ADC arg checking is very strict; it doesn't
|
|
# allow &, | etc., so we just generate an OR condition out of the pieces
|
|
my $patt = join("|", @_);
|
|
list_hub($patt, %hub);
|
|
}
|
|
|
|
sub vr {
|
|
# view-request parent request-number
|
|
usage() unless @_ == 2;
|
|
my ($repo, $n) = @_;
|
|
my ($child, $ref, %hub) = get_request_N($repo, $n);
|
|
# this also does a chdir to $repo, by the way
|
|
|
|
my %hub1; $hub1{$child}{$ref} = $hub{$child}{$ref};
|
|
list_hub('', %hub1);
|
|
print "\nMessage:\n$hub1{$child}{$ref}{COVER}\n";
|
|
}
|
|
|
|
sub vl {
|
|
# view-log parent request-number <git log options>
|
|
usage() unless @_ >= 2;
|
|
|
|
my ($repo, $n) = (shift, shift);
|
|
my ($child, $ref, %hub) = get_request_N($repo, $n);
|
|
|
|
# so now we can find the set of SHAs that we already have
|
|
# XXX should we include tags also?
|
|
my @known_shas = grep { chomp; } `git for-each-ref refs/heads --format='%(objectname)'`;
|
|
|
|
# make a copy of the child repo (Bob's repo) containing only the ref being
|
|
# offered for fetch, then cd to it. This is easier to do than to sanitise
|
|
# all possible git-log arguments. We're doing this to prevent Alice from
|
|
# seeing anything more than the ref offered.
|
|
temp_clone($child, $ref);
|
|
|
|
# verify the list of "known_shas" because what's known in Alice's repo may
|
|
# not be known here. While you're about it, negate them. (We don't want
|
|
# to use "--not" because we're not sure what arguments the user will want
|
|
# to add and we don't want to negate some of them by mistake
|
|
@known_shas = grep { $_ = `git rev-parse --verify -q $_`; chomp && s/^/^/ } @known_shas;
|
|
|
|
# run the log command
|
|
# XXX SECURITY XXX do we need to check these arguments? Don't forget they
|
|
# are restricted by $ADC_CMD_ARGS_PATT (defined in gitolite_rc.pm), which
|
|
# is pretty tight to start with, so we know this cannot be used to run
|
|
# external programs. The question is, are any of git-log's arguments
|
|
# dangerous in their own right?
|
|
my @args = ('git', 'log', $ref);
|
|
push @args, @known_shas if @known_shas;
|
|
check_SHAs($ref, @_);
|
|
# each SHA in @_ must be a parent of $ref. Non-shas are not allowed
|
|
# since all refs other than $ref have been deleted in the temp clone
|
|
push @args, @_ if @_;
|
|
system @args;
|
|
}
|
|
|
|
sub vd {
|
|
# view-diff parent request-number <git diff options>
|
|
usage() unless @_ >= 4;
|
|
# we just check for 4 arguments; I guess later on we could also check
|
|
# to make sure at least 2 of them are SHAs or something but unless
|
|
# there's a security risk it's not needed
|
|
|
|
my ($repo, $n) = (shift, shift);
|
|
my ($child, $ref, %hub) = get_request_N($repo, $n);
|
|
# this also does a chdir to $repo, by the way
|
|
|
|
# now go to the child repo (Bob's repo)
|
|
temp_clone($child, $ref);
|
|
|
|
# run the diff command
|
|
# XXX SECURITY XXX do we need to check these arguments? Don't forget they
|
|
# are restricted by $ADC_CMD_ARGS_PATT (defined in gitolite_rc.pm), which
|
|
# is pretty tight to start with, so we know this cannot be used to run
|
|
# external programs. The question is, are any of git-diff's arguments
|
|
# dangerous in their own right?
|
|
my @args = ('git', 'diff');
|
|
check_SHAs($ref, @_);
|
|
push @args, @_ if @_;
|
|
system @args;
|
|
}
|
|
|
|
sub reject {
|
|
# reject parent request-number
|
|
usage() unless @_ == 2;
|
|
my ($repo, $n) = @_;
|
|
writable_repo($repo); # yeah we're throwing away the return values
|
|
my ($child, $ref, %hub) = get_request_N($repo, $n);
|
|
|
|
map { die "request status is already '$_'\n" if $_ ne 'pending' } $hub{$child}{$ref}{STATUS};
|
|
|
|
# the 'cover letter' message comes from STDIN
|
|
my $cover = join("", <>);
|
|
$hub{$child}{$ref}{STATUS} = "rejected by $ENV{GL_USER}";
|
|
$hub{$child}{$ref}{COVER} .= "\n\nRejected. Message to requestor:\n$cover";
|
|
dump_hub(%hub);
|
|
}
|
|
|
|
sub fetch {
|
|
# fetch parent request-number
|
|
usage() unless @_ == 2;
|
|
my ($repo, $n) = @_;
|
|
writable_repo($repo); # yeah we're throwing away the return values
|
|
my ($child, $ref, %hub) = get_request_N($repo, $n);
|
|
|
|
map { die "request status is already '$_'\n" if $_ ne 'pending' } $hub{$child}{$ref}{STATUS};
|
|
|
|
print "user $hub{$child}{$ref}{BOB} asked you to\n\tgit fetch $BASE_FETCH_URL/$child $ref\n";
|
|
print "hit enter to accept the fetch request or Ctrl-C to cancel...";
|
|
<>;
|
|
|
|
my $fetched_ref = "refs/heads/requests/child/$ref";
|
|
# you're already chdir'd to parent, by get_request_N
|
|
system("git", "update-ref", "-d", "refs/heads/$fetched_ref");
|
|
system("git", "fetch", "$ENV{GL_REPO_BASE_ABS}/$child.git", "$ref:$fetched_ref");
|
|
|
|
$hub{$child}{$ref}{STATUS} = "fetched by $ENV{GL_USER}";
|
|
dump_hub(%hub);
|
|
}
|
|
|
|
sub accept {
|
|
# accept parent request-number
|
|
usage() unless @_ == 2;
|
|
my ($repo, $n) = @_;
|
|
writable_repo($repo); # yeah we're throwing away the return values
|
|
my ($child, $ref, %hub) = get_request_N($repo, $n);
|
|
|
|
map { die "request status is '$_'; must be 'fetched'\n" if $_ !~ /^fetched by / } $hub{$child}{$ref}{STATUS};
|
|
|
|
# the 'cover letter' message comes from STDIN
|
|
my $cover = join("", <>);
|
|
$hub{$child}{$ref}{STATUS} = "accepted by $ENV{GL_USER}";
|
|
$hub{$child}{$ref}{COVER} .= "\n\nAccepted. Message to requestor:\n$cover";
|
|
dump_hub(%hub);
|
|
}
|
|
|
|
# -------------------- service subs
|
|
|
|
sub assert {
|
|
my ($expr, $message) = @_;
|
|
eval $expr or die ($message ? "$message\n" : "assert '$expr' failed\n");
|
|
}
|
|
|
|
sub cd2repo {
|
|
my $repo = shift;
|
|
wrap_chdir("$ENV{GL_REPO_BASE_ABS}/$repo.git");
|
|
}
|
|
|
|
sub dump_hub {
|
|
# pwd assumed to git repo.git; dump a file called "gl-adc-hub-requests"
|
|
use Data::Dumper;
|
|
$Data::Dumper::Indent = 1;
|
|
$Data::Dumper::Sortkeys = 1;
|
|
my %hub = @_;
|
|
|
|
my $fh = wrap_open(">", "gl-adc-hub-requests");
|
|
print $fh Data::Dumper->Dump([\%hub], [qw(*hub)]);
|
|
close $fh;
|
|
}
|
|
|
|
sub get_hub {
|
|
# pwd assumed to git repo.git; "do" a file called "gl-adc-hub-requests"
|
|
|
|
return () unless -w "gl-adc-hub-requests";
|
|
our %hub = ();
|
|
do "gl-adc-hub-requests" or die "error parsing gl-adc-hub-requests\n";
|
|
return %hub;
|
|
}
|
|
|
|
sub get_request_N {
|
|
# given a repo and an N, return "child", "ref", and %hub (or die trying!)
|
|
|
|
# you can't look at pull requests for repos you don't have at least read access to
|
|
my ($repo, $creator) = readable_repo(shift);
|
|
cd2repo($repo);
|
|
my %hub = get_hub();
|
|
die "you have no pending requests\n" unless %hub;
|
|
|
|
my $n = shift || '';
|
|
usage() unless ($n =~ /^\d+$/);
|
|
|
|
my @hub = hub_sort(%hub);
|
|
die "you have only " . scalar(@hub) . " requests\n" if @hub < $n;
|
|
$n--; # make it 0-relative
|
|
return ($hub[$n]->{REPO}, $hub[$n]->{REF}, %hub);
|
|
}
|
|
|
|
sub hub_sort {
|
|
my %hub = @_;
|
|
my %sorted_hub = ();
|
|
for my $child (sort keys %hub) {
|
|
for my $ref (sort keys %{ $hub{$child} }) {
|
|
my $key = $hub{$child}{$ref}{TIME} . "-$child-$ref";
|
|
$sorted_hub{$key} = { REPO=>$child, REF=>$ref };
|
|
}
|
|
}
|
|
my @hub = ();
|
|
for my $key (sort keys %sorted_hub) {
|
|
push @hub, $sorted_hub{$key};
|
|
}
|
|
return @hub;
|
|
}
|
|
|
|
sub list_hub {
|
|
my ($status, %hub) = @_;
|
|
|
|
my $header = "#\tchild-repository-name\t(requestor)\tbranch-or-tag-to-pull\tstatus\n----\n";
|
|
my @hub = hub_sort(%hub);
|
|
my $sn = 0;
|
|
for my $pr (@hub) {
|
|
$sn++;
|
|
my $child = $pr->{REPO};
|
|
my $ref = $pr->{REF};
|
|
my $pr_status = $hub{$child}{$ref}{STATUS};
|
|
next if $status and $pr_status !~ /$status/;
|
|
print $header if $header; $header = '';
|
|
print "$sn\t$child\t($hub{$child}{$ref}{BOB})\t$ref\t$pr_status\n";
|
|
}
|
|
}
|
|
|
|
sub parent_repo {
|
|
my ($repo) = shift;
|
|
cd2repo($repo);
|
|
die "parent repo was not recorded, sorry!\n" unless -f $GL_FORKED_FROM;
|
|
my $gff = `cat $GL_FORKED_FROM`;
|
|
chomp($gff);
|
|
return $gff;
|
|
}
|
|
|
|
sub readable_repo {
|
|
my $repo = valid_repo(shift);
|
|
my ($perm, $creator) = check_access($repo);
|
|
die "$repo does not exist or you have no read access\n" unless $perm =~ /R/;
|
|
return ($repo, $creator);
|
|
}
|
|
|
|
sub valid_log_options {
|
|
}
|
|
|
|
sub valid_ref {
|
|
my ($repo, $ref) = @_;
|
|
cd2repo($repo);
|
|
die "invalid ref $ref\n" unless `git cat-file -t $ref` =~ /^commit$/;
|
|
die "invalid ref $ref\n" unless `git rev-parse $ref` =~ /^[0-9a-f]{40}$/;
|
|
return $ref;
|
|
}
|
|
|
|
sub valid_repo {
|
|
my $repo = shift;
|
|
$repo =~ s/\.git$//;
|
|
die "$repo does not exist or you have no read access\n" unless -d "$ENV{GL_REPO_BASE_ABS}/$repo.git";
|
|
return $repo;
|
|
}
|
|
|
|
sub writable_repo {
|
|
my $repo = valid_repo(shift);
|
|
my ($perm, $creator) = check_access($repo);
|
|
die "$repo does not exist or you have no write access\n" unless $perm =~ /W/;
|
|
return ($repo, $creator);
|
|
}
|
|
|
|
sub temp_clone {
|
|
my ($repo, $ref) = @_;
|
|
|
|
die "internal error; temp_clone called twice?\n" if $tempdir;
|
|
|
|
# some of this code is also in "rrr" branch
|
|
|
|
# first make a temp directory within $REPO_BASE
|
|
$ENV{TMPDIR} = $ENV{GL_REPO_BASE_ABS};
|
|
$tempdir = `mktemp -d -t gl-internal-temp-repo.XXXXXXXXXX`;
|
|
chomp($tempdir);
|
|
rename $tempdir, "$tempdir.git";
|
|
|
|
# make the clone
|
|
wrap_chdir("$ENV{GL_REPO_BASE_ABS}");
|
|
system("git clone --mirror -l $repo.git $tempdir.git >/dev/null 2>&1");
|
|
|
|
# go to the clone and delete refs he's not allowed to read
|
|
wrap_chdir("$ENV{GL_REPO_BASE_ABS}");
|
|
wrap_chdir("$tempdir.git");
|
|
# for each available ref
|
|
for my $ar (`git for-each-ref refs '--format=%(refname)'`) {
|
|
chomp($ar);
|
|
system('git', 'update-ref', '-d', $ar) unless $ar eq "refs/heads/$ref";
|
|
}
|
|
|
|
# you've already cd-d to the temp repo, just set the name up properly
|
|
$tempdir =~ s/^\Q$ENV{GL_REPO_BASE_ABS}\///;
|
|
}
|
|
|
|
sub check_SHAs {
|
|
my $ref = shift;
|
|
for (@_) {
|
|
next unless /^[0-9a-f]+$/i;
|
|
my $fullsha = `git rev-parse $_`;
|
|
chomp($fullsha);
|
|
die "invalid SHA: $_\n" unless $fullsha =~ /^[0-9a-f]{40}$/;
|
|
my $mergebase = `git merge-base $fullsha $ref`;
|
|
chomp($mergebase);
|
|
die "invalid SHA: $_\n" unless $mergebase eq $fullsha;
|
|
}
|
|
}
|