#!/usr/bin/perl -w # dgit repos policy hook script for Debian # # usages: # dgit-repos-policy-debian DISTRO DGIT-REPOS-DIR ACTION... # ie. # dgit-repos-policy-debian ... check-list [...] # dgit-repos-policy-debian ... check-package PACKAGE [...] # dgit-repos-policy-debian ... push PACKAGE \ # VERSION SUITE TAGNAME DELIBERATELIES [...] # # cwd for push is a temporary repo where the to-be-pushed objects have # been received; TAGNAME is the version-based tag # # if push requested FRESHREPO, push-confirm happens in said fresh repo # # policy hook for a particular package will be invoked only once at # a time use strict; use POSIX; use JSON; use File::Temp; use Debian::Dgit qw(:DEFAULT :policyflags); our $distro = shift @ARGV // die "need DISTRO"; our $repos = shift @ARGV // die "need DGIT-REPOS-DIR"; our $action = shift @ARGV // die "need ACTION"; our $publicmode = 02775; our $policydb = "dbi:SQLite:$repos/policy"; our $new_upload_propagation_slop = 3600*4 + 100; our $poldbh; our $pkg; our $pkgdir; our ($pkg_exists,$pkg_secret); # We assume that it is not possible for NEW to have a version older # than sid. # Whenever pushing, we check for # source-package-local tainted history # global tainted history # can be overridden by --deliberately except for an admin prohib taint # # ALL of the following apply only if history is secret: # # if NEW has no version, or a version which is not in our history[1] # (always) # check all suites # if any suite's version is in our history[1], publish our history # otherwise discard our history, # tainting --deliberately-include-questionable-history # # if NEW has a version which is in our history[1] # (on push only) # require explicit specification of one of # --deliberately-include-questionable-history # --deliberately-not-fast-forward # (latter will taint old NEW version --d-i-q-h) # (otherwise) # leave it be # # [1] looking for the relevant git tag for the version number and not # caring what that tag refers to. # # A wrinkle: if we approved a push recently, we treat NEW as having # a version which is in our history. This is because the package may # still be being uploaded. (We record this using the timestamp of the # package's git repo directory.) sub poldb_setup () { $poldbh = DBI->connect($policydb,'','', { RaiseError=>1, PrintError=>1, AutoCommit=>0 }); $poldbh->do("PRAGMA foreign_keys = ON"); $poldbh->do(<do(<do(<commit; } sub apiquery ($) { my ($subpath) = @_; local $/=undef; $!=0; $?=0; my $json = `dgit -d $distro archive-api-query $subpath`; defined $json or die "$subpath $! $?"; return decode_json $json; } sub specific_suite_has_vsn_in_our_history ($) { my ($suite) = @_; my $in_new = apiquery "/dsc_in_suite/$suite/$pkg"; foreach my $entry (@$in_new) { my $vsn = $entry->{version}; die "$pkg ?" unless defined $vsn; my $tag = debiantag $vsn; $?=0; my $r = system qw(git show-ref --verify --quiet), $tag; return 1 if !$r; next if $r==256; die "$pkg tag $tag $? $!"; } return 0; } sub new_has_vsn_in_our_history () { stat $pkgdir or die "$pkgdir $!"; my $mtime = ((stat _)[9]); my $age = time - $mtime; return 1 if $age < $new_upload_propagation_slop; return specific_suite_has_vsn_in_our_history('new'); } sub good_suite_has_vsn_in_our_history () { my $suites = apiquery "/suites"; foreach my $suitei (@$suites) { my $suite = $suitei->{name}; die unless defined $suite; next if $suite =~ m/\bnew$/; return 1 if specific_suite_has_vsn_in_our_history($suite); } return 0; } sub getpackage () { die unless @ARGV > 1; $pkg = shift @ARGV; die if $pkg =~ m#[^-+.0-9a-z]#; die unless $pkg =~ m#^[^-]#; $pkgdir = "$repos/$pkg"; if (!stat $pkgdir) { die "$pkgdir $!" unless $!==ENOENT; $pkg_exists = 0; } $pkg_exists = 1; $pkg_secret = !!(~(stat _)[2] & 05); } sub add_taint_by_tag ($$$) { my ($tagname, $refobjid, $refobjtype) = @_; my $tf = new File::Temp or die $!; print $tf "$refobjid^0\n" or die $!; my $gcfpid = open GCF, "-|"; defined $gcfpid or die $!; if (!$gcfpid) { open STDIN, "<&", $tf or die $!; exec 'git', 'cat-file'; die $!; } close $tf or die $!; $_ = ; m/^(\w+) (\w+) (\d+)\n/ or die "$objline ?"; my $gitobjid = $1; my $gitobjtype = $2; my $bytes = $3; my $gitobjdata; if ($gitobjtype eq 'commit' or $gitobjtype eq 'tag') { $!=0; read GCF, $gitobjdata, $bytes == $bytes or die "$gitobjid $bytes $!"; } close GCF; my $reason = "tag $tagname referred to this object in git tree but all". " previously pushed versions were found to have been". " removed from NEW (ie, rejected) (or never arrived)"; $poldbh->do("INSERT INTO taints". " (package, gitobjid, gitobjtype, gitobjdata, time, comment)", " VALUES (?,?,?,?,?,?)", {}, $pkg, $gitobjid, $gitobjtype, $gitobjdata, time, $reason); my $taint_id = $poldbh->last_insert_id(undef,undef,"taints","taint_id"); die unless defined $taint_id; $poldbh->do("INSERT INTO taintoverrides". " (taint_id, 'include-questionable-history')", " VALUES (?)", {}, $taint_id); } sub action__check_package () { getpackage(); return 0 unless $pkg_exists; return 0 unless $pkg_secret; chdir $pkgdir or die "$pkgdir $!"; return if new_has_vsn_in_our_history(); if (good_suite_has_vsn_in_our_history) { chmod $publicmode, "." or die $!; return 0; } git_for_each_ref('refs/tags', sub { my ($objid,$objtype,$fullrefname,$tagname) = @_; add_taint_by_tag($tagname,$objid,$objtype); }); $?=0; $!=0; close TAGL or die "git for-each-ref $? $!"; return FRESHREPO; } sub action_push () { # we suppose that NEW has a version which is already in our # history, as otherwise the repo would have been blown away getpackage(); return 0 unless $pkg_exists; return 0 unless $pkg_secret; xxx up to here } if (defined $pkg) { selectpackage; } my $fn = ${*::}{"action__$cmd"}; $fn or die "unknown ACTION"; poldb_setup(); my $rcode = $fn->(); die unless defined $rcode; poldb_commit(); exit $rcode;