#!/usr/local/cpanel/3rdparty/bin/perl
#
# cpanel - cp_util/update_php.pl                  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
#
# This provides incremental updates to existing PHP versions in EasyApache.
# It doesn't verify that patches work, nor will it add a new major version

use strict;
use warnings;
use File::Basename ();
use Getopt::Long   ();
use Test::More;

# Global static values
my $WGET      = q{/usr/bin/wget};
my $FILE      = q{/usr/bin/file};
my $MD5SUM    = q{/usr/bin/md5sum};
my $GIT       = q{/usr/local/cpanel/3rdparty/bin/git};
my $COMMIT    = q{commit};
my $EA_REMOTE = q{ssh://git@enterprise.cpanel.net:7999/cpanel/easyapache.git};    # r/w access
my $MKTAR     = q{cp_util/make_tar.pl};

# Global write-once/read-many values
my $NEW_PHP_FILENAME;
my $NEW_PHP_VERSION;
my $OLD_PHP_VERSION;

my @Steps = (
    { 'step' => q{Setup: Inside git repository},                   'func' => \&is_git_repo },
    { 'step' => q{Setup: Inside EasyApache repo},                  'func' => \&is_easyapache_repo },
    { 'step' => q{Setup: Correct directory},                       'func' => \&is_correct_dir },
    { 'step' => q{Setup: Pristine git repository},                 'func' => \&is_pristine_repo },
    { 'step' => q{Setup: Currently on master branch},              'func' => \&is_master_repo },
    { 'step' => q{Setup: Correct PHP URL},                         'func' => \&is_valid_url },
    { 'step' => q{Setup: Incremental upgrade check},               'func' => \&is_upgrade_incremental },
    { 'step' => q{Setup: Downloading PHP upstream tarball},        'func' => \&is_download_successful },
    { 'step' => q{Setup: Verifying upstream tarball},              'func' => \&is_valid_download },
    { 'step' => q{Setup: Create branch to store PHP update},       'func' => \&create_feature_branch },
    { 'step' => q{Setup: Clean auto-generated files},              'func' => \&clean_generated_files },
    { 'step' => q{Commit 1/3: Rename patch (directory)},           'func' => \&rename_patch_dir },
    { 'step' => q{Commit 1/3: Rename patch (ptar)},                'func' => \&rename_patch_ptar },
    { 'step' => q{Commit 1/3: Rename upstream (source: step 1)},   'func' => \&rename_upstream_source_step1 },
    { 'step' => q{Commit 1/3: Rename upstream (source: step 2)},   'func' => \&rename_upstream_source_step2 },
    { 'step' => q{Commit 1/3: Rename upstream (ptar)},             'func' => \&rename_upstream_ptar },
    { 'step' => q{Commit 1/3: Rename EasyApache code (directory)}, 'func' => \&rename_easyapache_code_dir },
    { 'step' => q{Commit 1/3: Rename EasyApache code (module)},    'func' => \&rename_easyapache_code_pm },
    { 'step' => q{Commit 1/3: Create commit},                      'func' => \&create_commit1 },
    { 'step' => q{Commit 2/3: Update upstream (source)},           'func' => \&update_upstream_source_code },
    { 'step' => q{Commit 2/3: Create commit},                      'func' => \&create_commit2 },
    { 'step' => q{Commit 2/3: Update upstream (ptar)},             'func' => \&update_upstream_ptar },
    { 'step' => q{Commit 2/3: Update upstream (gitignore)},        'func' => \&update_upstream_gitignore },
    { 'step' => q{Commit 3/3: Update patch (ptar)},                'func' => \&update_patch_ptar },
    { 'step' => q{Commit 3/3: Update patch (gitignore)},           'func' => \&update_patch_gitignore },
    { 'step' => q{Commit 3/3: Update perl modules},                'func' => \&update_perl_modules },
    { 'step' => q{Commit 3/3: Update profiles},                    'func' => \&update_profiles },
    { 'step' => q{Commit 3/3: Create commit},                      'func' => \&create_commit3 },
    { 'step' => q{Cleanup: Pristine git repository},               'func' => \&is_pristine_repo },
    { 'step' => q{Cleanup: Clean auto-generated files},            'func' => \&clean_generated_files },
);

# Run a command, and return the UNIX exit code
sub run_cmd {
    my $buf = shift;

    #print "@_\n";
    my $out = `@_ 2>&1`;
    chomp $out;
    $$buf = $out if $buf;
    return ( $? >> 8 );
}

# Get md5sum of file
sub md5 {
    my $file = shift;
    my $out;
    run_cmd( \$out, "$MD5SUM $file" );
    $out = ( split( /\s+/, $out ) )[0];
    return ( defined $out ? $out : '' );
}

# get file information
sub file {
    my $in = shift;
    my $out;
    run_cmd( \$out, "$FILE $in" );
    return $out;
}

# Convert a PHP version into the two-digit minor_patch string
sub php_version_2_ea_version {
    my $in = shift;
    my @tmp = split( /\./, $in );
    return $tmp[0], sprintf( '%d_%d', $tmp[1], $tmp[2] );
}

# Convert a PHP version into the Cpanel perl code path
sub php_version_2_perl_path {
    my $in = shift;
    my ( $major, $ea_ver ) = php_version_2_ea_version($in);
    return sprintf( 'Cpanel/Easy/PHP%d/%s', $major, $ea_ver );
}

# Convert a PHP version into the Cpanel targz upstream path
sub php_version_2_upstream_path {
    my $in = shift;
    my ( $major, $ea_ver ) = php_version_2_ea_version($in);
    return sprintf( 'targz/Cpanel/Easy/PHP%d/%s.pm.tar.gz.d', $major, $ea_ver );
}

# Convert a PHP version into the Cpanel targz upstream ptar
sub php_version_2_upstream_ptar {
    my $in = shift;
    my ( $major, $ea_ver ) = php_version_2_ea_version($in);
    return sprintf( 'targz/Cpanel/Easy/PHP%d/%s.pm.tar.gz.ptar', $major, $ea_ver );
}

# Convert a PHP version into the Cpanel targz cppatch source dir
sub php_version_2_cppatch_path {
    my $in = shift;
    my ( $major, $ea_ver ) = php_version_2_ea_version($in);
    return sprintf( 'targz/Cpanel/Easy/PHP%d/%s.pm.patch.tar.gz.d', $major, $ea_ver );
}

# Convert a PHP version into a Cpanel targz cppatch ptar
sub php_version_2_cppatch_ptar {
    my $in = shift;
    my ( $major, $ea_ver ) = php_version_2_ea_version($in);
    return sprintf( 'targz/Cpanel/Easy/PHP%d/%s.pm.patch.tar.gz.ptar', $major, $ea_ver );
}

# Generate a branch name
sub generate_branch_name {
    my $case = shift;

    # NOTE: $NEW_PHP_VERSION better be defined!
    return sprintf( 'EA-case-%s-auto-php-%s-update', $case, $NEW_PHP_VERSION );
}

# Make sure we're in the easyapache repo
sub is_git_repo {
    my ( $ret, $msg ) = qw( 1 Ok );

    # make sure this is a git repo
    unless ( run_cmd( undef, "$GIT remote" ) == 0 ) {
        $ret = 0;
        $msg = "Not in a git repository";
    }

    return ( $ret, $msg );
}

# At this point, we should know it's a repo.  But is it an EasyApache repo?
sub is_easyapache_repo {
    my ( $ret, $msg ) = qw( 1 Ok );

    my $out;
    run_cmd( \$out, "$GIT remote -v" );

    unless ( $out =~ /upstream(?:\-rw)?\s+\Q$EA_REMOTE\E/ms ) {
        $ret = 0;
        $msg = "Not an easyapache git repository:\n$out";
    }

    return ( $ret, $msg );
}

# Validate repo is in pristine shape.  This ensures that before
# we do any work, nothing will get in the way.  It also ensures
# that when we're done, nothing was left behind.
sub is_pristine_repo {
    my ( $ret, $msg ) = qw( 1 Ok );

    # make sure there aren't any file changes in the repo
    my $out;
    run_cmd( \$out, "$GIT status" );

    unless ( $out =~ /working\s+tree\s+clean/ ) {
        $ret = 0;
        $msg = "Note a pristine repo: $out";
    }

    return ( $ret, $msg );
}

# In order to upgrade, let's make sure we're at the top level directory of the repo.
# This simplifies the steps that come later.
sub is_correct_dir {
    my ( $ret, $msg ) = qw( 1 Ok );

    unless ( -d '.git' ) {
        $ret = 0;
        $msg = q{You must be at the top-level of the EasyApache repository};
    }

    return ( $ret, $msg );
}

# We always branch off of master to perform PHP updates
sub is_master_repo {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    return ( $ret, $msg ) if $args->{devel};    # w/o this, I can't test the script in a feature branch

    my $out;
    run_cmd( \$out, "$GIT symbolic-ref --short -q HEAD" );

    unless ( $out =~ /^master$/ ) {
        $ret = 0;
        $msg = "You must be in the master branch. Currently in: $out";
    }

    return ( $ret, $msg );
}

# Before we even decide to start the upgrade process, at least
# make sure that this is an incremental upgrade.
sub is_valid_url {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    if ( $args->{url} =~ /(php\-(\d+\.\d+\.\d+)\.tar\.gz)/ ) {
        $NEW_PHP_FILENAME = "/tmp/$1";
        $NEW_PHP_VERSION  = $2;
    }
    else {
        $ret = 0;
        $msg = q{Invalid PHP download URL};
    }

    return ( $ret, $msg );
}

# Ensures this version of PHP has a previous version installed.
# This script doesn't support upgrading major versions.
sub is_upgrade_incremental {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my ( $major, $minor, $patch ) = split( /\./, $NEW_PHP_VERSION );

    # check if an incremental upgrades exist in EasyApache
    while ( !$OLD_PHP_VERSION && $patch-- > 0 ) {
        my $old_version = sprintf( '%d.%d.%d', $major, $minor, $patch );
        my $old_path = php_version_2_upstream_path($old_version);
        $OLD_PHP_VERSION = $old_version if -e $old_path;
    }

    unless ($OLD_PHP_VERSION) {
        $ret = 0;
        $msg = q{Unable to find a previous incremental PHP version in EasyApache};
    }

    return ( $ret, $msg );
}

# Downloads the tarball from PHP's website and stores it in a temporary
# location
sub is_download_successful {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );
    my $url = $args->{url};

    # download the tarball
    note "Downloading PHP version: $NEW_PHP_VERSION";

    # already downloaded
    if ( -e $NEW_PHP_FILENAME && md5($NEW_PHP_FILENAME) eq $args->{md5} ) {
        return ( $ret, $msg );
    }

    unlink $NEW_PHP_FILENAME;    # clean up old version of this file

    # failed to download, or file is missing
    my $rc = run_cmd( undef, "$WGET --quiet --output-document=$NEW_PHP_FILENAME $url" );
    if ( $rc != 0 || !-e $NEW_PHP_FILENAME ) {
        $ret = 0;
        $msg = qq{Failed to download PHP tarball: $url};
    }

    return ( $ret, $msg );
}

