2 # dgit repos policy hook script for Debian
5 # dgit-repos-policy-debian DISTRO DGIT-REPOS-DIR ACTION...
7 # dgit-repos-policy-debian ... check-list [...]
8 # dgit-repos-policy-debian ... check-package PACKAGE [...]
9 # dgit-repos-policy-debian ... push PACKAGE \
10 # VERSION SUITE TAGNAME DELIBERATELIES [...]
11 # dgit-repos-policy-debian ... push-confirm PACKAGE \
12 # VERSION SUITE TAGNAME DELIBERATELIES [...]
14 # cwd for push is a temporary repo where the to-be-pushed objects have
15 # been received; TAGNAME is the version-based tag
17 # if push requested FRESHREPO, push-confirm happens in said fresh repo
19 # policy hook for a particular package will be invoked only once at
27 use Debian::Dgit qw(:DEFAULT :policyflags);
29 our $distro = shift @ARGV // die "need DISTRO";
30 our $repos = shift @ARGV // die "need DGIT-REPOS-DIR";
31 our $action = shift @ARGV // die "need ACTION";
33 our $publicmode = 02775;
34 our $policydb = "dbi:SQLite:$repos/policy";
35 our $new_upload_propagation_slop = 3600*4 + 100;
40 our ($pkg_exists,$pkg_secret);
42 our ($version,$suite,$tagname);
45 # We assume that it is not possible for NEW to have a version older
48 # Whenever pushing, we check for
49 # source-package-local tainted history
50 # global tainted history
51 # can be overridden by --deliberately except for an admin prohib taint
53 # ALL of the following apply only if history is secret:
55 # if NEW has no version, or a version which is not in our history[1]
58 # if any suite's version is in our history[1], publish our history
59 # otherwise discard our history,
60 # tainting --deliberately-include-questionable-history
62 # if NEW has a version which is in our history[1]
64 # require explicit specification of one of
65 # --deliberately-include-questionable-history
66 # --deliberately-not-fast-forward
67 # (latter will taint old NEW version --d-i-q-h)
71 # [1] looking for the relevant git tag for the version number and not
72 # caring what that tag refers to.
74 # A wrinkle: if we approved a push recently, we treat NEW as having
75 # a version which is in our history. This is because the package may
76 # still be being uploaded. (We record this using the timestamp of the
77 # package's git repo directory.)
81 $poldbh = DBI->connect($policydb,'','', {
82 RaiseError=>1, PrintError=>1, AutoCommit=>0
84 $poldbh->do("PRAGMA foreign_keys = ON");
87 CREATE TABLE IF NOT EXISTS taints (
88 taint_id INTEGER NOT NULL PRIMARY KEY ASC AUTOINCREMENT,
89 package TEXT NOT NULL,
90 gitobjid TEXT NOT NULL,
91 comment TEXT NOT NULL,
98 CREATE INDEX IF NOT EXISTS taints_by_package
99 ON taints (package, gitobject)
101 # any one of of the listed deliberatelies will override its taint
103 CREATE TABLE IF NOT EXISTS taintoverrides (
104 taint_id INTEGER NOT NULL
105 REFERENCES taints (taint_id)
108 DEFERRABLE INITIALLY DEFERRED,
109 deliberately TEXT NOT NULL,
110 PRIMARY KEY (taint_id, deliberately)
115 sub poldb_commit () {
122 $!=0; $?=0; my $json = `dgit -d $distro archive-api-query $subpath`;
123 defined $json or die "$subpath $! $?";
124 return decode_json $json;
127 sub specific_suite_has_vsn_in_our_history ($) {
129 my $in_new = apiquery "/dsc_in_suite/$suite/$pkg";
130 foreach my $entry (@$in_new) {
131 my $vsn = $entry->{version};
132 die "$pkg ?" unless defined $vsn;
133 my $tag = debiantag $vsn;
134 $?=0; my $r = system qw(git show-ref --verify --quiet), $tag;
137 die "$pkg tag $tag $? $!";
142 sub new_has_vsn_in_our_history () {
143 stat $pkgdir or die "$pkgdir $!";
144 my $mtime = ((stat _)[9]);
145 my $age = time - $mtime;
146 return 1 if $age < $new_upload_propagation_slop;
147 return specific_suite_has_vsn_in_our_history('new');
150 sub good_suite_has_vsn_in_our_history () {
151 my $suites = apiquery "/suites";
152 foreach my $suitei (@$suites) {
153 my $suite = $suitei->{name};
154 die unless defined $suite;
155 next if $suite =~ m/\bnew$/;
156 return 1 if specific_suite_has_vsn_in_our_history($suite);
162 die unless @ARGV > 1;
164 die if $pkg =~ m#[^-+.0-9a-z]#;
165 die unless $pkg =~ m#^[^-]#;
167 $pkgdir = "$repos/$pkg";
169 die "$pkgdir $!" unless $!==ENOENT;
173 $pkg_secret = !!(~(stat _)[2] & 05);
177 my ($refobj, $reason);
179 my $tf = new File::Temp or die $!;
180 print $tf "$refobj^0\n" or die $!;
182 my $gcfpid = open GCF, "-|";
183 defined $gcfpid or die $!;
185 open STDIN, "<&", $tf or die $!;
186 exec 'git', 'cat-file';
192 m/^(\w+) (\w+) (\d+)\n/ or die "$objline ?";
198 if ($gitobjtype eq 'commit' or $gitobjtype eq 'tag') {
199 $!=0; read GCF, $gitobjdata, $bytes == $bytes
200 or die "$gitobjid $bytes $!";
204 $poldbh->do("INSERT INTO taints".
205 " (package, gitobjid, gitobjtype, gitobjdata, time, comment)",
206 " VALUES (?,?,?,?,?,?)", {},
207 $pkg, $gitobjid, $gitobjtype, $gitobjdata, time, $reason);
209 my $taint_id = $poldbh->last_insert_id(undef,undef,"taints","taint_id");
210 die unless defined $taint_id;
212 $poldbh->do("INSERT INTO taintoverrides".
213 " (taint_id, deliberately)",
214 " VALUES (?, 'include-questionable-history')", {},
218 sub add_taint_by_tag ($$) {
219 my ($tagname,$refobjid) = @_;
221 "tag $tagname referred to this object in git tree but all".
222 " previously pushed versions were found to have been".
223 " removed from NEW (ie, rejected) (or never arrived)");
226 sub action__check_package () {
228 return 0 unless $pkg_exists;
229 return 0 unless $pkg_secret;
231 chdir $pkgdir or die "$pkgdir $!";
232 return if new_has_vsn_in_our_history();
234 if (good_suite_has_vsn_in_our_history) {
235 chmod $publicmode, "." or die $!;
239 git_for_each_ref('refs/tags', sub {
240 my ($objid,$objtype,$fullrefname,$tagname) = @_;
241 add_taint_by_tag($tagname,$refobjid);
243 $?=0; $!=0; close TAGL or die "git for-each-ref $? $!";
249 die unless @ARGV >= 4;
250 $version = shift @ARGV;
251 $suite = shift @ARGV;
252 $tagname = shift @ARGV;
253 my $delibs = shift @ARGV;
254 foreach my $delib (split /\,/, $delibs) {
255 $deliberately{$delib} = 1;
259 sub deliberately ($) { return $deliberately{$_[0]}; }
263 return 0 unless $pkg_exists;
264 return 0 unless $pkg_secret;
266 # we suppose that NEW has a version which is already in our
267 # history, as otherwise the repo would have been blown away
269 if (deliberately('not-fast-forward')) {
270 add_taint(server_ref($suite),
271 "suite $suite when --deliberately-not-fast-forward".
272 " specified in signed tag $tagname for upload of".
273 " version $version into suite $suite");
274 return NOFFCHECK|FRESHREPO;
276 if (deliberately('include-questionable-history')) {
279 die "Package is in NEW and has not been accepted or rejected yet;".
280 " use a --deliberately option to specify whether you are".
281 " keeping or discarding the previously pushed history. ".
282 " Please RTFM dgit(1).\n";
285 sub action_push_confirm () {
286 my $initq = $dbh->prepare(<<END);
287 SELECT taint_id, gitobjid FROM taints t
288 WHERE (package = ? OR package = '')
290 $initq->execute($pkg);
293 my $chkinput = tempfile();
294 while (my $taint = $initq->fetchrow_hashref()) {
295 push @taintids, $taint->{taint_id};
296 print $chkinput, $taint->{gitobjid}, "\n" or die $!;
298 flush $chkinput or die $!;
299 seek $chkinput,0,0 or die $!;
301 my $checkpid = open2("<&$chkinput", \*CHKOUT, qw(git cat-file --batch));
304 my ($taintinfoq,$overridesanyq,$untaintq,$overridesq);
306 my $overridesstmt = <<END;
307 SELECT deliberately FROM taintoverrides WHERE (
308 my @overridesv = sort keys %deliberately;
309 $overridesstmt .= join <<END, (<<END x @overridesv);
315 $overridesstmt .= <<END;
317 ORDER BY deliberately ASC
323 my $taintid = shift @taintids;
324 die unless defined $taintid;
326 next if m/^\w+ missing$/;
327 die unless m/^(\w+) (\s+) (\d+)\s/;
328 my ($objid,$objtype,$nbytes) = @_;
330 read CHKOUT, $_, $nbytes == $bytes or last;
332 $taintinfoq ||= $dbh->prepare(<<END);
333 SELECT package, time, comment FROM taints WHERE taint_id = ?
335 $taintinfoq->execute($taintid);
337 my $ti = $taintinfoq->fetchrow_hashref();
340 my $timeshow = defined $ti->{time}
341 ? " at time ".strftime("%Y-%m-%d %H:%M:%S Z", gmtime $ti->time)
343 my $pkgshow = length $ti->{package}
344 ? "package $ti->{package}"
349 History contains tainted $objtype $objid
350 Taint recorded$timeshow for $pkginfo
351 Reason: $ti->{comment}
354 $overridesq ||= $dbh->prepare($overridesstmt);
355 $overridesq->execute(@overridesv, $taintid);
356 my ($ovwhy) = $overridesq->fetchrow_array();
357 if (!defined $ovwhy) {
358 $overridesanyq ||= $dbh->prepare(<<END);
359 SELECT 1 FROM taintoverrides WHERE taint_id = ? LIMIT 1
361 $overridesanyq->execute($taintid);
362 my ($ovany) = $overridesanyq->fetchrow_array();
363 print STDERR $ovany ? <<END : <<END;
364 Could be forced using --deliberately. Consult documentation.
366 Uncorrectable error. If confused, consult administrator.
371 Forcing due to --deliberately-$ovwhy
373 $untaintq ||= $dbh->prepare(<<END);
374 DELETE FROM taints WHERE taint_id = ?
376 $untaintq->execute($taint_id);
380 $?=0; my $gotpid = waitpid $checkpid, WNOHANG;
381 die "@taintids $gotpid $? $!";
387 Rejecting push due to questionable history.
400 my $fn = ${*::}{"action__$cmd"};
401 $fn or die "unknown ACTION";
406 die unless defined $rcode;