5 # dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [<settings>] --ssh
6 # dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [<settings>] --cron
8 # --repos=GIT-REPOS-DIR default DISTRO-DIR/repos/
9 # --suites=SUITES-FILE default DISTRO-DIR/suites
10 # --suites-master=SUITES-FILE default DISTRO-DIR/suites-master
11 # --policy-hook=POLICY-HOOK default DISTRO-DIR/policy-hook
12 # --mirror-hook=MIRROR-HOOK default DISTRO-DIR/mirror-hook
13 # --dgit-live=DGIT-LIVE-DIR default DISTRO-DIR/dgit-live
14 # (DISTRO-DIR is not used other than as default and to pass to policy
17 # .../dgit-repos-server --pre-receive-hook PACKAGE
19 # Invoked as the ssh restricted command
21 # Works like git-receive-pack
23 # SUITES-FILE is the name of a file which lists the permissible suites
24 # one per line (#-comments and blank lines ignored). For --suites-master
25 # it is a list of the suite(s) which should, when pushed to, update
26 # `master' on the server (if fast forward).
28 # AUTH-SPEC is a :-separated list of
29 # KEYRING.GPG,AUTH-SPEC
30 # where AUTH-SPEC is one of
33 # (With --cron AUTH-SPEC is not used and may be the empty string.)
37 use Debian::Dgit qw(:DEFAULT :policyflags);
40 # DGIT-REPOS-DIR contains:
41 # git tree (or other object) lock (in acquisition order, outer first)
43 # _tmp/PACKAGE_prospective ! } SAME.lock, held during receive-pack
45 # _tmp/PACKAGE_incoming$$ ! } SAME.lock, held during receive-pack
46 # _tmp/PACKAGE_incoming$$_fresh ! }
48 # PACKAGE.git } PACKAGE.git.lock
49 # PACKAGE_garbage } (also covers executions of
50 # PACKAGE_garbage-old } policy hook script for PACKAGE)
51 # PACKAGE_garbage-tmp }
52 # policy* } (for policy hook script, covered by
53 # } lock only when invoked for a package)
55 # leaf locks, held during brief operaton only:
60 # _template } SAME.lock
62 # locks marked ! may be held during client data transfer
64 # What we do on push is this:
65 # - extract the destination repo name
66 # - make a hardlink clone of the destination repo
67 # - provide the destination with a stunt pre-receive hook
68 # - run actual git-receive-pack with that new destination
69 # as a result of this the stunt pre-receive hook runs; it does this:
70 # + understand what refs we are allegedly updating and
71 # check some correspondences:
72 # * we are updating only refs/tags/[archive/]DISTRO/* and refs/dgit/*
73 # * and only one of each
74 # * and the tag does not already exist
76 # * recover the suite name from the destination refs/dgit/ ref
77 # + disassemble the signed tag into its various fields and signature
79 # * parsing the first line of the tag message to recover
80 # the package name, version and suite
81 # * checking that the package name corresponds to the dest repo name
82 # * checking that the suite name is as recovered above
83 # + verify the signature on the signed tag
84 # and if necessary check that the keyid and package are listed in dm.txt
85 # + check various correspondences:
86 # * the signed tag must refer to a commit
87 # * the signed tag commit must be the refs/dgit value
88 # * the name in the signed tag must correspond to its ref name
89 # * the tag name must be [archive/]debian/<version> (massaged as needed)
90 # * the suite is one of those permitted
91 # * the signed tag has a suitable name
92 # * run the "push" policy hook
93 # * replay prevention for --deliberately-not-fast-forward
94 # * check the commit is a fast forward
95 # * handle a request from the policy hook for a fresh repo
96 # + push the signed tag and new dgit branch to the actual repo
98 # If the destination repo does not already exist, we need to make
99 # sure that we create it reasonably atomically, and also that
100 # we don't every have a destination repo containing no refs at all
101 # (because such a thing causes git-fetch-pack to barf). So then we
102 # do as above, except:
103 # - before starting, we take out our own lock for the destination repo
104 # - we create a prospective new destination repo by making a copy
106 # - we use the prospective new destination repo instead of the
107 # actual new destination repo (since the latter doesn't exist)
108 # - after git-receive-pack exits, we
109 # + check that the prospective repo contains a tag and head
110 # + rename the prospective destination repo into place
113 # - We are crash-only
114 # - Temporary working trees and their locks are cleaned up
115 # opportunistically by a program which tries to take each lock and
116 # if successful deletes both the tree and the lockfile
117 # - Prospective working trees and their locks are cleaned up by
118 # a program which tries to take each lock and if successful
119 # deletes any prospective working tree and the lock (but not
120 # of course any actual tree)
121 # - It is forbidden to _remove_ the lockfile without removing
122 # the corresponding temporary tree, as the lockfile is also
123 # a stampfile whose presence indicates that there may be
126 # Policy hook scripts are invoked like this:
127 # POLICY-HOOK-SCRIPT DISTRO DGIT-REPOS-DIR DGIT-LIVE-DIR DISTRO-DIR ACTION...
129 # POLICY-HOOK-SCRIPT ... check-list [...]
130 # POLICY-HOOK-SCRIPT ... check-package PACKAGE [...]
131 # POLICY-HOOK-SCRIPT ... push PACKAGE \
132 # VERSION SUITE TAGNAME DELIBERATELIES [...]
133 # POLICY-HOOK-SCRIPT ... push-confirm PACKAGE \
134 # VERSION SUITE TAGNAME DELIBERATELIES FRESH-REPO|'' [...]
136 # DELIBERATELIES is like this: --deliberately-foo,--deliberately-bar,...
138 # Exit status of policy hook is a bitmask.
139 # Bit weight constants are defined in Dgit.pm.
141 # suppress dgit-repos-server's fast-forward check ("push" only)
143 # blow away repo right away (ie, as if before push or fetch)
144 # ("check-package" and "push" only)
145 # any unexpected bits mean failure, and then known set bits are ignored
146 # if no unexpected bits set, operation continues (subject to meaning
147 # of any expected bits set). So, eg, exit 0 means "continue normally"
148 # and would be appropriate for an unknown action.
150 # cwd for push and push-confirm is a temporary repo where the incoming
151 # objects have been received; TAGNAME is the version-based tag.
153 # FRESH-REPO is '' iff the repo for this package already existed, or
154 # the pathname of the newly-created repo which will be renamed into
155 # place if everything goes well. (NB that this is generally not the
156 # same repo as the cwd, because the objects are first received into a
157 # temporary repo so they can be examined.) In this case FRESH-REPO
158 # contains exactly the objects and refs that will appear in the
159 # destination if push-confirm approves.
161 # if push requested FRESHREPO, push-confirm happens in the old working
162 # repo and FRESH-REPO is guaranteed not to be ''.
164 # policy hook for a particular package will be invoked only once at
165 # a time - (see comments about DGIT-REPOS-DIR, above)
167 # check-list and check-package are invoked via the --cron option.
168 # First, without any locking, check-list is called. It should produce
169 # a list of package names (one per line). Then check-package will be
170 # invoked for each named package, in each case after taking an
173 # If policy hook wants to run dgit (or something else in the dgit
174 # package), it should use DGIT-LIVE-DIR/dgit (etc.), or if that is
175 # ENOENT, use the installed version.
177 # Mirror hook scripts are invoked like this:
178 # MIRROR-HOOK-SCRIPT DISTRO-DIR ACTION...
179 # and currently there is only one action invoked by dgit-repos-server:
180 # MIRROR-HOOK-SCRIPT DISTRO-DIR updated-hook PACKAGE [...]
182 # Exit status of the mirror hook is advisory only. The mirror hook
183 # runs too late to do anything useful about a problem, so the only
184 # effect of a mirror hook exiting nonzero is a warning message to
185 # stderr (which the pushing user should end up seeing).
187 # If the mirror hook does not exist, it is silently skipped.
190 use Fcntl qw(:flock);
191 use File::Path qw(rmtree);
192 use File::Temp qw(tempfile);
201 our $suitesformasterfile;
216 #----- utilities -----
218 sub realdestrepo () { "$dgitrepos/$package.git"; }
220 sub acquirelock ($$) {
221 my ($lock, $must) = @_;
223 printdebug sprintf "locking %s %d\n", $lock, $must;
226 $fh = new IO::File $lock, ">" or die "open $lock: $!";
227 my $ok = flock $fh, $must ? LOCK_EX : (LOCK_EX|LOCK_NB);
229 die "flock $lock: $!" if $must;
230 printdebug " locking $lock failed\n";
233 next unless stat_exists $lock;
234 my $want = (stat _)[1];
236 my $got = (stat _)[1];
237 last if $got == $want;
242 sub acquirermtree ($$) {
243 my ($tree, $must) = @_;
244 my $fh = acquirelock("$tree.lock", $must);
252 sub locksometree ($) {
254 acquirelock("$tree.lock", 1);
257 sub lockrealtree () {
258 locksometree(realdestrepo);
261 sub mkrepotmp () { ensuredir "$dgitrepos/_tmp" };
263 sub removedtagsfile () { "$dgitrepos/_removed-tags/$package"; }
265 sub recorderror ($) {
267 my $w = $ENV{'DGIT_DRS_WORK'}; # we are in stunthook
270 open ERR, ">", "$w/drs-error" or die $!;
271 print ERR $why, "\n" or die $!;
280 recorderror "reject: $why";
281 die "\ndgit-repos-server: reject: $why\n\n";
288 die (shellquote @_)." $? $!" if $r;
292 my ($policyallowbits, @polargs) = @_;
293 # => ($exitstatuspolicybitmap);
294 die if $policyallowbits & ~0x3e;
295 my @cmd = ($policyhook,$distro,$dgitrepos,$dgitlive,$distrodir,@polargs);
298 die "system: $!" if $r < 0;
299 die "dgit-repos-server: policy hook failed (or rejected) ($?)\n"
300 if $r & ~($policyallowbits << 8);
301 printdebug sprintf "hook => %#x\n", $r;
305 sub mkemptyrepo ($$) {
306 my ($dir,$sharedperm) = @_;
307 runcmd qw(git init --bare --quiet), "--shared=$sharedperm", $dir;
310 sub mkrepo_fromtemplate ($) {
312 my $template = "$dgitrepos/_template";
313 my $templatelock = locksometree($template);
314 printdebug "copy template $template -> $dir\n";
315 my $r = system qw(cp -a --), $template, $dir;
316 !$r or die "create new repo $dir failed: $r $!";
320 sub movetogarbage () {
321 # realdestrepo must have been locked
323 my $real = realdestrepo;
324 return unless stat_exists $real;
326 my $garbagerepo = "$dgitrepos/${package}_garbage";
327 # We arrange to always keep at least one old tree, for recovery
328 # from mistakes. This is either $garbage or $garbage-old.
329 if (stat_exists "$garbagerepo") {
330 printdebug "movetogarbage: rmtree $garbagerepo-tmp\n";
331 rmtree "$garbagerepo-tmp";
332 if (rename "$garbagerepo-old", "$garbagerepo-tmp") {
333 printdebug "movetogarbage: $garbagerepo-old -> -tmp, rmtree\n";
334 rmtree "$garbagerepo-tmp";
336 die "$garbagerepo $!" unless $!==ENOENT;
337 printdebug "movetogarbage: $garbagerepo-old -> -tmp\n";
339 printdebug "movetogarbage: $garbagerepo -> -old\n";
340 rename "$garbagerepo", "$garbagerepo-old" or die "$garbagerepo $!";
343 ensuredir "$dgitrepos/_removed-tags";
344 open PREVIOUS, ">>", removedtagsfile or die removedtagsfile." $!";
345 git_for_each_ref([ map { 'refs/tags/'.$_ } debiantags('*',$distro) ],
347 my ($objid,$objtype,$fullrefname,$reftail) = @_;
348 print PREVIOUS "\n$objid $reftail .\n" or die $!;
350 close PREVIOUS or die $!;
352 printdebug "movetogarbage: $real -> $garbagerepo\n";
353 rename $real, $garbagerepo
355 or die "$garbagerepo $!";
358 sub policy_checkpackage () {
359 my $lfh = lockrealtree();
361 $policy = policyhook(FRESHREPO,'check-package',$package);
362 if ($policy & FRESHREPO) {
369 #----- git-receive-pack -----
371 sub fixmissing__git_receive_pack () {
373 $destrepo = "$dgitrepos/_tmp/${package}_prospective";
374 acquirermtree($destrepo, 1);
375 mkrepo_fromtemplate($destrepo);
378 sub makeworkingclone () {
380 $workrepo = "$dgitrepos/_tmp/${package}_incoming$$";
381 acquirermtree($workrepo, 1);
382 my $lfh = lockrealtree();
383 runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
385 rmtree "${workrepo}_fresh";
388 sub setupstunthook () {
389 my $prerecv = "$workrepo/hooks/pre-receive";
390 my $fh = new IO::File $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
391 or die "$prerecv: $!";
392 print $fh <<END or die "$prerecv: $!";
395 exec $0 --pre-receive-hook $package
397 close $fh or die "$prerecv: $!";
398 $ENV{'DGIT_DRS_WORK'}= $workrepo;
399 $ENV{'DGIT_DRS_DEST'}= $destrepo;
400 printdebug " stunt hook set up $prerecv\n";
403 sub dealwithfreshrepo () {
404 my $freshrepo = "${workrepo}_fresh";
405 return unless stat_exists $freshrepo;
406 $destrepo = $freshrepo;
410 my @cmd = ($mirrorhook,$distrodir,@_);
412 return unless stat_exists $mirrorhook;
416 dgit-repos-server: warning: mirror hook failed: %s
417 dgit-repos-server: push complete but may not fully visible.
419 ($r < 0 ? "exec: $!" :
420 $r == (124 << 8) ? "exited status 124 (timeout?)" :
421 !($r & ~0xff00) ? "exited ".($? >> 8) :
426 sub maybeinstallprospective () {
427 return if $destrepo eq realdestrepo;
429 if (open REJ, "<", "$workrepo/drs-error") {
432 REJ->error and die $!;
436 $!==&ENOENT or die $!;
439 printdebug " show-ref ($destrepo) ...\n";
441 my $child = open SR, "-|";
442 defined $child or die $!;
444 chdir $destrepo or die $!;
445 exec qw(git show-ref);
448 my %got = qw(newtag 0 omtag 0 head 0);
451 printdebug " show-refs| $_\n";
452 s/^\S*[1-9a-f]\S* (\S+)$/$1/ or die;
453 next if m{^refs/heads/master$};
455 m{^refs/tags/archive/} ? 'newtag' :
456 m{^refs/tags/} ? 'omtag' :
457 m{^refs/dgit/} ? 'head' :
462 $!=0; $?=0; close SR or $?==256 or die "$? $!";
464 printdebug "installprospective ?\n";
465 die Dumper(\%got)." -- missing refs in new repo"
466 unless $got{head} && grep { m/tag$/ && $got{$_} } keys %got;
470 if ($destrepo eq "${workrepo}_fresh") {
474 printdebug "install $destrepo => ".realdestrepo."\n";
475 rename $destrepo, realdestrepo or die $!;
476 remove realdestrepo.".lock" or die $!;
479 sub main__git_receive_pack () {
482 runcmd qw(git receive-pack), $workrepo;
484 maybeinstallprospective();
485 mirrorhook('updated-hook', $package);
488 #----- stunt post-receive hook -----
490 our ($tagname, $tagval, $suite, $oldcommit, $commit);
491 our ($version, %tagh);
492 our ($maint_tagname, $maint_tagval);
494 our ($tagexists_error);
497 printdebug " updates ...\n";
501 printdebug " upd.| $_\n";
502 m/^(\S+) (\S+) (\S+)$/ or die "$_ ?";
503 my ($old, $sha1, $refname) = ($1, $2, $3);
504 if ($refname =~ m{^refs/tags/(?=(?:archive/)?$distro/)}) {
507 $tagexists_error= "tag $tn already exists -".
508 " not replacing previously-pushed version"
510 } elsif ($refname =~ m{^refs/dgit/}) {
511 reject "pushing multiple heads!" if defined $suite;
516 reject "pushing unexpected ref!";
519 STDIN->error and die $!;
521 reject "push is missing tag ref update" unless %tags;
522 my @newtags = grep { m#^archive/# } keys %tags;
523 my @omtags = grep { !m#^archive/# } keys %tags;
524 reject "pushing too many similar tags" if @newtags>1 || @omtags>1;
526 ($tagname) = @newtags;
527 ($maint_tagname) = @omtags;
529 ($tagname) = @omtags or die;
531 $tagval = $tags{$tagname};
532 $maint_tagval = $tags{$maint_tagname // ''};
534 reject "push is missing head ref update" unless defined $suite;
535 printdebug " updates ok.\n";
539 printdebug " parsetag...\n";
540 open PT, ">dgit-tmp/plaintext" or die $!;
541 open DS, ">dgit-tmp/plaintext.asc" or die $!;
542 open T, "-|", qw(git cat-file tag), $tagval or die $!;
544 $!=0; $_=<T>; defined or die $!;
546 if (m/^(\S+) (.*)/) {
547 push @{ $tagh{$1} }, $2;
554 $!=0; $_=<T>; defined or die $!;
555 m/^($package_re) release (\S+) for \S+ \((\S+)\) \[dgit\]$/ or
556 reject "tag message not in expected format";
558 die unless $1 eq $package;
560 die "$3 != $suite " unless $3 eq $suite;
564 print PT $copyl or die $!;
565 $!=0; $_=<T>; defined or die "missing signature? $!";
567 if (m/^\[dgit ([^"].*)\]$/) { # [dgit "something"] is for future
570 if (s/^distro\=(\S+) //) {
571 die "$1 != $distro" unless $1 eq $distro;
572 } elsif (s/^(--deliberately-$deliberately_re) //) {
573 push @deliberatelies, $1;
574 } elsif (s/^previously:(\S+)=(\w+) //) {
575 die "previously $1 twice" if defined $previously{$1};
576 $previously{$1} = $2;
577 } elsif (s/^[-+.=0-9a-z]\S* //) {
579 die "unknown dgit info in tag ($_)";
584 last if m/^-----BEGIN PGP/;
595 printdebug " parsetag ok.\n";
598 sub checksig_keyring ($) {
599 my ($keyringfile) = @_;
600 # returns primary-keyid if signed by a key in this keyring
602 # or dies on other errors
606 printdebug " checksig keyring $keyringfile...\n";
608 our @cmd = (qw(gpgv --status-fd=1 --keyring),
610 qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext));
617 next unless s/^\[GNUPG:\] //;
619 printdebug " checksig| $_\n";
620 my @l = split / /, $_;
621 if ($l[0] eq 'NO_PUBKEY') {
623 } elsif ($l[0] eq 'VALIDSIG') {
625 $sigtype eq '00' or reject "signature is not of type 00!";
627 die unless defined $ok;
633 printdebug sprintf " checksig ok=%d\n", !!$ok;
638 sub dm_txt_check ($$) {
639 my ($keyid, $dmtxtfn) = @_;
640 printdebug " dm_txt_check $keyid $dmtxtfn\n";
641 open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
643 m/^fingerprint:\s+\Q$keyid\E$/oi
645 if (s/^allow:/ /i..0) {
648 or reject "key $keyid missing Allow section in permissions!";
653 or reject "package $package not allowed for key $keyid";
658 printdebug " dm_txt_check allow| $_\n";
659 foreach my $p (split /\s+/) {
660 if ($p eq $package) {
662 printdebug " dm_txt_check ok\n";
667 DT->error and die $!;
669 reject "key $keyid not in permissions list although in keyring!";
673 foreach my $kas (split /:/, $keyrings) {
674 printdebug "verifytag $kas...\n";
675 $kas =~ s/^([^,]+),// or die;
676 my $keyid = checksig_keyring $1;
677 if (defined $keyid) {
678 if ($kas =~ m/^a$/) {
679 printdebug "verifytag a ok\n";
681 } elsif ($kas =~ m/^m([^,]+)$/) {
682 dm_txt_check($keyid, $1);
683 printdebug "verifytag m ok\n";
690 reject "key not found in keyrings";
693 sub suite_is_in ($) {
695 printdebug "suite_is_in ($sf)\n";
696 if (!open SUITES, "<", $sf) {
697 $!==ENOENT or die $!;
705 return 1 if $_ eq $suite;
707 die $! if SUITES->error;
712 printdebug "checksuite ($suitesfile)\n";
713 return if suite_is_in $suitesfile;
714 reject "unknown suite";
717 sub checktagnoreplay () {
718 # We need to prevent a replay attack using an earlier signed tag.
719 # We also want to archive in the history the object ids of
720 # anything we remove, even if we get rid of the actual objects.
722 # So, we check that the signed tag mentions the name and tag
725 # (a) In the case of FRESHREPO: all tags and refs/heads/* in
726 # the repo. That is, effectively, all the things we are
729 # This prevents any tag implying a FRESHREPO push
730 # being replayed into a different state of the repo.
732 # There is still the folowing risk: If a non-ff push is of a
733 # head which is an ancestor of a previous ff-only push, the
734 # previous push can be replayed.
736 # So we keep a separate list, as a file in the repo, of all
737 # the tag object ids we have ever seen and removed. Any such
738 # tag object id will be rejected even for ff-only pushes.
740 # (b) In the case of just NOFFCHECK: all tags referring to the
741 # current head for the suite (there must be at least one).
743 # This prevents any tag implying a NOFFCHECK push being
744 # replayed to rewind from a different head.
746 # The possibility of an earlier ff-only push being replayed is
747 # eliminated as follows: the tag from such a push would still
748 # be in our repo, and therefore the replayed push would be
749 # rejected because the set of refs being updated would be
752 if (!open PREVIOUS, "<", removedtagsfile) {
753 die removedtagsfile." $!" unless $!==ENOENT;
755 # Protocol for updating this file is to append to it, not
756 # write-new-and-rename. So all updates are prefixed with \n
757 # and suffixed with " .\n" so that partial writes can be
760 next unless m/^(\w+) (.*) \.\n/;
761 next unless $1 eq $tagval;
762 reject "Replay of previously-rewound upload ($tagval $2)";
764 die removedtagsfile." $!" if PREVIOUS->error;
768 return unless $policy & (FRESHREPO|NOFFCHECK);
770 my $garbagerepo = "$dgitrepos/${package}_garbage";
776 my $check_ref_previously= sub {
777 my ($objid,$objtype,$fullrefname,$reftail) = @_;
778 my $supkey = $fullrefname;
779 $supkey =~ s{^refs/}{} or die "$supkey $objid ?";
780 my $supobjid = $previously{$supkey};
781 if (!defined $supobjid) {
782 printdebug "checktagnoreply - missing\n";
783 push @problems, "does not declare previously $supkey";
784 } elsif ($supobjid ne $objid) {
785 push @problems, "declared previously $supkey=$supobjid".
786 " but actually previously $supkey=$objid";
792 if ($policy & FRESHREPO) {
793 foreach my $kind (qw(tags heads)) {
794 git_for_each_ref("refs/$kind", $check_ref_previously);
797 my $branch= server_branch($suite);
798 my $branchhead= git_get_ref(server_ref($suite));
799 if (!length $branchhead) {
800 # No such branch - NOFFCHECK was unnecessary. Oh well.
801 printdebug "checktagnoreplay - not FRESHREPO, new branch, ok\n";
803 printdebug "checktagnoreplay - not FRESHREPO,".
804 " checking for overwriting refs/$branch=$branchhead\n";
805 git_for_each_tag_referring($branchhead, sub {
806 my ($tagobjid,$refobjid,$fullrefname,$tagname) = @_;
807 $check_ref_previously->($tagobjid,undef,$fullrefname,undef);
809 printdebug "checktagnoreplay - not FRESHREPO, nchecked=$nchecked";
810 push @problems, "does not declare previously any tag".
811 " referring to branch head $branch=$branchhead"
817 reject "replay attack prevention check failed:".
818 " signed tag for $version: ".
819 join("; ", @problems).
822 printdebug "checktagnoreplay - all ok ($tagval)\n"
827 my $vals = $tagh{$tag};
828 reject "missing header $tag in signed tag object" unless $vals;
829 reject "multiple headers $tag in signed tag object" unless @$vals == 1;
834 printdebug "checks\n";
836 tagh1('type') eq 'commit' or reject "tag refers to wrong kind of object";
837 tagh1('object') eq $commit or reject "tag refers to wrong commit";
838 tagh1('tag') eq $tagname or reject "tag name in tag is wrong";
840 my @expecttagnames = debiantags($version, $distro);
841 printdebug "expected tag @expecttagnames\n";
842 grep { $tagname eq $_ } @expecttagnames or die;
844 foreach my $othertag (grep { $_ ne $tagname } @expecttagnames) {
845 reject "tag $othertag (pushed with differing dgit version)".
847 " not replacing previously-pushed version"
848 if git_get_ref "refs/tags/".$othertag;
853 @policy_args = ($package,$version,$suite,$tagname,
854 join(",",@deliberatelies));
855 $policy = policyhook(NOFFCHECK|FRESHREPO, 'push', @policy_args);
857 if (defined $tagexists_error) {
858 if ($policy & FRESHREPO) {
859 printdebug "ignoring tagexists_error: $tagexists_error\n";
861 reject $tagexists_error;
868 # check that our ref is being fast-forwarded
869 printdebug "oldcommit $oldcommit\n";
870 if (!($policy & NOFFCHECK) && $oldcommit =~ m/[^0]/) {
871 $?=0; $!=0; my $mb = `git merge-base $commit $oldcommit`;
873 $mb eq $oldcommit or reject "not fast forward on dgit branch";
875 if ($policy & FRESHREPO) {
876 # It's a bit late to be discovering this here, isn't it ?
878 # What we do is: Generate a fresh destination repo right now,
879 # and arrange to treat it from now on as if it were a
882 # The presence of this fresh destination repo is detected by
883 # the parent, which responds by making a fresh master repo
884 # from the template. (If the repo didn't already exist then
885 # $destrepo was _prospective, and we change it here. This is
886 # OK because the parent's check for _fresh persuades it not to
889 $destrepo = "${workrepo}_fresh"; # workrepo lock covers
890 mkrepo_fromtemplate $destrepo;
895 my @cmdbase = (qw(git send-pack), $destrepo);
896 push @cmdbase, qw(--force) if $policy & NOFFCHECK;
899 push @cmd, "$commit:refs/dgit/$suite",
900 "$tagval:refs/tags/$tagname";
901 push @cmd, "$maint_tagval:refs/tags/$maint_tagname"
902 if defined $maint_tagname;
906 !$r or die "onward push to $destrepo failed: $r $!";
908 if (suite_is_in $suitesformasterfile) {
910 push @cmd, "$commit:refs/heads/master";
912 $!=0; my $r = system @cmd;
913 # tolerate errors (might be not ff)
914 !($r & ~0xff00) or die
915 "onward push to $destrepo#master failed: $r $!";
919 sub finalisepush () {
920 if ($destrepo eq realdestrepo) {
921 policyhook(0, 'push-confirm', @policy_args, '');
924 # We are to receive the push into a new repo (perhaps
925 # because the policy push hook asked us to with FRESHREPO, or
926 # perhaps because the repo didn't exist before).
928 # We want to provide the policy push-confirm hook with a repo
929 # which looks like the one which is going to be installed.
930 # The working repo is no good because it might contain
933 # So we push the objects into the prospective new repo right
934 # away. If the hook declines, we decline, and the prospective
935 # repo is never installed.
937 policyhook(0, 'push-confirm', @policy_args, $destrepo);
942 printdebug "stunthook in $workrepo\n";
943 chdir $workrepo or die "chdir $workrepo: $!";
944 mkdir "dgit-tmp" or $!==EEXIST or die $!;
950 printdebug "stunthook done.\n";
953 #----- git-upload-pack -----
955 sub fixmissing__git_upload_pack () {
956 $destrepo = "$dgitrepos/_empty";
957 my $lfh = locksometree($destrepo);
958 return if stat_exists $destrepo;
959 rmtree "$destrepo.new";
960 mkemptyrepo "$destrepo.new", "0644";
961 rename "$destrepo.new", $destrepo or die $!;
962 unlink "$destrepo.lock" or die $!;
966 sub main__git_upload_pack () {
967 my $lfh = locksometree($destrepo);
968 printdebug "git-upload-pack in $destrepo\n";
969 chdir $destrepo or die "$destrepo: $!";
971 runcmd qw(git upload-pack), ".";
974 #----- arg parsing and main program -----
984 # keys are used for DGIT_DRS_XXX too
985 'repos' => \$dgitrepos,
986 'suites' => \$suitesfile,
987 'suites-master' => \$suitesformasterfile,
988 'policy-hook' => \$policyhook,
989 'mirror-hook' => \$mirrorhook,
990 'dgit-live' => \$dgitlive,
993 our @hookenvs = qw(distro suitesfile suitesformasterfile policyhook
994 mirrorhook dgitlive keyrings dgitrepos distrodir);
996 # workrepo and destrepo handled ad-hoc
1001 my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
1011 or reject "command string not understood";
1015 my $funcn = $method;
1017 my $mainfunc = $main::{"main__$funcn"};
1019 reject "unknown method" unless $mainfunc;
1021 policy_checkpackage();
1023 if (stat_exists realdestrepo) {
1024 $destrepo = realdestrepo;
1026 printdebug " fixmissing $funcn\n";
1027 my $fixfunc = $main::{"fixmissing__$funcn"};
1031 printdebug " running main $funcn\n";
1038 my $listfh = tempfile();
1039 open STDOUT, ">&", $listfh or die $!;
1040 policyhook(0,'check-list');
1041 open STDOUT, ">&STDERR" or die $!;
1043 seek $listfh, 0, 0 or die $!;
1048 die unless m/^($package_re)$/;
1051 policy_checkpackage();
1053 die $! if $listfh->error;
1056 sub parseargsdispatch () {
1059 delete $ENV{'GIT_DIR'}; # if not run via ssh, our parent git process
1060 delete $ENV{'GIT_PREFIX'}; # sets these and they mess things up
1062 if ($ENV{'DGIT_DRS_DEBUG'}) {
1066 if ($ARGV[0] eq '--pre-receive-hook') {
1069 printdebug "in stunthook ".(shellquote @ARGV)."\n";
1070 foreach my $k (sort keys %ENV) {
1071 printdebug "$k=$ENV{$k}\n" if $k =~ m/^DGIT/;
1076 $package = shift @ARGV;
1077 ${ $main::{$_} } = $ENV{"DGIT_DRS_\U$_"} foreach @hookenvs;
1078 defined($workrepo = $ENV{'DGIT_DRS_WORK'}) or die;
1079 defined($destrepo = $ENV{'DGIT_DRS_DEST'}) or die;
1080 open STDOUT, ">&STDERR" or die $!;
1085 recorderror "$@" or die;
1092 $distrodir = argval();
1093 $keyrings = argval();
1095 foreach my $dk (keys %indistrodir) {
1096 ${ $indistrodir{$dk} } = "$distrodir/$dk";
1099 while (@ARGV && $ARGV[0] =~ m/^--([-0-9a-z]+)=/ && $indistrodir{$1}) {
1100 ${ $indistrodir{$1} } = $'; #';
1104 $ENV{"DGIT_DRS_\U$_"} = ${ $main::{$_} } foreach @hookenvs;
1106 die unless @ARGV==1;
1108 my $mode = shift @ARGV;
1109 die unless $mode =~ m/^--(\w+)$/;
1110 my $fn = ${*::}{"mode_$1"};
1116 while (my $fh = pop @lockfhs) { close $fh; }
1121 if (!chdir "$dgitrepos/_tmp") {
1122 $!==ENOENT or die $!;
1125 foreach my $lf (<*.lock>) {
1127 $tree =~ s/\.lock$//;
1128 next unless acquirermtree($tree, 0);
1129 remove $lf or warn $!;
1134 parseargsdispatch();