#
# HubFlow - a fork of the git-flow tools to apply Vincent Driessen's
# branching model to working with GitHub
#
# Original blog post presenting this model is found at:
#    http://nvie.com/git-model
#
# The HubFlow documentation is found at:
#    http://datasift.github.com/gitflow/
#
# Feel free to contribute to this project at:
#    http://github.com/datasift/gitflow
#
# Copyright 2010 Vincent Driessen. All rights reserved.
# Copyright 2012 MediaSift Ltd. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#    1. Redistributions of source code must retain the above copyright notice,
#       this list of conditions and the following disclaimer.
#
#    2. Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY VINCENT DRIESSEN ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL VINCENT DRIESSEN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of Vincent Driessen.
#

#
# Common variables
#

HUBFLOW_VERSION=1.5.2
HUBFLOW_REPO=https://github.com/datasift/gitflow

#
# Common functionality
#

# shell output
warn() { echo "$@" >&2; }
die() { warn "$@"; exit 1; }

escape() {
	echo "$1" | sed 's/\([\.\$\*]\)/\\\1/g'
}

# set logic
has() {
	local item=$1; shift
	echo " $@ " | grep -q " $(escape $item) "
}

# basic math
min() { [ "$1" -le "$2" ] && echo "$1" || echo "$2"; }
max() { [ "$1" -ge "$2" ] && echo "$1" || echo "$2"; }

# basic string matching
startswith() { [ "$1" != "${1#$2}" ]; }
endswith() { [ "$1" != "${1%$2}" ]; }

# convenience functions for checking shFlags flags
flag() { local FLAG; eval FLAG='$FLAGS_'$1; [ $FLAG -eq $FLAGS_TRUE ]; }
noflag() { local FLAG; eval FLAG='$FLAGS_'$1; [ -z $FLAG ] || [ $FLAG -ne $FLAGS_TRUE ]; }

#
# Git specific common functionality
#

git_local_branches() { git branch --no-color | sed 's/^[* ] //'; }
git_remote_branches() { git branch -r --no-color | sed 's/^[* ] //'; }
git_all_branches() { ( git branch --no-color; git branch -r --no-color) | sed 's/^[* ] //'; }
git_all_tags() { git tag; }

git_current_branch() {
	git branch --no-color | grep '^\* ' | grep -v 'no branch' | sed 's/^* //g'
}

git_is_clean_working_tree() {
	if ! git diff --no-ext-diff --ignore-submodules --quiet --exit-code; then
		return 1
	elif ! git diff-index --cached --quiet --ignore-submodules HEAD --; then
		return 2
	else
		return 0
	fi
}

git_repo_is_headless() {
	! git rev-parse --quiet --verify HEAD >/dev/null 2>&1
}

git_local_branch_exists() {
	has $1 $(git_local_branches)
}

git_remote_branch_exists() {
	has $1 $(git_remote_branches)
}

git_branch_exists() {
	has $1 $(git_all_branches)
}

git_tag_exists() {
	has $1 $(git_all_tags)
}

git_ping_remote() {
	# git-ls-remote does not work w/ Git 1.7.10.2 (Apple Git-33) when
	# executed with no parameters
	git ls-remote "$ORIGIN" > /dev/null 2>&1
}

#
# git_compare_branches()
#
# Tests whether branches and their "origin" counterparts have diverged and need
# merging first. It returns error codes to provide more detail, like so:
#
# 0    Branch heads point to the same commit
# 1    First given branch needs fast-forwarding
# 2    Second given branch needs fast-forwarding
# 3    Branch needs a real merge
# 4    There is no merge base, i.e. the branches have no common ancestors
#
git_compare_branches() {
	local commit1=$(git rev-parse "$1")
	local commit2=$(git rev-parse "$2")
	if [ "$commit1" != "$commit2" ]; then
		local base=$(git merge-base "$commit1" "$commit2")
		if [ $? -ne 0 ]; then
			return 4
		elif [ "$commit1" = "$base" ]; then
			return 1
		elif [ "$commit2" = "$base" ]; then
			return 2
		else
			return 3
		fi
	else
		return 0
	fi
}

#
# git_is_branch_merged_into()
#
# Checks whether branch $1 is succesfully merged into $2
#
git_is_branch_merged_into() {
	local subject=$1
	local base=$2
	local all_merges="$(git branch --no-color --contains $subject | sed 's/^[* ] //')"
	has $base $all_merges
}

#
# hubflow specific common functionality
#

# check if this repo has been inited for hubflow
hubflow_has_master_configured() {
	local master=$(git config --get hubflow.branch.master)
	[ "$master" != "" ] && git_local_branch_exists "$master"
}

hubflow_has_develop_configured() {
	local develop=$(git config --get hubflow.branch.develop)
	[ "$develop" != "" ] && git_local_branch_exists "$develop"
}

hubflow_has_prefixes_configured() {
	git config --get hubflow.prefix.feature >/dev/null 2>&1     && \
	git config --get hubflow.prefix.release >/dev/null 2>&1     && \
	git config --get hubflow.prefix.hotfix >/dev/null 2>&1      && \
	git config --get hubflow.prefix.support >/dev/null 2>&1     && \
	git config --get hubflow.prefix.versiontag >/dev/null 2>&1
}

hubflow_is_initialized() {
	hubflow_has_master_configured                    && \
	hubflow_has_develop_configured                   && \
	[ "$(git config --get hubflow.branch.master)" !=    \
	  "$(git config --get hubflow.branch.develop)" ] && \
	hubflow_has_prefixes_configured
}

# loading settings that can be overridden using git config
hubflow_load_settings() {
	export DOT_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
	export MASTER_BRANCH=$(git config --get hubflow.branch.master)
	export DEVELOP_BRANCH=$(git config --get hubflow.branch.develop)
	export ORIGIN=$(git config --get hubflow.origin || echo origin)
    export BRANCH=$(git_current_branch)
}

hubflow_change_branch() {
    # what is the current branch?
    local current_branch=$(git_current_branch)

    # do we need to switch branch?
    if [[ $current_branch != $1 ]] ; then
        git checkout "$1" || die "Unable to checkout branch '$1'"
    fi
}

hubflow_branch_push() {
    # make sure we have a stack to push onto
    if [[ -z BRANCH_STACK ]] ; then
        BRANCH_STACK=()
        BRANCH_STACK_SP=0
    fi

    # push the requested branch onto the stack
    BRANCH_STACK[$BRANCH_STACK_SP]="$1"
    let "BRANCH_STACK_SP += 1"

    # do we need to switch branch too?
    hubflow_change_branch "$1"
}

hubflow_branch_push_current_branch() {
    hubflow_branch_push "$(git_current_branch)"
}

hubflow_branch_pop() {
    # make sure we have a stack to pop from
    [[ -n $BRANCH_STACK ]] || die "Internal error: attempt to pop from non-existent BRANCH_STACK"

    # make sure the stack is not empty
    [[ $BRANCH_STACK_SP != 0 ]] || die "Internal error: attempt to pop from empty BRANCH_STACK"

    # pop the branch from the stack
    local popped_branch=${BRANCH_STACK[$BRANCH_STACK_SP]}
    BRANCH_STACK[$BRANCH_STACK_SP]=
    let "BRANCH_STACK_SP -= 1"

    # do we need to switch branch too?
    hubflow_change_branch "$popped_branch"
}

hubflow_branch_pop_no_checkout() {
    # make sure we have a stack to pop from
    [[ -n $BRANCH_STACK ]] || die "Internal error: attempt to pop from non-existent BRANCH_STACK"

    # make sure the stack is not empty
    [[ $BRANCH_STACK_SP != 0 ]] || die "Internal error: attempt to pop from empty BRANCH_STACK"

    # pop the branch from the stack
    BRANCH_STACK[$BRANCH_STACK_SP]=
    let "BRANCH_STACK_SP -= 1"
}

# $1: origin
# $2: branch to merge into
# $3: 'no_checkout_afterwards' if we do not want to checkout the current branch after the merge
hubflow_remote_merge_helper() {
    # is there anything to merge?
    git_compare_branches "$2" "$1/$2"
    if [[ $? -eq 0 ]] ; then
        return
    fi

    # switch to the right branch locally
    hubflow_branch_push "$2"

    # merge from origin
    # do not pull, as we will already have a local copy of the branch
    git merge "$1/$2"

    # did the merge work?
    if [[ $? -ne 0 ]] ; then
        # oops.. we have a merge conflict!
        #
        # tell the user what to do next
        echo
        echo "There were merge conflicts. To resolve the merge conflict manually, use:"
        echo
        echo "    git mergetool"
        echo "    git commit"
        echo
        echo "You should then push your changes back up to '$1', using:"
        echo
        echo "    git push '$1' '$2'"
        echo
        echo "After you have pushed back to '$1', please retry your HubFlow command"
        exit 1
    fi

    # switch back to the original branch
    if [[ -z $3 || $3 != 'no_checkout_afterwards' ]] ; then
        hubflow_branch_pop
    else
        hubflow_branch_pop_no_checkout
    fi
}

# $1: local branch to merge from
# $2: local branch to merge to
# $3: 'no_checkout_afterwards' if we do not want to checkout the current branch after the merge
hubflow_local_merge_helper() {
    # is there anything to merge?
    git_compare_branches "$1" "$2"
    if [[ $? -eq 0 ]] ; then
        return
    fi

    # switch to the right branch locally
    hubflow_branch_push "$2"

    # merge into branch
    if [ "$(git rev-list -n2 "$2..$1" | wc -l)" -eq 1 ]; then
        git merge --ff "$1"
    else
        git merge --no-ff "$1"
    fi

    # did the merge work?
    if [ $? -ne 0 ]; then
        # oops.. we have a merge conflict!
        #
        # tell the user what to do next
        echo
        echo "There were merge conflicts. To resolve the merge conflict manually, use:"
        echo
        echo "    git mergetool"
        echo "    git commit"
        echo
        echo "After you have resolved all merge conflicts, please retry your HubFlow command"
        exit 1
    fi

    # switch back to the original branch
    if [[ -z $3 || $3 != 'no_checkout_afterwards' ]] ; then
        hubflow_branch_pop
    else
        hubflow_branch_pop_no_checkout
    fi
}

hubflow_fetch_latest_changes_from_origin() {
    git remote update || die "Unable to get list of latest changes from '$ORIGIN'"
    git fetch "$ORIGIN" || die "Unable to fetch latest changes from '$ORIGIN'"
    git fetch --tags  || die "Unable to fetch latest tags from '$ORIGIN'"
}

hubflow_merge_latest_changes_from_origin() {
    # we need to fetch the latest changes from upstream, before we can merge
    hubflow_fetch_latest_changes_from_origin

    # remember which branch is currently checked out
    hubflow_branch_push_current_branch

    # merge the master branch first
    # merge conflicts on master will be rare
    if has "$ORIGIN/$MASTER_BRANCH" $(git_remote_branches); then
        hubflow_remote_merge_helper "$ORIGIN" "$MASTER_BRANCH" no_checkout_afterwards
        git_compare_branches "$MASTER_BRANCH" "$ORIGIN/$MASTER_BRANCH"
        if [[ $? -ne 0 ]] ; then
            git push "$ORIGIN" "$MASTER_BRANCH" || die "Push to '$ORIGIN' failed"
        fi
    fi

    # merge the develop branch next
    # merge conflicts on develop are sadly more common
    if has "$ORIGIN/$DEVELOP_BRANCH" $(git_remote_branches); then
        hubflow_remote_merge_helper "$ORIGIN" "$DEVELOP_BRANCH" no_checkout_afterwards
        git_compare_branches "$DEVELOP_BRANCH" "$ORIGIN/$DEVELOP_BRANCH"
        if [[ $? -ne 0 ]] ; then
            git push "$ORIGIN" "$DEVELOP_BRANCH" || die "Push to '$ORIGIN' failed"
        fi
    fi

    # switch back to the original branch
    hubflow_branch_pop
}

# $1: 'force' if push should overwrite existing origin contents
hubflow_push_latest_changes_to_origin() {
    local current_branch=$(git_current_branch)

    if ! git_branch_exists "$ORIGIN/$current_branch" ; then
        # create remote branch
        git push "$ORIGIN" "$current_branch:refs/heads/$current_branch"  || die "Push to '$ORIGIN' failed"
        # bring metadata back
        git fetch -q "$ORIGIN"

        # configure remote tracking
        git config "branch.$current_branch.remote" "$ORIGIN"
        git config "branch.$current_branch.merge" "refs/heads/$current_branch"

        # we're done
        return 1
    fi

    # we have an existing branch to update or overwrite
    if [[ $1 == "force" ]] ; then
        git push --force "$ORIGIN" "$current_branch:refs/heads/$current_branch" || die "Push to '$ORIGIN' failed"
        return 2
    fi

    git_compare_branches "$current_branch" "$ORIGIN/$current_branch"
    if [[ $? -ne 0 ]] ; then
        git push "$ORIGIN" "$current_branch:refs/heads/$current_branch" || die "Push to '$ORIGIN' failed"
        return 3
    fi

    # no action taken
    return 0
}

hubflow_get_latest_version_number() {
	LATEST="`git ls-remote --tags https://github.com/datasift/gitflow | grep -v '\^' | tail -n 1 | cut -d / -f 3`"

	echo $LATEST
}

hubflow_has_new_version_available() {
	LATEST=`hubflow_get_latest_version_number`
	if [[ $LATEST != $HUBFLOW_VERSION ]] ; then
		echo "A new version of the HubFlow tools is available."
		echo "To upgrade, run 'git hf upgrade'."
		echo
		return 0
	fi

	# no new version available
	return 1
}

#
# hubflow_track_repo
#
# Inputs:
# $1 = name prefix to resolve
# $2 = branch prefix to use
# $3 = origin
#
# When you want to switch to a remote feautre
# you must tell git to track it in your local config
# This script will take care of that
#
hubflow_track_repo(){
    local name=$1
    local prefix=$2
    local origin=$3

echo "$name"
    # return if feature exists
    if git_local_branch_exists "$prefix$name"; then
        return 1
    fi

    # track the branch if it does not exist
    if git_branch_exists "$origin/$prefix$name"; then
        git branch --track $prefix$name
        return 2
    fi

return 0
}


#
# hubflow_resolve_nameprefix
#
# Inputs:
# $1 = name prefix to resolve
# $2 = branch prefix to use
#
# Searches branch names from git_local_branches() to look for a unique
# branch name whose name starts with the given name prefix.
#
# There are multiple exit codes possible:
# 0: The unambiguous full name of the branch is written to stdout
#    (success)
# 1: No match is found.
# 2: Multiple matches found. These matches are written to stderr
#
hubflow_resolve_nameprefix() {
	local name=$1
	local prefix=$2
	local matches
	local num_matches

	# first, check if there is a perfect match
	if git_local_branch_exists "$prefix$name"; then
		echo "$name"
		return 0
	fi

	matches=$(echo "$(git_local_branches)" | grep "^$(escape "$prefix$name")")
	num_matches=$(echo "$matches" | wc -l)
	if [ -z "$matches" ]; then
		# no prefix match, so take it literally
		warn "No branch matches prefix '$name'"
		return 1
	else
		if [ $num_matches -eq 1 ]; then
			echo "${matches#$prefix}"
			return 0
		else
			# multiple matches, cannot decide
			warn "Multiple branches match prefix '$name':"
			for match in $matches; do
				warn "- $match"
			done
			return 2
		fi
	fi
}

#
# GitHub speicific common functionality
#
GITHUB_API_URL="https://api.github.com"
GITHUB_AUTH_TOKEN_KEY="github.oauth.token"

#
# Gets the GitHub OAuth token for this user/repo
# If no token has been created, the user will be prompted to create one.
#
github_auth_token() {
  GITHUB_TOKEN=$(git config --get "$GITHUB_AUTH_TOKEN_KEY")

  if [ -z "$GITHUB_TOKEN" ]; then
    read -p "GitHub Username: " USER
    read -s -p "GitHub Password: " PASS
    echo

    if [ -z "$USER" ]; then
      die "You must enter your GitHub username to authenticate with"
    fi

    if [ -z "$PASS" ]; then
      die "You must enter your GitHub password to authenticate with"
    fi

    GITHUB_TOKEN=$(curl -s \
        -u "$USER:$PASS" \
        -d '{"scopes":["repo"],"note":"gitflow automation"}' \
        "$GITHUB_API_URL/authorizations" |
        awk -F"," '{for(i=1;i<=NF;i++){if($i~/token/){print $i}}}' |
        awk -F":" '{print $2}'| awk -F"\"" '{print $2}')

    git config "$GITHUB_AUTH_TOKEN_KEY" "$GITHUB_TOKEN"
  fi
}

