chiark / gitweb /
19776662c681432469df7ae5382060593efdd997
[dgit.git] / Debian / Dgit.pm
1 # -*- perl -*-
2 # dgit
3 # Debian::Dgit: functions common to dgit and its helpers and servers
4 #
5 # Copyright (C) 2015-2016  Ian Jackson
6 #
7 #    This program is free software; you can redistribute it and/or modify
8 #    it under the terms of the GNU General Public License as published by
9 #    the Free Software Foundation; either version 3 of the License, or
10 #    (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU General Public License for more details.
16 #
17 #    You should have received a copy of the GNU General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 package Debian::Dgit;
21
22 use strict;
23 use warnings;
24
25 use Carp;
26 use POSIX;
27 use IO::Handle;
28 use Config;
29 use Digest::SHA;
30 use Data::Dumper;
31 use IPC::Open2;
32 use File::Path;
33
34 BEGIN {
35     use Exporter   ();
36     our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
37
38     $VERSION     = 1.00;
39     @ISA         = qw(Exporter);
40     @EXPORT      = qw(setup_sigwarn forkcheck_setup forkcheck_mainprocess
41                       dep14_version_mangle
42                       debiantags debiantag_old debiantag_new
43                       server_branch server_ref
44                       stat_exists link_ltarget
45                       hashfile
46                       fail ensuredir must_getcwd executable_on_path
47                       waitstatusmsg failedcmd_waitstatus
48                       failedcmd_report_cmd failedcmd
49                       runcmd cmdoutput cmdoutput_errok
50                       git_rev_parse git_cat_file
51                       git_get_ref git_for_each_ref
52                       git_for_each_tag_referring is_fast_fwd
53                       $package_re $component_re $deliberately_re
54                       $distro_re $versiontag_re $series_filename_re
55                       $branchprefix
56                       initdebug enabledebug enabledebuglevel
57                       printdebug debugcmd
58                       $debugprefix *debuglevel *DEBUG
59                       shellquote printcmd messagequote
60                       $negate_harmful_gitattrs
61                       changedir git_slurp_config_src
62                       playtree_setup);
63     # implicitly uses $main::us
64     %EXPORT_TAGS = ( policyflags => [qw(NOFFCHECK FRESHREPO NOCOMMITCHECK)] );
65     @EXPORT_OK   = @{ $EXPORT_TAGS{policyflags} };
66 }
67
68 our @EXPORT_OK;
69
70 our $package_re = '[0-9a-z][-+.0-9a-z]*';
71 our $component_re = '[0-9a-zA-Z][-+.0-9a-zA-Z]*';
72 our $deliberately_re = "(?:TEST-)?$package_re";
73 our $distro_re = $component_re;
74 our $versiontag_re = qr{[-+.\%_0-9a-zA-Z/]+};
75 our $branchprefix = 'dgit';
76 our $series_filename_re = qr{(?:^|\.)series(?!\n)$}s;
77
78 # policy hook exit status bits
79 # see dgit-repos-server head comment for documentation
80 # 1 is reserved in case something fails with `exit 1' and to spot
81 # dynamic loader, runtime, etc., failures, which report 127 or 255
82 sub NOFFCHECK () { return 0x2; }
83 sub FRESHREPO () { return 0x4; }
84 sub NOCOMMITCHECK () { return 0x8; }
85
86 our $debugprefix;
87 our $debuglevel = 0;
88
89 our $negate_harmful_gitattrs = "-text -eol -crlf -ident -filter";
90
91 our $forkcheck_mainprocess;
92
93 sub forkcheck_setup () {
94     $forkcheck_mainprocess = $$;
95 }
96
97 sub forkcheck_mainprocess () {
98     # You must have called forkcheck_setup or setup_sigwarn already
99     getppid != $forkcheck_mainprocess;
100 }
101
102 sub setup_sigwarn () {
103     forkcheck_setup();
104     $SIG{__WARN__} = sub { 
105         die $_[0] if forkcheck_mainprocess;
106     };
107 }
108
109 sub initdebug ($) { 
110     ($debugprefix) = @_;
111     open DEBUG, ">/dev/null" or die $!;
112 }
113
114 sub enabledebug () {
115     open DEBUG, ">&STDERR" or die $!;
116     DEBUG->autoflush(1);
117     $debuglevel ||= 1;
118 }
119     
120 sub enabledebuglevel ($) {
121     my ($newlevel) = @_; # may be undef (eg from env var)
122     die if $debuglevel;
123     $newlevel //= 0;
124     $newlevel += 0;
125     return unless $newlevel;
126     $debuglevel = $newlevel;
127     enabledebug();
128 }
129     
130 sub printdebug {
131     print DEBUG $debugprefix, @_ or die $! if $debuglevel>0;
132 }
133
134 sub messagequote ($) {
135     local ($_) = @_;
136     s{\\}{\\\\}g;
137     s{\n}{\\n}g;
138     s{\x08}{\\b}g;
139     s{\t}{\\t}g;
140     s{[\000-\037\177]}{ sprintf "\\x%02x", ord $& }ge;
141     $_;
142 }
143
144 sub shellquote {
145     my @out;
146     local $_;
147     defined or confess 'internal error' foreach @_;
148     foreach my $a (@_) {
149         $_ = $a;
150         if (!length || m{[^-=_./:0-9a-z]}i) {
151             s{['\\]}{'\\$&'}g;
152             push @out, "'$_'";
153         } else {
154             push @out, $_;
155         }
156     }
157     return join ' ', @out;
158 }
159
160 sub printcmd {
161     my $fh = shift @_;
162     my $intro = shift @_;
163     print $fh $intro," " or die $!;
164     print $fh shellquote @_ or die $!;
165     print $fh "\n" or die $!;
166 }
167
168 sub debugcmd {
169     my $extraprefix = shift @_;
170     printcmd(\*DEBUG,$debugprefix.$extraprefix,@_) if $debuglevel>0;
171 }
172
173 sub dep14_version_mangle ($) {
174     my ($v) = @_;
175     # DEP-14 patch proposed 2016-11-09  "Version Mangling"
176     $v =~ y/~:/_%/;
177     $v =~ s/\.(?=\.|$|lock$)/.#/g;
178     return $v;
179 }
180
181 sub debiantag_old ($$) { 
182     my ($v,$distro) = @_;
183     return "$distro/". dep14_version_mangle $v;
184 }
185
186 sub debiantag_new ($$) { 
187     my ($v,$distro) = @_;
188     return "archive/$distro/".dep14_version_mangle $v;
189 }
190
191 sub debiantags ($$) {
192     my ($version,$distro) = @_;
193     map { $_->($version, $distro) } (\&debiantag_new, \&debiantag_old);
194 }
195
196 sub server_branch ($) { return "$branchprefix/$_[0]"; }
197 sub server_ref ($) { return "refs/".server_branch($_[0]); }
198
199 sub stat_exists ($) {
200     my ($f) = @_;
201     return 1 if stat $f;
202     return 0 if $!==&ENOENT;
203     die "stat $f: $!";
204 }
205
206 sub _us () {
207     $::us // ($0 =~ m#[^/]*$#, $&);
208 }
209
210 sub fail { 
211     my $s = "@_\n";
212     $s =~ s/\n\n$/\n/;
213     my $prefix = _us().": ";
214     $s =~ s/^/$prefix/gm;
215     die $s;
216 }
217
218 sub ensuredir ($) {
219     my ($dir) = @_; # does not create parents
220     return if mkdir $dir;
221     return if $! == EEXIST;
222     die "mkdir $dir: $!";
223 }
224
225 sub must_getcwd () {
226     my $d = getcwd();
227     defined $d or fail "getcwd failed: $!";
228     return $d;
229 }
230
231 sub executable_on_path ($) {
232     my ($program) = @_;
233     return 1 if $program =~ m{/};
234     my @path = split /:/, ($ENV{PATH} // "/usr/local/bin:/bin:/usr/bin");
235     foreach my $pe (@path) {
236         my $here = "$pe/$program";
237         return $here if stat_exists $here && -x _;
238     }
239     return undef;
240 }
241
242 our @signames = split / /, $Config{sig_name};
243
244 sub waitstatusmsg () {
245     if (!$?) {
246         return "terminated, reporting successful completion";
247     } elsif (!($? & 255)) {
248         return "failed with error exit status ".WEXITSTATUS($?);
249     } elsif (WIFSIGNALED($?)) {
250         my $signum=WTERMSIG($?);
251         return "died due to fatal signal ".
252             ($signames[$signum] // "number $signum").
253             ($? & 128 ? " (core dumped)" : ""); # POSIX(3pm) has no WCOREDUMP
254     } else {
255         return "failed with unknown wait status ".$?;
256     }
257 }
258
259 sub failedcmd_report_cmd {
260     my $intro = shift @_;
261     $intro //= "failed command";
262     { local ($!); printcmd \*STDERR, _us().": $intro:", @_ or die $!; };
263 }
264
265 sub failedcmd_waitstatus {
266     if ($? < 0) {
267         return "failed to fork/exec: $!";
268     } elsif ($?) {
269         return "subprocess ".waitstatusmsg();
270     } else {
271         return "subprocess produced invalid output";
272     }
273 }
274
275 sub failedcmd {
276     # Expects $!,$? as set by close - see below.
277     # To use with system(), set $?=-1 first.
278     #
279     # Actual behaviour of perl operations:
280     #   success              $!==0       $?==0       close of piped open
281     #   program failed       $!==0       $? >0       close of piped open
282     #   syscall failure      $! >0       $?=-1       close of piped open
283     #   failure              $! >0       unchanged   close of something else
284     #   success              trashed     $?==0       system
285     #   program failed       trashed     $? >0       system
286     #   syscall failure      $! >0       unchanged   system
287     failedcmd_report_cmd undef, @_;
288     fail failedcmd_waitstatus();
289 }
290
291 sub runcmd {
292     debugcmd "+",@_;
293     $!=0; $?=-1;
294     failedcmd @_ if system @_;
295 }
296
297 sub cmdoutput_errok {
298     confess Dumper(\@_)." ?" if grep { !defined } @_;
299     debugcmd "|",@_;
300     open P, "-|", @_ or die "$_[0] $!";
301     my $d;
302     $!=0; $?=0;
303     { local $/ = undef; $d = <P>; }
304     die $! if P->error;
305     if (!close P) { printdebug "=>!$?\n"; return undef; }
306     chomp $d;
307     if ($debuglevel > 0) {
308         $d =~ m/^.*/;
309         my $dd = $&;
310         my $more = (length $' ? '...' : ''); #');
311         $dd =~ s{[^\n -~]|\\}{ sprintf "\\x%02x", ord $& }ge;
312         printdebug "=> \`$dd'",$more,"\n";
313     }
314     return $d;
315 }
316
317 sub cmdoutput {
318     my $d = cmdoutput_errok @_;
319     defined $d or failedcmd @_;
320     return $d;
321 }
322
323 sub link_ltarget ($$) {
324     my ($old,$new) = @_;
325     lstat $old or return undef;
326     if (-l _) {
327         $old = cmdoutput qw(realpath  --), $old;
328     }
329     my $r = link $old, $new;
330     $r = symlink $old, $new if !$r && $!==EXDEV;
331     $r or die "(sym)link $old $new: $!";
332 }
333
334 sub hashfile ($) {
335     my ($fn) = @_;
336     my $h = Digest::SHA->new(256);
337     $h->addfile($fn);
338     return $h->hexdigest();
339 }
340
341 sub git_rev_parse ($) {
342     return cmdoutput qw(git rev-parse), "$_[0]~0";
343 }
344
345 sub git_cat_file ($) {
346     my ($objname) = @_;
347     # => ($type, $data) or ('missing', undef)
348     # in scalar context, just the data
349     our ($gcf_pid, $gcf_i, $gcf_o);
350     if (!$gcf_pid) {
351         my @cmd = qw(git cat-file --batch);
352         debugcmd "GCF|", @cmd;
353         $gcf_pid = open2 $gcf_o, $gcf_i, @cmd or die $!;
354     }
355     printdebug "GCF>| ", $objname, "\n";
356     print $gcf_i $objname, "\n" or die $!;
357     my $x = <$gcf_o>;
358     printdebug "GCF<| ", $x;
359     if ($x =~ m/ (missing)$/) { return ($1, undef); }
360     my ($type, $size) = $x =~ m/^.* (\w+) (\d+)\n/ or die "$objname ?";
361     my $data;
362     (read $gcf_o, $data, $size) == $size or die "$objname $!";
363     $x = <$gcf_o>;
364     $x eq "\n" or die "$objname ($_) $!";
365     return ($type, $data);
366 }
367
368 sub git_for_each_ref ($$;$) {
369     my ($pattern,$func,$gitdir) = @_;
370     # calls $func->($objid,$objtype,$fullrefname,$reftail);
371     # $reftail is RHS of ref after refs/[^/]+/
372     # breaks if $pattern matches any ref `refs/blah' where blah has no `/'
373     # $pattern may be an array ref to mean multiple patterns
374     $pattern = [ $pattern ] unless ref $pattern;
375     my @cmd = (qw(git for-each-ref), @$pattern);
376     if (defined $gitdir) {
377         @cmd = ('sh','-ec','cd "$1"; shift; exec "$@"','x', $gitdir, @cmd);
378     }
379     open GFER, "-|", @cmd or die $!;
380     debugcmd "|", @cmd;
381     while (<GFER>) {
382         chomp or die "$_ ?";
383         printdebug "|> ", $_, "\n";
384         m#^(\w+)\s+(\w+)\s+(refs/[^/]+/(\S+))$# or die "$_ ?";
385         $func->($1,$2,$3,$4);
386     }
387     $!=0; $?=0; close GFER or die "$pattern $? $!";
388 }
389
390 sub git_get_ref ($) {
391     # => '' if no such ref
392     my ($refname) = @_;
393     local $_ = $refname;
394     s{^refs/}{[r]efs/} or die "$refname $_ ?";
395     return cmdoutput qw(git for-each-ref --format=%(objectname)), $_;
396 }
397
398 sub git_for_each_tag_referring ($$) {
399     my ($objreferring, $func) = @_;
400     # calls $func->($tagobjid,$refobjid,$fullrefname,$tagname);
401     printdebug "git_for_each_tag_referring ",
402         ($objreferring // 'UNDEF'),"\n";
403     git_for_each_ref('refs/tags', sub {
404         my ($tagobjid,$objtype,$fullrefname,$tagname) = @_;
405         return unless $objtype eq 'tag';
406         my $refobjid = git_rev_parse $tagobjid;
407         return unless
408             !defined $objreferring # caller wants them all
409             or $tagobjid eq $objreferring
410             or $refobjid eq $objreferring;
411         $func->($tagobjid,$refobjid,$fullrefname,$tagname);
412     });
413 }
414
415 sub is_fast_fwd ($$) {
416     my ($ancestor,$child) = @_;
417     my @cmd = (qw(git merge-base), $ancestor, $child);
418     my $mb = cmdoutput_errok @cmd;
419     if (defined $mb) {
420         return git_rev_parse($mb) eq git_rev_parse($ancestor);
421     } else {
422         $?==256 or failedcmd @cmd;
423         return 0;
424     }
425 }
426
427 sub changedir ($) {
428     my ($newdir) = @_;
429     printdebug "CD $newdir\n";
430     chdir $newdir or confess "chdir: $newdir: $!";
431 }
432
433 sub git_slurp_config_src ($) {
434     my ($src) = @_;
435     # returns $r such that $r->{KEY}[] = VALUE
436     my @cmd = (qw(git config -z --get-regexp), "--$src", qw(.*));
437     debugcmd "|",@cmd;
438
439     local ($debuglevel) = $debuglevel-2;
440     local $/="\0";
441
442     my $r = { };
443     open GITS, "-|", @cmd or die $!;
444     while (<GITS>) {
445         chomp or die;
446         printdebug "=> ", (messagequote $_), "\n";
447         m/\n/ or die "$_ ?";
448         push @{ $r->{$`} }, $'; #';
449     }
450     $!=0; $?=0;
451     close GITS
452         or ($!==0 && $?==256)
453         or failedcmd @cmd;
454     return $r;
455 }
456
457 sub playtree_setup ($) {
458     # for use in the playtree
459     my ($t_local_git_cfg) = @_;
460     # should be run in a directory .git/FOO/BAR of a working tree
461     runcmd qw(git init -q);
462     runcmd qw(git config gc.auto 0);
463     foreach my $copy (qw(user.email user.name user.useConfigOnly
464                          core.sharedRepository
465                          core.compression core.looseCompression
466                          core.bigFileThreshold core.fsyncObjectFiles)) {
467         my $v = $t_local_git_cfg->{$copy};
468         next unless $v;
469         runcmd qw(git config), $copy, $_ foreach @$v;
470     }
471     rmtree('.git/objects');
472     symlink '../../../../objects','.git/objects' or die $!;
473     ensuredir '.git/info';
474     open GA, "> .git/info/attributes" or die $!;
475     print GA "* $negate_harmful_gitattrs\n" or die $!;
476     close GA or die $!;
477 }
478
479 our $local_git_cfg;
480
481 1;