#!/usr/bin/perl
use strict;
use warnings;
use Pod::Usage;
use File::Spec;
use File::Path qw( remove_tree );
use File::Temp qw( tempdir );
use Cwd qw( cwd );
use Getopt::Long;
use Git::Repository;
use Git::Version::Compare qw( cmp_git );
use System::Command 1.117;    # loop_on

# command-line options
my %option = (
    source      => '/opt/src/git',
    destination => '/opt/git',
    limit       => 0,
    verbose     => 0,
    rc          => 1,
);
GetOptions(
    \%option,  'source=s', 'destination=s', 'verbose+', 'quiet',
    'since=s', 'until=s',  'limit=i',       'force',
    'list',    'missing',  'installed',     'rc!',
    'fetch',   'test',     'docs',
    'help',    'manual',
) or pod2usage( -verbose => 0 );

# simple help/manual
pod2usage( -verbose => 1 ) if $option{help};
pod2usage( -verbose => 2 ) if $option{manual};

# verbosity options
my %run_opt;
$option{quiet}   = 1     if $option{test};
$option{verbose} = 0     if $option{quiet};
$run_opt{stderr} = undef if !$option{verbose};
$run_opt{stdout} = sub { print shift } if $option{verbose} > 1;

# git.git
my $r = Git::Repository->new( work_tree => $option{source} );

# fetch recent commits
$r->run( 'fetch' ) if $option{fetch};

# map version numbers to tags
my %tag_for = map { ( my $v = substr $_, 1 ) =~ y/-/./; ( $v => $_ ) }
  grep /^v[^0]/ && !/^v1\.0rc/,    # skip anything before 1.0
  $r->run( tag => '-l', 'v*' );

# select the versions to build and install
my @versions = sort cmp_git @ARGV ? @ARGV : keys %tag_for;

# replace aliases with the canonical name
{
    my %alias = (
        '1.0.1'   => '1.0.0a',
        '1.0.2'   => '1.0.0b',
    );
    my %seen;
    @versions = grep !$seen{$_}++, map $alias{$_} || $_, @versions;
}

@versions = grep !/rc/, @versions if !$option{rc};

@versions = grep !is_installed($_), @versions if $option{missing};
@versions = grep is_installed($_),  @versions if $option{installed};

@versions = grep cmp_git( $option{since}, $_ ) <= 0, @versions if $option{since};
@versions = grep cmp_git( $_, $option{until} ) <= 0, @versions if $option{until};

@versions = $option{limit} > 0
  ? @versions[ -$option{limit} .. -1 ]       # <limit> most recent
  : @versions[ 0 .. -$option{limit} - 1 ]    # <limit> most ancient
  if $option{limit};


# pick up invalid versions
my @nope = grep !exists $tag_for{$_}, @versions;
die "Can't compile non-existent versions: @nope\n" if @nope;

# just list the selected versions
print map "$_\n", @versions and exit if $option{list};

# test outputs TAP
if ( $option{test} ) {
    require Test::More;
    import Test::More;
    plan( tests => scalar @versions );
    $option{destination} = tempdir( CLEANUP => 1 );
}

