#!/usr/bin/env perl
# Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
# Copyright [2016-2022] EMBL-European Bioinformatics Institute
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#      http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


use strict;
use warnings;

BEGIN {
  use Cwd;
  use File::Basename;
  use File::Spec;
  my $dirname = dirname(Cwd::realpath(__FILE__));
  my $lib = File::Spec->catdir($dirname, File::Spec->updir(), 'lib');
  if(-d $lib) {
    unshift(@INC, $lib);
  }
  else {
    die "Cannot find the lib directory in the expected location $lib";
  }
};

use Pod::Usage;
use Getopt::Long;
use EnsEMBL::Git;

run();

sub run {
  my $opts = parse_command_line();

  sanity_checks($opts);

  my $src = $opts->{source};
  my $trg = $opts->{target};
  my $merge = $opts->{merge};
  my $rebase = $opts->{rebase};
  my $remote = $opts->{remote};
  my $continue = $opts->{continue};

  # Check if we are aborting and then do so
  if($opts->{abort}) {
    print "* Aborting the current run\n";
    my $exit_status = 0;
    if($rebase) {
      $exit_status = system_ok('git rebase --abort');
    }
    elsif($merge) {
      $exit_status = system_ok('git reset --merge');
    }
    else {
      print STDERR "! Not sure what I am aborting here\n";
      $exit_status = 3;
    }
    exit $exit_status;
  }

  if(is_in_merge()) {
    print STDERR "! Cannot continue. You have not resolved the merge or committed any changes\n";
    if($merge && $continue) {
      print STDERR "! Once resolved rerun the git mgw --merge --continue command\n";
    }
    exit 3;
  }

  # Checkout target and get latest changes
  checkout($trg);
  if(! pull($remote, 'verbose')) {
    print STDERR "! Could not perform pull from $remote. Aborting\n";
    exit 3;
  }

  # Back to src
  checkout($src);
  if(! is_tree_clean()) {
    print STDERR "! Cannot continue; tree is not clean and rebases can fail\n";
    exit 4;
  }

  # Start the rebase strategy
  if($rebase) {
    my $current_sha1 = rev_parse('HEAD');

    if(!rebase($trg, $continue)) {
      print STDERR "! Could not finish rebase. Please check the error messages for the reason why\n";
      print STDERR "! To continue after resolving run: git mgw --rebase --continue\n";
      print STDERR "! To abort run: git mgw --rebase --abort\n";
      exit 5;
    }

    print "* Please take a moment to review your changes.\n";
    print "* Example cmd: git log --oneline --reverse ${trg}..${src}\n";
    if(! $opts->{noprompt} && ! prompt()) {
      print STDERR "! Process has been abandoned. Please review the changes\n";
      print STDERR "! You can reset the current changes using the following command (this will re-write your history and ref pointers)\n";
      print STDERR "! git reset $current_sha1\n";
      exit 6;
    }
  }

  # Now back to 'master' and merge if remote and local are in sync
  checkout($trg);
  print "* Checking that $trg is at the same revision as $remote/$trg\n";
  if(!is_origin_uptodate($trg, $remote)) {
    print "* Not at the same revision. Checking that $remote/$trg can be fast-forward to $trg\n";
    if(!can_fastforward_merge($trg, $remote)) {
      print STDERR "! Local and remote branches are not on the same hash and cannnot be fast-forward merged. Cannot continue\n";
      exit 7;
    }
    else {
      print "* $remote/$trg can be fast-forward merged to $trg. Allowing\n";
    }
  }
  else {
    print "* $trg and $remote/$trg are on the same revision\n";
  }
  
  # Merges use non-fast-forward and ask the user to confirm the final merge
  if($merge) {
    if($continue) {
      if(is_in_merge()) {
        print STDERR "! We are still in a merge. You must resolve the merge, add and commit before continuing\n";
        exit 8;
      }
      print "* Continuing the MGW flow\n";
    }
    else {
      checkout($trg);
      if(! no_ff_merge($src, "Automatic merging of $src into $trg")) {
        print STDERR "! Could not perform non-fast-forward merge of '${src}' into '${trg}'\n";
        print STDERR "! Check your merge status, resolve any problems and add into the index and commit\n";
        print STDERR "! To continue after resolving run: git mgw --merge --continue\n";
        print STDERR "! To abort run: git mgw --merge --abort\n";
        exit 8;
      }
    }
    print "* Please take a moment to review your changes\n";
    print "* Example cmd: git log --oneline --reverse ${trg}...${trg}^\n";
    if(! $opts->{noprompt} && ! prompt()) {
      print STDERR "! Abandoned work\n";
      exit 8;
    }
  }

  # Rebases need only do a ff merge since we've made sure it's going to be ok
  if($rebase) {
    checkout($trg);
    if(! ff_merge($src)) {
      print STDERR "! Could not perform fast-forward merge of '${src}' into '${trg}'\n";
      exit 8;
    }
  }

  if($opts->{nopush}) {
    print "* Leaving changes on $trg and not pushing\n";
  }
  else {
    print "* About to push to $remote\n";
    if(! $opts->{noprompt} && ! prompt()) {
      print STDERR "! Abandoned changes\n";
      exit 9;
    }

   if(!git_push($remote, $trg, 'verbose')) {
      print STDERR "! Could not push back to $remote using branch $trg. WARNING ON STILL ON '${trg}'\n";
      exit 10;
    }
    print "* Finished and pushed changes to $remote\n";
  }

  print "* Switching back to $src\n";
  checkout($src);

  # If we were merging we must rebase dev back against master
  if($merge) {
    print "* Rebasing $src against $trg as we were using the merge strategy\n";
    rebase($trg);
  }

  print "* DONE\n";

  exit 0;
}

