# $Id$
# vim:et:ft=sh:sts=2:sw=2
#
# git-flow -- A collection of Git extensions to provide high-level
# repository operations for Vincent Driessen's branching model.
#
# A blog post presenting this model is found at:
#    http://blog.avirtualhome.com/development-workflow-using-git/
#
# Feel free to contribute to this project at:
#    http://github.com/petervanderdoes/gitflow
#
# Authors:
# Copyright 2012-2019 Peter van der Does. All rights reserved.
#
# Original Author:
# Copyright 2010 Vincent Driessen. 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER 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.
#

#
# Common functionality
#

# Shell output
warn() { echo "$@" >&2; }
die() { warn "Fatal: $@"; exit 1; }
die_help() { warn $@; flags_help; exit 1; }

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

#
# String contains function
# $1 haystack
# $2 Needle
#
contains() {
	local return

	case $1 in
		*$2*)
			return=$FLAGS_TRUE
			;;
		*)
			return=$FLAGS_FALSE
			;;
	esac
	return $return
}

# 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; [ $FLAG -ne $FLAGS_TRUE ]; }

# check_boolean
# Check if given value can be interpreted as a boolean
#
# This function determines if the passed parameter is a valid boolean value.
#
# Param $1: string Value to check if it's a valid boolean
#
# Return: string FLAGS_TRUE|FLAGS_FALSE|FLAGS_ERROR
#	FLAGS_TRUE if the parameter is a boolean TRUE
#	FLAGS_FALSE if the parameter is a boolean FALSE
#	FLAGS_ERROR if the parameter is not a boolean
#
check_boolean() {
	local _return _value
	_value="${1}"
	case "${_value}" in
	${FLAGS_TRUE} | [yY] | [yY][eE][sS] | [tT] | [tT][rR][uU][eE])
		_return=${FLAGS_TRUE}
		;;
	${FLAGS_FALSE} | [nN] | [nN][oO] | [fF] | [fF][aA][lL][sS][eE])
		_return=${FLAGS_FALSE}
		;;

	*)
		_return=${FLAGS_ERROR}
		;;
	esac
	unset _value
	return ${_return}
}

#
# Git specific common functionality
#

git_local_branches() { git for-each-ref --sort refname --format='%(refname:short)' refs/heads; }
git_remote_branches() { git for-each-ref --sort refname --format='%(refname:short)' refs/remotes; }
git_all_branches() { git for-each-ref --sort refname --format='%(refname:short)' refs/remotes refs/heads; }
git_all_tags() { git for-each-ref --format='%(refname:short)' refs/tags; }

git_local_branches_prefixed() {
	[ -z $1 ] && die "Prefix parameter missing." # This should never happen.
	git for-each-ref --format='%(refname:short)' refs/heads/$1\* ;
}

git_current_branch() {
	local branch_name

	branch_name="$(git symbolic-ref --quiet HEAD)"
	[ -z $branch_name ] && branch_name="(unnamed branch)" || branch_name="$(git for-each-ref --format='%(refname:short)' $branch_name)"
	echo "$branch_name"
}

git_is_clean_working_tree() {
	git rev-parse --verify HEAD >/dev/null || exit 1
	git update-index -q --ignore-submodules --refresh

	# Check for unstaged changes
	git diff-files --quiet --ignore-submodules || return 1

	# Check for Uncommited changes
	git diff-index --cached --quiet --ignore-submodules HEAD -- || return 2

	return 0
}

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

git_local_branch_exists() {
	[ -n "$1" ] || die "Missing branch name"
	[ -n "$(git for-each-ref --format='%(refname:short)' refs/heads/$1)" ]
}

git_remote_branch_exists() {
	[ -n "$1" ] || die "Missing branch name"
	[ -n "$(git for-each-ref --format='%(refname:short)' refs/remotes/$1)" ]
}