# Ensures the file that was download is a tarball, and verify the
# md5sum
sub is_valid_download {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $file = file($NEW_PHP_FILENAME);
    like( $file, qr/gzip\s+compressed\s+data/i, q{Setup: Downloaded file is a tarball} );

    my $md5 = md5($NEW_PHP_FILENAME);

    unless ( $md5 eq $args->{md5} ) {
        $ret = 0;
        $msg = q{MD5 checksum incorrect};
    }

    return ( $ret, $msg );
}

# Create place to store PHP update
sub create_feature_branch {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );
    my $out;

    my $branch = generate_branch_name( $args->{case} );

    unless ( run_cmd( \$out, "$GIT checkout -b $branch" ) == 0 ) {
        $ret = 0;
        $msg = "Unable to create feature branch: $out";
    }

    return ( $ret, $msg );
}

# Remove old tar balls to ensure clean generation of ptar later
sub clean_generated_files {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $out;
    unless ( run_cmd( \$out, "$GIT clean -dxf" ) == 0 ) {
        $ret = 0;
        $msg = "Failed to clean git repository: $out";
    }

    return ( $ret, $msg );
}

sub rename_patch_dir {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $olddir = php_version_2_cppatch_path($OLD_PHP_VERSION);
    my $newdir = php_version_2_cppatch_path($NEW_PHP_VERSION);

    my $out;

    unless ( run_cmd( \$out, "mv $olddir $newdir" ) == 0 ) {
        $ret = 0;
        $msg = "Unable to rename patch directory: $out";
    }

    return ( $ret, $msg );
}

