summaryrefslogtreecommitdiff
path: root/Debian/Dgit.pm
blob: be8cbeea2a8667a69e6f0c54a4b9bacc2ffad94a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# -*- perl -*-
# dgit
# Debian::Dgit: functions common to dgit and its helpers and servers
#
# Copyright (C) 2015-2016  Ian Jackson
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

package Debian::Dgit;

use strict;
use warnings;

use Carp;
use POSIX;
use IO::Handle;
use Config;
use Digest::SHA;
use Data::Dumper;
use IPC::Open2;

BEGIN {
    use Exporter   ();
    our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);

    $VERSION     = 1.00;
    @ISA         = qw(Exporter);
    @EXPORT      = qw(setup_sigwarn
		      dep14_version_mangle
                      debiantags debiantag_old debiantag_new
		      server_branch server_ref
                      stat_exists link_ltarget
		      hashfile
                      fail ensuredir executable_on_path
                      waitstatusmsg failedcmd_waitstatus
		      failedcmd_report_cmd failedcmd
                      cmdoutput cmdoutput_errok
                      git_rev_parse git_cat_file
		      git_get_ref git_for_each_ref
                      git_for_each_tag_referring is_fast_fwd
                      $package_re $component_re $deliberately_re
		      $distro_re $versiontag_re
                      $branchprefix
                      initdebug enabledebug enabledebuglevel
                      printdebug debugcmd
                      $debugprefix *debuglevel *DEBUG
                      shellquote printcmd messagequote);
    # implicitly uses $main::us
    %EXPORT_TAGS = ( policyflags => [qw(NOFFCHECK FRESHREPO NOCOMMITCHECK)] );
    @EXPORT_OK   = @{ $EXPORT_TAGS{policyflags} };
}

our @EXPORT_OK;

our $package_re = '[0-9a-z][-+.0-9a-z]*';
our $component_re = '[0-9a-zA-Z][-+.0-9a-zA-Z]*';
our $deliberately_re = "(?:TEST-)?$package_re";
our $distro_re = $component_re;
our $versiontag_re = qr{[-+.\%_0-9a-zA-Z/]+};
our $branchprefix = 'dgit';

# policy hook exit status bits
# see dgit-repos-server head comment for documentation
# 1 is reserved in case something fails with `exit 1' and to spot
# dynamic loader, runtime, etc., failures, which report 127 or 255
sub NOFFCHECK () { return 0x2; }
sub FRESHREPO () { return 0x4; }
sub NOCOMMITCHECK () { return 0x8; }

our $debugprefix;
our $debuglevel = 0;

sub setup_sigwarn () {
    our $sigwarn_mainprocess = $$;
    $SIG{__WARN__} = sub { 
	die $_[0] unless getppid == $sigwarn_mainprocess;
    };
}

sub initdebug ($) { 
    ($debugprefix) = @_;
    open DEBUG, ">/dev/null" or die $!;
}

sub enabledebug () {
    open DEBUG, ">&STDERR" or die $!;
    DEBUG->autoflush(1);
    $debuglevel ||= 1;
}
    
sub enabledebuglevel ($) {
    my ($newlevel) = @_; # may be undef (eg from env var)
    die if $debuglevel;
    $newlevel //= 0;
    $newlevel += 0;
    return unless $newlevel;
    $debuglevel = $newlevel;
    enabledebug();
}
    
sub printdebug {
    print DEBUG $debugprefix, @_ or die $! if $debuglevel>0;
}

sub messagequote ($) {
    local ($_) = @_;
    s{\\}{\\\\}g;
    s{\n}{\\n}g;
    s{\x08}{\\b}g;
    s{\t}{\\t}g;
    s{[\000-\037\177]}{ sprintf "\\x%02x", ord $& }ge;
    $_;
}