git_remote_branch_delete() {
	[ -n "$1" ] || die "Missing branch name"
	if git_remote_branch_exists "$ORIGIN/$1"; then
		git_do push "$ORIGIN" :"$1" || die "Could not delete the remote $1 in $ORIGIN."
		return 0
	else
		warn "Trying to delete the remote branch $1, but it does not exists in $ORIGIN"
		return 1
	fi
}

git_branch_exists() {
	[ -n "$1" ] || die "Missing branch name"
	git_local_branch_exists "$1" || git_remote_branch_exists "$ORIGIN/$1"
}

git_tag_exists() {
	[ -n "$1" ] || die "Missing tag name"
	[ -n "$(git for-each-ref --format='%(refname:short)' refs/tags/$1)" ]
}

git_config_bool_exists() {
	local value

	[ -n "$1" ] || die "Missing config option"
	value=$(git config --get --bool $1)
	[ "$value" = "true" ]
}
#
# git_compare_refs()
#
# Tests whether two references have diverged and need merging
# first. It returns error codes to provide more detail, like so:
#
# 0    References point to the same commit
# 1    First given reference needs fast-forwarding
# 2    Second given reference needs fast-forwarding
# 3    References need a real merge
# 4    There is no merge base, i.e. the references have no common ancestors
#
git_compare_refs() {
	local commit1 commit2 base

	commit1=$(git rev-parse "$1"^{})
	commit2=$(git rev-parse "$2"^{})
	if [ "$commit1" != "$commit2" ]; then
		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 successfully merged into $2
#
git_is_branch_merged_into() {
	local merge_hash base_hash

	merge_hash=$(git merge-base "$1"^{} "$2"^{})
	base_hash=$(git rev-parse "$1"^{})

	# If the hashes are equal, the branches are merged.
	[ "$merge_hash" = "$base_hash" ]
}

#
# git_is_ancestor()
#
# This is the same function as git_is_branch_merged_into but
# for readability given a different name.
#
git_is_ancestor() {
	git_is_branch_merged_into "$1" "$2"
}

#
# git_fetch_branch()
#
# $1 Origin - Where to fetch from
# $2 Branch - Which branch to fetch
#
# This fetches the given branch from the given origin.
# Instead of storing it in FETCH_HEAD it will be stored in
# refs/remotes/<origin>/<branch>
#
git_fetch_branch() {
	local origin branch

	[ -n "$1" ] || die "Missing origin"
	[ -n "$2" ] || die "Missing branch name"
	origin="$1"
	branch="$2"
	if git_remote_branch_exists "$origin/$branch"; then
		git_do fetch -q "$origin" "$branch" || die "Could not fetch $branch from $origin."
	else
		warn "Trying to fetch branch '$origin/$branch' but it does not exist."
	fi
}

#
# gitflow specific common functionality
#

# Function used to check if the repository is git-flow enabled.
gitflow_has_master_configured() {
	local master

	master=$(git config --get gitflow.branch.master)
	[ "$master" != "" ] && git_local_branch_exists "$master"
}

gitflow_has_develop_configured() {
	local develop

	develop=$(git config --get gitflow.branch.develop)
	[ "$develop" != "" ] && git_local_branch_exists "$develop"
}

gitflow_is_initialized() {
	gitflow_has_master_configured                    && \
	gitflow_has_develop_configured                   && \
	[ "$(git config --get gitflow.branch.master)" != "$(git config --get gitflow.branch.develop)" ] && \
	git config --get-regexp gitflow.prefix >/dev/null 2>&1
}

# Loading settings that can be overridden using git config
gitflow_load_settings() {
	export GIT_CURRENT_REPO_DIR="$(git rev-parse --show-toplevel 2>/dev/null)"
	DOT_GIT_DIR=$(git rev-parse --git-dir)
	export DOT_GIT_DIR="$(cd ${DOT_GIT_DIR} >/dev/null 2>&1 && pwd)"
	export HOOKS_DIR="$(git config --get gitflow.path.hooks || echo ${DOT_GIT_DIR}/hooks)" # the second option is used to support previous versions of git-flow
	export MASTER_BRANCH=$(git config --get gitflow.branch.master)
	export DEVELOP_BRANCH=$(git config --get gitflow.branch.develop)
	export ORIGIN=$(git config --get gitflow.origin || echo origin)

	GITFLOW_CONFIG="$DOT_GIT_DIR/gitflow_config"
	if [ -f "$GITFLOW_CONFIG" ]; then # move all settings from old .git/gitflow_config to the local conf.
		warn "Migrating old \"$GITFLOW_CONFIG\" to the \"--local\" repo config."
		_config_lines=`git config --list --file="$GITFLOW_CONFIG"`;
		for _config_line in ${_config_lines}; do
			_key=${_config_line%=*}
			_value=${_config_line#=*}
			git_do config --local gitflow.${_key} ${_value}
		done;
		mv "$GITFLOW_CONFIG" "$GITFLOW_CONFIG".backup 2>/dev/null
	fi
}

#
# gitflow_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
#
gitflow_resolve_nameprefix() {
	local name prefix
	local match matches num_matches

	name=$1
	prefix=$2

	# 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 branches match '$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
}

#
# Check if the given branch is a git-flow branch
#
gitflow_is_prefixed_branch() {
	local branch return

	branch=$1
	case $branch in
	$(git config --get gitflow.prefix.feature)* | \
	$(git config --get gitflow.prefix.bugfix)* | \
	$(git config --get gitflow.prefix.release)* | \
	$(git config --get gitflow.prefix.hotfix)*  | \
	$(git config --get gitflow.prefix.support)* )
		return=0
		;;
	*)
		return=1
		;;
	esac
	return $return
}
#
# Update the config with the base of a new git-flow branch.
#
# @param $1 Base of the new branch
# @param $2 Name of the branch
#
gitflow_config_set_base_branch() {
	local base branch

	base=$1
	branch=$2
	$(git_do config --local "gitflow.branch.$branch.base" $base)
}

