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