#!/usr/bin/env bash
#
# CLI tool for GitHub release management
#
# Note:
#   For conventional-github-releaser to work you'll need to setup a new GitHub
#   app token, then add the `CONVENTIONAL_GITHUB_RELEASER_TOKEN` environment
#   variable to your shell. You can find more details here:
#   https://github.com/stevemao/$conventionalGithubReleaser#setup-token-for-cli
#
# Usage:
#   githubrelease -h
#
# defaults to conventional-recommended-bump
# and optional argument preset `angular`/ `jquery` ...
# defaults to conventional-commits-detector
#-------------------------------------------------------------------------------
# Does the following:
#  1. pull from remote origin for master and develop branches
#  2. rebase master onto develop
#  3. rebase develop onto master
#  4. grabs the short SHA of master
#  5. updates changelog
#  6. bumps version number
#  7. commits changes
#  8. creates a version tag
#  9. pushes master and tag to remote
# 10. rebase master onto develop
# 11. pushes develop to remote
# 12. creates a release on github
# 13. output message "Landed in <shortsha>"
#-------------------------------------------------------------------------------

set -e

chalk="$(_githubreleasehelper -p chalk)"
conventionalChangelog="$(_githubreleasehelper -p conventional-changelog)"
conventionalCommitsDetector="$(_githubreleasehelper -p conventional-commits-detector)"
conventionalGithubReleaser="$(_githubreleasehelper -p conventional-github-releaser)"
conventionalRecommendedBump="$(_githubreleasehelper -p conventional-recommended-bump)"
json="$(_githubreleasehelper -p json)"
trash="$(_githubreleasehelper -p trash)"
shortsha=""


# ------------------------------------------------------------------------------
# Helper commands
# ------------------------------------------------------------------------------
# Universal logger for this script
log() {
  local output="$counter"
  [[ $counter -lt 10 ]] && output="$counter "

  $chalk cyan "[step: $output] $1"

  [[ "$1" != "startOnMaster" ]] && [[ "$1" != "startOnDevelop" ]] && increment
}

# Step counter
increment() {
  ((counter+=1))
}

# Get the current Git branch
gitCurrBranch() {
  ref=$(command git symbolic-ref HEAD 2> /dev/null) || \
  ref=$(command git rev-parse --short HEAD 2> /dev/null) || return
  echo "${ref#refs/heads/}"
}

# ------------------------------------------------------------------------------
# Script commands
# ------------------------------------------------------------------------------
# Switch the current branch to <master>
startOnMaster() {
  log "startOnMaster"
  if [ $(gitCurrBranch) != $master ]; then
    git checkout $master
  fi
}

# Switch the current branch to <develop>
startOnDevelop() {
  log "startOnDevelop"
  if [ $(gitCurrBranch) != $develop ]; then
    git checkout $develop
  fi
}

# Pull latest changes from <master> and <develop> using rebase
updateBranches() {
  log "updateBranches"
  git pull --rebase origin $master &&
  git checkout $develop &&
  git pull --rebase origin $develop
}

# Rebase the <develop> branch onto <master>
rebaseDevelopOntoMaster() {
  log "rebaseDevelopOntoMaster"
  git rebase $develop
}

# Run npm tests
runTests() {
  log "runTests"
  if [[ -z $argnotrashnodemodules ]]; then
    $trash node_modules &>/dev/null
  fi
  ${npmclient} install --silent
  ${npmclient} run test
}

# Get the short sha
getShortSha() {
  echo "$(git rev-parse --short HEAD)"
}

# Make a backup of the package.json
backupPackage() {
  log "backupPackage"
  cp package.json _package.json
  [[ -f package-lock.json ]] && cp package-lock.json _package-lock.json
  [[ -f yarn.lock ]] && cp yarn.lock _yarn.lock
}

# Use npm to bump the version of the package.json
bumpPackage() {
  log "bumpPackage"
  if [[ "${npmclient}" == "yarn" ]]; then
    ${npmclient} version --no-git-tag-version --new-version ${bump} &>/dev/null
  else
    ${npmclient} version --no-git-tag-version ${bump} &>/dev/null
  fi
}

