chiark / gitweb /
changelog: start 9.14
[dgit.git] / infra / dgit-repos-server
index a8b9400b5a4729a2c92e0c86d318385dd1512c6c..bbf1aa215a34e054b5b4532254865365c7f6e3b4 100755 (executable)
@@ -3,7 +3,7 @@
 #
 # git protocol proxy to check dgit pushes etc.
 #
-# Copyright (C) 2014-2016  Ian Jackson
+# Copyright (C) 2014-2017,2019  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
@@ -21,6 +21,8 @@
 # usages:
 #   dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [<settings>] --ssh
 #   dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [<settings>] --cron
+#   dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [<settings>] \
+#      --tag2upload URL TAGNAME
 # settings
 #   --repos=GIT-REPOS-DIR      default DISTRO-DIR/repos/
 #   --suites=SUITES-FILE       default DISTRO-DIR/suites
@@ -50,6 +52,8 @@
 # (With --cron AUTH-SPEC is not used and may be the empty string.)
 
 use strict;
+use Carp;
+use IO::Handle;
 
 use Debian::Dgit::Infra; # must precede Debian::Dgit; - can change @INC!
 use Debian::Dgit qw(:DEFAULT :policyflags);
@@ -539,15 +543,12 @@ sub readupdates () {
     STDIN->error and die $!;
 
     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;
-    }
+    my @dtags = grep { m#^archive/# } keys %tags;
+    reject "need exactly one archive/* tag" if @dtags!=1;
+    my @mtags = grep { !m#^archive/# } keys %tags;
+    reject "pushing too many non-dgit tags" if @mtags>1;
+    ($tagname) = @dtags;
+    ($maint_tagname) = @mtags;
     $tagval = $tags{$tagname};
     $maint_tagval = $tags{$maint_tagname // ''};
 
@@ -555,8 +556,9 @@ sub readupdates () {
     printdebug " updates ok.\n";
 }
 
-sub parsetag () {
-    printdebug " parsetag...\n";
+sub readtag () {
+    printdebug " readtag...\n";
+
     open PT, ">dgit-tmp/plaintext" or die $!;
     open DS, ">dgit-tmp/plaintext.asc" or die $!;
     open T, "-|", qw(git cat-file tag), $tagval or die $!;
@@ -572,12 +574,11 @@ sub parsetag () {
        }
     }
     $!=0; $_=<T>; defined or die $!;
-    m/^($package_re) release (\S+) for \S+ \((\S+)\) \[dgit\]$/ or
-       reject "tag message not in expected format";
+}
 
-    die unless $1 eq $package;
-    $version = $2;
-    die "$3 != $suite " unless $3 eq $suite;
+sub parsetag_general ($$) {
+    my ($dgititemfn, $distrofn) = @_;
+    printdebug " parsetag...\n";
 
     my $copyl = $_;
     for (;;) {
@@ -587,14 +588,11 @@ sub parsetag () {
        if (m/^\[dgit ([^"].*)\]$/) { # [dgit "something"] is for future
            $_ = $1." ";
            while (length) {
-               if (s/^distro\=(\S+) //) {
-                   die "$1 != $distro" unless $1 eq $distro;
-               } elsif (s/^(--deliberately-$deliberately_re) //) {
-                   push @deliberatelies, $1;
-               } elsif (s/^previously:(\S+)=(\w+) //) {
-                   die "previously $1 twice" if defined $previously{$1};
-                   $previously{$1} = $2;
-               } elsif (s/^[-+.=0-9a-z]\S* //) {
+               if ($dgititemfn->()) {
+               } elsif (s/^distro\=(\S+) //) {
+                   $distrofn->($1);
+               } elsif (s/^([-+.=0-9a-z]\S*) //) {
+                   printdebug " parsetag ignoring unrecognised \`$1'\n";
                } else {
                    die "unknown dgit info in tag ($_)";
                }
@@ -603,6 +601,7 @@ sub parsetag () {
        }
        last if m/^-----BEGIN PGP/;
     }
+
     $_ = $copyl;
     for (;;) {
        print DS or die $!;
@@ -615,6 +614,30 @@ sub parsetag () {
     printdebug " parsetag ok.\n";
 }
 
+sub parsetag () {
+    readtag();
+    m/^($package_re) release (\S+) for \S+ \((\S+)\) \[dgit\]$/ or
+       reject "tag message not in expected format";
+    die unless $1 eq $package;
+    $version = $2;
+    die "$3 != $suite " unless $3 eq $suite;
+
+    parsetag_general sub {
+       if (s/^(--deliberately-$deliberately_re) //) {
+           push @deliberatelies, $1;
+       } elsif (s/^previously:(\S+)=(\w+) //) {
+           die "previously $1 twice" if defined $previously{$1};
+           $previously{$1} = $2;
+       } else {
+           return 0;
+       }
+       return 1;
+    }, sub {
+       my ($gotdistro) = @_;
+       die "$gotdistro != $distro" unless $gotdistro eq $distro;
+    };
+}
+
 sub checksig_keyring ($) {
     my ($keyringfile) = @_;
     # returns primary-keyid if signed by a key in this keyring
@@ -761,7 +784,7 @@ sub checktagnoreplay () {
     #     current head for the suite (there must be at least one).
     #
     #     This prevents any tag implying a NOFFCHECK push being
-    #     replayed to rewind from a different head.
+    #     replayed to overwrite a different head.
     #
     #     The possibility of an earlier ff-only push being replayed is
     #     eliminated as follows: the tag from such a push would still
@@ -850,20 +873,23 @@ sub tagh1 ($) {
     return $vals->[0];
 }
 
-sub checks () {
+sub basic_tag_checks() {
     printdebug "checks\n";
 
     tagh1('type') eq 'commit' or reject "tag refers to wrong kind of object";
     tagh1('object') eq $commit or reject "tag refers to wrong commit";
     tagh1('tag') eq $tagname or reject "tag name in tag is wrong";
+}
+
+sub checks () {
+    basic_tag_checks();
 
     my @expecttagnames = debiantags($version, $distro);
     printdebug "expected tag @expecttagnames\n";
     grep { $tagname eq $_ } @expecttagnames or die;
 
     foreach my $othertag (grep { $_ ne $tagname } @expecttagnames) {
-       reject "tag $othertag (pushed with differing dgit version)".
-           " already exists -".
+       reject "tag $othertag already exists -".
            " not replacing previously-pushed version"
            if git_get_ref "refs/tags/".$othertag;
     }
@@ -1047,6 +1073,228 @@ our @hookenvs = qw(distro suitesfile suitesformasterfile policyhook
 
 # workrepo and destrepo handled ad-hoc
 
+sub mode_tag2upload () {
+    # CALLER MUST PREVENT MULTIPLE CONCURRENT RUNS IN SAME CWD
+    # If we fail (exit nonzero), caller should capture our stderr,
+    #  and retry some bounded number of times in some appropriate way
+    # Uses whatever ambient gpg key is available
+    @ARGV==2 or die;
+
+    my $url;
+    ($url,$tagval) = @ARGV;
+
+    $ENV{DGIT_DRS_EMAIL_NOREPLY} // die;
+
+    my $start = time // die;
+    my @t = gmtime $start;
+
+    die if $url =~ m/[^[:graph:]]/;
+    die if $tagval =~ m/[^[:graph:]]/;
+
+    open OL, ">>overall.log" or die $!;
+    autoflush OL 1;
+    my $quit = sub {
+       printf OL "%04d-%02d-%02d %02d:%02d:%02d (%04ds): %s %s: %s\n",
+           $t[5] + 1900, @t[4,3,2,1,0], (time-$start), $url, $tagval, $_[0];
+       exit 0;
+    };
+
+    $ENV{DGIT_DRS_ANY_URL} or $url =~ m{^https://}s
+       or $quit->("url scheme not as expected");
+
+    $tagval =~ m{^$distro/($versiontag_re)$}s
+       or $quit->("tag name not for us");
+
+    $version = $1;
+    $version =~ y/_\%\#/:~/d;
+
+    my $work = 'work';
+
+    my $tagref = "refs/tags/$tagval";
+
+    rmtree $work;
+    rmtree 'bpd';
+    mkdir $work or die $!;
+    mkdir 'bpd' or die $!;
+    unlink <*.orig*>;
+    dif $! if <*.orig*>;
+    changedir $work;
+    runcmd qw(git init -q);
+    runcmd qw(git remote add origin), $url;
+    runcmd qw(git fetch --depth=1 origin), "$tagref:$tagref";
+    changedir ".git";
+    mkdir 'dgit-tmp' or die $!;
+
+    my $tagger;
+    open T, "-|", qw(git cat-file tag), $tagval or die $!;
+    {
+       local $/ = undef;
+       $!=0; $_=<T>; defined or die $!;
+
+       # quick and dirty check, will check properly later
+       m/^\[dgit[^"]* please-upload(?:\]| )/m or
+           $quit->("tag missing please-upload request");
+
+       m/^tagger (.*) \d+ [-+]\d+$/m or
+           $quit->("failed to fish tagger out of tag");
+       $tagger = $1;
+    };
+
+    readtag();
+    m/^($package_re) release (\S+) for ($suite_re)$/ or
+       $quit->("tag headline not for us");
+    $package = $1;
+    my $tagmversion = $2;
+    $suite = $3;
+
+
+    # This is for us.  From now on, we will capture errors to
+    # be emailed to the tagger.
+
+    open H, ">>dgit-tmp/tagupl.email" or die $!;
+    print H <<END or die $!;
+Subject: push-to-upload failed, $package $version ($distro)
+X-Debian-Push-Distro: $distro
+X-Debian-Push-Package: $package
+END
+    printf H "To: %s", $tagger or die $!; # no newline
+    flush H or die $!;
+
+    open L, ">>dgit-tmp/tagupl.log" or die $!;
+
+    my $child = fork() // die $!;
+    if ($child) {
+       # we are the parent
+       # if child exits 0, it has called $quit->()
+       $!=0; waitpid $child, 0 == $child or die $!;
+       printdebug "child $child ?=$?\n";
+       exit 0 unless $?;
+       print L "execution child: ", waitstatusmsg(), "\n" or die $!;
+       close L or die $!;
+       print H <<END or die $!;
+
+
+Processing of tag $tagval
+From url $url
+Was not successful:
+
+END
+       $ENV{DGIT_DRS_SENDMAIL} //= '/usr/lib/sendmail';
+
+       close H or die $!;
+       runcmd qw(sh -ec), <<"END";
+            cd dgit-tmp
+            cat tagupl.log >>tagupl.email
+            $ENV{DGIT_DRS_SENDMAIL} -oee -odb -oi -t  \\
+                -f$ENV{DGIT_DRS_EMAIL_NOREPLY}        \\
+                <tagupl.email
+END
+       $quit->("failed, emailed");
+    }
+
+    open STDERR, ">&L" or die $!;
+    open STDOUT, ">&STDERR" or die $!;
+    open DEBUG, ">&STDERR" if $debuglevel;
+
+    reject "version mismatch $tagmversion != $version "
+       unless $tagmversion eq $version;
+
+    my %need = map { $_ => 1 } qw(please-upload split);
+    my ($upstreamc, $upstreamt);
+    my $quilt;
+    my $distro_ok;
+
+    confess if defined $upstreamt;
+
+    parsetag_general sub {
+       if (m/^(\S+) / && exists $need{$1}) {
+           $_ = $';
+           delete $need{$1};
+       } elsif (s/^upstream=(\w+) //) {
+           $upstreamc = $1;
+       } elsif (s/^upstream-tag=(\S+) //) {
+           $upstreamt = $1;
+       } elsif (s/^--quilt=([-+0-9a-z]+) //) {
+           $quilt = $1;
+       } else {
+           return 0;
+       }
+       return 1;
+    }, sub {
+       my ($gotdistro) = @_;
+       $distro_ok ||= $gotdistro eq $distro;
+    };
+
+    $quit->("not for this distro") unless $distro_ok;
+
+    reject "missing \"$_\"" foreach keys %need;
+
+    verifytag();
+
+    reject "upstream tag and not commitish, or v-v"
+       unless defined $upstreamt == defined $upstreamc;
+
+    my @dgit;
+    push @dgit, $ENV{DGIT_DRS_DGIT} // 'dgit';
+    push @dgit, '-wn';
+    push @dgit, "-p$package";
+    push @dgit, '--build-products-dir=../bpd';
+
+    changedir "..";
+    runcmd (@dgit, qw(setup-gitattributes));
+
+    my @fetch = qw(git fetch origin --unshallow);
+    if (defined $upstreamt) {
+       runcmd qw(git check-ref-format), "refs/tags/$upstreamt";
+       my $utagref = "refs/tags/$upstreamt";
+       push @fetch, "$utagref:$utagref";
+    }
+    runcmd @fetch;
+
+    runcmd qw(git checkout -q), "refs/tags/$tagval";
+
+    my $clogp = parsechangelog();
+    my $clogf = sub {
+       my ($f, $exp) = @_;
+       my $got = getfield $clogp, $f;
+       return if $got eq $exp;
+       reject "mismatch: changelog $f $got != $exp";
+    };
+    $clogf->('Version', $version);
+    $clogf->('Source',  $package);
+
+    @fetch = (@dgit, qw(--for-push fetch), $suite);
+    debugcmd "+",@_;
+    $!=0; $?=-1;
+    if (system @fetch) {
+       failedcmd @fetch unless $? == 4*256;
+    }
+    # this is just to get the orig, so we don't really care about the ref
+    if (defined $upstreamc) {
+       my $need_upstreamc = git_rev_parse "refs/tags/$upstreamt";
+       $upstreamc eq $need_upstreamc or reject
+           "upstream-commitish=$upstreamc but tag refers to $need_upstreamc";
+       runcmd qw(git deborig), "$upstreamc";
+    }
+
+    my @dgitcmd;
+    push @dgitcmd, @dgit;
+    push @dgitcmd, qw(--force-uploading-source-only);
+    if (defined $quilt) {
+       push @dgitcmd, "--quilt=$quilt";
+       if ($quilt =~ m/baredebian/) {
+           die "needed upstream commmitish with --quilt=baredebian"
+               unless defined $upstreamc;
+           push @dgitcmd, "--upstream-commitish=refs/tags/$upstreamt";
+       }
+    }
+    push @dgitcmd, qw(push-source --new --overwrite), $suite;
+    
+    runcmd @dgitcmd;
+
+    $quit->('done');
+}
+
 sub mode_ssh () {
     die if @ARGV;
 
@@ -1155,7 +1403,7 @@ sub parseargsdispatch () {
 
     $ENV{"DGIT_DRS_\U$_"} = ${ $main::{$_} } foreach @hookenvs;
 
-    die unless @ARGV==1;
+    die unless @ARGV>=1;
 
     my $mode = shift @ARGV;
     die unless $mode =~ m/^--(\w+)$/;