#!/usr/local/cpanel/3rdparty/bin/perl

# cpanel - cPanel/Ecommerce/OSCommerce/update_src Copyright(c) 2014 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

use strict;
use warnings;

use File::Copy          ();
use File::Path          ();
use File::Spec          ();
use Cpanel::ExtractFile ();

our $UPSTREAM = "upstream";

exit _main(@ARGV) unless caller;

sub _main {
    print_help() if @_ != 3 || grep { $_ eq '--help' } @_;
    my ( $file, $oldver, $newver ) = @_;

    my $gitDir = File::Spec->rel2abs('src');

    # Note the location of the package_src_update script so we can tell the user to run it later.
    my $package_script = `git rev-parse --show-toplevel`;
    chomp $package_script;
    $package_script .= "/bin/package_src_update";

    # Identify the git repository and working directory (no need to 'cd' to that directory).
    local $ENV{GIT_DIR}       = "$gitDir/.git";
    local $ENV{GIT_WORK_TREE} = $gitDir;

    # Get the 'original' branch from upstream.
    system 'git', 'fetch', $UPSTREAM and die "Cannot fetch remote '$UPSTREAM'";
    system qw(git checkout -q -B original), "$UPSTREAM/original" and die "Cannot checkout branch 'original'";

    # Remove everything in the git work tree, except for the .git dir.
    empty_git_working_tree($gitDir);

    # Extract the new version of the cPAddon into the git work tree.
    Cpanel::ExtractFile::extractfile( $file, dir => $gitDir );

    # If file contents are stored in a directory (tarball instead of tarbomb),
    # then move the files.
    convert_ball_to_bomb($gitDir);

    # Add files to git, completely overwriting old ones.
    system qw(git add -A) and die "Unable to add the new version";
    system qw(git commit -q -m), "Version $newver" and die "Cannot commit the new version";
    system qw(git checkout -q --detach) and die "An error occurred while detaching";

    # Remove the non-catalog/ files.  We don't rely on shell expansion because it
    # doesn't handle hidden files.
    my $root_dir = `git ls-tree -z --name-only HEAD`;
    foreach my $file ( split m/\0/, $root_dir ) {
        system qw(git rm -q -r), $file if $file && $file ne 'catalog';
    }
    system qw(git commit -q -m), "Remove all but the catalog/ directory" and die "Error making commit";

    # Move all of the catalog/* files into the root directory
    my $catalog_dir = `git ls-tree -z --name-only HEAD catalog/`;
    foreach my $file ( split m/\0/, $catalog_dir ) {
        system qw(git mv), $file, "." and die "Error moving file: $file";
    }
    system qw(git commit -q -m), "'Change into' the catalog/ directory" and die "Error making commit";

    # Remove install/ for security reasons (recommended by OSCommerce).
    system qw(git rm -q -r install/) and die "Cannot remove the install directory";
    system qw(git commit -q -m), "Remove the install/ directory for security" and die "Error making commit";

    # Get the location of the current HEAD, for use later (since we are currently
    # on a detached HEAD).
    my $onto = `git rev-parse HEAD`;
    chomp $onto;

    # Determine the upstream commit for our git-rebase.  Since we make 3 commits automatically
    # (above), we grab the 3rd commit listed.
    my $history = `git rev-list --reverse HEAD..$UPSTREAM/$oldver`;
    my ( undef, undef, $upstream ) = split m/\n/, $history, 4;

    # Rebase old cPanel customizations onto new version. The user may
    # need to resolve conflicts at this point.  We rebase instead of
    # cherry-pick to get around the 'format.signoff' config value.
    system qw(git branch --no-track), $newver, "$UPSTREAM/$oldver" and die "Cannot create branch '$newver'";
    system qw(git rebase -Xpatience), "--onto=$onto", $upstream, $newver;
    my $exit = ( $? >> 8 ) || $?;

    print <<EOF;

Please verify that the update was successful.  Check the src/ directory
to ensure the git rebase was successful and that all necessary patches
were applied.  Once this has been verified, please run the following:
'$package_script $oldver $newver'.

Additionally, be sure to update the mysql file using the instructions
available at https://cpanel.wiki/display/RT/Manually+Modify+cPAddons.
EOF
    return $exit;
}