sub rename_patch_ptar {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $oldptar = php_version_2_cppatch_ptar($OLD_PHP_VERSION);
    my $newptar = php_version_2_cppatch_ptar($NEW_PHP_VERSION);

    if ( -e $oldptar ) {
        my $out;

        unless ( run_cmd( \$out, "mv $oldptar $newptar" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to rename patch ptar: $out};
        }
    }
    else {
        $ret = 0;
        $msg = qq{Unable to find old ptar file: $oldptar};
    }

    return ( $ret, $msg );
}

sub rename_upstream_source_step1 {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $oldphp = sprintf( '%s/php-%s', php_version_2_upstream_path($OLD_PHP_VERSION), $OLD_PHP_VERSION );
    my $newphp = sprintf( '%s/php-%s', php_version_2_upstream_path($OLD_PHP_VERSION), $NEW_PHP_VERSION );

    if ( -e $oldphp ) {
        my $out;

        unless ( run_cmd( \$out, "mv $oldphp $newphp" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to rename upstream source: $out};
        }
    }
    else {
        $ret = 0;
        $msg = qq{Unable to locate old PHP directory: $oldphp};
    }

    return ( $ret, $msg );
}

sub rename_upstream_source_step2 {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $olddir = php_version_2_upstream_path($OLD_PHP_VERSION);
    my $newdir = php_version_2_upstream_path($NEW_PHP_VERSION);

    if ( -e $olddir ) {
        my $out;

        unless ( run_cmd( \$out, "mv $olddir $newdir" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to rename upstream source directory: $out};
        }
    }
    else {
        $ret = 0;
        $msg = qq{Unable to locate old upstream source code: $olddir};
    }

    return ( $ret, $msg );
}

