#!/usr/bin/env bash

#
# deploy(1) - Minimalistic shell script to deploy Git repositories.
# Released under the MIT License.
#
# https://github.com/visionmedia/deploy
#

VERSION="0.6.0"
CONFIG=./deploy.conf
LOG=/tmp/pm2-deploy.log
TEST=1
FORCE=0
REF=
ENV=

#
# Read PIPED JSON
#
read conf

#
# Output usage information.
#

usage() {
  cat <<-EOF

  Usage: deploy [options] <env> [command]

  Options:

    -C, --chdir <path>   change the working directory to <path>
    -c, --config <path>  set config path. defaults to ./deploy.conf
    -T, --no-tests       ignore test hook
    -V, --version        output program version
    -h, --help           output help information
    -f, --force          skip local change checking

  Commands:

    setup                run remote setup commands
    revert [n]           revert to [n]th last deployment or 1
    config [key]         output config file or [key]
    curr[ent]            output current release commit
    prev[ious]           output previous release commit
    exec|run <cmd>       execute the given <cmd>
    list                 list previous deploy commits
    ref [ref]            deploy [ref]

EOF
}

#
# Abort with <msg>
#

abort() {
  echo
  echo "  $@" 1>&2
  echo
  exit 1
}

#
# Log <msg>.
#

log() {
  echo "  ○ $@"
}

#
# Get config value by <key>.
#

config_get() {
    local key=$1
    echo $(expr "$conf" : '.*"'$key'":"\([^"]*\)"')
}

#
# Output version.
#

version() {
  echo $VERSION
}

#
# Return the ssh command to run.
#

ssh_command() {
  local user="`config_get user`"
  if test -n "$user"; then
      local url="$user@`config_get host`"
  else
      local url="`config_get host`"
  fi
  local unexpanded_key="`config_get key`"
  local key="${unexpanded_key/#\~/$HOME}"
  local forward_agent="`config_get forward-agent`"
  local port="`config_get port`"
  local needs_tty="`config_get needs_tty`"
  local ssh_options="`config_get ssh_options`"

  test -n "$forward_agent" && local agent="-A"
  test -n "$key" && local identity="-i $key"
  test -n "$port" && local port="-p $port"
  test -n "$needs_tty" && local tty="-t"
  test -n "ssh_options" && local ssh_opts="$ssh_options"
  echo "ssh $ssh_opts $tty $agent $port $identity $url"
}

#
# Run the given remote <cmd>.
#

runRemote() {
  local shell="`ssh_command`"
  echo $shell "\"$@\"" >> $LOG
  $shell $@
}

#
# Run the given local <cmd>.
#

runLocal() {
  echo "\"$@\"" >> $LOG
  /usr/bin/env bash -c "$*"
}

#
# Run the given <cmd> either locally or remotely
#

run() {
  local host="`config_get host`"
  if [[ $host == localhost ]]
  then
    runLocal $@
  else
    runRemote $@
  fi
}

#
# Output config or [key].
#

config() {
    echo $(expr "$conf" : '.*"$key":"\([^"]*\)"')
}

#
# Execute hook <name> relative to the path configured.
#

hook() {
  test -n "$1" || abort hook name required
  local hook=$1
  local path=`config_get path`
  local cmd=`config_get $hook`
  if test -n "$cmd"; then
    log "executing $hook \`$cmd\`"
    run "cd $path/current; \
      SHARED=\"$path/shared\" \
      $cmd 2>&1 | tee -a $LOG; \
      exit \${PIPESTATUS[0]}"
    test $? -eq 0
  else
    log hook $hook
  fi
}

#
# Pre Setup hook runs on the host before the actual setup runs
# multiple commands or a script
#

hook_pre_setup() {
  local cmd=`config_get pre-setup`
  if test -n "$cmd"; then
    local is_script=($cmd)
    if [ -f "${is_script[0]}" ]; then
      log "executing pre-setup script \`$cmd\`"
      local shell="`ssh_command`"
      runLocal "$shell 'bash -s' -- < $cmd"
    else
      log "executing pre-setup \`$cmd\`"
      run "$cmd"
    fi
    test $? -eq 0
  else
    log hook pre-setup
  fi
}

#
# Run setup.
#

