X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=dgit.git;a=blobdiff_plain;f=infra%2Fdgit-repos-server;h=6131774e7c43c7e2816cad08bfbcc3b54329947e;hp=e2bec01e810b1dc512a2e7851a66934ae466282d;hb=6dbc82e06ae1e6840dfa2ef2cc6b496561e6f333;hpb=683b0eb026fa6b43cc0a9ae122764f9a5d031f59 diff --git a/infra/dgit-repos-server b/infra/dgit-repos-server index e2bec01e..6131774e 100755 --- a/infra/dgit-repos-server +++ b/infra/dgit-repos-server @@ -1,15 +1,35 @@ #!/usr/bin/perl -w # dgit-repos-server # +# git protocol proxy to check dgit pushes etc. +# +# Copyright (C) 2014-2016 Ian Jackson +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # usages: # dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [] --ssh # dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [] --cron # settings # --repos=GIT-REPOS-DIR default DISTRO-DIR/repos/ # --suites=SUITES-FILE default DISTRO-DIR/suites +# --suites-master=SUITES-FILE default DISTRO-DIR/suites-master # --policy-hook=POLICY-HOOK default DISTRO-DIR/policy-hook +# --mirror-hook=MIRROR-HOOK default DISTRO-DIR/mirror-hook # --dgit-live=DGIT-LIVE-DIR default DISTRO-DIR/dgit-live -# (DISTRO-DIR is not used other than as default and to pass to policy hook) +# (DISTRO-DIR is not used other than as default and to pass to policy +# and mirror hooks) # internal usage: # .../dgit-repos-server --pre-receive-hook PACKAGE # @@ -17,8 +37,10 @@ # # Works like git-receive-pack # -# SUITES is the name of a file which lists the permissible suites -# one per line (#-comments and blank lines ignored) +# SUITES-FILE is the name of a file which lists the permissible suites +# one per line (#-comments and blank lines ignored). For --suites-master +# it is a list of the suite(s) which should, when pushed to, update +# `master' on the server (if fast forward). # # AUTH-SPEC is a :-separated list of # KEYRING.GPG,AUTH-SPEC @@ -28,7 +50,10 @@ # (With --cron AUTH-SPEC is not used and may be the empty string.) use strict; -$SIG{__WARN__} = sub { die $_[0]; }; + +use Debian::Dgit::Infra; # must precede Debian::Dgit; - can change @INC! +use Debian::Dgit qw(:DEFAULT :policyflags); +setup_sigwarn(); # DGIT-REPOS-DIR contains: # git tree (or other object) lock (in acquisition order, outer first) @@ -62,7 +87,7 @@ $SIG{__WARN__} = sub { die $_[0]; }; # as a result of this the stunt pre-receive hook runs; it does this: # + understand what refs we are allegedly updating and # check some correspondences: -# * we are updating only refs/tags/debian/* and refs/dgit/* +# * we are updating only refs/tags/[archive/]DISTRO/* and refs/dgit/* # * and only one of each # * and the tag does not already exist # and @@ -79,7 +104,7 @@ $SIG{__WARN__} = sub { die $_[0]; }; # * the signed tag must refer to a commit # * the signed tag commit must be the refs/dgit value # * the name in the signed tag must correspond to its ref name -# * the tag name must be debian/ (massaged as needed) +# * the tag name must be [archive/]debian/ (massaged as needed) # * the suite is one of those permitted # * the signed tag has a suitable name # * run the "push" policy hook @@ -116,7 +141,7 @@ $SIG{__WARN__} = sub { die $_[0]; }; # a stampfile whose presence indicates that there may be # cleanup to do # -# Policy hook script is invoked like this: +# Policy hook scripts are invoked like this: # POLICY-HOOK-SCRIPT DISTRO DGIT-REPOS-DIR DGIT-LIVE-DIR DISTRO-DIR ACTION... # ie. # POLICY-HOOK-SCRIPT ... check-list [...] @@ -128,12 +153,17 @@ $SIG{__WARN__} = sub { die $_[0]; }; # # DELIBERATELIES is like this: --deliberately-foo,--deliberately-bar,... # -# Exit status is a bitmask. Bit weight constants are defined in Dgit.pm. +# Exit status of policy hook is a bitmask. +# Bit weight constants are defined in Dgit.pm. # NOFFCHECK (2) # suppress dgit-repos-server's fast-forward check ("push" only) # FRESHREPO (4) # blow away repo right away (ie, as if before push or fetch) # ("check-package" and "push" only) +# NOCOMMITCHECK (8) +# suppress dgit-repos-server's check that commits do +# not lack "committer" info (eg as produced by #849041) +# ("push" only) # any unexpected bits mean failure, and then known set bits are ignored # if no unexpected bits set, operation continues (subject to meaning # of any expected bits set). So, eg, exit 0 means "continue normally" @@ -163,16 +193,26 @@ $SIG{__WARN__} = sub { die $_[0]; }; # appropriate lock. # # If policy hook wants to run dgit (or something else in the dgit -# package), it should use DGIT-LIVE-DIR/dgit (etc.) - +# package), it should use DGIT-LIVE-DIR/dgit (etc.), or if that is +# ENOENT, use the installed version. +# +# Mirror hook scripts are invoked like this: +# MIRROR-HOOK-SCRIPT DISTRO-DIR ACTION... +# and currently there is only one action invoked by dgit-repos-server: +# MIRROR-HOOK-SCRIPT DISTRO-DIR updated-hook PACKAGE [...] +# +# Exit status of the mirror hook is advisory only. The mirror hook +# runs too late to do anything useful about a problem, so the only +# effect of a mirror hook exiting nonzero is a warning message to +# stderr (which the pushing user should end up seeing). +# +# If the mirror hook does not exist, it is silently skipped. use POSIX; use Fcntl qw(:flock); use File::Path qw(rmtree); use File::Temp qw(tempfile); -use Debian::Dgit qw(:DEFAULT :policyflags); - initdebug(''); our $func; @@ -180,7 +220,9 @@ our $dgitrepos; our $package; our $distro; our $suitesfile; +our $suitesformasterfile; our $policyhook; +our $mirrorhook; our $dgitlive; our $distrodir; our $destrepo; @@ -189,7 +231,7 @@ our $keyrings; our @lockfhs; our @deliberatelies; -our %supersedes; +our %previously; our $policy; our @policy_args; @@ -258,7 +300,7 @@ sub recorderror ($) { sub reject ($) { my ($why) = @_; recorderror "reject: $why"; - die "dgit-repos-server: reject: $why\n"; + die "\ndgit-repos-server: reject: $why\n\n"; } sub runcmd { @@ -273,7 +315,7 @@ sub policyhook { # => ($exitstatuspolicybitmap); die if $policyallowbits & ~0x3e; my @cmd = ($policyhook,$distro,$dgitrepos,$dgitlive,$distrodir,@polargs); - debugcmd '+',@cmd; + debugcmd '+M',@cmd; my $r = system @cmd; die "system: $!" if $r < 0; die "dgit-repos-server: policy hook failed (or rejected) ($?)\n" @@ -290,10 +332,11 @@ sub mkemptyrepo ($$) { sub mkrepo_fromtemplate ($) { my ($dir) = @_; my $template = "$dgitrepos/_template"; - locksometree($template); + my $templatelock = locksometree($template); printdebug "copy template $template -> $dir\n"; my $r = system qw(cp -a --), $template, $dir; !$r or die "create new repo $dir failed: $r $!"; + close $templatelock; } sub movetogarbage () { @@ -321,7 +364,8 @@ sub movetogarbage () { ensuredir "$dgitrepos/_removed-tags"; open PREVIOUS, ">>", removedtagsfile or die removedtagsfile." $!"; - git_for_each_ref('refs/tags/'.debiantag('*'), sub { + git_for_each_ref([ map { 'refs/tags/'.$_ } debiantags('*',$distro) ], + sub { my ($objid,$objtype,$fullrefname,$reftail) = @_; print PREVIOUS "\n$objid $reftail .\n" or die $!; }, $real); @@ -384,6 +428,23 @@ sub dealwithfreshrepo () { $destrepo = $freshrepo; } +sub mirrorhook { + my @cmd = ($mirrorhook,$distrodir,@_); + debugcmd '+',@cmd; + return unless stat_exists $mirrorhook; + my $r = system @cmd; + if ($r) { + printf STDERR <> 8) : + "wait status $?"); + } +} + sub maybeinstallprospective () { return if $destrepo eq realdestrepo; @@ -406,22 +467,25 @@ sub maybeinstallprospective () { exec qw(git show-ref); die $!; } - my %got = qw(tag 0 head 0); + my %got = qw(newtag 0 omtag 0 head 0); while () { chomp or die; printdebug " show-refs| $_\n"; s/^\S*[1-9a-f]\S* (\S+)$/$1/ or die; + next if m{^refs/heads/master$}; my $wh = - m{^refs/tags/} ? 'tag' : + m{^refs/tags/archive/} ? 'newtag' : + m{^refs/tags/} ? 'omtag' : m{^refs/dgit/} ? 'head' : die; + use Data::Dumper; die if $got{$wh}++; } $!=0; $?=0; close SR or $?==256 or die "$? $!"; printdebug "installprospective ?\n"; die Dumper(\%got)." -- missing refs in new repo" - if grep { !$_ } values %got; + unless $got{head} && grep { m/tag$/ && $got{$_} } keys %got; lockrealtree(); @@ -440,27 +504,29 @@ sub main__git_receive_pack () { runcmd qw(git receive-pack), $workrepo; dealwithfreshrepo(); maybeinstallprospective(); + mirrorhook('updated-hook', $package); } #----- stunt post-receive hook ----- our ($tagname, $tagval, $suite, $oldcommit, $commit); our ($version, %tagh); +our ($maint_tagname, $maint_tagval); our ($tagexists_error); sub readupdates () { printdebug " updates ...\n"; + my %tags; while () { chomp or die; printdebug " upd.| $_\n"; m/^(\S+) (\S+) (\S+)$/ or die "$_ ?"; my ($old, $sha1, $refname) = ($1, $2, $3); - if ($refname =~ m{^refs/tags/(?=debian/)}) { - reject "pushing multiple tags!" if defined $tagname; - $tagname = $'; #'; - $tagval = $sha1; - $tagexists_error= "tag $tagname already exists -". + if ($refname =~ m{^refs/tags/(?=(?:archive/)?$distro/)}) { + my $tn = $'; #'; + $tags{$tn} = $sha1; + $tagexists_error= "tag $tn already exists -". " not replacing previously-pushed version" if $old =~ m/[^0]/; } elsif ($refname =~ m{^refs/dgit/}) { @@ -474,7 +540,19 @@ sub readupdates () { } STDIN->error and die $!; - reject "push is missing tag ref update" unless defined $tagname; + reject "push is missing tag ref update" unless %tags; + my @newtags = grep { m#^archive/# } keys %tags; + my @omtags = grep { !m#^archive/# } keys %tags; + reject "pushing too many similar tags" if @newtags>1 || @omtags>1; + if (@newtags) { + ($tagname) = @newtags; + ($maint_tagname) = @omtags; + } else { + ($tagname) = @omtags or die; + } + $tagval = $tags{$tagname}; + $maint_tagval = $tags{$maint_tagname // ''}; + reject "push is missing head ref update" unless defined $suite; printdebug " updates ok.\n"; } @@ -515,9 +593,9 @@ sub parsetag () { die "$1 != $distro" unless $1 eq $distro; } elsif (s/^(--deliberately-$deliberately_re) //) { push @deliberatelies, $1; - } elsif (s/^supersede:(\S+)=(\w+) //) { - die "supersede $1 twice" if defined $supersedes{$1}; - $supersedes{$1} = $2; + } elsif (s/^previously:(\S+)=(\w+) //) { + die "previously $1 twice" if defined $previously{$1}; + $previously{$1} = $2; } elsif (s/^[-+.=0-9a-z]\S* //) { } else { die "unknown dgit info in tag ($_)"; @@ -584,7 +662,7 @@ sub dm_txt_check ($$) { printdebug " dm_txt_check $keyid $dmtxtfn\n"; open DT, '<', $dmtxtfn or die "$dmtxtfn $!"; while (
) { - m/^fingerprint:\s+$keyid$/oi + m/^fingerprint:\s+\Q$keyid\E$/oi ..0 or next; if (s/^allow:/ /i..0) { } else { @@ -634,17 +712,27 @@ sub verifytag () { reject "key not found in keyrings"; } -sub checksuite () { - printdebug "checksuite ($suitesfile)\n"; - open SUITES, "<", $suitesfile or die $!; +sub suite_is_in ($) { + my ($sf) = @_; + printdebug "suite_is_in ($sf)\n"; + if (!open SUITES, "<", $sf) { + $!==ENOENT or die $!; + return 0; + } while () { chomp; next unless m/\S/; next if m/^\#/; s/\s+$//; - return if $_ eq $suite; + return 1 if $_ eq $suite; } die $! if SUITES->error; + return 0; +} + +sub checksuite () { + printdebug "checksuite ($suitesfile)\n"; + return if suite_is_in $suitesfile; reject "unknown suite"; } @@ -707,17 +795,17 @@ sub checktagnoreplay () { my $nchecked = 0; my @problems; - my $check_ref_superseded= sub { + my $check_ref_previously= sub { my ($objid,$objtype,$fullrefname,$reftail) = @_; my $supkey = $fullrefname; $supkey =~ s{^refs/}{} or die "$supkey $objid ?"; - my $supobjid = $supersedes{$supkey}; + my $supobjid = $previously{$supkey}; if (!defined $supobjid) { printdebug "checktagnoreply - missing\n"; - push @problems, "does not supersede $supkey"; + push @problems, "does not declare previously $supkey"; } elsif ($supobjid ne $objid) { - push @problems, "supersedes $supkey=$supobjid". - " but previously $supkey=$objid"; + push @problems, "declared previously $supkey=$supobjid". + " but actually previously $supkey=$objid"; } else { $nchecked++; } @@ -725,11 +813,11 @@ sub checktagnoreplay () { if ($policy & FRESHREPO) { foreach my $kind (qw(tags heads)) { - git_for_each_ref("refs/$kind", $check_ref_superseded); + git_for_each_ref("refs/$kind", $check_ref_previously); } } else { my $branch= server_branch($suite); - my $branchhead= git_get_ref($branch); + my $branchhead= git_get_ref(server_ref($suite)); if (!length $branchhead) { # No such branch - NOFFCHECK was unnecessary. Oh well. printdebug "checktagnoreplay - not FRESHREPO, new branch, ok\n"; @@ -738,10 +826,10 @@ sub checktagnoreplay () { " checking for overwriting refs/$branch=$branchhead\n"; git_for_each_tag_referring($branchhead, sub { my ($tagobjid,$refobjid,$fullrefname,$tagname) = @_; - $check_ref_superseded->($tagobjid,undef,$fullrefname,undef); + $check_ref_previously->($tagobjid,undef,$fullrefname,undef); }); printdebug "checktagnoreplay - not FRESHREPO, nchecked=$nchecked"; - push @problems, "does not supersede any tag". + push @problems, "does not declare previously any tag". " referring to branch head $branch=$branchhead" unless $nchecked; } @@ -771,17 +859,22 @@ sub checks () { tagh1('object') eq $commit or reject "tag refers to wrong commit"; tagh1('tag') eq $tagname or reject "tag name in tag is wrong"; - my $v = $version; - $v =~ y/~:/_%/; + my @expecttagnames = debiantags($version, $distro); + printdebug "expected tag @expecttagnames\n"; + grep { $tagname eq $_ } @expecttagnames or die; - printdebug "translated version $v\n"; - $tagname eq "debian/$v" or die; + foreach my $othertag (grep { $_ ne $tagname } @expecttagnames) { + reject "tag $othertag (pushed with differing dgit version)". + " already exists -". + " not replacing previously-pushed version" + if git_get_ref "refs/tags/".$othertag; + } lockrealtree(); @policy_args = ($package,$version,$suite,$tagname, join(",",@deliberatelies)); - $policy = policyhook(NOFFCHECK|FRESHREPO, 'push', @policy_args); + $policy = policyhook(NOFFCHECK|FRESHREPO|NOCOMMITCHECK, 'push', @policy_args); if (defined $tagexists_error) { if ($policy & FRESHREPO) { @@ -801,6 +894,27 @@ sub checks () { chomp $mb; $mb eq $oldcommit or reject "not fast forward on dgit branch"; } + + # defend against commits generated by #849041 + if (!($policy & NOCOMMITCHECK)) { + my @checks = qw(%ae %at + %ce %ct); + my @chk = qw(git log -z); + push @chk, '--pretty=tformat:%H%n'. + (join "", map { $_, '%n' } @checks); + push @chk, "^$oldcommit" if $oldcommit =~ m/[^0]/; + push @chk, $commit;; + printdebug " ~NOCOMMITCHECK @chk\n"; + open CHK, "-|", @chk or die $!; + local $/ = "\0"; + while () { + next unless m/^$/m; + m/^\w+(?=\n)/ or die; + reject "corrupted object $& (missing metadata)"; + } + $!=0; $?=0; close CHK or $?==256 or die "$? $!"; + } + if ($policy & FRESHREPO) { # It's a bit late to be discovering this here, isn't it ? # @@ -821,14 +935,28 @@ sub checks () { } sub onwardpush () { - my @cmd = (qw(git send-pack), $destrepo); - push @cmd, qw(--force) if $policy & NOFFCHECK; + my @cmdbase = (qw(git send-pack), $destrepo); + push @cmdbase, qw(--force) if $policy & NOFFCHECK; + + my @cmd = @cmdbase; push @cmd, "$commit:refs/dgit/$suite", "$tagval:refs/tags/$tagname"; + push @cmd, "$maint_tagval:refs/tags/$maint_tagname" + if defined $maint_tagname; debugcmd '+',@cmd; $!=0; my $r = system @cmd; !$r or die "onward push to $destrepo failed: $r $!"; + + if (suite_is_in $suitesformasterfile) { + @cmd = @cmdbase; + push @cmd, "$commit:refs/heads/master"; + debugcmd '+', @cmd; + $!=0; my $r = system @cmd; + # tolerate errors (might be not ff) + !($r & ~0xff00) or die + "onward push to $destrepo#master failed: $r $!"; + } } sub finalisepush () { @@ -899,12 +1027,14 @@ our %indistrodir = ( # keys are used for DGIT_DRS_XXX too 'repos' => \$dgitrepos, 'suites' => \$suitesfile, + 'suites-master' => \$suitesformasterfile, 'policy-hook' => \$policyhook, + 'mirror-hook' => \$mirrorhook, 'dgit-live' => \$dgitlive, ); -our @hookenvs = qw(distro suitesfile policyhook - dgitlive keyrings dgitrepos distrodir); +our @hookenvs = qw(distro suitesfile suitesformasterfile policyhook + mirrorhook dgitlive keyrings dgitrepos distrodir); # workrepo and destrepo handled ad-hoc