#
# Get the base of a branch as set by gitflow_set_branch
#
# @param $1 Name of the branch
# @return string|empty String when a base is found otherwise empty
#
gitflow_config_get_base_branch() {
	local branch

	branch=$1
	echo $(git config --local --get "gitflow.branch.$branch.base")
}

#
# Remove the section that contains the base of a branch as set by gitflow_set_branch
#
# @param $1 Name of the branch
#
gitflow_config_remove_base_section() {
	local branch

	branch=$1
	$(git_do config --local --remove-section "gitflow.branch.$branch" 2>/dev/null)
}

#
# Remove the base of the git-flow branch from the.
# @param $1 Name of the branch
#
gitflow_config_remove_base_branch() {
	local base

	base=$1
	$(git_do config --local --unset "gitflow.branch.$branch.base" 2>/dev/null)
}

#
# Remove the base of the git-flow branch from the.
# @param $1 Name of the branch
#
gitflow_config_rename_sections() {
	local new
	local old

	old=$1
	new=$2
	$(git_do config --local --rename-section "gitflow.branch.$old" "gitflow.branch.$new" 2>/dev/null)
}

# gitflow_override_flag_boolean()
#
# Override a boolean flag
#
# Param $1: string The name of the config variable e.g. "feature.start.fetch"
# Param $2: string The flag name
#
gitflow_override_flag_boolean() {
	local _variable

	_variable=$(git config --bool --get gitflow.$1 2>&1)
	case $? in
	0)
		[ "${_variable}" = "true" ] && eval "FLAGS_${2}=${FLAGS_TRUE}" || eval "FLAGS_${2}=${FLAGS_FALSE}"
		;;
	128)
		die "${_variable}"
		;;
	esac
	unset _variable
	return ${FLAGS_TRUE}
}

# gitflow_override_flag_string()
#
# Override a string flag
#
# Param $1: string The name of the config variable e.g. "feature.start.fetch"
# Param $2: string The flag name
#
gitflow_override_flag_string() {
	local _variable

	_variable=$(git config --get gitflow.$1 2>&1)
	case $? in
	0)
		eval "FLAGS_${2}=\"${_variable}\""
		;;
	esac
	unset _variable
	return ${FLAGS_TRUE}
}

