#!/bin/sh

# MIT License
#
# Copyright (c) 2018 SciSpike, LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# This script implements the release branch workflow for node.js projects.
#
# Requirements:
# git
# docker (unless ymlx & match are available on the path as reported by `which`)

if [ -n "$RELEASE_DEBUG" ]; then
  set -x
fi

ORIGIN=${ORIGIN:-origin}
MASTER=${MASTER:-master}
TAG_PREFIX=${TAG_PREFIX:-''}
TAG_SUFFIX=${TAG_SUFFIX:-''}
BRANCH_PREFIX=${BRANCH_PREFIX:-'v'}
BRANCH_SUFFIX=${BRANCH_SUFFIX:-''} # '.x' is a common one
if [ -z "$NO_GIT_DISCOVERY_ACROSS_FILESYSTEM" ]; then
  GIT_DISCOVERY_ACROSS_FILESYSTEM=1 # needed when running in a docker container
fi
# support custom "pre" tokens
PRE=${PRE:-pre}
PRE_USAGE=pre
if [ "$PRE" != "pre" ]; then
  PRE_USAGE="$PRE_USAGE|$PRE"
fi
# support custom "rc" tokens
RC=${RC:-rc}
RC_USAGE=rc
if [ "$RC" != "rc" ]; then
  RC_USAGE="$RC_USAGE|$RC"
fi

if [ "$PRE" == "$RC" ]; then
  echo "ERROR: PRE ($PRE) cannot be the same as RC ($RC)" >&2
  exit 1
fi

usage() {
    cat<<EOF
usage:
if on $MASTER branch: release $PRE_USAGE|$RC_USAGE
if on release branch: release major|minor|patch|$PRE_USAGE

optional supported environment variables:

variable                            description
--------                            -----------
ORIGIN                              name of git remote, default 'origin'
MASTER                              name of master branch, default 'master'
TAG_PREFIX                          prefix for tags, default ''
TAG_SUFFIX                          suffix for tags, default ''
BRANCH_PREFIX                       prefix for release branches, default 'v'
BRANCH_SUFFIX                       suffix for release branches, default '' ('.x' is common)
NO_GIT_DISCOVERY_ACROSS_FILESYSTEM  whether to not set GIT_DISCOVERY_ACROSS_FILESYSTEMS, default ''
PACKAGE_JSON                        name of package.json file
EOF
}

RELEASE_LEVEL="$1"
case "$RELEASE_LEVEL" in
  major|minor|patch|pre|rc|$PRE|$RC)
    # ok
    ;;
  h|he|hel|help)
    usage
    exit 0
    ;;
  *)
    if [ -n "$1" ]; then
      echo "ERROR: specify release level of 'pre' or '$PRE' for prerelease, 'rc' or '$RC' for release candidate, 'patch', 'minor', or 'major'" >&2
    fi
    usage
    exit 1
    ;;
esac

FX=fx
if [ -n "$NO_USE_LOCAL_FX" ] || ! which $FX; then
  FX="docker run --rm -i matthewadams12/fx fx"
fi
MATCH=match
if [ -n "$NO_USE_LOCAL_MATCH" ] || ! which $MATCH; then
  MATCH="docker run --rm -i matthewadams12/match"
fi

PACKAGE_JSON="${PACKAGE_JSON:-package.json}"
echo "INFO: using package file: $PACKAGE_JSON"

getVersion() {
  cat "$PACKAGE_JSON" | $FX this.version | sed 's/"//g'
}

# usage: setVersion version
setVersion() {
  V=$1
  PACKAGE_JSON_CONTENT="$(cat $PACKAGE_JSON | $FX "it => { it.version = \"$V\"; return it; }")"
  echo "$PACKAGE_JSON_CONTENT" | sed 's|\\|\\\\|g' > $PACKAGE_JSON
  echo "INFO: set 'version' attribute in $PACKAGE_JSON to $V"
  echo "$PACKAGE_JSON is now:"
  cat "$PACKAGE_JSON"
  echo "INFO: running npm install to update package-lock.json"
  if [ -f /.dockerenv ]; then
    NODE_MODULES_WORKDIR=/tmp/node-work
    mkdir -p $NODE_MODULES_WORKDIR
    PREFIX_ARG="--prefix $NODE_MODULES_WORKDIR"
  fi
  npm install $PREFIX_ARG
}

echo "INFO: checking required preconditions"

git pull

if ! git diff --exit-code --no-patch; then
  echo 'ERROR: you have modified tracked files; only release from clean directories!' >&2
  exit 3
else
  echo 'INFO: no modified tracked files'
fi