sub rename_upstream_ptar {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $oldptar = php_version_2_upstream_ptar($OLD_PHP_VERSION);
    my $newptar = php_version_2_upstream_ptar($NEW_PHP_VERSION);

    if ( -e $oldptar ) {
        my $out;

        unless ( run_cmd( \$out, "mv $oldptar $newptar" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to rename upstream ptar: $out};
        }
    }
    else {
        $ret = 0;
        $msg = qq{Unable to locate old upstream ptar: $oldptar};
    }

    return ( $ret, $msg );
}

# Renames the EasyApache code directory (not the perl module itself)
sub rename_easyapache_code_dir {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $olddir = php_version_2_perl_path($OLD_PHP_VERSION);
    my $newdir = php_version_2_perl_path($NEW_PHP_VERSION);

    my $out;
    unless ( run_cmd( \$out, "mv $olddir $newdir" ) == 0 ) {
        $ret = 0;
        $msg = qq{Unable to rename $olddir: $out};
    }

    return ( $ret, $msg );
}

# Renames the EasyApache code PHP perl module
sub rename_easyapache_code_pm {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $oldpm = sprintf( '%s.pm', php_version_2_perl_path($OLD_PHP_VERSION) );
    my $newpm = sprintf( '%s.pm', php_version_2_perl_path($NEW_PHP_VERSION) );

    my $out;
    unless ( run_cmd( \$out, "mv $oldpm $newpm" ) == 0 ) {
        $ret = 0;
        $msg = qq{Unable to rename $oldpm: $out};
    }

    return ( $ret, $msg );
}

