chiark / gitweb /
playground refactoring: Dgit.pm: Provide ensure_a_playground
[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 use File::Basename;
34
35 BEGIN {
36     use Exporter   ();
37     our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
38
39     $VERSION     = 1.00;
40     @ISA         = qw(Exporter);
41     @EXPORT      = qw(setup_sigwarn forkcheck_setup forkcheck_mainprocess
42                       dep14_version_mangle
43                       debiantags debiantag_old debiantag_new
44                       server_branch server_ref
45                       stat_exists link_ltarget
46                       hashfile
47                       fail ensuredir must_getcwd executable_on_path
48                       waitstatusmsg failedcmd_waitstatus
49                       failedcmd_report_cmd failedcmd
50                       runcmd cmdoutput cmdoutput_errok
51                       git_rev_parse git_cat_file
52                       git_get_ref git_for_each_ref
53                       git_for_each_tag_referring is_fast_fwd
54                       $package_re $component_re $deliberately_re
55                       $distro_re $versiontag_re $series_filename_re
56                       $branchprefix
57                       initdebug enabledebug enabledebuglevel
58                       printdebug debugcmd
59                       $debugprefix *debuglevel *DEBUG
60                       shellquote printcmd messagequote
61                       $negate_harmful_gitattrs
62                       changedir git_slurp_config_src
63                       playtree_setup);
64     # implicitly uses $main::us
65     %EXPORT_TAGS = ( policyflags => [qw(NOFFCHECK FRESHREPO NOCOMMITCHECK)],
66                      playground => [qw(record_maindir $maindir $local_git_cfg
67                                        fresh_playground $playground
68                                        ensure_a_playground)]);
69     @EXPORT_OK   = ( @{ $EXPORT_TAGS{policyflags} },
70                      @{ $EXPORT_TAGS{playground} } );
71 }
72
73 our @EXPORT_OK;
74
75 our $package_re = '[0-9a-z][-+.0-9a-z]*';
76 our $component_re = '[0-9a-zA-Z][-+.0-9a-zA-Z]*';
77 our $deliberately_re = "(?:TEST-)?$package_re";
78 our $distro_re = $component_re;
79 our $versiontag_re = qr{[-+.\%_0-9a-zA-Z/]+};
80 our $branchprefix = 'dgit';
81 our $series_filename_re = qr{(?:^|\.)series(?!\n)$}s;
82
83 # policy hook exit status bits
84 # see dgit-repos-server head comment for documentation
85 # 1 is reserved in case something fails with `exit 1' and to spot
86 # dynamic loader, runtime, etc., failures, which report 127 or 255
87 sub NOFFCHECK () { return 0x2; }
88 sub FRESHREPO () { return 0x4; }
89 sub NOCOMMITCHECK () { return 0x8; }
90
91 our $debugprefix;
92 our $debuglevel = 0;
93
94 our $negate_harmful_gitattrs = "-text -eol -crlf -ident -filter";
95
96 our $forkcheck_mainprocess;
97
98 sub forkcheck_setup () {
99     $forkcheck_mainprocess = $$;
100 }
101
102 sub forkcheck_mainprocess () {
103     # You must have called forkcheck_setup or setup_sigwarn already
104     getppid != $forkcheck_mainprocess;
105 }
106
107 sub setup_sigwarn () {
108     forkcheck_setup();
109     $SIG{__WARN__} = sub { 
110         die $_[0] if forkcheck_mainprocess;
111     };
112 }
113
114 sub initdebug ($) { 
115     ($debugprefix) = @_;
116     open DEBUG, ">/dev/null" or die $!;
117 }
118
119 sub enabledebug () {
120     open DEBUG, ">&STDERR" or die $!;
121     DEBUG->autoflush(1);
122     $debuglevel ||= 1;
123 }
124     
125 sub enabledebuglevel ($) {
126     my ($newlevel) = @_; # may be undef (eg from env var)
127     die if $debuglevel;
128     $newlevel //= 0;
129     $newlevel += 0;
130     return unless $newlevel;
131     $debuglevel = $newlevel;
132     enabledebug();
133 }
134     
135 sub printdebug {
136     print DEBUG $debugprefix, @_ or die $! if $debuglevel>0;
137 }
138
139 sub messagequote ($) {
140     local ($_) = @_;
141     s{\\}{\\\\}g;
142     s{\n}{\\n}g;
143     s{\x08}{\\b}g;
144     s{\t}{\\t}g;
145     s{[\000-\037\177]}{ sprintf "\\x%02x", ord $& }ge;
146     $_;
147 }
148
149 sub shellquote {
150     my @out;
151     local $_;
152     defined or confess 'internal error' foreach @_;
153     foreach my $a (@_) {
154         $_ = $a;
155         if (!length || m{[^-=_./:0-9a-z]}i) {
156             s{['\\]}{'\\$&'}g;
157             push @out, "'$_'";
158         } else {
159             push @out, $_;
160         }
161     }
162     return join ' ', @out;
163 }
164
165 sub printcmd {
166     my $fh = shift @_;
167     my $intro = shift @_;
168     print $fh $intro," " or die $!;
169     print $fh shellquote @_ or die $!;
170     print $fh "\n" or die $!;
171 }
172
173 sub debugcmd {
174     my $extraprefix = shift @_;
175     printcmd(\*DEBUG,$debugprefix.$extraprefix,@_) if $debuglevel>0;
176 }
177
178 sub dep14_version_mangle ($) {
179     my ($v) = @_;
180     # DEP-14 patch proposed 2016-11-09  "Version Mangling"
181     $v =~ y/~:/_%/;
182     $v =~ s/\.(?=\.|$|lock$)/.#/g;
183     return $v;
184 }
185
186 sub debiantag_old ($$) { 
187     my ($v,$distro) = @_;
188     return "$distro/". dep14_version_mangle $v;
189 }
190
191 sub debiantag_new ($$) { 
192     my ($v,$distro) = @_;
193     return "archive/$distro/".dep14_version_mangle $v;
194 }
195
196 sub debiantags ($$) {
197     my ($version,$distro) = @_;
198     map { $_->($version, $distro) } (\&debiantag_new, \&debiantag_old);
199 }
200
201 sub server_branch ($) { return "$branchprefix/$_[0]"; }
202 sub server_ref ($) { return "refs/".server_branch($_[0]); }
203
204 sub stat_exists ($) {
205     my ($f) = @_;
206     return 1 if stat $f;
207     return 0 if $!==&ENOENT;
208     die "stat $f: $!";
209 }
210
211 sub _us () {
212     $::us // ($0 =~ m#[^/]*$#, $&);
213 }
214
215 sub fail { 
216     my $s = "@_\n";
217     $s =~ s/\n\n$/\n/;
218     my $prefix = _us().": ";
219     $s =~ s/^/$prefix/gm;
220     die $s;
221 }
222
223 sub ensuredir ($) {
224     my ($dir) = @_; # does not create parents
225     return if mkdir $dir;
226     return if $! == EEXIST;
227     die "mkdir $dir: $!";
228 }
229
230 sub must_getcwd () {
231     my $d = getcwd();
232     defined $d or fail "getcwd failed: $!";
233     return $d;
234 }
235
236 sub executable_on_path ($) {
237     my ($program) = @_;
238     return 1 if $program =~ m{/};
239     my @path = split /:/, ($ENV{PATH} // "/usr/local/bin:/bin:/usr/bin");
240     foreach my $pe (@path) {
241         my $here = "$pe/$program";
242         return $here if stat_exists $here && -x _;
243     }
244     return undef;
245 }
246
247 our @signames = split / /, $Config{sig_name};
248
249 sub waitstatusmsg () {
250     if (!$?) {
251         return "terminated, reporting successful completion";
252     } elsif (!($? & 255)) {
253         return "failed with error exit status ".WEXITSTATUS($?);
254     } elsif (WIFSIGNALED($?)) {
255         my $signum=WTERMSIG($?);
256         return "died due to fatal signal ".
257             ($signames[$signum] // "number $signum").
258             ($? & 128 ? " (core dumped)" : ""); # POSIX(3pm) has no WCOREDUMP
259     } else {
260         return "failed with unknown wait status ".$?;
261     }
262 }
263
264 sub failedcmd_report_cmd {
265     my $intro = shift @_;
266     $intro //= "failed command";
267     { local ($!); printcmd \*STDERR, _us().": $intro:", @_ or die $!; };
268 }
269
270 sub failedcmd_waitstatus {
271     if ($? < 0) {
272         return "failed to fork/exec: $!";
273     } elsif ($?) {
274         return "subprocess ".waitstatusmsg();
275     } else {
276         return "subprocess produced invalid output";
277     }
278 }
279
280 sub failedcmd {
281     # Expects $!,$? as set by close - see below.
282     # To use with system(), set $?=-1 first.
283     #
284     # Actual behaviour of perl operations:
285     #   success              $!==0       $?==0       close of piped open
286     #   program failed       $!==0       $? >0       close of piped open
287     #   syscall failure      $! >0       $?=-1       close of piped open
288     #   failure              $! >0       unchanged   close of something else
289     #   success              trashed     $?==0       system
290     #   program failed       trashed     $? >0       system
291     #   syscall failure      $! >0       unchanged   system
292     failedcmd_report_cmd undef, @_;
293     fail failedcmd_waitstatus();
294 }
295
296 sub runcmd {
297     debugcmd "+",@_;
298     $!=0; $?=-1;
299     failedcmd @_ if system @_;
300 }
301
302 sub cmdoutput_errok {
303     confess Dumper(\@_)." ?" if grep { !defined } @_;
304     debugcmd "|",@_;
305     open P, "-|", @_ or die "$_[0] $!";
306     my $d;
307     $!=0; $?=0;
308     { local $/ = undef; $d = <P>; }
309     die $! if P->error;
310     if (!close P) { printdebug "=>!$?\n"; return undef; }
311     chomp $d;
312     if ($debuglevel > 0) {
313         $d =~ m/^.*/;
314         my $dd = $&;
315         my $more = (length $' ? '...' : ''); #');
316         $dd =~ s{[^\n -~]|\\}{ sprintf "\\x%02x", ord $& }ge;
317         printdebug "=> \`$dd'",$more,"\n";
318     }
319     return $d;
320 }
321
322 sub cmdoutput {
323     my $d = cmdoutput_errok @_;
324     defined $d or failedcmd @_;
325     return $d;
326 }
327
328 sub link_ltarget ($$) {
329     my ($old,$new) = @_;
330     lstat $old or return undef;
331     if (-l _) {
332         $old = cmdoutput qw(realpath  --), $old;
333     }
334     my $r = link $old, $new;
335     $r = symlink $old, $new if !$r && $!==EXDEV;
336     $r or die "(sym)link $old $new: $!";
337 }
338
339 sub hashfile ($) {
340     my ($fn) = @_;
341     my $h = Digest::SHA->new(256);
342     $h->addfile($fn);
343     return $h->hexdigest();
344 }
345
346 sub git_rev_parse ($) {
347     return cmdoutput qw(git rev-parse), "$_[0]~0";
348 }
349
350 sub git_cat_file ($) {
351     my ($objname) = @_;
352     # => ($type, $data) or ('missing', undef)
353     # in scalar context, just the data
354     our ($gcf_pid, $gcf_i, $gcf_o);
355     if (!$gcf_pid) {
356         my @cmd = qw(git cat-file --batch);
357         debugcmd "GCF|", @cmd;
358         $gcf_pid = open2 $gcf_o, $gcf_i, @cmd or die $!;
359     }
360     printdebug "GCF>| ", $objname, "\n";
361     print $gcf_i $objname, "\n" or die $!;
362     my $x = <$gcf_o>;
363     printdebug "GCF<| ", $x;
364     if ($x =~ m/ (missing)$/) { return ($1, undef); }
365     my ($type, $size) = $x =~ m/^.* (\w+) (\d+)\n/ or die "$objname ?";
366     my $data;
367     (read $gcf_o, $data, $size) == $size or die "$objname $!";
368     $x = <$gcf_o>;
369     $x eq "\n" or die "$objname ($_) $!";
370     return ($type, $data);
371 }
372
373 sub git_for_each_ref ($$;$) {
374     my ($pattern,$func,$gitdir) = @_;
375     # calls $func->($objid,$objtype,$fullrefname,$reftail);
376     # $reftail is RHS of ref after refs/[^/]+/
377     # breaks if $pattern matches any ref `refs/blah' where blah has no `/'
378     # $pattern may be an array ref to mean multiple patterns
379     $pattern = [ $pattern ] unless ref $pattern;
380     my @cmd = (qw(git for-each-ref), @$pattern);
381     if (defined $gitdir) {
382         @cmd = ('sh','-ec','cd "$1"; shift; exec "$@"','x', $gitdir, @cmd);
383     }
384     open GFER, "-|", @cmd or die $!;
385     debugcmd "|", @cmd;
386     while (<GFER>) {
387         chomp or die "$_ ?";
388         printdebug "|> ", $_, "\n";
389         m#^(\w+)\s+(\w+)\s+(refs/[^/]+/(\S+))$# or die "$_ ?";
390         $func->($1,$2,$3,$4);
391     }
392     $!=0; $?=0; close GFER or die "$pattern $? $!";
393 }
394
395 sub git_get_ref ($) {
396     # => '' if no such ref
397     my ($refname) = @_;
398     local $_ = $refname;
399     s{^refs/}{[r]efs/} or die "$refname $_ ?";
400     return cmdoutput qw(git for-each-ref --format=%(objectname)), $_;
401 }
402
403 sub git_for_each_tag_referring ($$) {
404     my ($objreferring, $func) = @_;
405     # calls $func->($tagobjid,$refobjid,$fullrefname,$tagname);
406     printdebug "git_for_each_tag_referring ",
407         ($objreferring // 'UNDEF'),"\n";
408     git_for_each_ref('refs/tags', sub {
409         my ($tagobjid,$objtype,$fullrefname,$tagname) = @_;
410         return unless $objtype eq 'tag';
411         my $refobjid = git_rev_parse $tagobjid;
412         return unless
413             !defined $objreferring # caller wants them all
414             or $tagobjid eq $objreferring
415             or $refobjid eq $objreferring;
416         $func->($tagobjid,$refobjid,$fullrefname,$tagname);
417     });
418 }
419
420 sub is_fast_fwd ($$) {
421     my ($ancestor,$child) = @_;
422     my @cmd = (qw(git merge-base), $ancestor, $child);
423     my $mb = cmdoutput_errok @cmd;
424     if (defined $mb) {
425         return git_rev_parse($mb) eq git_rev_parse($ancestor);
426     } else {
427         $?==256 or failedcmd @cmd;
428         return 0;
429     }
430 }
431
432 sub changedir ($) {
433     my ($newdir) = @_;
434     printdebug "CD $newdir\n";
435     chdir $newdir or confess "chdir: $newdir: $!";
436 }
437
438 sub git_slurp_config_src ($) {
439     my ($src) = @_;
440     # returns $r such that $r->{KEY}[] = VALUE
441     my @cmd = (qw(git config -z --get-regexp), "--$src", qw(.*));
442     debugcmd "|",@cmd;
443
444     local ($debuglevel) = $debuglevel-2;
445     local $/="\0";
446
447     my $r = { };
448     open GITS, "-|", @cmd or die $!;
449     while (<GITS>) {
450         chomp or die;
451         printdebug "=> ", (messagequote $_), "\n";
452         m/\n/ or die "$_ ?";
453         push @{ $r->{$`} }, $'; #';
454     }
455     $!=0; $?=0;
456     close GITS
457         or ($!==0 && $?==256)
458         or failedcmd @cmd;
459     return $r;
460 }
461
462 # ========== playground handling ==========
463
464 # terminology:
465 #
466 #   $maindir      user's git working tree
467 #   $playground   area in .git/ where we can make files, unpack, etc. etc.
468 #   playtree      git working tree sharing object store with the user's
469 #                 inside playground, or identical to it
470 #
471 # other globals
472 #
473 #   $local_git_cfg    hash of arrays of values: git config from $maindir
474 #
475 # expected calling pattern
476 #
477 #  firstly
478 #
479 #    [record_maindir]
480 #      must be run in directory containing .git
481 #      assigns to $maindir if not already set
482 #      also calls git_slurp_config_src to record git config
483 #        in $local_git_cfg, unless it's already set
484 #
485 #    fresh_playground SUBDIR_PATH_COMPONENTS
486 #      e.g fresh_playground 'dgit/unpack' ('.git/' is implied)
487 #      default SUBDIR_PATH_COMPONENTS is $playground_subdir
488 #      calls record_maindir
489 #      sets up a new playground (destroying any old one)
490 #      assigns to $playground and returns the same pathname
491 #      caller may call multiple times with different subdir paths
492 #       createing different playgrounds; but $playground global can
493 #       refer only to one, obv.
494 #
495 #    ensure_a_playground SUBDIR_PATH_COMPONENTS
496 #      like fresh_playground except:
497 #      merely ensures the directory exists; does not delete an existing one
498 #      never sets global $playground
499 #
500 #  then can use
501 #
502 #    changedir $playground
503 #    changedir $maindir
504 #
505 #    playtree_setup $local_git_cfg
506 #            # ^ call in some (perhaps trivial) subdir of $playground
507 #
508 #    rmtree $playground
509
510 # ----- maindir -----
511
512 our $maindir;
513 our $local_git_cfg;
514
515 sub record_maindir () {
516     $maindir //= must_getcwd();
517     $local_git_cfg //= git_slurp_config_src 'local';
518 }
519
520 # ----- playgrounds -----
521
522 our $playground;
523
524 sub ensure_a_playground_parent ($) {
525     my ($spc) = @_;
526     record_maindir();
527     $spc = ".git/$spc";
528     my $parent = dirname $spc;
529     mkdir $parent or $!==EEXIST or fail "failed to mkdir $parent: $!";
530     return $spc;
531 }    
532
533 sub ensure_a_playground ($) {
534     my ($spc) = @_;
535     $spc = ensure_a_playground_parent $spc;
536     mkdir $spc or $!==EEXIST or fail "failed to mkdir a playground $spc: $!";
537     return $spc;
538 }    
539
540 sub fresh_playground ($) {
541     my ($spc) = @_;
542     $spc = ensure_a_playground_parent $spc;
543     rmtree $spc;
544     mkdir $spc or die "$spc $!";
545     return $playground = "$maindir/$spc";
546 }
547
548 # ----- playtrees -----
549
550 sub playtree_setup (;$) {
551     my ($t_local_git_cfg) = @_;
552     $t_local_git_cfg //= $local_git_cfg;
553     # for use in the playtree
554     # $maindir must be set, eg by calling record_maindir or fresh_playground
555     runcmd qw(git init -q);
556     runcmd qw(git config gc.auto 0);
557     foreach my $copy (qw(user.email user.name user.useConfigOnly
558                          core.sharedRepository
559                          core.compression core.looseCompression
560                          core.bigFileThreshold core.fsyncObjectFiles)) {
561         my $v = $t_local_git_cfg->{$copy};
562         next unless $v;
563         runcmd qw(git config), $copy, $_ foreach @$v;
564     }
565     rmtree('.git/objects');
566     symlink "$maindir/.git/objects",'.git/objects' or die $!;
567     ensuredir '.git/info';
568     open GA, "> .git/info/attributes" or die $!;
569     print GA "* $negate_harmful_gitattrs\n" or die $!;
570     close GA or die $!;
571 }
572
573 1;