chiark / gitweb /
dgit-repos-push-receiver: wip
[dgit.git] / dgit-repos-push-receiver
1 #!/usr/bin/perl -w
2 # dgit-repos-push-receiver
3 #
4 # usages:
5 #  .../dgit-repos-push-receiver DGIT-REPOS-DIR --ssh
6 #  .../dgit-repos-push-receiver DGIT-REPOS-DIR PACKAGE
7 # internal usage:
8 #  .../dgit-repos-push-receiver --pre-receive-hook PACKAGE
9 #
10 # Invoked as the ssh restricted command
11 #
12 # Works like git-receive-pack
13
14 use strict;
15
16 # What we do is this:
17 #  - extract the destination repo name somehow
18 #  - make a hardlink clone of the destination repo
19 #  - provide the destination with a stunt pre-receive hook
20 #  - run actual git-receive-pack with that new destination
21 #   as a result of this the stunt pre-receive hook runs; it does this
22 #     find the keyring(s) to use for verification
23 #     verify the signed tag
24 #     check that the signed tag has a suitable name
25 #     parse the signed tag body to extract the intended
26 #       distro and suite
27 #     check that the distro is right
28 #     check that the suite is the same as the branch we are
29 #       supposed to update
30 #     check that the signed tag refers to the same commit
31 #       as the new suite
32 #     check that the signer was correct
33 #     push the signed tag to the actual repo
34 #     push the new dgit branch head to the actual repo
35
36 use POSIX;
37 use Fcntl qw(:flock);
38
39 our $package_re = '[0-9a-z][-+.0-9a-z]+';
40
41 our $dgitrepos;
42 our $package;
43 our $destrepo;
44 our $workrepo;
45
46 sub acquirelock ($$) {
47     my ($lock, $must) = @_;
48     for (;;) {
49         my $fh = new IO::File, ">", $lock or die "open $lock: $!";
50         my $ok = flock $fh, $must ? LOCK_EX : (LOCK_EX|LOCK_NB);
51         if (!$ok) {
52             return unless $must;
53             die "flock $lock: $!";
54         }
55         if (!stat $lock) {
56             next if $! == ENOENT;
57             die "stat $lock: $!";
58         }
59         my $want = (stat _)[1];
60         stat $fh or die $!;
61         my $got = (stat _)[1];
62         return $fh if $got == $want;
63     }
64 }
65
66 sub makeworkingclone () {
67     $workrepo = "$dgitrepos/_tmp/$package_incoming$$";
68     my $lock = "$workrepo.lock";
69     my $lockfh = acquirelock($lock, 1);
70     if (!stat $destrepo) {
71         $! == ENOENT or die "stat dest repo $destrepo: $!";
72         mkdir $workrepo or die "create work repo $workrepo: $!";
73         runcmd qw(git init --bare), $workrepo;
74     } else {
75         runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
76     }
77 }
78
79 sub setupstunthook () {
80     my $prerecv = "$workrepo/hooks/pre-receive";
81     my $fh = new IO::File, $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
82         or die "$prerecv: $!";
83     print $fh <<END or die "$prerecv: $!";
84 #!/bin/sh
85 set -e
86 exec $0 --pre-receive-hook $package
87 END
88     close $fh or die "$prerecv: $!";
89     $ENV{'DGIT_RPR_WORK'}= $workrepo;
90     $ENV{'DGIT_RPR_DEST'}= $destrepo;
91 }
92
93 #----- stunt post-receive hook -----
94
95 our ($tagname, $tagval, $suite, $commit);
96 our ($version, %tagh);
97
98 sub readupdates () {
99     while (<STDIN>) {
100         m/^\S+ (\S+) (\S+)$/ or die "$_ ?";
101         my ($sha1, $refname) = ($1, $2);
102         if ($refname =~ m{^refs/tags/(?=debian/)}) {
103             die if defined $tagname;
104             $tagname = $'; #';
105             $tagval = $sha1;
106         } elsif ($refname =~ m{^refs/dgit/}) {
107             die if defined $suite;
108             $suite = $'; #';
109             $commit = $sha1;
110         } else {
111             die;
112         }
113     }
114     STDIN->error and die $!;
115
116     die unless defined $refname;
117     die unless defined $branchname;
118 }
119
120 sub parsetag () {
121     open PT, ">dgit-tmp/plaintext" or die $!;
122     open DS, ">dgit-tmp/plaintext.asc" or die $!;
123     open T, "-|", qw(git cat-file tag), $tagval or die $!;
124     my %tagh;
125     for (;;) {
126         $!=0; $_=<T>; defined or die $!;
127         print PT or die $!;
128         if (m/^(\S+) (.*)/) {
129             push @{ $tagh{$1} }, $2;
130         } elsif (!m/\S/) {
131             last;
132         } else {
133             die;
134         }
135     }
136     $!=0; $_=<T>; defined or die $!;
137     m/^($package_re) release (\S+) for (\S+) \[dgit\]$/ or die;
138
139     die unless $1 eq $package;
140     $version = $2;
141     die unless $3 eq $suite;
142
143     for (;;) {
144         print PT or die $!;
145         $!=0; $_=<T>; defined or die $!;
146         last if m/^-----BEGIN PGP/;
147     }
148     for (;;) {
149         print DS or die $!;
150         $!=0; $_=<T>;
151         last if !defined;
152     }
153     T->error and die $!;
154     close PT or die $!;
155     close DS or die $!;
156 }
157
158 sub checktag () {
159     tagh1('object') eq $branchval or die;
160     tagh1('type') eq 'commit' or die;
161     tagh1('tag') eq $tagname or die;
162
163     my $v = $version;
164     $v =~ y/~:/_%/;
165     $tagname eq "debian/$v" or die;
166 }
167
168
169 sub stunthook () {
170     chdir $workrepo or die "chdir $workrepo: $!";
171     mkdir "dgit-tmp" or $!==EEXIST or die $!;
172     readupdates();
173     parsetag();
174     verifytag();
175     checktag();
176 ... ...
177 }
178
179 #----- arg parsing and main program -----
180
181 sub parseargs () {
182     die unless @ARGV;
183
184     if ($ARGV[0] eq '--pre-receive-hook') {
185         shift @ARGV;
186         @ARGV == 1 or die;
187         $package = shift @ARGV;
188         defined($workrepo = $ENV{'DGIT_RPR_WORK'}) or die;
189         defined($destrepo = $ENV{'DGIT_RPR_DEST'}) or die;
190         open STDOUT, ">&STDERR" or die $!;
191         stunthook();
192         exit 0;
193     }
194
195     die if $ARGV[0] =~ m/^-/;
196     $dgitrepos = shift;
197
198     die unless @ARGV;
199     if ($ARGV[0] != m/^-/) {
200         @ARGV == 1 or die;
201         $package = shift @ARGV;
202     } elsif ($ARGV[0] eq '--ssh') {
203         shift @ARGV;
204         !@ARGV or die;
205         my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
206         $cmd =~ m{
207             ^
208             (?:\S*/)?
209             (?:dgit-repos-push-receiver|git-receive-pack)
210             \s+
211             (?:\S*/)?
212             ($package_re)\.git
213             $
214         }ox 
215             or die "requested command $cmd not understood";
216         $package = $1;
217     } else {
218         die;
219     }
220
221     $destrepo = "$dgitrepos/$package.git";
222 }
223
224 sub main () {
225     parseargs();
226     makeworkingclone();
227     setupstunthook();
228     runcmd qw(git receive-pack), $destdir;
229 }