if ! git diff --cached --exit-code --no-patch; then
  echo 'ERROR: you have cached modified tracked files; only release from clean directories!' >&2
  exit 3
else
  echo 'INFO: no cached modified tracked files'
fi

if [ -n "$(git status -s)" ]; then
  echo 'ERROR: You have unignored untracked files; only release from clean directories!' >&2
  exit 3
else
  echo 'INFO: no unignored untracked files'
fi

BRANCH="$(git status | head -n 1 | awk '{ print $3 }')"
if ! $MATCH "^(master|$BRANCH_PREFIX[0-9]{1,}\.[0-9]{1,}$BRANCH_SUFFIX)$" "$BRANCH"; then # it is not a master or a release branch
  echo 'ERROR: you can only release from the master branch or release branches!' >&2
  exit 3
else
  echo "INFO: on branch $BRANCH, from which releases are allowed"
fi

if ! git diff --exit-code -no-patch $BRANCH $ORIGIN/$BRANCH; then
  echo "ERROR: Local branch $BRANCH differs from remote branch $ORIGIN/$BRANCH" >&2
  exit 3
else
  echo "INFO: no differences between local & remote branch $BRANCH"
fi

if [ "$BRANCH" = "$MASTER" ]; then
  case "$RELEASE_LEVEL" in
    pre|rc|$PRE|$RC)
      # ok
      ;;
    *)
      echo "ERROR: only 'pre'/'$PRE' or 'rc'/'$RC' releases are permitted from the $MASTER branch." >&2
      exit 6
      ;;
  esac
else # this is a release branch
  case "$RELEASE_LEVEL" in
      rc|patch|minor|major)
        # ok
        ;;
      *)
        echo "ERROR: only 'rc'/'$RC', 'patch', 'minor', or 'major' releases are permitted from a release branch." >&2
        exit 7
        ;;
  esac
fi

echo "INFO: ok to proceed with $RELEASE_LEVEL from branch $BRANCH"

echo "INFO: determining current version from $PACKAGE_JSON"

VERSION="$(getVersion)"
if ! $MATCH "\-($PRE|$RC)\.[0-9]{1,}$" "$VERSION"; then
  echo "ERROR: repository is in an inconsistent state: version does NOT end in prerelease suffix $PRE!" >&2
  exit 3
fi

echo "INFO: current version is $VERSION"

# usage: apply message [tag [remote branch]]
applyChanges() {
  git add .
  git commit --allow-empty -m "$1"
  echo "INFO: committed changes with message: $1"

  MSG="INFO: pushed commits"

  if [ -n "$2" ]; then
    tag="$TAG_PREFIX$2$TAG_SUFFIX"
    git tag "$tag"
    echo "INFO: tagged $tag"
    MSG="$MSG & tags"
  fi

  SET_UPSTREAM_ARGS=
  if [ -n "$3" ] && [ -n "$4" ]; then
    SET_UPSTREAM_ARGS="-u $3 $4"
    MSG="$MSG & set tracked upstream to $3/$4"
  fi

  git push $SET_UPSTREAM_ARGS
  git push --tags

  echo "$MSG"
}

