X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=blobdiff_plain;f=infra%2Fdgit-repos-server;h=2d61db988be72b7ffcd61794ba50487342e9748c;hb=b769d51ab9e8b6102ec82197c6cd4005b87a9ada;hp=1fcc1fd34c1a9f4623262c79a471c5117b5405d1;hpb=f466135abc563fca56534e3796d175bd82cc2e15;p=dgit.git diff --git a/infra/dgit-repos-server b/infra/dgit-repos-server index 1fcc1fd3..2d61db98 100755 --- a/infra/dgit-repos-server +++ b/infra/dgit-repos-server @@ -7,6 +7,7 @@ # 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 # --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) @@ -17,8 +18,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 @@ -62,7 +65,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/DISTRO/* and refs/dgit/* # * and only one of each # * and the tag does not already exist # and @@ -126,6 +129,8 @@ $SIG{__WARN__} = sub { die $_[0]; }; # POLICY-HOOK-SCRIPT ... push-confirm PACKAGE \ # VERSION SUITE TAGNAME DELIBERATELIES FRESH-REPO|'' [...] # +# DELIBERATELIES is like this: --deliberately-foo,--deliberately-bar,... +# # Exit status is a bitmask. Bit weight constants are defined in Dgit.pm. # NOFFCHECK (2) # suppress dgit-repos-server's fast-forward check ("push" only) @@ -137,18 +142,19 @@ $SIG{__WARN__} = sub { die $_[0]; }; # of any expected bits set). So, eg, exit 0 means "continue normally" # and would be appropriate for an unknown action. # -# cwd for push and push-confirm is a temporary repo where the -# to-be-pushed objects have been received; TAGNAME is the -# version-based tag +# cwd for push and push-confirm is a temporary repo where the incoming +# objects have been received; TAGNAME is the version-based tag. # # FRESH-REPO is '' iff the repo for this package already existed, or # the pathname of the newly-created repo which will be renamed into # place if everything goes well. (NB that this is generally not the # same repo as the cwd, because the objects are first received into a -# temporary repo so they can be examined.) +# temporary repo so they can be examined.) In this case FRESH-REPO +# contains exactly the objects and refs that will appear in the +# destination if push-confirm approves. # -# if push requested FRESHREPO, push-confirm happens in said fresh repo -# and FRESH-REPO is guaranteed not to be ''. +# if push requested FRESHREPO, push-confirm happens in the old working +# repo and FRESH-REPO is guaranteed not to be ''. # # policy hook for a particular package will be invoked only once at # a time - (see comments about DGIT-REPOS-DIR, above) @@ -160,7 +166,8 @@ $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. use POSIX; @@ -177,6 +184,7 @@ our $dgitrepos; our $package; our $distro; our $suitesfile; +our $suitesformasterfile; our $policyhook; our $dgitlive; our $distrodir; @@ -186,8 +194,9 @@ our $keyrings; our @lockfhs; our @deliberatelies; -our %supersedes; +our %previously; our $policy; +our @policy_args; #----- utilities ----- @@ -286,22 +295,18 @@ 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 () { # realdestrepo must have been locked - ensuredir "$dgitrepos/_removed-tags"; - open PREVIOUS, ">>", removedtagsfile or die removedtagsfile." $!"; - git_for_each_ref(debiantag('*'), sub { - my ($objid,$objtype,$fullrefname,$reftail) = @_; - print PREVIOUS "\n$objid $reftail .\n" or die $!; - }); - close PREVIOUS or die $!; + my $real = realdestrepo; + return unless stat_exists $real; my $garbagerepo = "$dgitrepos/${package}_garbage"; # We arrange to always keep at least one old tree, for recovery @@ -319,7 +324,15 @@ sub movetogarbage () { printdebug "movetogarbage: $garbagerepo -> -old\n"; rename "$garbagerepo", "$garbagerepo-old" or die "$garbagerepo $!"; } - my $real = realdestrepo; + + ensuredir "$dgitrepos/_removed-tags"; + open PREVIOUS, ">>", removedtagsfile or die removedtagsfile." $!"; + git_for_each_ref('refs/tags/'.debiantag('*',$distro), sub { + my ($objid,$objtype,$fullrefname,$reftail) = @_; + print PREVIOUS "\n$objid $reftail .\n" or die $!; + }, $real); + close PREVIOUS or die $!; + printdebug "movetogarbage: $real -> $garbagerepo\n"; rename $real, $garbagerepo or $! == ENOENT @@ -404,6 +417,7 @@ sub maybeinstallprospective () { 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/dgit/} ? 'head' : @@ -424,7 +438,7 @@ sub maybeinstallprospective () { printdebug "install $destrepo => ".realdestrepo."\n"; rename $destrepo, realdestrepo or die $!; - remove "$destrepo.lock" or die $!; + remove realdestrepo.".lock" or die $!; } sub main__git_receive_pack () { @@ -440,6 +454,8 @@ sub main__git_receive_pack () { our ($tagname, $tagval, $suite, $oldcommit, $commit); our ($version, %tagh); +our ($tagexists_error); + sub readupdates () { printdebug " updates ...\n"; while () { @@ -447,11 +463,11 @@ sub readupdates () { printdebug " upd.| $_\n"; m/^(\S+) (\S+) (\S+)$/ or die "$_ ?"; my ($old, $sha1, $refname) = ($1, $2, $3); - if ($refname =~ m{^refs/tags/(?=debian/)}) { + if ($refname =~ m{^refs/tags/(?=$distro/)}) { reject "pushing multiple tags!" if defined $tagname; $tagname = $'; #'; $tagval = $sha1; - reject "tag $tagname already exists -". + $tagexists_error= "tag $tagname already exists -". " not replacing previously-pushed version" if $old =~ m/[^0]/; } elsif ($refname =~ m{^refs/dgit/}) { @@ -506,9 +522,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 ($_)"; @@ -625,17 +641,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"; } @@ -698,17 +724,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++; } @@ -716,11 +742,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"; @@ -729,10 +755,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 "checktagnoreply - not FRESHREPO, nchecked=$nchecked"; - push @problems, "does not supersede any tag". + printdebug "checktagnoreplay - not FRESHREPO, nchecked=$nchecked"; + push @problems, "does not declare previously any tag". " referring to branch head $branch=$branchhead" unless $nchecked; } @@ -744,7 +770,7 @@ sub checktagnoreplay () { join("; ", @problems). "\n"; } - printdebug "checktagnoreply - all ok\n" + printdebug "checktagnoreplay - all ok ($tagval)\n" } sub tagh1 ($) { @@ -762,18 +788,24 @@ 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/~:/_%/; - - printdebug "translated version $v\n"; - $tagname eq "debian/$v" or die; + my $expecttagname = debiantag $version, $distro; + printdebug "expected tag $expecttagname\n"; + $tagname eq $expecttagname or die; lockrealtree(); - my @policy_args = ($package,$version,$suite,$tagname, - join(",",@deliberatelies)); + @policy_args = ($package,$version,$suite,$tagname, + join(",",@deliberatelies)); $policy = policyhook(NOFFCHECK|FRESHREPO, 'push', @policy_args); + if (defined $tagexists_error) { + if ($policy & FRESHREPO) { + printdebug "ignoring tagexists_error: $tagexists_error\n"; + } else { + reject $tagexists_error; + } + } + checktagnoreplay(); checksuite(); @@ -784,37 +816,68 @@ sub checks () { chomp $mb; $mb eq $oldcommit or reject "not fast forward on dgit branch"; } - if ($policy & FRESHREPO) { - # This is troublesome. We have been asked by the policy hook - # to receive the push into a fresh repo. But of course we - # have actually already mostly received the push into the working - # repo. (This is unavoidable because the instruction to use a new - # repo comes ultimately from the signed tag for the dgit push, - # which has to have been received into some repo.) + # It's a bit late to be discovering this here, isn't it ? + # + # What we do is: Generate a fresh destination repo right now, + # and arrange to treat it from now on as if it were a + # prospective repo. + # + # The presence of this fresh destination repo is detected by + # the parent, which responds by making a fresh master repo + # from the template. (If the repo didn't already exist then + # $destrepo was _prospective, and we change it here. This is + # OK because the parent's check for _fresh persuades it not to + # use _prospective.) # - # So what we do is generate a fresh working repo right now and - # push the head and tag into it. The presence of this fresh - # working repo is detected by the parent, which responds by - # making a fresh master repo from the template. - $destrepo = "${workrepo}_fresh"; # workrepo lock covers mkrepo_fromtemplate $destrepo; } - - my $willinstall = ($destrepo eq realdestrepo ? '' : $destrepo); - policyhook(0, 'push-confirm', @policy_args, $willinstall); } 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"; 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 () { + if ($destrepo eq realdestrepo) { + policyhook(0, 'push-confirm', @policy_args, ''); + onwardpush(); + } else { + # We are to receive the push into a new repo (perhaps + # because the policy push hook asked us to with FRESHREPO, or + # perhaps because the repo didn't exist before). + # + # We want to provide the policy push-confirm hook with a repo + # which looks like the one which is going to be installed. + # The working repo is no good because it might contain + # previous history. + # + # So we push the objects into the prospective new repo right + # away. If the hook declines, we decline, and the prospective + # repo is never installed. + onwardpush(); + policyhook(0, 'push-confirm', @policy_args, $destrepo); + } } sub stunthook () { @@ -825,7 +888,7 @@ sub stunthook () { parsetag(); verifytag(); checks(); - onwardpush(); + finalisepush(); printdebug "stunthook done.\n"; } @@ -863,11 +926,12 @@ our %indistrodir = ( # keys are used for DGIT_DRS_XXX too 'repos' => \$dgitrepos, 'suites' => \$suitesfile, + 'suites-master' => \$suitesformasterfile, 'policy-hook' => \$policyhook, 'dgit-live' => \$dgitlive, ); -our @hookenvs = qw(distro suitesfile policyhook +our @hookenvs = qw(distro suitesfile suitesformasterfile policyhook dgitlive keyrings dgitrepos distrodir); # workrepo and destrepo handled ad-hoc