sub shellquote {
    my @out;
    local $_;
    foreach my $a (@_) {
	$_ = $a;
	if (!length || m{[^-=_./:0-9a-z]}i) {
	    s{['\\]}{'\\$&'}g;
	    push @out, "'$_'";
	} else {
	    push @out, $_;
	}
    }
    return join ' ', @out;
}

sub printcmd {
    my $fh = shift @_;
    my $intro = shift @_;
    print $fh $intro," " or die $!;
    print $fh shellquote @_ or die $!;
    print $fh "\n" or die $!;
}

sub debugcmd {
    my $extraprefix = shift @_;
    printcmd(\*DEBUG,$debugprefix.$extraprefix,@_) if $debuglevel>0;
}

sub dep14_version_mangle ($) {
    my ($v) = @_;
    # DEP-14 patch proposed 2016-11-09  "Version Mangling"
    $v =~ y/~:/_%/;
    $v =~ s/\.(?=\.|$|lock$)/.#/g;
    return $v;
}

sub debiantag_old ($$) { 
    my ($v,$distro) = @_;
    return "$distro/". dep14_version_mangle $v;
}

sub debiantag_new ($$) { 
    my ($v,$distro) = @_;
    return "archive/$distro/".dep14_version_mangle $v;
}

sub debiantags ($$) {
    my ($version,$distro) = @_;
    map { $_->($version, $distro) } (\&debiantag_new, \&debiantag_old);
}

sub server_branch ($) { return "$branchprefix/$_[0]"; }
sub server_ref ($) { return "refs/".server_branch($_[0]); }

sub stat_exists ($) {
    my ($f) = @_;
    return 1 if stat $f;
    return 0 if $!==&ENOENT;
    die "stat $f: $!";
}

sub _us () {
    $::us // ($0 =~ m#[^/]*$#, $&);
}

sub fail { 
    my $s = "@_\n";
    $s =~ s/\n\n$/\n/;
    my $prefix = _us().": ";
    $s =~ s/^/$prefix/gm;
    die $s;
}

sub ensuredir ($) {
    my ($dir) = @_; # does not create parents
    return if mkdir $dir;
    return if $! == EEXIST;
    die "mkdir $dir: $!";
}

sub executable_on_path ($) {
    my ($program) = @_;
    return 1 if $program =~ m{/};
    my @path = split /:/, ($ENV{PATH} // "/usr/local/bin:/bin:/usr/bin");
    foreach my $pe (@path) {
	my $here = "$pe/$program";
	return $here if stat_exists $here && -x _;
    }
    return undef;
}

our @signames = split / /, $Config{sig_name};

sub waitstatusmsg () {
    if (!$?) {
	return "terminated, reporting successful completion";
    } elsif (!($? & 255)) {
	return "failed with error exit status ".WEXITSTATUS($?);
    } elsif (WIFSIGNALED($?)) {
	my $signum=WTERMSIG($?);
	return "died due to fatal signal ".
	    ($signames[$signum] // "number $signum").
	    ($? & 128 ? " (core dumped)" : ""); # POSIX(3pm) has no WCOREDUMP
    } else {
	return "failed with unknown wait status ".$?;
    }
}

sub failedcmd_report_cmd {
    my $intro = shift @_;
    $intro //= "failed command";
    { local ($!); printcmd \*STDERR, _us().": $intro:", @_ or die $!; };
}

sub failedcmd_waitstatus {
    if ($? < 0) {
	return "failed to fork/exec: $!";
    } elsif ($?) {
	return "subprocess ".waitstatusmsg();
    } else {
	return "subprocess produced invalid output";
    }
}

sub failedcmd {
    # Expects $!,$? as set by close - see below.
    # To use with system(), set $?=-1 first.
    #
    # Actual behaviour of perl operations:
    #   success              $!==0       $?==0       close of piped open
    #   program failed       $!==0       $? >0       close of piped open
    #   syscall failure      $! >0       $?=-1       close of piped open
    #   failure              $! >0       unchanged   close of something else
    #   success              trashed     $?==0       system
    #   program failed       trashed     $? >0       system
    #   syscall failure      $! >0       unchanged   system
    failedcmd_report_cmd undef, @_;
    fail failedcmd_waitstatus();
}

sub cmdoutput_errok {
    confess Dumper(\@_)." ?" if grep { !defined } @_;
    debugcmd "|",@_;
    open P, "-|", @_ or die "$_[0] $!";
    my $d;
    $!=0; $?=0;
    { local $/ = undef; $d = <P>; }
    die $! if P->error;
    if (!close P) { printdebug "=>!$?\n"; return undef; }
    chomp $d;
    if ($debuglevel > 0) {
	$d =~ m/^.*/;
	my $dd = $&;
	my $more = (length $' ? '...' : ''); #');
	$dd =~ s{[^\n -~]|\\}{ sprintf "\\x%02x", ord $& }ge;
	printdebug "=> \`$dd'",$more,"\n";
    }
    return $d;
}

sub cmdoutput {
    my $d = cmdoutput_errok @_;
    defined $d or failedcmd @_;
    return $d;
}

sub link_ltarget ($$) {
    my ($old,$new) = @_;
    lstat $old or return undef;
    if (-l _) {
	$old = cmdoutput qw(realpath  --), $old;
    }
    my $r = link $old, $new;
    $r = symlink $old, $new if !$r && $!==EXDEV;
    $r or die "(sym)link $old $new: $!";
}

sub hashfile ($) {
    my ($fn) = @_;
    my $h = Digest::SHA->new(256);
    $h->addfile($fn);
    return $h->hexdigest();
}

sub git_rev_parse ($) {
    return cmdoutput qw(git rev-parse), "$_[0]~0";
}

sub git_cat_file ($) {
    my ($objname) = @_;
    # => ($type, $data) or ('missing', undef)
    # in scalar context, just the data
    our ($gcf_pid, $gcf_i, $gcf_o);
    if (!$gcf_pid) {
	my @cmd = qw(git cat-file --batch);
	debugcmd "GCF|", @cmd;
	$gcf_pid = open2 $gcf_o, $gcf_i, @cmd or die $!;
    }
    printdebug "GCF>| ", $objname, "\n";
    print $gcf_i $objname, "\n" or die $!;
    my $x = <$gcf_o>;
    printdebug "GCF<| ", $x;
    if ($x =~ m/ (missing)$/) { return ($1, undef); }
    my ($type, $size) = $x =~ m/^.* (\w+) (\d+)\n/ or die "$objname ?";
    my $data;
    (read $gcf_o, $data, $size) == $size or die "$objname $!";
    $x = <$gcf_o>;
    $x eq "\n" or die "$objname ($_) $!";
    return ($type, $data);
}

sub git_for_each_ref ($$;$) {
    my ($pattern,$func,$gitdir) = @_;
    # calls $func->($objid,$objtype,$fullrefname,$reftail);
    # $reftail is RHS of ref after refs/[^/]+/
    # breaks if $pattern matches any ref `refs/blah' where blah has no `/'
    # $pattern may be an array ref to mean multiple patterns
    $pattern = [ $pattern ] unless ref $pattern;
    my @cmd = (qw(git for-each-ref), @$pattern);
    if (defined $gitdir) {
	@cmd = ('sh','-ec','cd "$1"; shift; exec "$@"','x', $gitdir, @cmd);
    }
    open GFER, "-|", @cmd or die $!;
    debugcmd "|", @cmd;
    while (<GFER>) {
	chomp or die "$_ ?";
	printdebug "|> ", $_, "\n";
	m#^(\w+)\s+(\w+)\s+(refs/[^/]+/(\S+))$# or die "$_ ?";
	$func->($1,$2,$3,$4);
    }
    $!=0; $?=0; close GFER or die "$pattern $? $!";
}

sub git_get_ref ($) {
    # => '' if no such ref
    my ($refname) = @_;
    local $_ = $refname;
    s{^refs/}{[r]efs/} or die "$refname $_ ?";
    return cmdoutput qw(git for-each-ref --format=%(objectname)), $_;
}

sub git_for_each_tag_referring ($$) {
    my ($objreferring, $func) = @_;
    # calls $func->($tagobjid,$refobjid,$fullrefname,$tagname);
    printdebug "git_for_each_tag_referring ",
        ($objreferring // 'UNDEF'),"\n";
    git_for_each_ref('refs/tags', sub {
	my ($tagobjid,$objtype,$fullrefname,$tagname) = @_;
	return unless $objtype eq 'tag';
	my $refobjid = git_rev_parse $tagobjid;
	return unless
	    !defined $objreferring # caller wants them all
	    or $tagobjid eq $objreferring
	    or $refobjid eq $objreferring;
	$func->($tagobjid,$refobjid,$fullrefname,$tagname);
    });
}

sub is_fast_fwd ($$) {
    my ($ancestor,$child) = @_;
    my @cmd = (qw(git merge-base), $ancestor, $child);
    my $mb = cmdoutput_errok @cmd;
    if (defined $mb) {
	return git_rev_parse($mb) eq git_rev_parse($ancestor);
    } else {
	$?==256 or failedcmd @cmd;
	return 0;
    }
}

1;