#!/usr/bin/env bash

# READ THIS BEFORE MAKING CHANGES:
#
# If you want to add a new pre-commit check, here are the rules:
#
#   1. Create a bash function for your check (see e.g. ui_lint below).
#      NOTE: Each function will be called in a sub-shell so you can freely
#      change directory without worrying about interference.
#   2. Add the name of the function to the CHECKS variable.
#   3. If no changes relevant to your new check are staged, then
#      do not output anything at all - this would be annoying noise.
#      In this case, call 'return 0' from your check function to return
#      early without blocking the commit.
#   4. If any non-trivial check-specific thing has to be invoked,
#      then output '==> [check description]' as the first line of
#      output. Each sub-check should output '--> [subcheck description]'
#      after it has run, indicating success or failure.
#   5. Call 'block [reason]' to block the commit. This ensures the last
#      line of output calls out that the commit was blocked - which may not
#      be obvious from random error messages generated in 4.
#
# At the moment, there are no automated tests for this hook, so please run it
# locally to check you have not broken anything - breaking this will interfere
# with other peoples' workflows significantly, so be sure, check everything twice.

set -euo pipefail

# Call block to block the commit with a message.
block() {
  echo "$@"
  echo "Commit blocked - see errors above."
  exit 1
}

# Add all check functions to this space separated list.
# They are executed in this order (see end of file).
CHECKS="ui_lint circleci_verify"

MIN_CIRCLECI_VERSION=0.1.5575

# Run ui linter if changes in that dir detected.
ui_lint() {
  local DIR=ui LINTER=node_modules/.bin/lint-staged

  # Silently succeed if no changes staged for $DIR
  if git diff --name-only --cached --exit-code -- $DIR/; then
    return 0
  fi

  # Silently succeed if the linter has not been installed.
  # We assume that if you're doing UI dev, you will have installed the linter
  # by running yarn.
  if [ ! -x $DIR/$LINTER ]; then
    return 0
  fi

  echo "==> Changes detected in $DIR/: Running linter..."

  # Run the linter from the UI dir.
  cd $DIR
  $LINTER || block "UI lint failed"
}

# Check .circleci/config.yml is up to date and valid, and that all changes are
# included together in this commit.
circleci_verify() {
  # Change to the root dir of the repo.
  cd "$(git rev-parse --show-toplevel)"

  # Fail early if we accidentally used '.yaml' instead of '.yml'
  if ! git diff --name-only --cached --exit-code -- '.circleci/***.yaml'; then
    # This is just for consistency, as I keep making this mistake - Sam.
    block "ERROR: File(s) with .yaml extension detected. Please rename them .yml instead."
  fi

  # Succeed early if no changes to yml files in .circleci/ are currently staged.
  # make ci-verify is slow so we really don't want to run it unnecessarily.
  if git diff --name-only --cached --exit-code -- '.circleci/***.yml'; then
    return 0
  fi
  # Make sure to add no explicit output before this line, as it would just be noise
  # for those making non-circleci changes.
  echo "==> Verifying config changes in .circleci/"
  echo "--> OK: All files are .yml not .yaml"

  # Ensure commit includes _all_ files in .circleci/
  # So not only are the files up to date, but we are also committing them in one go.
  if ! git diff --name-only --exit-code -- '.circleci/***.yml'; then
    echo "ERROR: Some .yml diffs in .circleci/ are staged, others not."
    block "Please commit the entire .circleci/ directory together, or omit it altogether."
  fi

  echo "--> OK: All .yml files in .circleci are staged."

  if ! REASON=$(check_circleci_cli_version); then
    echo "*** WARNING: Unable to verify changes in .circleci/:"
    echo "--> $REASON"
    # We let this pass if there is no valid circleci version installed.
    return 0
  fi

  if ! make -C .circleci ci-verify; then
    block "ERROR: make ci-verify failed"
  fi

  echo "--> OK: make ci-verify succeeded."
}

check_circleci_cli_version() {
  if ! command -v circleci > /dev/null 2>&1; then
    echo "circleci cli not installed." 
    return 1
  fi

  CCI="circleci --skip-update-check"

  if ! THIS_VERSION=$($CCI version) > /dev/null 2>&1; then
    # Guards against very old versions that do not have --skip-update-check.
    echo "The installed circleci cli is too old. Please upgrade to at least $MIN_CIRCLECI_VERSION." 
    return 1
  fi

  # SORTED_MIN is the lower of the THIS_VERSION and MIN_CIRCLECI_VERSION.
  if ! SORTED_MIN="$(printf "%s\n%s" "$MIN_CIRCLECI_VERSION" "$THIS_VERSION" | sort -V | head -n1)"; then
    echo "Failed to sort versions. Please open an issue to report this."
    return 1
  fi

  if [ "$THIS_VERSION" != "${THIS_VERSION#$MIN_CIRCLECI_VERSION}" ]; then
    return 0 # OK - Versions have the same prefix, so we consider them equal.
  elif [ "$SORTED_MIN" = "$MIN_CIRCLECI_VERSION" ]; then
    return 0 # OK - MIN_CIRCLECI_VERSION is lower than THIS_VERSION.
  fi

  # Version too low.
  echo "The installed circleci cli v$THIS_VERSION is too old. Please upgrade to at least $MIN_CIRCLECI_VERSION"
  return 1
}

for CHECK in $CHECKS; do
  # Force each check into a subshell to avoid crosstalk.
  ( $CHECK ) || exit $?
done