sub print_help {
    print <<EOF;
Usage: $0 <archive_file> <old_version> <new_version>

Updates the contents of the src/ submodule with the latest version of
the source (from the <archive_file>).  This script then reapplies all
patches made on <old_version> to the new source code for <new_version>.

Instructions on how to use this script are documented at
https://cpanel.wiki/display/RT/Manually+Modify+cPAddons.

This is a custom version that only works for OSCommerce.
EOF
    exit;
}

=head2 C<< empty_git_working_tree($dir) >>

Removes all files from the git working tree, but leaves the git repository untouched.

=over 4

=item C<$dir> [in]

Git working directory.

=back

B<Returns:> nothing

B<Dies:> if the given directory cannot be opened.

B<Warns:> if an error occurs while reading the directory.

I<Note:> This function assumes that the git directory is C<< "$dir/.git" >>.

=cut

sub empty_git_working_tree {
    my $dir = shift;

    my $gitWorkDir = File::Spec->rel2abs($dir);
    my $gitDir = File::Spec->rel2abs( '.git', $gitWorkDir );

    opendir my $dh, $gitWorkDir or die "Cannot open dir '$gitWorkDir': $!";
    while ( my $file = readdir $dh ) {
        next if $file =~ m/^\.\.?$/;

        my $path = File::Spec->catfile( $gitWorkDir, $file );
        next if $path eq $gitDir;

        File::Path::remove_tree($path);
    }
    closedir $dh or warn "Cannot close dir '$gitWorkDir': $!";
}

=head2 C<< convert_ball_to_bomb($dir) >>

Converts a tarball (or zipball) into a tarbomb.  A tarball has a top level directory that
contains every other file in the archive.  A tarbomb, however, stores several files or
directories in the top level which extract into the working directory and is generally
considered bad etiquette.

If the given directory (C<$dir>) contains only one entry, a sub-directory, then the contents
of that sub-directory are moved into C<$dir> and the sub-directory is deleted.  The '.', '..',
and '.git' directories are ignored when counting.

=over 4

=item C<$dir> [in]

Directory to inspect and potentially fix.

=back

B<Returns:> nothing

B<Dies:> if the given directory cannot be opened.

B<Warns:> if an error occurs while reading the directory.

=cut

sub convert_ball_to_bomb {
    my ($dir) = @_;

    opendir my $dh, $dir or die "Cannot open dir '$dir': $!";
    my @files = grep { !m/^\.(?:\.|git)?$/ } readdir $dh;
    closedir $dh or warn "Cannot close dir '$dir': $!";

    if ( @files == 1 && -d "$dir/$files[0]" ) {

        # Detected a "ball"; convert to a "bomb"

        # TODO: handle case where "$dir/$files[0]/$files[0]" exists
        move_directory_contents( "$dir/$files[0]", $dir );
        rmdir "$dir/$files[0]";
    }

    return;
}

=head2 C<< move_directory_contents($src, $dst) >>

Moves every file found in C<$src> into C<$dst>.

=over 4

=item C<$src> [in]

Directory from which the files are moved.

=item C<$dst> [in]

Directory into which the files are moved.  The directory must already exist.

=back

B<Returns:> nothing

B<Dies:> if either directory cannot be opened.

B<Warns:> if an error occurs while reading a directory.

=cut

sub move_directory_contents {
    my ( $srcDir, $dstDir ) = @_;

    opendir my $dh, $srcDir or die "Cannot open dir '$srcDir': $!";
    while ( my $file = readdir $dh ) {
        next if $file =~ m/^\.\.?$/;

        my $srcFile = File::Spec->catfile( $srcDir, $file );
        my $dstFile = File::Spec->catfile( $dstDir, $file );

        File::Copy::move( $srcFile, $dstFile );
    }
    closedir $dh or warn "Cannot close dir '$srcDir': $!";

    return;
}
