#! /usr/bin/perl

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# package Tmp version 1.0
#
# Create temporary files/directories and ensures they are removed at
# program end.
#
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{
  package Tmp;

  use File::Temp;
  use strict 'vars';

  sub new
  {
    my $self = {};
    my $save_tmp = shift;

    bless $self;

    my $x = $0;
    $x =~ s#.*/##;
    $x =~ s/(\s+|"|\\|')/_/;
    $x = 'tmp' if$x eq "";

    my $t = File::Temp::tempdir("/tmp/$x.XXXXXXXX", CLEANUP => $save_tmp ? 0 : 1);

    $self->{base} = $t;

    if(!$save_tmp) {
      my $s_t = $SIG{TERM};
      $SIG{TERM} = sub { File::Temp::cleanup; &$s_t if $s_t };

      my $s_i = $SIG{INT};
      $SIG{INT} = sub { File::Temp::cleanup; &$s_i if $s_i };
    }

    return $self
  }

  sub dir
  {
    my $self = shift;
    my $dir = shift;
    my $t;

    if($dir ne "" && !-e("$self->{base}/$dir")) {
      $t = "$self->{base}/$dir";
      die "error: mktemp failed\n" unless mkdir $t, 0755;
    }
    else {
      chomp ($t = `mktemp -d $self->{base}/XXXX`);
      die "error: mktemp failed\n" if $?;
    }

    return $t;
  }

  sub file
  {
    my $self = shift;
    my $file = shift;
    my $t;

    if($file ne "" && !-e("$self->{base}/$file")) {
      $t = "$self->{base}/$file";
      open my $f, ">$t";
      close $f;
    }
    else {
      chomp ($t = `mktemp $self->{base}/XXXX`);
      die "error: mktemp failed\n" if $?;
    }

    return $t;
  }

  # helper function
  sub umount
  {
    my $mp = shift;

    if(open(my $f, "/proc/mounts")) {
      while(<$f>) {
        if((split)[1] eq $mp) {
          # print STDERR "umount $mp\n";
          ::susystem("umount $mp");
          return;
        }
      }
      close $f;
    }
  }

  sub mnt
  {
    my $self = shift;
    my $dir = shift;

    my $t = $self->dir($dir);

    if($t ne '') {
      eval 'END { umount $t }';

      my $s_t = $SIG{TERM};
      $SIG{TERM} = sub { umount $t; &$s_t if $s_t };

      my $s_i = $SIG{INT};
      $SIG{INT} = sub { umount $t; &$s_i if $s_i };
    }

    return $t;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
use strict;

use Getopt::Long;

use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Terse = 1;
$Data::Dumper::Indent = 1;

our $VERSION = "0.0";

sub usage;
sub get_rc;
sub get_gitdata;
sub get_dist;
sub update_spec;
sub update_changelog;
sub update_tag;
sub do_sr;


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
my $config;

my $opt_sr;
my $opt_wait;
my $opt_save_temp;
my $opt_try;
my $opt_test;
my $opt_delay = 30;
my $opt_target;
my $opt_package;
my $opt_spec;

GetOptions(
  'sr'          => \$opt_sr,
  'wait-for-ok' => \$opt_wait,
  'delay=i'     => \$opt_delay,
  'try'         => \$opt_try,
  'test'        => \$opt_test,
  'target=s'    => \$opt_target,
  'package=s'   => \$opt_package,
  'spec=s'      => \$opt_spec,
  'save-temp'   => \$opt_save_temp,
  'version'     => sub { print "$VERSION\n"; exit 0 },
  'help'        => sub { usage 0 },
) || usage 1;

my $tmp = Tmp::new($opt_save_temp);   

my $tmpdir = $tmp->dir('package');
my $tmpdir2 = $tmp->dir('testpackage');

$ENV{PATH} = "$ENV{HOME}/bin:/usr/bin:/bin:/usr/sbin:/sbin";

get_rc;

if($opt_package) {
  $config->{package} = $opt_package;
}
else {
  get_gitdata;
}

usage 1 unless $config->{package};

get_dist;

exit do_sr if $opt_sr;

chomp($config->{version} = `git2log --version`);
die "no version info\n" if $config->{version} eq "";

my @s = map { s#^[^/]*/##; $_ } glob("package/*");
@s = grep { !/\.changes$/ } @s;
die "no source files\n" if !@s;

$config->{archive} = "$config->{package}-$config->{version}.tar";
for (sort @s) {
  $config->{archive} = $_, last if /^$config->{archive}/;
}
die "package/$config->{archive}: archive missing\n" unless -f "package/$config->{archive}";

@s = grep { $_ ne $config->{archive} } @s;
$config->{sources}{$_} = 1 for @s;

print "      Package: $config->{package}\n";
print "      Version: $config->{version}\n";
print "   GIT Branch: $config->{branch}\n";
print "    Spec File: $opt_spec\n" if $opt_spec;
print "         Dist: $config->{dist}\n";
print "      Project: $config->{prj}\n";
print " Test Project: $config->{test}\n" if $config->{test};
print "    Submit To: $config->{sr}\n";
print "   Maintainer: $config->{email}\n";
print "           BS: $config->{bs}\n";
print "      TMP Dir: $tmpdir\n";

print "Checking out $config->{prj}/$config->{package}...\n";
system "cd $tmpdir ; osc -A https://$config->{bs} co -c $config->{prj}/$config->{package} >/dev/null";
system "ls -og $tmpdir/$config->{package} | tail -n +2";

if($config->{test}) {
  print "Checking out $config->{test}/$config->{package}...\n";
  if(system "cd $tmpdir2 ; osc -A https://$config->{bs} co -c $config->{test}/$config->{package} >/dev/null") {
    die "missing test project $config->{test}/$config->{package}\n"
  }
  # system "ls -og $tmpdir2/$config->{package} | tail -n +2";
}

my @specs = map { s#.*/##; s#\.spec$##; $_ } glob("$tmpdir/$config->{package}/*.spec");
if(@specs) {
  $config->{spec_name} = $specs[0];
  print "Package has several spec files; using $config->{spec_name} as base\n" if @specs > 1;
}

if($opt_spec) {
  if(open my $f, $opt_spec) {
    $config->{spec_file_alt} = [ <$f> ];
    close $f;
  }
}

if(open my $f, "$tmpdir/$config->{package}/$config->{spec_name}.spec") {
  $config->{spec_file} = [ <$f> ];
  close $f;
}

die "missing spec file\n" if !defined $config->{spec_file};

if(open my $f, "$tmpdir/$config->{package}/$config->{spec_name}.changes") {
  $config->{changes} = [ <$f> ];
  close $f;
}

die "missing changes\n" if !defined $config->{changes};

update_spec;

# write new spec file
if(open my $f, ">$tmpdir/$config->{package}/$config->{spec_name}.spec") {
  print $f @{$config->{spec_file}};
  close $f;
}

update_changelog;

my $new_changelog = "$tmpdir/$config->{package}/$config->{spec_name}.changes";

# write new changes file
if(open my $f, ">$new_changelog") {
  print $f @{$config->{changes}};
  close $f;
}

# delete obsolete files
for ($config->{rm_archive}, keys %{$config->{rm_sources}}, keys %{$config->{rm_patches}}) {
  # print "unlink $tmpdir/$config->{package}/$_\n";
  unlink "$tmpdir/$config->{package}/$_";
}

# copy new files except *.changes (we would overwrite our newly generated one)
rename $new_changelog, "$new_changelog.tmp";
system "cp package/* $tmpdir/$config->{package}/";
rename "$new_changelog.tmp", $new_changelog;

# copy changes and specs
if(@specs > 1) {
  my $base = shift @specs;
  my $theme = $base;
  $theme =~ s/.*-//;
  for my $s (@specs) {
    my $stheme = $s;
    $stheme =~ s/.*-//;
    system "cp $tmpdir/$config->{package}/$base.changes $tmpdir/$config->{package}/$s.changes";
    system "perl -ple 's/^%define\\s+(\\S+)\\s+$theme\\s*\$/%define \$1 $stheme/' $tmpdir/$config->{package}/$base.spec > $tmpdir/$config->{package}/$s.spec";
  }
}

# create new tag if needed
update_tag if !$config->{test};

# updating bs project
if($config->{test}) {
  system "rm -f $tmpdir2/$config->{package}/*";
  system "cp -a $tmpdir/$config->{package}/* $tmpdir2/$config->{package}/";

  system "cd $tmpdir2/$config->{package} ; osc -A https://$config->{bs} addremove";
  print "Submitting changes to $config->{test}/$config->{package}...\n";
  system "cd $tmpdir2/$config->{package} ; osc -A https://$config->{bs} ci -m '- release $config->{version}'" if !$opt_try;
  system "ls -og $tmpdir2/$config->{package} | tail -n +2";
}
else {
  system "cd $tmpdir/$config->{package} ; osc -A https://$config->{bs} addremove";
  print "Submitting changes to $config->{prj}/$config->{package}...\n";
  system "cd $tmpdir/$config->{package} ; osc -A https://$config->{bs} ci -m '- release $config->{version}'" if !$opt_try;
  system "ls -og $tmpdir/$config->{package} | tail -n +2";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# usage($exit_code)
#
# Print help text and exit.
#
sub usage
{
  print <<"= = = = = = = =";
Usage: tobs [OPTIONS]
Submit from git to build system.

General options:

  --target TARGET           Choose config from section TARGET in config file.
  --spec FILE               Use FILE as spec file template instead of the spec file from
                            the build service project.
  --try                     Don\'t actually do anything.
  --test                    Submit to test project. A test project is an alternative
                            project defined in .tobsrc. Sources are taken from the regular
                            project but the new code is submitted to the test project.

  --version                 Show tobs version.
  --save-temp               Keep temporary files.
  --help                    Write this help text.

Create submit request:

  --sr                      Create submit request from devel project to target project.
  --wait-for-ok             Wait until package has built ok on at least one architecture
                            in devel project.
  --delay N                 Wait N seconds between polling for build results (default: 30).
  --package PACKAGE         Set package name to PACKAGE. If this option is missing, the
                            package name is determined from the checked out git repository.
                            If you use this option you must also specify a TARGET with
                            the --target option.

Note: You are expected to create the necessary files running 'make archive' before
using tobs.

Configuration file:

  \$HOME/.tobsrc

  Typical .ini style with entries in key=value form and section names in brackets ('[]').

  Section names are arbitrary but can be thought of as target distribution.
  See README for some config entry examples.

Examples:

  # submit from current git dir to devel project
  tobs

  # prepare everything but don't actually submit anything
  tobs --try

  # create submit request from devel project to target project
  tobs --sr

= = = = = = = =

  exit shift;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub get_rc
{
  my $sec;
  my @sections;

  if(open my $f, "$ENV{HOME}/.tobsrc") {
    while(<$f>) {
      if(/^\s*\[(\S+)\]/) {
        push @sections, $sec = { dist => $1 };
        next;
      }

      $sec->{$1} = $2 if /^\s*(\S+)\s*=\s*(\S+)/;
    }

    close $f;
  }

  $config->{rc} = \@sections;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub get_gitdata
{
  die "no git repository found\n" unless -d '.git';

  my $branch = `git2log --branch`;

  chomp $branch;

  die "failed to determine branch\n" if !$branch;

  $config->{branch} = $branch;

  my $pack = `git config --get remote.origin.url 2>/dev/null`;

  if($pack =~ m#([^/.]+)\.git\s*$#) {
    $pack = $1;
  }
  elsif($pack =~ m#([^/.]+?)\s*$#) {
    $pack = $1;
  }

  $config->{package} = $pack if $pack ne "";

  if(open my $p, "git tag 2>/dev/null |") {
    while(<$p>) {
      s/\/?\s*$//;
      $config->{tags}{$_} = 1 if $_ ne "";
    }
    close $p;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub get_dist
{
  my $dist;

  for (@{$config->{rc}}) {
    next if $opt_target && $_->{dist} ne $opt_target;
    if($_->{branch} eq $config->{branch} || $opt_target) {
      # when git repo  and package name differ...
      if($_->{package} ne "" && $_->{gitname} eq $config->{package}) {
        $config->{package} = $_->{package};
        $dist = $_;
        last;
      }
      elsif($_->{package} eq $config->{package}) {
        $dist = $_;
        last;
      }
      $dist = $_, last if !$_->{package};
    }
  }

  die "no config for " . ($opt_target ? $opt_target : $config->{branch}) . "\n" unless $dist;

  $config->{dist} = $dist->{dist};
  $config->{prj} = $dist->{prj};
  $config->{bs} = $dist->{bs};
  $config->{bs_sr} = $dist->{bs_sr};
  if($opt_test) {
    if($dist->{test}) {
      $config->{test} = $dist->{test};
    }
    else {
      die "no test project defined for $config->{package} in $config->{dist}\n";
    }
  }
  $config->{sr} = $dist->{sr};
  $config->{sr} .= "/$config->{package}" if $config->{sr} && $config->{sr} !~ m#/#;

  if($ENV{USER_NAME}) {
    $config->{email} = $ENV{USER_NAME};
  }
  elsif(open my $p, "osc -A https://$config->{bs} maintainer -e -B $config->{prj} $config->{package} |") {
    while (<$p>) {
      s/^\s+//;
      s/,.*$//;
      s/\s*$//;
      if(/\@/) {
        $config->{email} = $_;
        last;
      }
    }
    close $p;
  }

  $config->{email} = (getpwuid $<)[0] if !$config->{email};

  $config->{email} .= "\@suse.com" if $config->{email} !~ /\@/;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub update_spec
{
  my $spec_version;
  my $spec_name;
  my $setup = 0;

  # If the user has specified an alternative spec file source we still have
  # to scan the 'real' spec file to get the version info (to know which log
  # entries to add to the changelog).
  if($config->{spec_file_alt}) {
    for (@{$config->{spec_file}}) {
      last if /^%package\s+\-n/;

      if(/^Version:(\s*)(\S+)/) {
        $spec_version = $config->{spec_version} = $2;
        last;
      }
    }

    # the original spec is no longer needed
    $config->{spec_file} = $config->{spec_file_alt};
    delete $config->{spec_file_alt};
  }

  for (@{$config->{spec_file}}) {
    last if /^%package\s+\-n/;

    if(/^Version:(\s*)(\S+)/) {
      $spec_version = $config->{spec_version} = $2 unless defined $spec_version;
      $_ = "Version:$1$config->{version}\n";
    }

    if(/^Name:(\s*)(\S+)/) {
      $spec_name = $2;
    }

    if(/^Source:(\s*)((\S+)\.tar\.(bz2|gz|xz))/) {
      $config->{rm_archive} = $2;
      my $s = $1;
      my $n = $config->{archive};
      $config->{rm_archive} =~ s/%\{name\}/$spec_name/g;
      $config->{rm_archive} =~ s/%\{version\}/$spec_version/g;
      $n =~ s/^$spec_name\-/%\{name\}-/ if $spec_name ne '';
      $n =~ s/\-$config->{version}\.tar/-%\{version\}.tar/ if $spec_version ne "";
      $_ = "Source:$s$n\n";
      my $i = 1;
      chop $s;
      for my $x (sort keys %{$config->{sources}}) {
        $_ .= "Source$i:$s$x\n";
        $i++;
      }
    }

    if(/^Source\d+:(\s*)((\S+)\.tar\.(bz2|gz|xz))/) {
      $config->{rm_sources}{$2} = 1;
      undef $_;
    }

    if(/^Patch(\d*):\s*(\S+)/) {
      $config->{rm_patches}{$2} = 1;
      print "Dropping patch: $2\n";
      undef $_;
    }

    if(/^%patch/) {
      undef $_;
    }

    if(/^%setup/) {
      if($setup) {
        undef $_;
      }
      else {
        $setup = 1;
        my $i = 1;
        for my $x (keys %{$config->{sources}}) {
          $_ .= "%setup -T -D -a $i\n";
          $i++;
        }
      }
    }
  }

  $config->{spec_file} = [ grep defined, @{$config->{spec_file}} ];

  for(my $i = 1; $i < @{$config->{spec_file}}; $i++) {
    $config->{spec_file}[$i] = $config->{spec_file}[$i - 1] = undef if $config->{spec_file}[$i] =~ /^%endif/ && $config->{spec_file}[$i - 1] =~ /%ifarch\s/;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub update_changelog
{
  my $changelog;

  if($config->{version} ne $config->{spec_version}) {
    $changelog = `git2log --changelog --format=obs --start=$config->{spec_version}`;

    die "Version $config->{spec_version} not found in changelog\n" if $changelog eq "";
  }

  my $log;

  for (sort keys %{$config->{sources}}) {
    $log .= "- adding $_\n" if !$config->{rm_sources}{$_};
  }

  for (sort keys %{$config->{rm_sources}}) {
    $log .= "- removing $_\n" if !$config->{sources}{$_};
  }

  for (sort keys %{$config->{rm_patches}}) {
    $log .= "- removing patch $_\n";
  }

  # insert at the end of the first log entry
  if($log) {
    $changelog =~ s/(\n-[^\n]*\n)\n/$1$log\n/;
  }

  if($changelog) {
    unshift @{$config->{changes}}, $changelog;

    print "New changelog entry:\n", $changelog;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub update_tag
{
  my $tag = "$config->{branch}-$config->{version}";
  $tag =~ s/^master-//;

  if(!$config->{tags}{$tag}) {
    print "Creating tag $tag\n";
    system "git tag $tag ; git push origin tags/$tag" if !$opt_try;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Wait for OBS build results and submit package if we got a positive result.
#
# We loop until we see a positive build result or we're sure the OBS is not
# building anymore.
#
# The package build is considered ok if there's at least a single positive
# build at this point. If not, it has failed.
#
# Note that failed builds do not worry us as many projects are set up to
# build several targets and not all of them are expected to work.
#
sub do_sr
{
  my $bs = $config->{bs};
  my $bs_prefix;

  if($config->{bs_sr}) {
    ($bs, $bs_prefix) = split /,/, $config->{bs_sr};
  }

  print "      Package: $config->{package}\n";
  print "         Dist: $config->{dist}\n";
  print "      Project: $bs_prefix$config->{prj}\n";
  print " Test Project: $bs_prefix$config->{test}\n" if $config->{test};
  print "    Submit To: $config->{sr}\n";
  print "   Maintainer: $config->{email}\n";
  print "           BS: $bs\n";

  my $err = 0;

  if($config->{test}) {
    print "\n===  No submit requests from test projects!  ===\n";
    return $err;
  }

  my @s = split '/', $config->{sr};

  if($opt_wait) {
    my $ok;
    my $failed;
    my $building;
    my $delay = $opt_delay;

    print "Waiting for build results of $config->{prj}/$s[1]...\n";
    $| = 1;

    do {
      sleep $delay;
      if(open my $p, "osc -A https://$bs r --csv $bs_prefix$config->{prj} $s[1] |") {
        # sample line:
        #
        # openSUSE_Factory|x86_64|unpublished|False|succeeded|
        #
        # field 0, 1, 2: not relevant
        # field 3: False or True; if True, state is going to change
        # field 4: status
        # field 5: sometimes a 6th field is added with the real state
        #
        while (<$p>) {
          chomp;
          my @i = split /\|/;
          if(
            $i[3] eq "False" &&
            (
              $i[4] eq "succeeded" ||
              $i[4] eq "finished" && $i[5] eq "succeeded"
            )
          ) {
            $ok = 1;
          }
          elsif(
            $i[3] eq "False" &&
            (
              $i[4] eq "failed" ||
              $i[4] eq "unresolvable" ||
              $i[4] eq "broken" ||
              $i[4] eq "finished" && $i[5] eq "failed"
            )
          ) {
            $failed = 1;
          }
          elsif(
            $i[3] eq "False" &&
            !(
              $i[4] eq "excluded" ||
              $i[4] eq "disabled"
            )
          ) {
            $building = 1;
          }
        }
        $failed = 1 if !$building;
        close $p;
      }
      else {
        last;
      }
    } while(!$ok && $building);

    if(!$ok && $failed) {
      print "Build failed\n";

      return 1;
    }

    print "Build ok\n";
  }

  if(!$config->{sr}) {
    print "No submit request created\n";

    return $err;
  }

  print "Creating submit request to $config->{sr}\n";

  if(!$opt_try) {
    my $sr_resp = $tmp->file();
    my $user = $config->{email};
    $user =~ s/\@.*$//;
    system "echo y | osc -A https://$bs sr -m 'submitted by $user via jenkins' --yes --nodevelproject $bs_prefix$config->{prj} $s[1] $s[0] >$sr_resp 2>&1";
    $err = $? >> 8;

    my $resp_msg;

    if(open my $f, $sr_resp) {
      local $/ = undef;
      $resp_msg = <$f>;
      close $f;
    }

    if($err) {
      if($resp_msg =~ /The request contains no actions./) {
        $resp_msg =~ s/^.*HTTP Error 400:.*\n//m;
        $resp_msg .= "no request created\nFinished: SUCCESS\n";
        $err = 0;
      }
    }

    print $resp_msg;
  }

  return $err;
}