# gitflow_create_squash_message()
#
# Create the squash message, overriding the one generated by git itself
#
# Param $1: string The line to be added
# Param $2: string The base of the branch that will me merged
# Param $3: string The branch that will be merged.
#
gitflow_create_squash_message() {
	echo Squashed commit of the following:
	echo
	echo $1
	echo
	git log --no-merges --pretty=medium ^"$2" $3
}

#
# Parameter functions
#
gitflow_require_name_arg() {
	if [ "$NAME" = "" ]; then
		die_help "Missing argument <name>"
	fi
}

gitflow_expand_nameprefix_arg() {
	local expanded_name exitcode

	gitflow_require_name_arg

	expanded_name=$(gitflow_resolve_nameprefix "$NAME" "$PREFIX")
	exitcode=$?
	case $exitcode in
	0)
		NAME=$expanded_name
		BRANCH=$PREFIX$NAME
		;;
	*)
		exit 1
		;;
	esac
}

gitflow_require_version_arg() {
	if [ "$VERSION" = "" ]; then
		die_help "Missing argument <version>"
	fi
}

gitflow_expand_versionprefix_arg() {
	local expanded_version exitcode

	gitflow_require_version_arg

	version=$(gitflow_resolve_nameprefix "$VERSION" "$PREFIX")
	exitcode=$?
	case $exitcode in
	0)
		VERSION=$version
		BRANCH=$PREFIX$VERSION
		;;
	*)
		exit 1
		;;
	esac
}


gitflow_require_base_arg() {
	if [ "$BASE" = "" ]; then
		die_help "Missing argument <base>"
	fi
}