# build install select versions
chdir $option{source} or die "Can't chdir to $option{source}: $!";
for my $version (@versions) {

    # skip if that git already exists (and runs)
    if ( is_installed($version) && !$option{force} ) {
        print "*** GIT $version ALREADY INSTALLED ***\n" if !$option{quiet};
        next;
    }
    else {
        print "*** GIT $version ***\n" if !$option{quiet};
        $r->run( checkout => '-f', '-q', $tag_for{$version} );
        $r->run( clean => '-xqdf' );

        # Fix various issues in the Git sources

        # Fix GIT-VERSION-GEN to use `git describe` instead of `git-describe`
        if (
            cmp_git( $version, '1.3.3' ) <= 0
            && cmp_git( '1.1.0', $version ) <= 0
            && do { no warnings; `git-describe`; $? != 0 }
          )
        {
            local ( $^I, @ARGV ) = ( '', 'GIT-VERSION-GEN' );
            s/git-describe/git describe/, print while <>;
        }

        # fix GIT_VERSION in the Makefile
        if ( cmp_git( $version, '1.0.9' ) == 0 ) {
            local ( $^I, @ARGV ) = ( '', 'Makefile' );
            s/^GIT_VERSION = .*/GIT_VERSION = $version/, print while <>;
        }

        # add missing #include <sys/resource.h>
        elsif (   cmp_git( $version, '1.7.5.rc0' ) <= 0
            && cmp_git( '1.7.4.2', $version ) <= 0 )
        {
            $r->run( 'cherry-pick', '-n',
                'ebae9ff95de2d0b36b061c7db833df4f7e01a41d' );

            # force the expected version number
            my $version_file = File::Spec->catfile( $r->work_tree, 'version' );
            open my $fh, '>', $version_file
              or die "Can't open $version_file: $!";
            print $fh "$version\n";
        }

        # settings
        my $prefix = File::Spec->catdir( $option{destination}, $version );
        my @make = ( make => "prefix=$prefix" );

        # clean up environment (possibly set by local::lib)
        local $ENV{PERL_MB_OPT};
        local $ENV{PERL_MM_OPT};
        remove_tree( $prefix ) if -e $prefix;

        # build
        run_cmd( @make => '-j3' );

        # install
        run_cmd( @make => 'install' );
        run_cmd( @make => 'install-doc' ) if $option{docs};

        # test the installation and remove all
        if ( $option{test} ) {
            ok( is_installed($version), "$version installed successfully" );
            remove_tree( $prefix );
        }
    }
}

sub run_cmd {
    print "* @_\n" if !$option{quiet};
    System::Command->loop_on( command => \@_, %run_opt ) or die "FAIL: @_\n";
}

sub is_installed {
    my ($version) = @_;
    my $git =
      File::Spec->catfile( $option{destination}, $version, 'bin', 'git' );
    return eval { Git::Repository->version_eq( $version, { git => $git } ) };
}

__END__

=pod

=head1 NAME

build-git - Build and install any Git

=head1 SYNOPSIS

    # clone git.git

    # build and install Git 1.7.2
    $ build-git 1.7.2

    # build and install all versions between 1.6.5 and 2.1.0
    $ build-git --since 1.6.5 --until 2.1.0

    # build and install all versions of Git (since 1.0.0)
    $ build-git

    # build and install the 5 most recent versions of the selection
    $ build-git --limit 5 ...

    # build and install the 5 most ancient versions of the selection
    $ build-git --limit -5 ...

    # fetch the latest commit and install the latest git
    $ build-git --fetch --limit 1

=head1 OPTIONS AND ARGUMENTS

=head2 Options

 --source <directory>         The location of the git.git clone checkout

 --destination <directory>    The location of the Git collection

 --fetch                      Start by doing a `git fetch`

 --list                       List the selected versions and exit

 --test                       Compile, install, test and uninstall
                              the selected versions. Outputs TAP.
                              (Implies --quiet and force --destination to
                              a temporary directory.)

 --since <version>            Select versions greater or equal to <version>

 --until <version>            Select versions less or equal to <version>

 --missing                    Select only non-installed versions

 --installed                  Select only installed versions
                              (incompatible with the --missing option)

 --limit <count>              Limit the number of versions in the selection
                              (if <count> is positive, keep the most recent
                              ones, if <count> is negative, keep the oldest)

 --verbose                    Used once, shows the STDERR of the commands
                              run to compile and install Git.
                              Used twice, also shows the STDOUT.

 --quiet                      Silence all output, including the progress status

=head2 Arguments

If no argument is given, all versions are selected.

=head1 DESCRIPTION

B<build-git> is a small utility to build and install any version of Git.

It automatically applies some necessary patches that are needed to compile
Git on recent systems.

It is used to test the L<Git::Repository> module against all versions
of Git.

=head1 AUTHOR

Philippe Bruhat (BooK) <book@cpan.org>

=head1 COPYRIGHT

Copyright 2016 Philippe Bruhat (BooK), all rights reserved.

=head1 LICENSE

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut
