#!/usr/bin/env bash
#
# Automates the release tagging process for PuppetDB.
# This script tags and pushes all four branches at once.
# Two branches must be tagged in FOSS PuppetDB, and an additional two branches
# must be tagged in PuppetDB extensions.
#
# Env vars
# PDB_PATH=~/wrk/pdb/<branch>
# PDB_EXT_PATH=~/wrk/ext/<branch>
# PDB_GIT_REMOTE=upstream
# PDB_EXT_GIT_REMOTE=upstream
#
# Usage:
#    tag-release branch release_version
#    tag-release 6.x    6.19.1
#
# for a private release
# PDB_GIT_REMOTE=upstream-private ./tag-release 6.x 6.19.1
set -euo pipefail

# 
update_version_var () {
   local file="$1"
   local varname="$2"
   local new_version="$3"

   SED_ADDRESS="(def $varname"
   SED_REGEX="\".*\""
   SED_REPLACEMENT="\"$new_version\""
   SED_COMMAND="s|$SED_REGEX|$SED_REPLACEMENT|"

   sed -i -e "/$SED_ADDRESS/ $SED_COMMAND" "$file"
}

# This is an automated safety check that the script should
# run before creating a new commit. It verifies that the local
# branch doesn't have any additional commits that the remote does not.
# Otherwise, the additional commits might get pushed to the remote in
# the process of tagging. There still might be un-committed changes in any files
# that it is editting, so commits should be staged locally, then the commit
# should be displayed for the user, and prompt for confirmation before
# pushing to any remotes.
ensure_local_git_updated () {
  local remote="$1"
  if ! git diff-index --quiet HEAD; then
      echo "Uncommitted changes; won't proceed"
      exit 2
  fi
  git fetch "$remote"
  git checkout "$branch"
  git merge --ff-only "$remote/$branch"
  local local_ref remote_ref
  local_ref="$(git rev-parse HEAD)"
  remote_ref="$(git rev-parse $remote/$branch)"
  if test "$remote_ref" != "$local_ref"; then
    echo "Local ref $local_ref at $(pwd) did not match remote ref $remote_ref for branch $branch" >&2
    exit 2
  fi
}

usage() {
  cat <<-USAGE
Usage: $(basename $0) <git-branch> <release-version>

Enivironment Variables (and their defaults)

  PDB_PATH=$HOME/wrk/pdb/<git-branch>
  PDB_EXT_PATH=$HOME/wrk/ext/<git-branch>
  PDB_GIT_REMOTE=upstream
  PDB_EXT_GIT_REMOTE=upstream

Examples

  Releasing 6.19.1 off of puppetdb branch 6.x
    $(basename $0) 6.x 6.19.1

  Releasing 7.7.1 off of puppetdb branch main
    $(basename $0) main 7.7.1

  For a release using puppetdb-private
     PDB_GIT_REMOTE=upstream-private $(basename $0) 6.x 6.19.1
USAGE
}

misuse() {
  usage 1>&2
  exit 2
}

expected_args=2
if [[ $# -ne $expected_args ]]; then
  echo "Wrong number of arguments $#, expected $expected_args" >&2
  misuse
fi

branch="$1"
version="$2"
pdb_repo="${PDB_PATH:-$HOME/wrk/pdb/$branch}"
ext_repo="${PDB_EXT_PATH:-$HOME/wrk/ext/$branch}"
pdb_remote="${PDB_GIT_REMOTE:-upstream}"
ext_remote="${PDB_EXT_GIT_REMOTE:-upstream}"

# version_bump is intended to increment the release version for post-release
# development. Given a version of numbers separated by dots (no -SNAPSHOT allowed),
# creates a new version with the right-most digit incremented by one
# and appends a `-SNAPSHOT` to the version.
version_bump() {
    local ver="$1"
    local last_component last_component_len incremented
    if ! echo "$ver" | grep -qE '^[0-9]+(\.[0-9]+)*$'; then
        printf 'Version %q is not of the form a.b.c...' "$ver"
        exit 2
    fi
    last_component="${ver##*.}"
    last_component_len=${#last_component}
    incremented=$(("$last_component" + 1))
    echo "${ver:0:-${last_component_len}}$incremented"
}

update-repo()
(
    local repo="$1" remote="$2" branch="$3" version="$4"
    update_vars=( "${@:5}" )

    cd "$repo"
    ensure_local_git_updated "$remote"
    for var in "${update_vars[@]}"; do
        update_version_var project.clj "$var" "$version"
    done
    git add project.clj
    git commit --message "(maint) Update version to $version for release"
    git tag --annotate --message "$version" "$version"

    local next_version
    next_version="$(version_bump "$version")"-SNAPSHOT
    for var in "${update_vars[@]}"; do
        update_version_var project.clj "$var" "$next_version"
    done
    git add project.clj
    git commit --message "(maint) Update version to $next_version"

    # Display information to help user confirm/deny pushing the puppetdb tag
    git log --graph \
        --pretty=format:'%C(yellow)%h%C(reset) %C(blue)%an%C(reset) %C(cyan)%cr%C(reset) %s %C(green)%d%C(reset)' \
        "$remote/$branch"^..
    git show "$version"

    echo   '----------------------------------------'
    echo   "Proposed commands:"
    printf "  git push %q %q\n" "$remote" "$version^0:$branch"
    printf "  git push %q %q\n" "$remote" "$version"
    printf "  git push %q %q\n" "$remote" "$branch"
    echo   '----------------------------------------'
    read -p "Do you want to push the tag and commit to $remote/$branch [y/N]: " confirm
    if [[ "$confirm" = y* ]] ; then
        # Push the tag first so that anything watching (hooks) will
        # see the pushes in branch order and won't e.g. decide to skip
        # work because it has already seen the commit/tag.  We need
        # version^0 below for the commit associated with the tag
        # because using the tag directly isn't allowed.
        echo "Pushing release..."
        git push "$remote" "$version^0:$branch"
        echo "Pushing tag..."
        git push "$remote" "$version"
        echo "Pushing new snapshot version..."
        git push "$remote" "$branch"
    else
        echo "Skipping pushing release, tag, and commit"
    fi
)

update-repo "$pdb_repo" "$pdb_remote" "$branch" "$version" pdb-version
update-repo "$ext_repo" "$ext_remote" "$branch" "$version" pdb-version pe-pdb-version