gitflow_use_current_branch_name() {
	local current_branch

	current_branch=$(git_current_branch)

	if startswith "$current_branch" "$PREFIX"; then
		BRANCH=$current_branch
		NAME=${BRANCH#$PREFIX}
	else
		warn "The current HEAD is no ${SUBCOMMAND} branch."
		warn "Please specify a <name> argument."
		exit 1
	fi
}

gitflow_use_current_branch_version() {
	local current_branch

	current_branch=$(git_current_branch)

	if startswith "$current_branch" "$PREFIX"; then
		BRANCH=$current_branch
		VERSION=${BRANCH#$PREFIX}
	else
		warn "The current HEAD is no ${SUBCOMMAND} branch."
		warn "Please specify a <version> argument."
		exit 1
	fi
}

gitflow_rename_branch() {
	# Parse arguments
	FLAGS "$@" || exit $?
	eval set -- "${FLAGS_ARGV}"

	# read arguments into global variables
	if [ -z $1 ]; then
		NEW_NAME=''
	else
		NEW_NAME=$1
	fi

	if [ -z $2 ]; then
		NAME=''
	else
		NAME=$2
	fi
	BRANCH=${PREFIX}${NAME}
	NEW_BRANCH=${PREFIX}${NEW_NAME}

	if [ -z "$NEW_NAME" ]; then
		die "No new name given."
	fi

	# Use current branch if no name is given
	if [ "$NAME" = "" ]; then
		gitflow_use_current_branch_name
	fi


	# Sanity checks
	require_branch "$BRANCH"
	require_branch_absent "$NEW_BRANCH"

	run_pre_hook "$NAME" "$ORIGIN" "$BRANCH"
	git_do branch -m "$BRANCH" "$NEW_BRANCH" || die "Error renaming branch '$BRANCH' to '$NEW_BRANCH'"
	gitflow_config_rename_sections "$BRANCH" "$NEW_BRANCH"
	run_post_hook "$NAME" "$ORIGIN" "$BRANCH"

	echo
	echo "Summary of actions:"
	echo "- Branch '$BRANCH' has been renamed to '$NEW_BRANCH'."
	echo "- You are now on branch '$(git_current_branch)'"
	echo
}
#
# Assertions for use in git-flow subcommands
#

require_git_repo() {
	git rev-parse 2>/dev/null || die "Not a git repository"
}

require_gitflow_initialized() {
	gitflow_is_initialized || die "Not a gitflow-enabled repo yet. Please run 'git flow init' first."
	$(git config --get gitflow.prefix.versiontag >/dev/null 2>&1) || die "Version tag not set. Please run 'git flow init'."
}

require_clean_working_tree() {
	local result

	git_is_clean_working_tree
	result=$?
	if [ $result -eq 1 ]; then
		die "Working tree contains unstaged changes. Aborting."
	fi
	if [ $result -eq 2 ]; then
		die "Index contains uncommited changes. Aborting."
	fi
}

require_base_is_local_branch() {
	git_local_branch_exists "$1" || die "Base '$1' needs to be a branch. It does not exist and is required."
}

require_local_branch() {
	git_local_branch_exists "$1" || die "Local branch '$1' does not exist and is required."
}

require_remote_branch() {
	git_remote_branch_exists "$1" || die "Remote branch '$1' does not exist and is required."
}

require_branch() {
	git_branch_exists "$1" || die "Branch '$1' does not exist and is required."
}

require_branch_absent() {
	git_branch_exists "$1" && die "Branch '$1' already exists. Pick another name."
}

require_local_branch_absent() {
	git_local_branch_exists "$1" && die "Branch '$1' already exists. Pick another name."
}

require_tag_absent() {
	git_tag_exists "$1" && die "Tag '$1' already exists. Pick another name."
}

require_branches_equal() {
	local compare_refs_result

	require_local_branch "$1"
	require_remote_branch "$2"
	git_compare_refs "$1" "$2"
	compare_refs_result=$?

	if [ $compare_refs_result -gt 0 ]; then
		warn "Branches '$1' and '$2' have diverged."
		if [ $compare_refs_result -eq 1 ]; then
			die "And branch '$1' may be fast-forwarded."
		elif [ $compare_refs_result -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
}

#
# Show commands if flag is set.
#
git_do() {
	if flag showcommands; then
		echo "git $@" >&2
	fi

	git "$@"
}

#
# run_filter_hook
#
# Looks for a Git hook script called as defined by the first variable
#
#     filter-flow-command
#
# If such a hook script exists and is executable, it is called with the given
# positional arguments.
#
run_filter_hook() {
	local command scriptfile return

	command=$1
	shift
	scriptfile="${HOOKS_DIR}/filter-flow-${command}"
	if [ -x "$scriptfile" ]; then
		return=`$scriptfile "$@"`
		if [ $? -eq 127 ]; then
			echo "$return"
			exit 127
		fi
			echo $return
	else
		echo "$@"
	fi
}

#
# run_pre_hook
#
# Looks for a Git hook script called
#
#     pre-flow-<subcmd>-<subaction>
#
# If such a hook script exists and is executable, it is called with the given
# positional arguments.  If its return code non-zero, the git-flow action is
# aborted.
#
run_pre_hook() {
	local scriptfile exitcode

	scriptfile="${HOOKS_DIR}/pre-flow-${SUBCOMMAND}-${SUBACTION}"
	exitcode=0
	if [ -x "$scriptfile" ]; then
		"$scriptfile" "$@"
		exitcode=$?

		if [ $exitcode -gt 0 ]; then
			die "Hook command $scriptfile ended with exit code $exitcode."
		fi
	fi
}

#
# run_post_hook
#
# Looks for a Git hook script called
#
#     post-flow-<subcmd>-<subaction>
#
# If such a hook script exists and is executable, it is called with the given
# positional arguments.  Its return code is ignored.
#
run_post_hook() {
	local scriptfile

	scriptfile="${HOOKS_DIR}/post-flow-${SUBCOMMAND}-${SUBACTION}"
	if [ -x "$scriptfile" ]; then
		"$scriptfile" "$@"
	fi
}

flags_help() {
	eval "$( echo "$OPTIONS_SPEC" | git rev-parse --parseopt -- "-h" || echo exit $? )"
}
