chiark / gitweb /
git-debrebase: Provide conclude subcommand
[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_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 fail { 
225     my $s = "@_\n";
226     $s =~ s/\n\n$/\n/;
227     my $prefix = _us().": ";
228     $s =~ s/^/$prefix/gm;
229     die $s;
230 }
231
232 sub ensuredir ($) {
233     my ($dir) = @_; # does not create parents
234     return if mkdir $dir;
235     return if $! == EEXIST;
236     die "mkdir $dir: $!";
237 }
238
239 sub must_getcwd () {
240     my $d = getcwd();
241     defined $d or fail "getcwd failed: $!";
242     return $d;
243 }
244
245 sub executable_on_path ($) {
246     my ($program) = @_;
247     return 1 if $program =~ m{/};
248     my @path = split /:/, ($ENV{PATH} // "/usr/local/bin:/bin:/usr/bin");
249     foreach my $pe (@path) {
250         my $here = "$pe/$program";
251         return $here if stat_exists $here && -x _;
252     }
253     return undef;
254 }
255
256 our @signames = split / /, $Config{sig_name};
257
258 sub waitstatusmsg () {
259     if (!$?) {
260         return "terminated, reporting successful completion";
261     } elsif (!($? & 255)) {
262         return "failed with error exit status ".WEXITSTATUS($?);
263     } elsif (WIFSIGNALED($?)) {
264         my $signum=WTERMSIG($?);
265         return "died due to fatal signal ".
266             ($signames[$signum] // "number $signum").
267             ($? & 128 ? " (core dumped)" : ""); # POSIX(3pm) has no WCOREDUMP
268     } else {
269         return "failed with unknown wait status ".$?;
270     }
271 }
272
273 sub failedcmd_report_cmd {
274     my $intro = shift @_;
275     $intro //= "failed command";
276     { local ($!); printcmd \*STDERR, _us().": $intro:", @_ or die $!; };
277 }
278
279 sub failedcmd_waitstatus {
280     if ($? < 0) {
281         return "failed to fork/exec: $!";
282     } elsif ($?) {
283         return "subprocess ".waitstatusmsg();
284     } else {
285         return "subprocess produced invalid output";
286     }
287 }
288
289 sub failedcmd {
290     # Expects $!,$? as set by close - see below.
291     # To use with system(), set $?=-1 first.
292     #
293     # Actual behaviour of perl operations:
294     #   success              $!==0       $?==0       close of piped open
295     #   program failed       $!==0       $? >0       close of piped open
296     #   syscall failure      $! >0       $?=-1       close of piped open
297     #   failure              $! >0       unchanged   close of something else
298     #   success              trashed     $?==0       system
299     #   program failed       trashed     $? >0       system
300     #   syscall failure      $! >0       unchanged   system
301     failedcmd_report_cmd undef, @_;
302     fail failedcmd_waitstatus();
303 }
304
305 sub runcmd {
306     debugcmd "+",@_;
307     $!=0; $?=-1;
308     failedcmd @_ if system @_;
309 }
310
311 sub cmdoutput_errok {
312     confess Dumper(\@_)." ?" if grep { !defined } @_;
313     debugcmd "|",@_;
314     open P, "-|", @_ or die "$_[0] $!";
315     my $d;
316     $!=0; $?=0;
317     { local $/ = undef; $d = <P>; }
318     die $! if P->error;
319     if (!close P) { printdebug "=>!$?\n"; return undef; }
320     chomp $d;
321     if ($debuglevel > 0) {
322         $d =~ m/^.*/;
323         my $dd = $&;
324         my $more = (length $' ? '...' : ''); #');
325         $dd =~ s{[^\n -~]|\\}{ sprintf "\\x%02x", ord $& }ge;
326         printdebug "=> \`$dd'",$more,"\n";
327     }
328     return $d;
329 }
330
331 sub cmdoutput {
332     my $d = cmdoutput_errok @_;
333     defined $d or failedcmd @_;
334     return $d;
335 }
336
337 sub link_ltarget ($$) {
338     my ($old,$new) = @_;
339     lstat $old or return undef;
340     if (-l _) {
341         $old = cmdoutput qw(realpath  --), $old;
342     }
343     my $r = link $old, $new;
344     $r = symlink $old, $new if !$r && $!==EXDEV;
345     $r or die "(sym)link $old $new: $!";
346 }
347
348 sub hashfile ($) {
349     my ($fn) = @_;
350     my $h = Digest::SHA->new(256);
351     $h->addfile($fn);
352     return $h->hexdigest();
353 }
354
355 sub git_rev_parse ($) {
356     return cmdoutput qw(git rev-parse), "$_[0]~0";
357 }
358
359 sub git_cat_file ($;$) {
360     my ($objname, $etype) = @_;
361     # => ($type, $data) or ('missing', undef)
362     # in scalar context, just the data
363     # if $etype defined, dies unless type is $etype or in @$etype
364     our ($gcf_pid, $gcf_i, $gcf_o);
365     my $chk = sub {
366         my ($gtype, $data) = @_;
367         if ($etype) {
368             $etype = [$etype] unless ref $etype;
369             confess "$objname expected @$etype but is $gtype"
370                 unless grep { $gtype eq $_ } @$etype;
371         }
372         return ($gtype, $data);
373     };
374     if (!$gcf_pid) {
375         my @cmd = qw(git cat-file --batch);
376         debugcmd "GCF|", @cmd;
377         $gcf_pid = open2 $gcf_o, $gcf_i, @cmd or die $!;
378     }
379     printdebug "GCF>| ", $objname, "\n";
380     print $gcf_i $objname, "\n" or die $!;
381     my $x = <$gcf_o>;
382     printdebug "GCF<| ", $x;
383     if ($x =~ m/ (missing)$/) { return $chk->($1, undef); }
384     my ($type, $size) = $x =~ m/^.* (\w+) (\d+)\n/ or die "$objname ?";
385     my $data;
386     (read $gcf_o, $data, $size) == $size or die "$objname $!";
387     $x = <$gcf_o>;
388     $x eq "\n" or die "$objname ($_) $!";
389     return $chk->($type, $data);
390 }
391
392 sub git_get_symref (;$) {
393     my ($symref) = @_;  $symref //= 'HEAD';
394     # => undef if not a symref, otherwise refs/...
395     my @cmd = (qw(git symbolic-ref -q HEAD));
396     my $branch = cmdoutput_errok @cmd;
397     if (!defined $branch) {
398         $?==256 or failedcmd @cmd;
399     } else {
400         chomp $branch;
401     }
402     return $branch;
403 }
404
405 sub git_for_each_ref ($$;$) {
406     my ($pattern,$func,$gitdir) = @_;
407     # calls $func->($objid,$objtype,$fullrefname,$reftail);
408     # $reftail is RHS of ref after refs/[^/]+/
409     # breaks if $pattern matches any ref `refs/blah' where blah has no `/'
410     # $pattern may be an array ref to mean multiple patterns
411     $pattern = [ $pattern ] unless ref $pattern;
412     my @cmd = (qw(git for-each-ref), @$pattern);
413     if (defined $gitdir) {
414         @cmd = ('sh','-ec','cd "$1"; shift; exec "$@"','x', $gitdir, @cmd);
415     }
416     open GFER, "-|", @cmd or die $!;
417     debugcmd "|", @cmd;
418     while (<GFER>) {
419         chomp or die "$_ ?";
420         printdebug "|> ", $_, "\n";
421         m#^(\w+)\s+(\w+)\s+(refs/[^/]+/(\S+))$# or die "$_ ?";
422         $func->($1,$2,$3,$4);
423     }
424     $!=0; $?=0; close GFER or die "$pattern $? $!";
425 }
426
427 sub git_get_ref ($) {
428     # => '' if no such ref
429     my ($refname) = @_;
430     local $_ = $refname;
431     s{^refs/}{[r]efs/} or die "$refname $_ ?";
432     return cmdoutput qw(git for-each-ref --format=%(objectname)), $_;
433 }
434
435 sub git_for_each_tag_referring ($$) {
436     my ($objreferring, $func) = @_;
437     # calls $func->($tagobjid,$refobjid,$fullrefname,$tagname);
438     printdebug "git_for_each_tag_referring ",
439         ($objreferring // 'UNDEF'),"\n";
440     git_for_each_ref('refs/tags', sub {
441         my ($tagobjid,$objtype,$fullrefname,$tagname) = @_;
442         return unless $objtype eq 'tag';
443         my $refobjid = git_rev_parse $tagobjid;
444         return unless
445             !defined $objreferring # caller wants them all
446             or $tagobjid eq $objreferring
447             or $refobjid eq $objreferring;
448         $func->($tagobjid,$refobjid,$fullrefname,$tagname);
449     });
450 }
451
452 sub git_check_unmodified () {
453     foreach my $cached (qw(0 1)) {
454         my @cmd = qw(git diff --quiet);
455         push @cmd, qw(--cached) if $cached;
456         push @cmd, qw(HEAD);
457         debugcmd "+",@cmd;
458         $!=0; $?=-1; system @cmd;
459         return if !$?;
460         if ($?==256) {
461             fail
462                 $cached
463                 ? "git index contains changes (does not match HEAD)"
464                 : "working tree is dirty (does not match HEAD)";
465         } else {
466             failedcmd @cmd;
467         }
468     }
469 }
470
471 sub is_fast_fwd ($$) {
472     my ($ancestor,$child) = @_;
473     my @cmd = (qw(git merge-base), $ancestor, $child);
474     my $mb = cmdoutput_errok @cmd;
475     if (defined $mb) {
476         return git_rev_parse($mb) eq git_rev_parse($ancestor);
477     } else {
478         $?==256 or failedcmd @cmd;
479         return 0;
480     }
481 }
482
483 sub changedir ($) {
484     my ($newdir) = @_;
485     printdebug "CD $newdir\n";
486     chdir $newdir or confess "chdir: $newdir: $!";
487 }
488
489 sub git_slurp_config_src ($) {
490     my ($src) = @_;
491     # returns $r such that $r->{KEY}[] = VALUE
492     my @cmd = (qw(git config -z --get-regexp), "--$src", qw(.*));
493     debugcmd "|",@cmd;
494
495     local ($debuglevel) = $debuglevel-2;
496     local $/="\0";
497
498     my $r = { };
499     open GITS, "-|", @cmd or die $!;
500     while (<GITS>) {
501         chomp or die;
502         printdebug "=> ", (messagequote $_), "\n";
503         m/\n/ or die "$_ ?";
504         push @{ $r->{$`} }, $'; #';
505     }
506     $!=0; $?=0;
507     close GITS
508         or ($!==0 && $?==256)
509         or failedcmd @cmd;
510     return $r;
511 }
512
513 # ========== playground handling ==========
514
515 # terminology:
516 #
517 #   $maindir      user's git working tree
518 #   playground    area in .git/ where we can make files, unpack, etc. etc.
519 #   playtree      git working tree sharing object store with the user's
520 #                 inside playground, or identical to it
521 #
522 # other globals
523 #
524 #   $local_git_cfg    hash of arrays of values: git config from $maindir
525 #
526 # expected calling pattern
527 #
528 #  firstly
529 #
530 #    [record_maindir]
531 #      must be run in directory containing .git
532 #      assigns to $maindir if not already set
533 #      also calls git_slurp_config_src to record git config
534 #        in $local_git_cfg, unless it's already set
535 #
536 #    fresh_playground SUBDIR_PATH_COMPONENTS
537 #      e.g fresh_playground 'dgit/unpack' ('.git/' is implied)
538 #      default SUBDIR_PATH_COMPONENTS is playground_subdir
539 #      calls record_maindir
540 #      sets up a new playground (destroying any old one)
541 #      returns playground pathname
542 #      caller may call multiple times with different subdir paths
543 #       createing different playgrounds
544 #
545 #    ensure_a_playground SUBDIR_PATH_COMPONENTS
546 #      like fresh_playground except:
547 #      merely ensures the directory exists; does not delete an existing one
548 #
549 #  then can use
550 #
551 #    changedir playground
552 #    changedir $maindir
553 #
554 #    playtree_setup $local_git_cfg
555 #            # ^ call in some (perhaps trivial) subdir of playground
556 #
557 #    rmtree playground
558
559 # ----- maindir -----
560
561 # these three all go together
562 our $maindir;
563 our $maindir_gitdir;
564 our $maindir_gitcommon;
565
566 our $local_git_cfg;
567
568 sub record_maindir () {
569     if (!defined $maindir) {
570         $maindir = must_getcwd();
571         if (!stat "$maindir/.git") {
572             fail "cannot stat $maindir/.git: $!";
573         }
574         if (-d _) {
575             # we fall back to this in case we have a pre-worktree
576             # git, which may not know git rev-parse --git-common-dir
577             $maindir_gitdir    = "$maindir/.git";
578             $maindir_gitcommon = "$maindir/.git";
579         } else {
580             $maindir_gitdir    = cmdoutput qw(git rev-parse --git-dir);
581             $maindir_gitcommon = cmdoutput qw(git rev-parse --git-common-dir);
582         }
583     }
584     $local_git_cfg //= git_slurp_config_src 'local';
585 }
586
587 # ----- playgrounds -----
588
589 sub ensure_a_playground_parent ($) {
590     my ($spc) = @_;
591     record_maindir();
592     $spc = "$maindir_gitdir/$spc";
593     my $parent = dirname $spc;
594     mkdir $parent or $!==EEXIST
595         or fail "failed to mkdir playground parent $parent: $!";
596     return $spc;
597 }    
598
599 sub ensure_a_playground ($) {
600     my ($spc) = @_;
601     $spc = ensure_a_playground_parent $spc;
602     mkdir $spc or $!==EEXIST or fail "failed to mkdir a playground $spc: $!";
603     return $spc;
604 }    
605
606 sub fresh_playground ($) {
607     my ($spc) = @_;
608     $spc = ensure_a_playground_parent $spc;
609     rmtree $spc;
610     mkdir $spc or fail "failed to mkdir the playground $spc: $!";
611     return $spc;
612 }
613
614 # ----- playtrees -----
615
616 sub playtree_setup (;$) {
617     my ($t_local_git_cfg) = @_;
618     $t_local_git_cfg //= $local_git_cfg;
619     # for use in the playtree
620     # $maindir must be set, eg by calling record_maindir or fresh_playground
621     runcmd qw(git init -q);
622     runcmd qw(git config gc.auto 0);
623     foreach my $copy (qw(user.email user.name user.useConfigOnly
624                          core.sharedRepository
625                          core.compression core.looseCompression
626                          core.bigFileThreshold core.fsyncObjectFiles)) {
627         my $v = $t_local_git_cfg->{$copy};
628         next unless $v;
629         runcmd qw(git config), $copy, $_ foreach @$v;
630     }
631     # this is confusing: we have
632     #   .                   playtree, not a worktree, has .git/, our cwd
633     #   $maindir            might be a worktree so
634     #   $maindir_gitdir     contains our main working "dgit", HEAD, etc.
635     #   $maindir_gitcommon  the shared stuff, including .objects
636     rmtree('.git/objects');
637     symlink "$maindir_gitcommon/objects",'.git/objects' or die $!;
638     ensuredir '.git/info';
639     open GA, "> .git/info/attributes" or die $!;
640     print GA "* $negate_harmful_gitattrs\n" or die $!;
641     close GA or die $!;
642 }
643
644 1;