# -*- perl -*-

package Debian::Dgit;

use strict;
use warnings;

use POSIX;
use IO::Handle;
use Config;

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

    $VERSION     = 1.00;
    @ISA         = qw(Exporter);
    @EXPORT      = qw(debiantag server_branch server_ref
                      stat_exists fail waitstatusmsg failedcmd
                      cmdoutput cmdoutput_errok
                      git_rev_parse git_for_each_ref
                      git_for_each_tag_referring
                      $package_re $component_re $deliberately_re
                      $branchprefix
                      initdebug enabledebug enabledebuglevel
                      printdebug debugcmd
                      $debugprefix *debuglevel *DEBUG
                      shellquote printcmd);
    # implicitly uses $main::us
    %EXPORT_TAGS = ( policyflags => [qw(NOFFCHECK FRESHREPO)] );
    @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 $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; }

our $debugprefix;
our $debuglevel = 0;

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 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 debiantag ($) { 
    my ($v) = @_;
    $v =~ y/~:/_%/;
    return "debian/$v";
}

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";
    my $prefix = _us().": ";
    $s =~ s/^/$prefix/gm;
    die $s;
}

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 {
    { local ($!); printcmd \*STDERR, _us().": failed command:", @_ or die $!; };
    if ($!) {
	fail "failed to fork/exec: $!";
    } elsif ($?) {
	fail "subprocess ".waitstatusmsg();
    } else {
	fail "subprocess produced invalid output";
    }
}

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

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

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

sub git_for_each_ref ($$) {
    my ($pattern,$func) = @_;
    # calls $func->($objid,$objtype,$fullrefname,$reftail);
    # $reftail is RHS of ref after refs/\w+/
    # breaks if $pattern matches any ref `refs/blah' where blah has no `/'
    my @cmd = (qw(git for-each-ref), $pattern);
    open GFER, "-|", @cmd or die $!;
    debugcmd "|", @cmd;
    while (<GFER>) {
	chomp or die "$_ ?";
	printdebug "|> ", $_, "\n";
	m#^(\w+)\s+(\w+)\s+(refs/\w+/(\S+))$# or die "$_ ?";
	$func->($1,$2,$3,$4);
    }
    $!=0; $?=0; close GFER or die "$pattern $? $!";
}

sub git_for_each_tag_referring ($$) {
    my ($objreferring, $func) = @_;
    # calls $func->($objid,$fullrefname,$tagname);
    printdebug "git_for_each_tag_referring ",
        ($objreferring // 'UNDEF'),"\n";
    git_for_each_ref('refs/tags', sub {
	my ($objid,$objtype,$fullrefname,$tagname) = @_;
	return unless $objtype eq 'tag';
	return if defined $objreferring and $objid ne $objreferring;
	$func->($objid,$fullrefname,$tagname);
    });
}

1;