# Generate a changelog entry and save it to <changelog>
updateChangelog() {
  log "updateChangelog"
  local changelog="./CHANGELOG.md"
  if [[ ! -f "$changelog" ]]; then
    touch "$changelog"
  fi &&
  $conventionalChangelog -i CHANGELOG.md -s -p ${preset}
  if [[ -z $argnoprompt ]]; then
    echo ""
    $chalk yellow "Review your \"$changelog\"."
    $chalk yellow "Would you like to continue?"
    select yn in "Yes" "No"; do
        case $yn in
            Yes ) break;;
            No ) exit;;
        esac
    done
  fi
}

# Add the <changelog> to Git and commit it
addChangelog() {
  log "addChangelog"
  local version=$(cat package.json | $json version)
  git add CHANGELOG.md
  git commit -m "docs(CHANGELOG): $version"
}

# Reset the package.json to the backup made earlier
resetPackage() {
  log "resetPackage"
  mv -f _package.json package.json
  [[ -f package-lock.json ]] && mv -f _package-lock.json package-lock.json
  [[ -f yarn.lock ]] && mv -f _yarn.lock yarn.lock
}

# Using npm, bump the package.json version, create new tag, and commit change
# to Git
npmBump() {
  if [[ -z $argnogittag ]]; then
    log "npmBump: create tag"
    if [[ "${npmclient}" == "yarn" ]]; then
      ${npmclient} config set version-git-message "chore(release): %s"
      ${npmclient} version --new-version ${bump}
    else
      ${npmclient} version ${bump} -m "chore(release): %s"
    fi
  else
    log "npmBump: only bump"
    if [[ "${npmclient}" == "yarn" ]]; then
      ${npmclient} version --no-git-tag-version --new-version ${bump} &>/dev/null
    else
      ${npmclient} version --no-git-tag-version ${bump} &>/dev/null
    fi
    local version=$(cat package.json | $json version)
    git add package.json
    git commit -m "chore(release): $version"
  fi
}

# Rebase the <master> branch onto <develop>
rebaseMasterOntoDevelop() {
  log "rebaseMasterOntoDevelop"
  git rebase $master
}

# Push <master> and <develop> branches and all tags to remote origin
pushAll() {
  log "pushAll"
  git push origin $master &&
  git push origin $develop &&
  git push --tags
}

# Cut a release on GitHub and append the changelog to the GitHub release
conventionalGitHubReleaser() {
  if [[ -z $argnogittag ]]; then
    log "conventionalGitHubReleaser: cut a release on GitHub"
    $conventionalGithubReleaser -p ${preset}
  else
    log "conventionalGitHubReleaser: skip release on GitHub"
  fi
}

landedMessage() {
  log "landedMessage"
  $chalk green "Landed in $shortsha"
}

# ------------------------------------------------------------------------------
#
# ------------------------------------------------------------------------------
# Defaults
defaultremote="origin"
defaultmaster="master"
defaultdevelop="develop"
defaultchangelog="CHANGELOG.md"
defaultnpmclient="npm"
defaultstep=1

# Usage descriptions
flag_h="Show this message."
flag_v="Display the version of this script."
flag_f="Display the flags used when showing config."
flag_l="Generate a changelog entry and save it to <changelog>."
flag_p="Push <master>, <develop>, and tags to <remote>."
flag_u="Update <master> and <develop> branches from <remote> using rebase."
flag_n="Do not display any prompts"
flag_t="Do not trash the node_modules folder before running npm run test."
flag_T="Do not create a git tag and do no run conventionalGithubReleaser."
args_c="Location of the <changelog>. Defaults to 'CHANGELOG.md'."
args_C="The NPM client to use. Defaults to 'npm'."
args_m="The <master> branch. Defaults to 'master'."
args_d="The <develop> branch. Defaults to 'develop'."
args_r="The <remote> push and pull from. Defaults to 'origin'."
args_P="The preset style used to generate the changelog. Examples include"
args_P2="['angular' | 'jquery' ...]. Defaults to 'conventional-recommended-bump'."
args_s="Allows you to start at a specific step. Useful for when the script"
args_s2="fails before completing all steps. See AVAILABLE STEPS below."
args_b="The semver you would like to use for the release (default: patch)."
args_b2="Can be: [ major | minor | patch | v0.0.0 ]"

