#! /bin/bash
#
# Usage: tag-release.sh <release-ver> [<ref>]
#
# Creates a new bugfix branch and release tag for a release.
# Optionally, the release can be based off a specific git ref.
# Default is HEAD of the current branch.
#
# The script does a lot of error checking, but boils down to
# the following actions.
#
# For major/minor releases (X.Y.0) from master branch:
#   git checkout -b X.Y.x
#   git tag -a X.Y.Z -m "X.Y.Z" HEAD
# and optionally:
#   git push -u origin X.Y.x
#   git push origin refs/tags/X.Y.Z
#
# For bugfix releases from associated branch (X.Y.x):
#   git tag -a X.Y.Z -m "X.Y.Z" HEAD
# and optionally:
#   git push origin refs/tags/X.Y.Z
#

SELF="${BASH_SOURCE[0]}"
SELF_NAME=$(basename "${SELF}")

GIT_EXE='git'

function validate_repo()
{
    local HASH err AHEAD BEHIND proceed

    # Check whether we have git
    if ! hash ${GIT_EXE} 2>/dev/null; then
        echo "Command '${GIT_EXE}' not found." 1>&2
        return 1
    fi

    # Check if there is a valid git repo here
    HASH=$(${GIT_EXE} rev-parse HEAD)
    err=$?
    if [[ ${err} -ne 0 ]]; then
        echo "Not a valid repository." 1>&2
        return ${err}
    elif [[ -z ${HASH} ]]; then
        echo "Not a valid repository." 1>&2
        return 1
    fi

    if [[ -n "$(${GIT_EXE} status --porcelain)" ]]; then
        echo "There are uncommitted changes.  Aborting." 1>&2
        return 1
    fi

    echo "Fetching repo data..."
    ${GIT_EXE} fetch
    err=$?
    if [[ ${err} -ne 0 ]]; then
        echo "Failed to fetch repo data." 1>&2
        return ${err}
    fi
    AHEAD=$(${GIT_EXE} rev-list @{u}..HEAD --count)
    BEHIND=$(${GIT_EXE} rev-list HEAD..@{u} --count)
    if [[ ${AHEAD} -ne 0 ]]; then
        echo "There are unpushed changes. Continue anyway? (y/N)"
        read proceed
        if [[ ( "x${proceed}" != "xy" ) && ( "x${proceed}" != "xY" ) ]] ; then
            echo "Aborting..."
            return 1
        fi
    fi
    if [[ ${BEHIND} -ne 0 ]]; then
        echo "There are unmerged upstream changes. Continue anyway? (y/N)"
        read proceed
        if [[ ( "x${proceed}" != "xy" ) && ( "x${proceed}" != "xY" ) ]] ; then
            echo "Aborting..."
            return 1
        fi
    fi
}

function tag_release()
{
    local TAG REF COMMIT BRANCH proceed new_branch ERR HASH

    TAG=${1}
    REF=${2}

    if [ "x${TAG}" == "x" ]; then
        echo "Missing release tag (e.g. 1.0.0)"
        echo "Usage: ${SELF_NAME} tag [commit]"
        return 1
    fi

    # bugfix branch name
    BRANCH=${TAG%.[0-9]*}.x

    if [ "x${REF}" == "x" ]; then
        echo "Creating release tag ${TAG} and branch ${BRANCH} from HEAD, proceed? (y/N)"
        # retrive full hash of HEAD
        COMMIT=$(${GIT_EXE} rev-list HEAD --max-count=1)
    else
        echo "Creating release tag ${TAG} and branch ${BRANCH} from ${REF}, proceed? (y/N)"
        # retrieve full hash from ref or short hash
        COMMIT=$(${GIT_EXE} rev-list ${REF} --max-count=1)
    fi
    read proceed
    if [[ ( "x${proceed}" != "xy" ) && ( "x${proceed}" != "xY" ) ]] ; then
        echo "Aborting..."
        return 0
    fi

    # check if the remote branch already exists
    ${GIT_EXE} rev-parse --quiet --verify origin/${BRANCH} > /dev/null
    if [ $? -ne 0 ]; then
        # remote branch does not exist
        new_branch=1
        # does the branch already exist locally?
        ${GIT_EXE} rev-parse --quiet --verify ${BRANCH} > /dev/null
        if [ $? -ne 0 ]; then
            # local branch does not exist
            # create bugfix branch from commit
            ${GIT_EXE} checkout "${COMMIT}" -b "${BRANCH}"
            ERR=$?
            if [ ${ERR} -ne 0 ]; then
                echo "Failed to create branch ${BRANCH}"
                return ${ERR}
            fi
        else
            # local branch already exists
            # When the branch already exists, make sure it is being used!
            current_branch=$(${GIT_EXE} rev-parse --abbrev-ref HEAD)
            if [ "$current_branch" != "${BRANCH}" ]; then
                echo "You did not checkout the correct branch ${BRANCH} for tag ${TAG}"
                return 1
            fi
        fi
    else
        new_branch=0
        # When the branch already exists, make sure it is being used!
        current_branch=$(${GIT_EXE} rev-parse --abbrev-ref HEAD)
        if [ "$current_branch" != "${BRANCH}" ]; then
            echo "You did not checkout the correct branch ${BRANCH} for tag ${TAG}"
            return 1
        fi
    fi

    # at this point we should be at the head of the tracking branch
    # for this release.  Make certain that HEAD matches COMMIT
    HASH=$(${GIT_EXE} rev-list HEAD --max-count=1)
    if [ ${HASH} != ${COMMIT} ]; then
        echo "Commit specified does not match current branch HEAD"
        return 1
    fi

    # create tag
    ${GIT_EXE} tag -a "${TAG}" -m "${TAG}" HEAD
    ERR=$?
    if [ ${ERR} -ne 0 ]; then
        echo "Failed to create tag ${TAG}"
        # cleanup... remove the branch that was created
        ${GIT_EXE} branch -d "${BRANCH}"
        return ${ERR}
    fi

    # checkout tag in preparation for building release
    # this should put you in a "detached HEAD" state
    ${GIT_EXE} checkout "${TAG}"
    ERR=$?
    if [ ${ERR} -ne 0 ]; then
        echo "Failed to checkout tag ${TAG}"
        # cleanup... remove the branch that was created
        ${GIT_EXE} branch -d "${BRANCH}"
        return ${ERR}
    fi

    remote=$(${GIT_EXE} config remote.origin.url)
    echo
    echo "Do you wish to push this release branch and tag to $remote? (y/N)"
    echo "You may want to do this manually after creating and verifying release."
    echo "e.g."
    echo "    git push -u origin ${BRANCH}"
    echo "    git push origin refs/tags/${TAG}"
    read proceed
    if [[ ( "x${proceed}" == "xy" ) || ( "x${proceed}" == "xY" ) ]] ; then
        if [ $new_branch .eq 1 ]; then
            ${GIT_EXE} push -u origin "${BRANCH}"
            ERR=$?
            if [ ${ERR} -ne 0 ]; then
                echo "Failed to push branch ${BRANCH} to remote"
                return ${ERR}
            fi
        fi
        ${GIT_EXE} push origin refs/tags/"${TAG}"
        ERR=$?
        if [ ${ERR} -ne 0 ]; then
            echo "Failed to push tag ${BRANCH} to remote"
            return ${ERR}
        fi
    else
        echo "Branch and tag are local, changes not pushed to remote!"
    fi

}

function main()
{
    if validate_repo; then
        tag_release "$@"
    else
        return $?
    fi
}

main "$@"