setup() {
  local path=`config_get path`
  local repo=`config_get repo`
  local ref=`config_get ref`
  local fetch=`config_get fetch`
  local branch=${ref#*/}

  hook_pre_setup || abort pre-setup hook failed
  run "mkdir -p $path/{shared/{logs,pids},source}"
  test $? -eq 0 || abort setup paths failed
  log running setup
  log cloning $repo
  if test "$fetch" != "fast"; then
    log "full fetch"
    run "git clone --branch $branch $repo $path/source"
  else
    log "fast fetch"
    run "git clone --depth=5 --branch $branch $repo $path/source"
  fi
  test $? -eq 0 || abort failed to clone
  run "ln -sfn $path/source $path/current"
  test $? -eq 0 || abort symlink failed
  hook post-setup || abort post-setup hook failed
  log setup complete
}

#
# Deploy [ref].
#

deploy() {
  local ref=$1
  local branch=$2
  if test -z "$branch"; then
    branch=${ref#*/}
  fi
  local path=`config_get path`
  local fetch=`config_get fetch`

  log "deploying ${ref}"

  # PR #50
  log executing pre-deploy-local
  local pdl=`config_get pre-deploy-local`
  runLocal $pdl || abort pre-deploy-local hook failed

  hook pre-deploy || abort pre-deploy hook failed

  if run "cd $path/source && git rev-parse --quiet --verify $branch" > /dev/null; then
    log fast forward $branch
    run "cd $path/source && git checkout $branch && git pull origin $branch"
    test $? -eq 0 || abort git pull failed
  else
    # fetch source
    log fetching updates

    if test "$fetch" != "fast"; then
      log "full fetch"
      run "cd $path/source && git fetch --all --tags"
    else
      log "fast fetch"
      run "cd $path/source && git fetch --depth=5 --all --tags"
    fi

    test $? -eq 0 || abort fetch failed

    # checkout ref to branch
    log checkout $ref to $branch
    run "cd $path/source && git checkout $ref -b $branch"
    test $? -eq 0 || abort git checkout failed
  fi

  # link current
  run "ln -sfn $path/source $path/current"
  test $? -eq 0 || abort symlink failed

  # deploy log
  run "cd $path/source && \
      echo \`git rev-parse --short HEAD\` \
      >> $path/.deploys"
  test $? -eq 0 || abort deploy log append failed

  hook post-deploy || abort post-deploy hook failed

  if test $TEST -eq 1; then
    hook test
    if test $? -ne 0; then
      log tests failed, reverting deploy
      quickly_revert_to 1 && log "revert complete" && exit
    fi
  else
    log ignoring tests
  fi

  # done
  log successfully deployed $ref
}

#
# Get current commit.
#

current_commit() {
  local path=`config_get path`
  run "cd $path/source && \
      git rev-parse --short HEAD"
}

#
# Get <n>th deploy commit.
#

nth_deploy_commit() {
  local n=$1
  local path=`config_get path`
  run "cat $path/.deploys | tail -n $n | head -n 1 | cut -d ' ' -f 1"
}

#
# List deploys.
#

list_deploys() {
  local path=`config_get path`
  run "tac $path/.deploys | awk '{printf \"%d\t%s\n\", NR-1, \$0}'"
}

#
# Revert to the <n>th last deployment, ignoring tests.
#

quickly_revert_to() {
  local n=$1
  log "quickly reverting $n deploy(s)"
  local commit=`nth_deploy_commit $((n + 1))`
  TEST=0 deploy "$commit"
}

#
# Revert to the <n>th last deployment.
#

revert_to() {
  local n=$1
  log "reverting $n deploy(s)"
  local commit=`nth_deploy_commit $((n + 1))`
  deploy "$commit"
}

#
# Ensure all changes are committed and pushed before deploying.
#

check_for_local_changes() {
  local path=`config_get path`

  if [ $FORCE -eq 1 ]
  then
      return
  fi
  git --no-pager diff --exit-code --quiet          || abort "commit or stash your changes before deploying"
  git --no-pager diff --exit-code --quiet --cached || abort "commit your staged changes before deploying"
  [ -z "`git rev-list @{upstream}.. -n 1`" ]       || abort "push your changes before deploying"
}

# parse argv
while test $# -ne 0; do
  arg=$1; shift
  case $arg in
    -h|--help) usage; exit ;;
    -V|--version) version; exit ;;
    -C|--chdir) log cd $1; cd $1; shift ;;
    -T|--no-tests) TEST=0 ;;
    -F|--force) FORCE=1 ;;
    run|exec)  run "cd `config_get path`/current && $@"; exit ;;
    console)  console; exit ;;
    curr|current)  current_commit; exit ;;
    prev|previous)  nth_deploy_commit 2; exit ;;
    revert)  revert_to ${1-1}; exit ;;
    setup)  setup $@; exit ;;
    list)  list_deploys; exit ;;
    config) config $@; exit ;;
    ref) REF=$1; BRANCH=$2 ;;
  esac
done

check_for_local_changes

# deploy
deploy "${REF:-`config_get ref`}" "${BRANCH}"