usage() {
  cat << EOF
usage: githubrelease options

Release a npm package to github.

FLAG OPTIONS:
   -h     $flag_h
   -v     $flag_v
   -f     $flag_f
   -l     $flag_l
   -p     $flag_p
   -u     $flag_u
   -n     $flag_n
   -t     $flag_t
   -T     $flag_T

ARGUMENT OPTIONS:
   -c     $args_c
   -C     $args_C
   -m     $args_m
   -d     $args_d
   -r     $args_r
   -P     $args_P
          $args_P2
   -s     $args_s
          $args_s2
   -b     $args_b
          $args_b2

AVAILABLE STEPS:
    1     Update <master> and <develop> branches from <remote> using rebase.
    2     Push <master>, <develop>, and tags to <remote>.
    3     Run 'npm test'
    4     Rebase <develop> onto <master>.
    5     Make a copy of 'package.json' to '_package.json'.
    6     Temporarily bump the semver of 'package.json'.
    7     Save a changelog entry to <changelog>.
    8     Commit <changelog> update to Git.
    9     Reset 'package.json' by moveing '_package.json' to 'package.json'.
   10     Run 'npm version' command, which bumps 'package.json' and creates a
          tag (only creates a tag if '-T' was not passed).
   11     Rebase <master> onto <develop>.
   12     Push <master>, <develop>, and tags to <remote>.
   13     Run 'conventional-github-releaser' to create a release in GitHub.
   14     Output landed message which contains the short sha of HEAD.

EXAMPLES:
   1. Update all branches
   2. Push changes from all branches
   3. Cut a release

   githubrelease -u
   githubrelease -p
   githubrelease -b patch
EOF
}
# Argument variables
argflags=
arglog=
argpull=
argpush=
argnoprompt=
argnotrashnodemodules=
argnogittag=
argchangelog=
argnpmclient=
argmaster=
argdevelop=
argremote=
argpreset=
argstep=
argbump=

# Variables to use in script
changelog=
npmclient=
master=
develop=
remote=
preset=
step=
bump=
flagschosen=()

# Grab command line arguments and flags
while getopts "hvflpuntTc:C:r:m:d:P:s:b:" OPTION
do
  case $OPTION in
    # Flags
    h)
      usage
      exit 1
      ;;
    v)
      echo "githubrelease v$(_githubreleasehelper --version)"
      exit 1
      ;;
    f)
      argflags=1
      flagschosen+=("[-f] $flag_f")
      ;;
    l)
      arglog=1
      flagschosen+=("[-l] $flag_l")
      ;;
    p)
      argpush=1
      flagschosen+=("[-p] $flag_p")
      ;;
    u)
      argpull=1
      flagschosen+=("[-u] $flag_u")
      ;;
    n)
      argnoprompt=1
      flagschosen+=("[-n] $flag_n")
      ;;
    t)
      argnotrashnodemodules=1
      flagschosen+=("[-t] $flag_t")
      ;;
    T)
      argnogittag=1
      flagschosen+=("[-T] $flag_T")
      ;;
    # Flags with args
    c)
      argchangelog=$OPTARG
      flagschosen+=("[-c] $args_c")
      ;;
    C)
      argnpmclient=$OPTARG
      flagschosen+=("[-C] $args_C")
      ;;
    r)
      argremote=$OPTARG
      flagschosen+=("[-r] $args_r")
      ;;
    m)
      argmaster=$OPTARG
      flagschosen+=("[-m] $args_m")
      ;;
    d)
      argdevelop=$OPTARG
      flagschosen+=("[-d] $args_d")
      ;;
    P)
      argpreset=$OPTARG
      flagschosen+=("[-P] $args_P $args_P2")
      ;;
    s)
      argstep=$OPTARG
      flagschosen+=("[-s] $args_s $args_s2")
      ;;
    b)
      argbump=$OPTARG
      flagschosen+=("[-b] $args_b $args_b2")
      ;;
    ?)
      usage
      exit
      ;;
  esac
done

printflags() {
  if [[ -n $argflags ]]; then
    echo ""
    echo ""
    echo "Flags Chosen:"
    for desc in "${flagschosen[@]}"; do
      echo "$desc"
    done
  fi
}