sub parse_command_line {
  my $opts = {
    source => 'dev',
    target => 'master',
    remote => 'origin',
    nopush => 0,
    noprompt => 0,
    continue => 0,
    abort => 0,
    merge => 0,
    rebase => 0,
    help => 0,
    man => 0
  };

  GetOptions($opts, qw/
    source=s
    target=s
    remote=s
    nopush!
    noprompt!
    continue!
    abort!
    merge!
    rebase!
    help|?
    man
  /) or pod2usage(2);

  pod2usage(1) if $opts->{help};
  pod2usage(-exitval => 0, -verbose => 2) if $opts->{man};

  return $opts;
}

# Do ALOT of checks before we can proceed. 
# 1) Check we have a strategy and just 1 at that
# 2) Make sure this is a Git repo
# 3) Check the branches exist
# 4) Make sure src does not track a repo (otherwise rebase is bad) if using --rebase
# 5) Confirm with the user
sub sanity_checks {
  my ($opts) = @_;
  my $src = $opts->{source};
  my $trg = $opts->{target};
  my $rem = $opts->{remote};

  if(! $opts->{merge} && ! $opts->{rebase}) {
    pod2usage(-exitval => 1, -verbose => 1, -msg => 'You need to specify the --rebase or --merge strategy');
  }
  if($opts->{merge} && $opts->{rebase}) {
    pod2usage(-exitval => 1, -verbose => 1, -msg => 'Specify either --rebase or --merge');
  }
  if(! is_git_repo()) {
    pod2usage(-exitval => 1, -verbose => 1, -msg => 'Current directory is not a Git repository');
  }
  if(! branch_exists($src)) {
    pod2usage(-exitval => 1, -verbose => 1, -msg => "The branch $src does not exist. Rerun with --source"); 
  }
  if(! branch_exists($trg)) {
    pod2usage(-exitval => 1, -verbose => 1, -msg => "The branch $trg does not exist. Rerun with --target"); 
  }

  if($opts->{rebase}) {
    my $src_merge = get_config("branch.${src}.merge");
    my $src_remote = get_config("branch.${src}.remote");

    if($src_merge) {
      pod2usage(-exitval => 1, -verbose => 1, 
        -msg => "The $src branch is setup to merge with '$src_merge'. Do not do this. This branch must be a local branch non-tracking branch"); 
    }
    if($src_remote) {
      pod2usage(-exitval => 1, -verbose => 1, 
        -msg => "The $src branch is tracking a remote '$src_remote'. Do not do this. This branch must be a local branch non-tracking branch"); 
    }
  }

  printf  "* MGW strategy is '%s'\n", ( $opts->{merge} ? 'merge' : 'rebase');
  print   "* Source branch is '$src'\n";
  print   "* Target branch is '$trg'\n";
  print   "* Remote name is '$rem'\n";
  if(! $opts->{noprompt} && ! prompt()) {
    print STDERR "! Aborting on user request\n";
    exit 2;
  }

  return;
}

__END__
=pod

=head1 NAME

git-mgw - Perform the Minimal Git Workflow

=head1 SYNOPSIS

git mgw [--rebase | --merge] [-source BRANCH] [-target BRANCH] [-remote ORIGIN] [-nopush] [-noprompt] [--continue] [--abort] [-h] [-m]

=head1 DESCRIPTION

Provides the Minimal Git Workflow which is an Anacode flow. It involves
developing code on to a non-tracking branch (dev), rebasing
against a tracking target (master), merging target with source and then 
pushing the resulting fast forward merge to origin.

A second strategy can be used which forces a non-fast forward merge
and promotes "bubbles" of development. This is a useful strategy for
branches/ref which have been shared to a remote.

=head1 OPTIONS

=over 8

=item B<--rebase>

Use the rebase strategy. Can only be applied to a source branch which is not remote tracking.

=item B<--merge>

Use the merge strategy. Safe with all types of branches.

=item B<--source>

The source branch of changes. Defaults to dev. Cannot be a remote tracking branch if using the --rebase strategy

=item B<--target>

The target branch of the changes. Defaults to master.

=item B<--remote>

The name of the remote. Defaults to origin.

=item B<--nopush>

Do not push changes.

=item B<--continue>

If a previous run of git-mgw has been aborted because of a merge or rebase conflict you can opt to continue mgw once those issues have been resolved.

=item B<--continue>

If a previous run of git-mgw has been aborted because of a merge or rebase conflict you can opt to abort mgw and go back to the original version.

=item B<--noprompt>

Skip any user checks during this entire procedure

=item B<--help>

Print the help information

=item B<--man>

Print a man page

=back

=cut