if [ "$BRANCH" = "$MASTER" ]; then # this will be either an rc, resulting in a new release branch, or a pre
  MATCHES="$($MATCH "^([0-9]{1,})\.([0-9]{1,})\.0\-$PRE\.([0-9]{1,})$" "$VERSION")"
  if [ -z "$MATCHES" ]; then
    echo "ERROR: the version does not match the format of major.minor.0-$PRE.n required in the $MASTER branch." >&2
    exit 8
  else
    echo "INFO: version $VERSION matches expected format for branch $BRANCH"
  fi

  # create release branch
  MAJOR="$(echo "$MATCHES" | awk '{ print $2 }')"
  MINOR="$(echo "$MATCHES" | awk '{ print $3 }')"
  PATCH=0
  PRERELEASE="$(echo "$MATCHES" | awk '{ print $4 }')"

  case "$RELEASE_LEVEL" in
  rc|$RC) # then it's time to create a new release branch
      NEW_RELEASE_BRANCH="$BRANCH_PREFIX$MAJOR.$MINOR$BRANCH_SUFFIX"
      git checkout -b $NEW_RELEASE_BRANCH

      NEW_RELEASE_BRANCH_VERSION="$MAJOR.$MINOR.0-$RC.0"

      setVersion $NEW_RELEASE_BRANCH_VERSION

      applyChanges "release $NEW_RELEASE_BRANCH_VERSION" $NEW_RELEASE_BRANCH_VERSION $ORIGIN $NEW_RELEASE_BRANCH
      echo "INFO: created release branch $NEW_RELEASE_BRANCH and tagged $NEW_RELEASE_BRANCH_VERSION for release"

      # return to master branch
      git checkout $MASTER
      echo "INFO: checked out $MASTER"

      git cherry-pick $NEW_RELEASE_BRANCH # cherry pick from release branch to get release candidate commit in master
      echo "INFO: cherry-picked $NEW_RELEASE_BRANCH $RC commit into $MASTER"

      # advance master version
      NEXT_VERSION="$MAJOR.$(($MINOR+1)).0-$PRE.0"

      setVersion $NEXT_VERSION $DOCKER_BUILD_STEP_NAMES

      applyChanges "bump to $NEXT_VERSION [skip ci]"

      # return to release branch & prepare for next prerelease
      git checkout $NEW_RELEASE_BRANCH
      echo "INFO: checked out $NEW_RELEASE_BRANCH"

      NEXT_RELEASE_BRANCH_VERSION="$MAJOR.$MINOR.0-$RC.1"

      setVersion $NEXT_RELEASE_BRANCH_VERSION $DOCKER_BUILD_STEP_NAMES

      applyChanges "bump to $NEXT_RELEASE_BRANCH_VERSION [skip ci]"

      exit 0
      ;;

  pre|$PRE)
      setVersion $VERSION

      applyChanges "release $VERSION" $VERSION

      NEXT_VERSION=$MAJOR.$MINOR.$PATCH-$PRE.$((PRERELEASE+1))

      setVersion $NEXT_VERSION

      applyChanges "bump to $NEXT_VERSION [skip ci]"

      exit 0
      ;;
  esac
fi

# If we get this far, we are releasing something from a release branch.

MATCHES="$($MATCH "^([0-9]{1,})\.([0-9]{1,})\.([0-9]{1,})\-$RC\.([0-9]{1,})$" "$VERSION")"
if [ -z "$MATCHES" ]; then
  echo "ERROR: the version does not match the format of major.minor.patch-$RC.n required in the release branch." >&2
  exit 8
else
    echo "INFO: version $VERSION matches expected format for branch $BRANCH"
fi

MAJOR="$(echo "$MATCHES" | awk '{ print $2 }')"
MINOR="$(echo "$MATCHES" | awk '{ print $3 }')"
PATCH="$(echo "$MATCHES" | awk '{ print $4 }')"
PRERELEASE="$(echo "$MATCHES" | awk '{ print $5 }')"

case "$RELEASE_LEVEL" in
  major|minor|patch)

    # NOTE: if RELEASE_LEVEL is 'minor' & we're prepped for a major release, no harm, no foul.
    # A major release is the same as a minor release, only that the minor version is 0.

    if [ $RELEASE_LEVEL = major ] && [ $MINOR != 0 ]; then
      echo "ERROR: this branch is not prepared for a major release because the minor version is $MINOR, not 0." >&2
      exit 10
    else
      NEXT_RELEASE_BRANCH_VERSION="$MAJOR.$MINOR.1-$RC.0"
    fi
    if [ $RELEASE_LEVEL = minor ] && [ $PATCH != 0 ]; then
      echo "ERROR: a minor release has already been performed in this release branch; only patch releases are allowed here now." >&2
      exit 11
    else
      NEXT_RELEASE_BRANCH_VERSION="$MAJOR.$MINOR.1-$RC.0"
    fi
    if [ $RELEASE_LEVEL = patch ] && [ $PATCH = 0 ]; then
      echo "ERROR: you must release a minor release before releasing a patch in this release branch." >&2
      exit 12
    else
      NEXT_RELEASE_BRANCH_VERSION="$MAJOR.$MINOR.$((PATCH+1))-$RC.0"
    fi

    echo "INFO: $RELEASE_LEVEL ok in branch $BRANCH"

    RELEASE_VERSION="$MAJOR.$MINOR.$PATCH"

    setVersion $RELEASE_VERSION

    applyChanges "release $RELEASE_VERSION" $RELEASE_VERSION

    setVersion $NEXT_RELEASE_BRANCH_VERSION

    applyChanges "bump to $NEXT_RELEASE_BRANCH_VERSION [skip ci]"

    exit 0
    ;;

  rc|$RC)
    setVersion $VERSION

    applyChanges "release $VERSION" $VERSION

    NEXT_RELEASE_BRANCH_VERSION="$MAJOR.$MINOR.$PATCH-$RC.$((PRERELEASE+1))"

    setVersion $NEXT_RELEASE_BRANCH_VERSION

    applyChanges "bump to $NEXT_RELEASE_BRANCH_VERSION [skip ci]"

    exit 0
    ;;
esac