# Parse arguments and apply defaults if an argument was not passed
[[ -n $argchangelog ]] && changelog=$argchangelog || changelog=$defaultchangelog
[[ -n $argnpmclient ]] && npmclient=$argnpmclient || npmclient=$defaultnpmclient
[[ -n $argremote ]] && remote=$argremote || remote=$defaultremote
[[ -n $argmaster ]] && master=$argmaster || master=$defaultmaster
[[ -n $argdevelop ]] && develop=$argdevelop || develop=$defaultdevelop
[[ -n $argpreset ]] && preset=$argpreset || preset=$($conventionalCommitsDetector)
[[ -n $argstep ]] && step=$argstep || step=$defaultstep
# Check preset and default to "angular"
[[ "$($conventionalRecommendedBump -p ${preset} 2>&1)" == *"does not exist"* ]] && preset="angular"
[[ -n $argbump ]] && bump=$argbump || bump=$($conventionalRecommendedBump -p ${preset} 2> /dev/null)

counter=$step

# Config setup info
currVersion=$(cat package.json | $json version)
nextVersion=$(_githubreleasehelper --inc $currVersion --release $bump)

# Output config setup
$chalk cyan "========== CONFIG =========="
$chalk white << EOF
log           : $arglog
pull          : $argpull
push          : $argpush
changelog     : $changelog
npmclient     : $npmclient
remote        : $remote
master        : $master
develop       : $develop
preset        : $preset <detected: "$($conventionalCommitsDetector)", chosen: "$preset">
bump          : $bump <detected: "$($conventionalRecommendedBump -p $($conventionalCommitsDetector) 2> /dev/null)", chosen: "$bump">
currVersion   : $currVersion
nextVersion   : $nextVersion
counter       : $counter
step          : $step
noprompts     : $argnoprompt
nogittag      : $argnogittag
notrashnodemodules: $argnotrashnodemodules $(printflags)
EOF
$chalk cyan "============================"
if [[ -z $argnoprompt ]]; then
  echo ""
  $chalk yellow "Based on the config above would you like to continue?"
  select yn in "Yes" "No"; do
      case $yn in
          Yes ) break;;
          No ) exit;;
      esac
  done
fi

# githubrelease -l
# githubrelease -l -b minor
if [[ -n $arglog ]]; then
  backupPackage
  bumpPackage
  updateChangelog
  resetPackage
  exit 1
fi

# githubrelease -u
if [[ -n $argpull ]]; then
  startOnMaster && updateBranches &&
  exit 1
fi

# githubrelease -p
if [[ -n $argpush ]]; then
  pushAll &&
  exit 1
fi

# Run the release script
if [[ $step -le  1 ]]; then startOnMaster && updateBranches;           fi && # Step 1
if [[ $step -le  2 ]]; then pushAll;                                   fi && # Step 2
if [[ $step -le  3 ]]; then startOnDevelop && runTests;                fi && # Step 3
if [[ $step -le  4 ]]; then startOnMaster && rebaseDevelopOntoMaster;  fi && # Step 4
if [[ $step -ge  5 ]]; then startOnMaster;                             fi && # Step >= 5
if [[ $step -ge  5 ]]; then shortsha="$(getShortSha)";                 fi && # Step >= 5
if [[ $step -le  5 ]]; then backupPackage;                             fi && # Step 5
if [[ $step -le  6 ]]; then bumpPackage;                               fi && # Step 6
if [[ $step -le  7 ]]; then updateChangelog;                           fi && # Step 7
if [[ $step -le  8 ]]; then addChangelog;                              fi && # Step 8
if [[ $step -le  9 ]]; then resetPackage;                              fi && # Step 9
if [[ $step -le 10 ]]; then npmBump;                                   fi && # Step 10
if [[ $step -le 11 ]]; then startOnDevelop && rebaseMasterOntoDevelop; fi && # Step 11
if [[ $step -le 12 ]]; then pushAll;                                   fi && # Step 12
if [[ $step -le 13 ]]; then conventionalGitHubReleaser;                fi && # Step 13
if [[ $step -le 14 ]]; then landedMessage;                             fi && # Step 14
$chalk green "All done!" || $chalk red "Epic fail!"
