chiark / gitweb /
dgit-repos-policy-debian: Fix misleading varible name
[dgit.git] / infra / dgit-repos-server
1 #!/usr/bin/perl -w
2 # dgit-repos-server
3 #
4 # usages:
5 #  .../dgit-repos-server DISTRO SUITES KEYRING-AUTH-SPEC \
6 #      DGIT-REPOS-DIR POLICY-HOOK-SCRIPT --ssh
7 # internal usage:
8 #  .../dgit-repos-server --pre-receive-hook PACKAGE
9 #
10 # Invoked as the ssh restricted command
11 #
12 # Works like git-receive-pack
13 #
14 # SUITES is the name of a file which lists the permissible suites
15 # one per line (#-comments and blank lines ignored)
16 #
17 # KEYRING-AUTH-SPEC is a :-separated list of
18 #   KEYRING.GPG,AUTH-SPEC
19 # where AUTH-SPEC is one of
20 #   a
21 #   mDM.TXT
22
23 use strict;
24
25 # DGIT-REPOS-DIR contains:
26 # git tree (or other object)      lock (in acquisition order, outer first)
27 #
28 #  _tmp/PACKAGE_prospective       ! } SAME.lock, held during receive-pack
29 #
30 #  _tmp/PACKAGE_incoming$$        ! } SAME.lock, held during receive-pack
31 #  _tmp/PACKAGE_incoming$$_fresh  ! }
32 #
33 #  PACKAGE.git                      } PACKAGE.git.lock
34 #  PACKAGE_garbage                  }   (also covers executions of
35 #  PACKAGE_garbage-old              }    policy hook script for PACKAGE)
36 #  PACKAGE_garbage-tmp              }
37 #  policy*                          } (for policy hook script, covered by
38 #                                   }  lock only when invoked for a package)
39 #
40 # leaf locks, held during brief operaton only:
41 #
42 #  _empty                           } SAME.lock
43 #  _empty.new                       }
44 #
45 #  _template                        } SAME.lock
46 #
47 # locks marked ! may be held during client data transfer
48
49 # What we do on push is this:
50 #  - extract the destination repo name
51 #  - make a hardlink clone of the destination repo
52 #  - provide the destination with a stunt pre-receive hook
53 #  - run actual git-receive-pack with that new destination
54 #   as a result of this the stunt pre-receive hook runs; it does this:
55 #    + understand what refs we are allegedly updating and
56 #      check some correspondences:
57 #        * we are updating only refs/tags/debian/* and refs/dgit/*
58 #        * and only one of each
59 #        * and the tag does not already exist
60 #      and
61 #        * recover the suite name from the destination refs/dgit/ ref
62 #    + disassemble the signed tag into its various fields and signature
63 #      including:
64 #        * parsing the first line of the tag message to recover
65 #          the package name, version and suite
66 #        * checking that the package name corresponds to the dest repo name
67 #        * checking that the suite name is as recovered above
68 #    + verify the signature on the signed tag
69 #      and if necessary check that the keyid and package are listed in dm.txt
70 #    + check various correspondences:
71 #        * the signed tag must refer to a commit
72 #        * the signed tag commit must be the refs/dgit value
73 #        * the name in the signed tag must correspond to its ref name
74 #        * the tag name must be debian/<version> (massaged as needed)
75 #        * the suite is one of those permitted
76 #        * the signed tag has a suitable name
77 #        * run the "push" policy hook
78 #        * replay prevention for --deliberately-not-fast-forward
79 #        * check the commit is a fast forward
80 #        * handle a request from the policy hook for a fresh repo
81 #    + push the signed tag and new dgit branch to the actual repo
82 #
83 # If the destination repo does not already exist, we need to make
84 # sure that we create it reasonably atomically, and also that
85 # we don't every have a destination repo containing no refs at all
86 # (because such a thing causes git-fetch-pack to barf).  So then we
87 # do as above, except:
88 #  - before starting, we take out our own lock for the destination repo
89 #  - we create a prospective new destination repo by making a copy
90 #    of _template
91 #  - we use the prospective new destination repo instead of the
92 #    actual new destination repo (since the latter doesn't exist)
93 #  - after git-receive-pack exits, we
94 #    + check that the prospective repo contains a tag and head
95 #    + rename the prospective destination repo into place
96 #
97 # Cleanup strategy:
98 #  - We are crash-only
99 #  - Temporary working trees and their locks are cleaned up
100 #    opportunistically by a program which tries to take each lock and
101 #    if successful deletes both the tree and the lockfile
102 #  - Prospective working trees and their locks are cleaned up by
103 #    a program which tries to take each lock and if successful
104 #    deletes any prospective working tree and the lock (but not
105 #    of course any actual tree)
106 #  - It is forbidden to _remove_ the lockfile without removing
107 #    the corresponding temporary tree, as the lockfile is also
108 #    a stampfile whose presence indicates that there may be
109 #    cleanup to do
110 #
111 # Policy hook script is invoked like this:
112 #   POLICY-HOOK-SCRIPT DISTRO DGIT-REPOS-DIR ACTION...
113 # ie.
114 #   POLICY-HOOK-SCRIPT ... check-list [...]
115 #   POLICY-HOOK-SCRIPT ... check-package PACKAGE [...]
116 #   POLICY-HOOK-SCRIPT ... push|push-confirm PACKAGE \
117 #         VERSION SUITE TAGNAME DELIBERATELIES [...]
118 #
119 # Exit status is a bitmask.  Bit weight constants are defined in Dgit.pm.
120 #    NOFFCHECK   (2)
121 #         suppress dgit-repos-server's fast-forward check ("push" only)
122 #    FRESHREPO   (4)
123 #         blow away repo right away (ie, as if before push or fetch)
124 #         ("check-package" and "push" only)
125 # any unexpected bits mean failure, and then known set bits are ignored
126 # if no unexpected bits set, operation continues (subject to meaning
127 # of any expected bits set).  So, eg, exit 0 means "continue normally"
128 # and would be appropriate for an unknown action.
129 #
130 # cwd for push and push-confirm is a temporary repo where the
131 # to-be-pushed objects have been received; TAGNAME is the
132 # version-based tag
133 #
134 # if push requested FRESHREPO, push-confirm happens in said fresh repo
135 #
136 # policy hook for a particular package will be invoked only once at
137 # a time - (see comments about DGIT-REPOS-DIR, above)
138
139
140 use POSIX;
141 use Fcntl qw(:flock);
142 use File::Path qw(rmtree);
143
144 use Debian::Dgit qw(:DEFAULT :policyflags);
145
146 open DEBUG, ">/dev/null" or die $!;
147
148 our $func;
149 our $dgitrepos;
150 our $package;
151 our $distro;
152 our $suitesfile;
153 our $policyhook;
154 our $realdestrepo;
155 our $destrepo;
156 our $workrepo;
157 our $keyrings;
158 our @lockfhs;
159 our $debug='';
160 our @deliberatelies;
161 our %supersedes;
162 our $policy;
163
164 #----- utilities -----
165
166 sub debug {
167     print DEBUG "$debug @_\n";
168 }
169
170 sub acquirelock ($$) {
171     my ($lock, $must) = @_;
172     my $fh;
173     printf DEBUG "$debug locking %s %d\n", $lock, $must;
174     for (;;) {
175         close $fh if $fh;
176         $fh = new IO::File $lock, ">" or die "open $lock: $!";
177         my $ok = flock $fh, $must ? LOCK_EX : (LOCK_EX|LOCK_NB);
178         if (!$ok) {
179             die "flock $lock: $!" if $must;
180             debug " locking $lock failed";
181             return undef;
182         }
183         next unless stat_exists $lock;
184         my $want = (stat _)[1];
185         stat $fh or die $!;
186         my $got = (stat _)[1];
187         last if $got == $want;
188     }
189     return $fh;
190 }
191
192 sub acquirermtree ($$) {
193     my ($tree, $must) = @_;
194     my $fh = acquirelock("$tree.lock", $must);
195     if ($fh) {
196         push @lockfhs, $fh;
197         rmtree $tree;
198     }
199     return $fh;
200 }
201
202 sub locksometree ($) {
203     my ($tree) = @_;
204     acquirelock("$tree.lock", 1);
205 }
206
207 sub lockrealtree () {
208     locksometree($realdestrepo);
209 }
210
211 sub mkrepotmp () {
212     my $tmpdir = "$dgitrepos/_tmp";
213     return if mkdir $tmpdir;
214     return if $! == EEXIST;
215     die $!;
216 }
217
218 sub recorderror ($) {
219     my ($why) = @_;
220     my $w = $ENV{'DGIT_DRS_WORK'}; # we are in stunthook
221     if (defined $w) {
222         chomp $why;
223         open ERR, ">", "$w/drs-error" or die $!;
224         print ERR $why, "\n" or die $!;
225         close ERR or die $!;
226         return 1;
227     }
228     return 0;
229 }
230
231 sub reject ($) {
232     my ($why) = @_;
233     recorderror "reject: $why";
234     die "dgit-repos-server: reject: $why\n";
235 }
236
237 sub debugcmd {
238     if ($debug) {
239         use Data::Dumper;
240         local $Data::Dumper::Indent = 0;
241         local $Data::Dumper::Terse = 1;
242         debug "|".Dumper(\@_);
243     }
244 }
245
246 sub runcmd {
247     debugcmd @_;
248     $!=0; $?=0;
249     my $r = system @_;
250     die "@_ $? $!" if $r;
251 }
252
253 sub policyhook {
254     my ($policyallowbits, @polargs) = @_;
255     # => ($exitstatuspolicybitmap);
256     die if $policyallowbits & ~0x3e;
257     my @cmd = ($policyhook,$distro,$dgitrepos,@polargs);
258     debugcmd @cmd;
259     my $r = system @cmd;
260     die "system: $!" if $r < 0;
261     die "hook (@cmd) failed ($?)" if $r & ~($policyallowbits << 8);
262     return $r >> 8;
263 }
264
265 sub mkemptyrepo ($$) {
266     my ($dir,$sharedperm) = @_;
267     runcmd qw(git init --bare --quiet), "--shared=$sharedperm", $dir;
268 }
269
270 sub mkrepo_fromtemplate ($) {
271     my ($dir) = @_;
272     my $template = "$dgitrepos/_template";
273     locksometree($template);
274     debug "copy template $template -> $dir";
275     my $r = system qw(cp -a --), $template, $dir;
276     !$r or die "create new repo $dir failed: $r $!";
277 }
278
279 sub movetogarbage () {
280     # $realdestrepo must have been locked
281     my $garbagerepo = "$dgitrepos/${package}_garbage";
282     # We arrange to always keep at least one old tree, for anti-rewind
283     # purposes (and, I guess, recovery from mistakes).  This is either
284     # $garbage or $garbage-old.
285     if (stat_exists "$garbagerepo") {
286         rmtree "$garbagerepo-tmp";
287         if (rename "$garbagerepo-old", "$garbagerepo-tmp") {
288             rmtree "$garbagerepo-tmp";
289         } else {
290             die "$garbagerepo $!" unless $!==ENOENT;
291         }
292         rename "$garbagerepo", "$garbagerepo-old" or die "$garbagerepo $!";
293     }
294     rename $realdestrepo, $garbagerepo
295         or $! == ENOENT
296         or die "$garbagerepo $!";
297 }
298
299 #----- git-receive-pack -----
300
301 sub fixmissing__git_receive_pack () {
302     mkrepotmp();
303     $destrepo = "$dgitrepos/_tmp/${package}_prospective";
304     acquirermtree($destrepo, 1);
305     mkrepo_fromtemplate($destrepo);
306 }
307
308 sub makeworkingclone () {
309     mkrepotmp();
310     $workrepo = "$dgitrepos/_tmp/${package}_incoming$$";
311     acquirermtree($workrepo, 1);
312     my $lfh = lockrealtree();
313     runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
314     close $lfh;
315     rmtree "${workrepo}_fresh";
316 }
317
318 sub setupstunthook () {
319     my $prerecv = "$workrepo/hooks/pre-receive";
320     my $fh = new IO::File $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
321         or die "$prerecv: $!";
322     print $fh <<END or die "$prerecv: $!";
323 #!/bin/sh
324 set -e
325 exec $0 --pre-receive-hook $package
326 END
327     close $fh or die "$prerecv: $!";
328     $ENV{'DGIT_DRS_WORK'}= $workrepo;
329     $ENV{'DGIT_DRS_DEST'}= $destrepo;
330     debug " stunt hook set up $prerecv";
331 }
332
333 sub dealwithfreshrepo () {
334     my $freshrepo = "${workrepo}_fresh";
335     return unless stat_exists $freshrepo;
336     $destrepo = $freshrepo;
337 }
338
339 sub maybeinstallprospective () {
340     return if $destrepo eq $realdestrepo;
341
342     if (open REJ, "<", "$workrepo/drs-error") {
343         local $/ = undef;
344         my $msg = <REJ>;
345         REJ->error and die $!;
346         print STDERR $msg;
347         exit 1;
348     } else {
349         $!==&ENOENT or die $!;
350     }
351
352     debug " show-ref ($destrepo) ...";
353
354     my $child = open SR, "-|";
355     defined $child or die $!;
356     if (!$child) {
357         chdir $destrepo or die $!;
358         exec qw(git show-ref);
359         die $!;
360     }
361     my %got = qw(tag 0 head 0);
362     while (<SR>) {
363         chomp or die;
364         debug " show-refs| $_";
365         s/^\S*[1-9a-f]\S* (\S+)$/$1/ or die;
366         my $wh =
367             m{^refs/tags/} ? 'tag' :
368             m{^refs/dgit/} ? 'head' :
369             die;
370         die if $got{$wh}++;
371     }
372     $!=0; $?=0; close SR or $?==256 or die "$? $!";
373
374     debug "installprospective ?";
375     die Dumper(\%got)." -- missing refs in new repo"
376         if grep { !$_ } values %got;
377
378     lockrealtree();
379
380     if ($destrepo eq "${workrepo}_fresh") {
381         movetogarbage;
382     }
383
384     debug "install $destrepo => $realdestrepo";
385     rename $destrepo, $realdestrepo or die $!;
386     remove "$destrepo.lock" or die $!;
387 }
388
389 sub main__git_receive_pack () {
390     makeworkingclone();
391     setupstunthook();
392     runcmd qw(git receive-pack), $workrepo;
393     dealwithfreshrepo();
394     maybeinstallprospective();
395 }
396
397 #----- stunt post-receive hook -----
398
399 our ($tagname, $tagval, $suite, $oldcommit, $commit);
400 our ($version, %tagh);
401
402 sub readupdates () {
403     debug " updates ...";
404     while (<STDIN>) {
405         chomp or die;
406         debug " upd.| $_";
407         m/^(\S+) (\S+) (\S+)$/ or die "$_ ?";
408         my ($old, $sha1, $refname) = ($1, $2, $3);
409         if ($refname =~ m{^refs/tags/(?=debian/)}) {
410             reject "pushing multiple tags!" if defined $tagname;
411             $tagname = $'; #';
412             $tagval = $sha1;
413             reject "tag $tagname already exists -".
414                 " not replacing previously-pushed version"
415                 if $old =~ m/[^0]/;
416         } elsif ($refname =~ m{^refs/dgit/}) {
417             reject "pushing multiple heads!" if defined $suite;
418             $suite = $'; #';
419             $oldcommit = $old;
420             $commit = $sha1;
421         } else {
422             reject "pushing unexpected ref!";
423         }
424     }
425     STDIN->error and die $!;
426
427     reject "push is missing tag ref update" unless defined $tagname;
428     reject "push is missing head ref update" unless defined $suite;
429     debug " updates ok.";
430 }
431
432 sub parsetag () {
433     debug " parsetag...";
434     open PT, ">dgit-tmp/plaintext" or die $!;
435     open DS, ">dgit-tmp/plaintext.asc" or die $!;
436     open T, "-|", qw(git cat-file tag), $tagval or die $!;
437     for (;;) {
438         $!=0; $_=<T>; defined or die $!;
439         print PT or die $!;
440         if (m/^(\S+) (.*)/) {
441             push @{ $tagh{$1} }, $2;
442         } elsif (!m/\S/) {
443             last;
444         } else {
445             die;
446         }
447     }
448     $!=0; $_=<T>; defined or die $!;
449     m/^($package_re) release (\S+) for \S+ \((\S+)\) \[dgit\]$/ or
450         reject "tag message not in expected format";
451
452     die unless $1 eq $package;
453     $version = $2;
454     die "$3 != $suite " unless $3 eq $suite;
455
456     my $copyl = $_;
457     for (;;) {
458         print PT $copyl or die $!;
459         $!=0; $_=<T>; defined or die "missing signature? $!";
460         $copyl = $_;
461         if (m/^\[dgit ([^"].*)\]$/) { # [dgit "something"] is for future
462             $_ = $1." ";
463             while (length) {
464                 if (s/^distro\=(\S+) //) {
465                     die "$1 != $distro" unless $1 eq $distro;
466                 } elsif (s/^(--deliberately-$package_re) //) {
467                     push @deliberatelies, $1;
468                 } elsif (s/^supersede:(\S+)=(\w+) //) {
469                     die "supersede $1 twice" if defined $supersedes{$1};
470                     $supersedes{$1} = $2;
471                 } elsif (s/^[-+.=0-9a-z]\S* //) {
472                 } else {
473                     die "unknown dgit info in tag ($_)";
474                 }
475             }
476             next;
477         }
478         last if m/^-----BEGIN PGP/;
479     }
480     $_ = $copyl;
481     for (;;) {
482         print DS or die $!;
483         $!=0; $_=<T>;
484         last if !defined;
485     }
486     T->error and die $!;
487     close PT or die $!;
488     close DS or die $!;
489     debug " parsetag ok.";
490 }
491
492 sub checksig_keyring ($) {
493     my ($keyringfile) = @_;
494     # returns primary-keyid if signed by a key in this keyring
495     # or undef if not
496     # or dies on other errors
497
498     my $ok = undef;
499
500     debug " checksig keyring $keyringfile...";
501
502     our @cmd = (qw(gpgv --status-fd=1 --keyring),
503                    $keyringfile,
504                    qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext));
505     debugcmd @cmd;
506
507     open P, "-|", @cmd
508         or die $!;
509
510     while (<P>) {
511         next unless s/^\[GNUPG:\] //;
512         chomp or die;
513         debug " checksig| $_";
514         my @l = split / /, $_;
515         if ($l[0] eq 'NO_PUBKEY') {
516             last;
517         } elsif ($l[0] eq 'VALIDSIG') {
518             my $sigtype = $l[9];
519             $sigtype eq '00' or reject "signature is not of type 00!";
520             $ok = $l[10];
521             die unless defined $ok;
522             last;
523         }
524     }
525     close P;
526
527     debug sprintf " checksig ok=%d", !!$ok;
528
529     return $ok;
530 }
531
532 sub dm_txt_check ($$) {
533     my ($keyid, $dmtxtfn) = @_;
534     debug " dm_txt_check $keyid $dmtxtfn";
535     open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
536     while (<DT>) {
537         m/^fingerprint:\s+$keyid$/oi
538             ..0 or next;
539         if (s/^allow:/ /i..0) {
540         } else {
541             m/^./
542                 or reject "key $keyid missing Allow section in permissions!";
543             next;
544         }
545         # in right stanza...
546         s/^[ \t]+//
547             or reject "package $package not allowed for key $keyid";
548         # in allow field...
549         s/\([^()]+\)//;
550         s/\,//;
551         chomp or die;
552         debug " dm_txt_check allow| $_";
553         foreach my $p (split /\s+/) {
554             if ($p eq $package) {
555                 # yay!
556                 debug " dm_txt_check ok";
557                 return;
558             }
559         }
560     }
561     DT->error and die $!;
562     close DT or die $!;
563     reject "key $keyid not in permissions list although in keyring!";
564 }
565
566 sub verifytag () {
567     foreach my $kas (split /:/, $keyrings) {
568         debug "verifytag $kas...";
569         $kas =~ s/^([^,]+),// or die;
570         my $keyid = checksig_keyring $1;
571         if (defined $keyid) {
572             if ($kas =~ m/^a$/) {
573                 debug "verifytag a ok";
574                 return; # yay
575             } elsif ($kas =~ m/^m([^,]+)$/) {
576                 dm_txt_check($keyid, $1);
577                 debug "verifytag m ok";
578                 return;
579             } else {
580                 die;
581             }
582         }   
583     }
584     reject "key not found in keyrings";
585 }
586
587 sub checksuite () {
588     debug "checksuite ($suitesfile)";
589     open SUITES, "<", $suitesfile or die $!;
590     while (<SUITES>) {
591         chomp;
592         next unless m/\S/;
593         next if m/^\#/;
594         s/\s+$//;
595         return if $_ eq $suite;
596     }
597     die $! if SUITES->error;
598     reject "unknown suite";
599 }
600
601 sub checktagnoreplay () {
602     # We check that the signed tag mentions the name and value of
603     # (a) in the case of FRESHREPO all tags in the repo;
604     # (b) in the case of just NOFFCHECK all tags referring to
605     # the current head for the suite (there must be at least one).
606     # This prevents a replay attack using an earlier signed tag.
607     return unless $policy & (FRESHREPO|NOFFCHECK);
608
609     my $garbagerepo = "$dgitrepos/${package}_garbage";
610     lockrealtree();
611
612     local $ENV{GIT_DIR};
613     foreach my $garb ("$garbagerepo", "$garbagerepo-old") {
614         if (stat_exists $garb) {
615             $ENV{GIT_DIR} = $garb;
616             last;
617         }
618     }
619     if (!defined $ENV{GIT_DIR}) {
620         # Nothing to overwrite so the FRESHREPO and NOFFCHECK were
621         # pointless.  Oh well.
622         debug "checktagnoreplay - no garbage, ok";
623         return;
624     }
625
626     my $onlyreferring;
627     if (!($policy & FRESHREPO)) {
628         my $branch = server_branch($suite);
629         $!=0; $?=0; $_ =
630             `git for-each-ref --format='%(objectname)' '[r]efs/$branch'`;
631         defined or die "$branch $? $!";
632         $? and die "$branch $?";
633         if (!length) {
634             # No such branch - NOFFCHECK was unnecessary.  Oh well.
635             debug "checktagnoreplay - not FRESHREPO, new branch, ok";
636             return;
637         }
638         m/^(\w+)\n$/ or die "$branch $_ ?";
639         $onlyreferring = $1;
640         debug "checktagnoreplay - not FRESHREPO,".
641             " checking for overwriting refs/$branch=$onlyreferring";
642     }
643
644     my @problems;
645
646     git_for_each_tag_referring($onlyreferring, sub {
647         my ($objid,$fullrefname,$tagname) = @_;
648         debug "checktagnoreplay - overwriting $fullrefname=$objid";
649         my $supers = $supersedes{$fullrefname};
650         if (!defined $supers) {
651             push @problems, "does not supersede $fullrefname";
652         } elsif ($supers ne $objid) {
653             push @problems,
654  "supersedes $fullrefname=$supers but previously $fullrefname=$objid";
655         } else {
656             # ok;
657         }
658     });
659
660     if (@problems) {
661         reject "replay attack prevention check failed:".
662             " signed tag for $version: ".
663             join("; ", @problems).
664             "\n";
665     }
666     debug "checktagnoreply - all ok"
667 }
668
669 sub tagh1 ($) {
670     my ($tag) = @_;
671     my $vals = $tagh{$tag};
672     reject "missing header $tag in signed tag object" unless $vals;
673     reject "multiple headers $tag in signed tag object" unless @$vals == 1;
674     return $vals->[0];
675 }
676
677 sub checks () {
678     debug "checks";
679
680     tagh1('type') eq 'commit' or reject "tag refers to wrong kind of object";
681     tagh1('object') eq $commit or reject "tag refers to wrong commit";
682     tagh1('tag') eq $tagname or reject "tag name in tag is wrong";
683
684     my $v = $version;
685     $v =~ y/~:/_%/;
686
687     debug "translated version $v";
688     $tagname eq "debian/$v" or die;
689
690     lockrealtree();
691
692     my @policy_args = ($package,$version,$suite,$tagname,
693                        join(",",@deliberatelies));
694     $policy = policyhook(NOFFCHECK|FRESHREPO, 'push', @policy_args);
695
696     checktagnoreplay();
697     checksuite();
698
699     # check that our ref is being fast-forwarded
700     debug "oldcommit $oldcommit";
701     if (!($policy & NOFFCHECK) && $oldcommit =~ m/[^0]/) {
702         $?=0; $!=0; my $mb = `git merge-base $commit $oldcommit`;
703         chomp $mb;
704         $mb eq $oldcommit or reject "not fast forward on dgit branch";
705     }
706
707     if ($policy & FRESHREPO) {
708         # This is troublesome.  We have been asked by the policy hook
709         # to receive the push into a fresh repo.  But of course we
710         # have actually already mostly received the push into the working
711         # repo.  (This is unavoidable because the instruction to use a new
712         # repo comes ultimately from the signed tag for the dgit push,
713         # which has to have been received into some repo.)
714         #
715         # So what we do is generate a fresh working repo right now and
716         # push the head and tag into it.  The presence of this fresh
717         # working repo is detected by the parent, which responds by
718         # making a fresh master repo from the template.
719
720         $destrepo = "${workrepo}_fresh"; # workrepo lock covers
721         mkrepo_fromtemplate $destrepo;
722     }
723
724     policyhook(0, 'push-confirm', @policy_args);
725 }
726
727 sub onwardpush () {
728     my @cmd = (qw(git send-pack), $destrepo);
729     push @cmd, qw(--force) if $policy & NOFFCHECK;
730     push @cmd, "$commit:refs/dgit/$suite",
731                "$tagval:refs/tags/$tagname";
732     debugcmd @cmd;
733     $!=0;
734     my $r = system @cmd;
735     !$r or die "onward push to $destrepo failed: $r $!";
736 }
737
738 sub stunthook () {
739     debug "stunthook";
740     chdir $workrepo or die "chdir $workrepo: $!";
741     mkdir "dgit-tmp" or $!==EEXIST or die $!;
742     readupdates();
743     parsetag();
744     verifytag();
745     checks();
746     onwardpush();
747     debug "stunthook done.";
748 }
749
750 #----- git-upload-pack -----
751
752 sub fixmissing__git_upload_pack () {
753     $destrepo = "$dgitrepos/_empty";
754     my $lfh = locksometree($destrepo);
755     return if stat_exists $destrepo;
756     rmtree "$destrepo.new";
757     mkemptyrepo "$destrepo.new", "0644";
758     rename "$destrepo.new", $destrepo or die $!;
759     unlink "$destrepo.lock" or die $!;
760     close $lfh;
761 }
762
763 sub main__git_upload_pack () {
764     my $lfh = locksometree($destrepo);
765     chdir $destrepo or die "$destrepo: $!";
766     close $lfh;
767     runcmd qw(git upload-pack), ".";
768 }
769
770 #----- arg parsing and main program -----
771
772 sub argval () {
773     die unless @ARGV;
774     my $v = shift @ARGV;
775     die if $v =~ m/^-/;
776     return $v;
777 }
778
779 sub parseargsdispatch () {
780     die unless @ARGV;
781
782     delete $ENV{'GIT_DIR'}; # if not run via ssh, our parent git process
783     delete $ENV{'GIT_PREFIX'}; # sets these and they mess things up
784
785     if ($ENV{'DGIT_DRS_DEBUG'}) {
786         $debug='=';
787         open DEBUG, ">&STDERR" or die $!;
788     }
789
790     if ($ARGV[0] eq '--pre-receive-hook') {
791         if ($debug) { $debug.="="; }
792         shift @ARGV;
793         @ARGV == 1 or die;
794         $package = shift @ARGV;
795         defined($distro = $ENV{'DGIT_DRS_DISTRO'}) or die;
796         defined($suitesfile = $ENV{'DGIT_DRS_SUITES'}) or die;
797         defined($workrepo = $ENV{'DGIT_DRS_WORK'}) or die;
798         defined($destrepo = $ENV{'DGIT_DRS_DEST'}) or die;
799         defined($keyrings = $ENV{'DGIT_DRS_KEYRINGS'}) or die $!;
800         defined($policyhook = $ENV{'DGIT_DRS_POLICYHOOK'}) or die $!;
801         open STDOUT, ">&STDERR" or die $!;
802         eval {
803             stunthook();
804         };
805         if ($@) {
806             recorderror "$@" or die;
807             die $@;
808         }
809         exit 0;
810     }
811
812     $ENV{'DGIT_DRS_DISTRO'} = $distro = argval();
813     $ENV{'DGIT_DRS_SUITES'} = argval();
814     $ENV{'DGIT_DRS_KEYRINGS'} = argval();
815     $dgitrepos = argval();
816     $ENV{'DGIT_DRS_POLICYHOOK'} = $policyhook = argval();
817
818     die unless @ARGV==1 && $ARGV[0] eq '--ssh';
819
820     my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
821     $cmd =~ m{
822         ^
823         (?: \S* / )?
824         ( [-0-9a-z]+ )
825         \s+
826         '? (?: \S* / )?
827         ($package_re) \.git
828         '?$
829     }ox 
830     or reject "command string not understood";
831     my $method = $1;
832     $package = $2;
833     $realdestrepo = "$dgitrepos/$package.git";
834
835     my $funcn = $method;
836     $funcn =~ y/-/_/;
837     my $mainfunc = $main::{"main__$funcn"};
838
839     reject "unknown method" unless $mainfunc;
840
841     my $lfh = lockrealtree();
842
843     $policy = policyhook(FRESHREPO,'check-package',$package);
844     if ($policy & FRESHREPO) {
845         movetogarbage;
846     }
847
848     close $lfh;
849
850     if (stat_exists $realdestrepo) {
851         $destrepo = $realdestrepo;
852     } else {
853         debug " fixmissing $funcn";
854         my $fixfunc = $main::{"fixmissing__$funcn"};
855         &$fixfunc;
856     }
857
858     debug " running main $funcn";
859     &$mainfunc;
860 }
861
862 sub unlockall () {
863     while (my $fh = pop @lockfhs) { close $fh; }
864 }
865
866 sub cleanup () {
867     unlockall();
868     if (!chdir "$dgitrepos/_tmp") {
869         $!==ENOENT or die $!;
870         return;
871     }
872     foreach my $lf (<*.lock>) {
873         my $tree = $lf;
874         $tree =~ s/\.lock$//;
875         next unless acquirermtree($tree, 0);
876         remove $lf or warn $!;
877         unlockall();
878     }
879 }
880
881 parseargsdispatch();
882 cleanup();