sub create_commit1 {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my @add = (
        sprintf( '%s.pm', php_version_2_perl_path($NEW_PHP_VERSION) ),
        php_version_2_perl_path($NEW_PHP_VERSION),
        php_version_2_cppatch_path($NEW_PHP_VERSION),
        php_version_2_cppatch_ptar($NEW_PHP_VERSION),
        php_version_2_upstream_path($NEW_PHP_VERSION),
        php_version_2_upstream_ptar($NEW_PHP_VERSION),
    );

    my @rem = (
        sprintf( '%s.pm', php_version_2_perl_path($OLD_PHP_VERSION) ),
        php_version_2_perl_path($OLD_PHP_VERSION),
        php_version_2_cppatch_path($OLD_PHP_VERSION),
        php_version_2_cppatch_ptar($OLD_PHP_VERSION),
        php_version_2_upstream_path($OLD_PHP_VERSION),
        php_version_2_upstream_ptar($OLD_PHP_VERSION),
    );

    for my $d (@add) {
        my $out;

        unless ( run_cmd( \$out, "$GIT add $d" ) == 0 ) {
            $ret = 0;
            $msg = qq{Unable to add $d: $out};
        }

        last unless $ret;
    }

    if ($ret) {
        my $out;

        for my $d (@rem) {
            unless ( run_cmd( \$out, "$GIT rm -rf $d" ) == 0 ) {
                $ret = 0;
                $msg = qq{Unable to remove $d: $out};
            }

            last unless $ret;
        }
    }

    if ($ret) {
        my $out;
        unless (
            run_cmd(
                \$out, qq|$GIT $COMMIT -m "Update PHP to v$NEW_PHP_VERSION, drop v$OLD_PHP_VERSION

Case $args->{case}: Commit one of three; simply set up the directories and
old code to make the following two commits easier to see actual
changes."|
            ) == 0
          ) {
            $ret = 0;
            $msg = qq{Unable to commit changes: $out};
        }
    }

    return ( $ret, $msg );
}

sub update_upstream_source_code {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );
    my $out;

    # remove old source code (this is actually the previous version)
    my $targzd = php_version_2_upstream_path($NEW_PHP_VERSION);
    my $phppath = sprintf( '%s/php-%s', $targzd, $NEW_PHP_VERSION );

    unless ( run_cmd( \$out, "rm -rf $phppath" ) == 0 ) {
        $ret = 0;
        $msg = qq{Unable to remove old source code, $phppath: $out};
    }

    if ($ret) {
        unless ( run_cmd( \$out, "tar xfz $NEW_PHP_FILENAME -C $targzd" ) == 0 ) {
            $ret = 0;
            $msg = qq{Unable to extract new source code, $NEW_PHP_FILENAME: $out};
        }
    }

    if ($ret) {
        unless ( run_cmd( \$out, "rm -f $phppath/.gitignore" ) == 0 ) {
            $ret = 0;
            $msg = qq{Unable to remove upstream gitignore: $out};
        }
    }

    return ( $ret, $msg );
}

sub create_commit2 {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );
    my $out;

    # TODO: Sometimes, files are deleted between PHP versions.  This causes the "git add ." to
    # fail because it complains about adding files that were deleted.  This will mean that
    # we'll have to eventually parse a "git status" output for files that were removed, then
    # "git rm" those files before performing a "git add ."

    unless ( run_cmd( \$out, "$GIT add ." ) == 0 ) {
        $ret = 0;
        $msg = qq{Failed to add updated PHP files to git: $out};
    }

    if ($ret) {
        unless (
            run_cmd(
                \$out, qq|$GIT $COMMIT -m "Update PHP to v$NEW_PHP_VERSION, drop v$OLD_PHP_VERSION

Case $args->{case}: Commit two of three; update upstream source code."|
            ) == 0
          ) {
            $ret = 0;
            $msg = qq{Unable to commit changes: $out};
        }
    }

    return ( $ret, $msg );
}

sub update_upstream_ptar {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my ( $newfile, $dir1, $newext ) = File::Basename::fileparse( php_version_2_upstream_ptar($NEW_PHP_VERSION), '.ptar' );
    my $targzd = php_version_2_upstream_path($NEW_PHP_VERSION);

    run_cmd( undef, "rm -f $dir1/$newfile$newext" );
    run_cmd( undef, "rm -f $dir1/$newfile" );

    my $out;

    unless ( run_cmd( \$out, "$MKTAR $targzd" ) == 0 ) {
        $ret = 0;
        $msg = qq{Failed to update upstream ptar: $out};
    }

    if ($ret) {
        unless ( run_cmd( \$out, "$GIT add $dir1/$newfile$newext" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to add $dir1/$newfile$newext to git: $out};
        }
    }

    return ( $ret, $msg );
}