# Ensures we have been authorized on GitHub
require_github_auth_token() {
  github_auth_token

  if [ -z "$GITHUB_TOKEN" ]; then
    die "Failed to authorize GitHub user '$USER' for '$GITHUB_ORIGIN'"
  fi
}

# Gets the name of the origin repo on GitHub
# e.g. datasift/gitflow
github_origin_repo() {
  # get the URL for the $ORIGIN repo
  GITHUB_ORIGIN=$(git remote show -n "$ORIGIN" | grep 'Fetch URL' | cut -c 14-)
  # echo "$GITHUB_ORIGIN" 1>&2

  # if the URL doesn't point at GitHub, blank it
  if echo "$GITHUB_ORIGIN" | grep 'git@github.com:' > /dev/null ; then
  	GITHUB_ORIGIN=$(echo $GITHUB_ORIGIN | cut -d : -f 2- | sed -e 's|\.git||g;' )
  elif echo "$GITHUB_ORIGIN" | grep 'https://github.com' > /dev/null ; then
  	GITHUB_ORIGIN=$(echo $GITHUB_ORIGIN | cut -c 20- | sed -e 's|\.git||g;' )
  else
     GITHUB_ORIGIN=
  fi
  #echo "$GITHUB_ORIGIN" 1>&2
}

require_github_origin_repo() {
  github_origin_repo

  if [ -z $GITHUB_ORIGIN ]; then
    die "The remote repo for this branch, '$ORIGIN' is not a GitHub repository."
  fi
}

# Posts an authenticated request to GitHub and returns the response
github_post() {
  require_github_auth_token

  resp=$(curl -s -H "Authorization: bearer $GITHUB_TOKEN" -d "$2" "$GITHUB_API_URL$1")
  echo $resp
}

#
# Assertions for use in git-flow subcommands
#

require_git_repo() {
	if ! git rev-parse --git-dir >/dev/null 2>&1; then
		die "fatal: Not a git repository"
	fi
}

require_remote_available() {
	if ! git_ping_remote $1 ; then
		die "fatal: remote repository $1 is unavailable; are you offline?"
	fi
}

require_hubflow_initialized() {
	if ! hubflow_is_initialized; then
		die "fatal: Not a hubflow-enabled repo yet. Please run \"git hf init\" first."
	fi
}

require_clean_working_tree() {
	git_is_clean_working_tree
	local result=$?
	if [ $result -eq 1 ]; then
		die "fatal: Working tree contains unstaged changes. Aborting."
	fi
	if [ $result -eq 2 ]; then
		die "fatal: Index contains uncommited changes. Aborting."
	fi
}

require_local_branch() {
	if ! git_local_branch_exists $1; then
		die "fatal: Local branch '$1' does not exist and is required."
	fi
}

require_remote_branch() {
	if ! has $1 $(git_remote_branches); then
		die "Remote branch '$1' does not exist and is required."
	fi
}

require_branch() {
	if ! has $1 $(git_all_branches); then
		die "Branch '$1' does not exist and is required."
	fi
}

require_branch_absent() {
	if has $1 $(git_all_branches); then
		die "Branch '$1' already exists. Pick another name."
	fi
}

require_tag_absent() {
	for tag in $(git_all_tags); do
		if [ "$1" = "$tag" ]; then
			die "Tag '$1' already exists. Pick another name."
		fi
	done
}

require_branches_equal() {
	require_local_branch "$1"
	require_remote_branch "$2"
	git_compare_branches "$1" "$2"
	local status=$?
	if [ $status -gt 0 ]; then
		warn "Branches '$1' and '$2' have diverged."
		if [ $status -eq 1 ]; then
			die "And branch '$1' may be fast-forwarded."
		elif [ $status -eq 2 ]; then
			# Warn here, since there is no harm in being ahead
			warn "And local branch '$1' is ahead of '$2'."
		else
			die "Branches need merging first."
		fi
	fi
}