sub update_upstream_gitignore {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );
    my $out;

    my ( $newfile, $dir1, $newext ) = File::Basename::fileparse( php_version_2_upstream_ptar($NEW_PHP_VERSION), '.ptar' );
    my ( $oldfile, $dir2, $oldext ) = File::Basename::fileparse( php_version_2_upstream_ptar($OLD_PHP_VERSION), '.ptar' );

    $newfile = quotemeta($newfile);
    $oldfile = quotemeta($oldfile);

    unless ( run_cmd( \$out, "$^X -pi -e 's/$oldfile/$newfile/g' targz/Cpanel/Easy/.gitignore" ) == 0 ) {
        $ret = 0;
        $msg = qq{Failed to update repo .gitignore with new upstream tarball: $out};
    }

    if ($ret) {
        unless ( run_cmd( \$out, "$GIT add targz/Cpanel/Easy/.gitignore" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to git add updated .gitignore with upstream tarball: $out};
        }
    }

    if ($ret) {
        unless ( run_cmd( \$out, "$GIT $COMMIT --amend --no-edit" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to amend commit with updated .gitignore: $out};
        }
    }

    return ( $ret, $msg );
}

sub update_patch_ptar {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );

    my $ptar = php_version_2_cppatch_ptar($NEW_PHP_VERSION);
    my ( $file, $dir, $ext ) = File::Basename::fileparse( $ptar, '.ptar' );

    run_cmd( undef, "rm -f $ptar" );
    run_cmd( undef, "rm -f $file" );

    my $out;

    my $pdir = php_version_2_cppatch_path($NEW_PHP_VERSION);

    unless ( run_cmd( \$out, "$MKTAR $pdir" ) == 0 ) {
        $ret = 0;
        $msg = qq{Failed to update patch ptar: $out};
    }

    if ($ret) {
        unless ( run_cmd( \$out, "$GIT add $ptar" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to add updated patch ptar to git: $out};
        }
    }

    return ( $ret, $msg );
}

sub update_patch_gitignore {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );
    my $out;

    my ( $newfile, $dir1, $newext ) = File::Basename::fileparse( php_version_2_cppatch_ptar($NEW_PHP_VERSION), '.ptar' );
    my ( $oldfile, $dir2, $oldext ) = File::Basename::fileparse( php_version_2_cppatch_ptar($OLD_PHP_VERSION), '.ptar' );

    $newfile = quotemeta($newfile);
    $oldfile = quotemeta($oldfile);

    unless ( run_cmd( \$out, "$^X -pi -e 's/$oldfile/$newfile/g' targz/Cpanel/Easy/.gitignore" ) == 0 ) {
        $ret = 0;
        $msg = qq{Failed to update repo .gitignore with new upstream tarball: $out};
    }

    if ($ret) {
        unless ( run_cmd( \$out, "$GIT add targz/Cpanel/Easy/.gitignore" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to git add updated .gitignore with upstream tarball: $out};
        }
    }

    return ( $ret, $msg );
}

sub update_perl_modules {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );
    my $out;

    my ( $nmaj, $newmin ) = php_version_2_ea_version($NEW_PHP_VERSION);
    my ( $omaj, $oldmin ) = php_version_2_ea_version($OLD_PHP_VERSION);

    unless ( run_cmd( \$out, "find Cpanel/Easy/PHP$nmaj.pm Cpanel/Easy/PHP$nmaj/$newmin.pm Cpanel/Easy/PHP$nmaj/$newmin -type f | xargs $^X -pi -e 's/$oldmin/$newmin/g'" ) == 0 ) {
        $ret = 0;
        $msg = qq{Failed to update EA version numbers in perl modules: $out};
    }

    if ($ret) {
        my $new = quotemeta($NEW_PHP_VERSION);
        my $old = quotemeta($OLD_PHP_VERSION);

        unless ( run_cmd( \$out, "$^X -pi -e 's/$old/$new/g' Cpanel/Easy/PHP$nmaj/$newmin.pm" ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to update PHP version number in perl modules: $out};
        }
    }

    if ($ret) {
        unless ( run_cmd( \$out, "$GIT add ." ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to add perl modules to git: $out};
        }
    }

    return ( $ret, $msg );
}

sub update_profiles {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );
    my $out;

    my ( $nmaj, $newmin ) = php_version_2_ea_version($NEW_PHP_VERSION);
    my ( $omaj, $oldmin ) = php_version_2_ea_version($OLD_PHP_VERSION);

    unless ( run_cmd( \$out, "find profiles/easy/apache/profile/ -type f | xargs $^X -pi -e 's/$oldmin/$newmin/g'" ) == 0 ) {
        $ret = 0;
        $msg = qq{Failed to update profiles with new PHP version: $out};
    }

    if ($ret) {
        unless ( run_cmd( \$out, "$GIT add ." ) == 0 ) {
            $ret = 0;
            $msg = qq{Failed to add profiles to git: $out};
        }
    }

    return ( $ret, $msg );
}

sub create_commit3 {
    my $args = shift;
    my ( $ret, $msg ) = qw( 1 Ok );
    my $out;

    unless (
        run_cmd(
            \$out, qq|$GIT $COMMIT -m "Update PHP to v$NEW_PHP_VERSION, drop v$OLD_PHP_VERSION

Case $args->{case}: Commit three of three; easyapache source code."|
        ) == 0
      ) {
        $ret = 0;
        $msg = qq{Unable to commit changes: $out};
    }

    return ( $ret, $msg );
}

sub usage {
    print "\nPerforms incremental PHP update to your local Git repo\n";
    print "Usage: $0 --url <tarball> --md5 <string> --case <case-id>\n";
    print "\n";
}

sub main {
    my %args;

    my $res = Getopt::Long::GetOptions(
        "a"      => \$args{add_all},        # add "-a" to all "git commit" commands
        "url=s"  => \$args{url},            # The full URL where the tarball can be downloaded
        "md5=s"  => \$args{md5},            # The expected md5sum of the tarball
        "case=s" => \$args{case},           # The case number associated with the PHP update
        "devel"  => \$args{devel},          # Allow dev to test in a branch other than master
        "i"      => \$args{interactive},    # User interactively must confirm moving to the next step
    );

    unless (
           defined $args{url}
        && defined $args{md5}
        && $args{md5} =~ /^[a-f0-9]{32}$/i
        && defined $args{case}
        && (   $args{case} =~ /^[1-9]\d+$/
            || $args{case} =~ /^[A-Z]{2,6}-[1-9]\d+$/ )
      ) {
        usage();
        return 0;
    }

    # add -a to commit if "-a" is passed, this takes care of things like unstaged deleted files
    $COMMIT = q{commit -a} if $args{add_all};

    # Perform the upgrade
    for ( my ( $i, $total ) = ( 0, $#Steps + 1 ); $i <= $#Steps; $i++ ) {
        if ( $args{interactive} ) {
            interact();
        }
        my $step = $Steps[$i];
        my ( $ret, @msg ) = $step->{func}->( \%args );
        ok( $ret, $step->{step} );
        BAIL_OUT("@msg") unless $ret;
    }

    done_testing();
}

# provides input loop and runtime menu
sub interact {
    my ( $input, $break );
    while ( ( not defined $input or $input = 'status' ) and not defined $break ) {
        print qq{Hit the 'any' key to move on to the next step. Git info available: (s)tatus, (l)og.\n};
        $input = <STDIN>;
        chomp $input;
        if ( $input eq 's' ) {
            my $out;
            run_cmd( \$out, "$GIT status" );
            print qq{$out\n};
        }
        elsif ( $input eq 'l' ) {
            my $out;
            run_cmd( \$out, "$GIT log | head -n 30" );
            print qq{$out\n};
        }
        else { ++$break }
        $input = undef;
    }
}

exit( main(@ARGV) ? 0 : 1 );

