#
# 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.
#

require_git_repo
require_hubflow_initialized
hubflow_load_settings
PREFIX=$(git config --get hubflow.prefix.feature)

usage() {
	echo "usage: git hf feature [list] [-v]"
	echo "       git hf feature start <name> [<base>]"
	echo "       git hf feature submit [<name>] [<base>]"
	echo "       git hf feature finish [-rFkD] [<name|nameprefix>]"
	echo "       git hf feature track <name>"
	echo "       git hf feature diff [<name|nameprefix>]"
	echo "       git hf feature rebase [-i] [<name|nameprefix>]"
	echo "       git hf feature checkout [<name|nameprefix>]"
	echo "       git hf feature pull [-r] [<remote> [<name>]]"
	echo "       git hf feature push [<remote> [<name>]]"
	echo "       git hf feature cancel [-f] <name>"
}

cmd_default() {
	cmd_list "$@"
}

cmd_list() {
	DEFINE_boolean verbose false 'verbose (more) output' v
	parse_args "$@"

	local feature_branches
	local current_branch
	local short_names
	feature_branches=$(echo "$(git_local_branches)" | grep "^$PREFIX")
	if [ -z "$feature_branches" ]; then
		warn "No feature branches exist."
		warn ""
		warn "You can start a new feature branch:"
		warn ""
		warn "    git hf feature start <name> [<base>]"
		warn ""
		exit 0
	fi
	current_branch=$(git branch --no-color | grep '^\* ' | grep -v 'no branch' | sed 's/^* //g')
	short_names=$(echo "$feature_branches" | sed "s ^$PREFIX  g")

	# determine column width first
	local width=0
	local branch
	for branch in $short_names; do
		local len=${#branch}
		width=$(max $width $len)
	done
	width=$(($width+3))

	local branch
	for branch in $short_names; do
		local fullname=$PREFIX$branch
		local base=$(git merge-base "$fullname" "$DEVELOP_BRANCH")
		local develop_sha=$(git rev-parse "$DEVELOP_BRANCH")
		local branch_sha=$(git rev-parse "$fullname")
		if [ "$fullname" = "$current_branch" ]; then
			printf "* "
		else
			printf "  "
		fi
		if flag verbose; then
			printf "%-${width}s" "$branch"
			if [ "$branch_sha" = "$develop_sha" ]; then
				printf "(no commits yet)"
			elif [ "$base" = "$branch_sha" ]; then
				printf "(is behind develop, may ff)"
			elif [ "$base" = "$develop_sha" ]; then
				printf "(based on latest develop)"
			else
				printf "(may be rebased)"
			fi
		else
			printf "%s" "$branch"
		fi
		echo
	done
}

cmd_help() {
	usage
	exit 0
}

require_name_arg() {
	if [ "$NAME" = "" ]; then
		warn "Missing argument <name>"
		usage
		exit 1
	fi
}

expand_nameprefix_arg() {
	require_name_arg

	local expanded_name
	local exitcode
    track_repo=$(hubflow_track_repo "$NAME" "$PREFIX" "$ORIGIN")	
    expanded_name=$(hubflow_resolve_nameprefix "$NAME" "$PREFIX")
	exitcode=$?
	case $exitcode in
		0) NAME=$expanded_name
		   BRANCH=$PREFIX$NAME
		   ;;
		*) exit 1 ;;
	esac
}

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

expand_nameprefix_arg_or_current() {
	if [ "$NAME" != "" ]; then
		expand_nameprefix_arg
		require_branch "$PREFIX$NAME"
	else
		use_current_feature_branch_name
	fi
}

name_or_current() {
	if [ -z "$NAME" ]; then
		use_current_feature_branch_name
	fi
}

parse_args() {
	# parse options
	FLAGS "$@" || exit $?
	eval set -- "${FLAGS_ARGV}"

	# read arguments into global variables
	NAME=$1
	BRANCH=$PREFIX$NAME
}

parse_remote_name() {
	# parse options
	FLAGS "$@" || exit $?
	eval set -- "${FLAGS_ARGV}"

	# read arguments into global variables
	REMOTE=$1
	NAME=$2
	BRANCH=$PREFIX$NAME
}

cmd_start() {
	DEFINE_boolean fetch true 'fetch from $ORIGIN before creating the new branch' F
	parse_args "$@"
	BASE=${2:-$DEVELOP_BRANCH}
	require_name_arg

	# sanity checks
	require_clean_working_tree
	require_remote_available
	if flag fetch ; then
		hubflow_fetch_latest_changes_from_origin
	fi
	require_branch_absent "$ORIGIN/$BRANCH"

	# if the origin branch counterpart exists, assert that the local branch
	# isn't behind it (to avoid unnecessary rebasing)
	if git_branch_exists "$ORIGIN/$DEVELOP_BRANCH"; then
		require_branches_equal "$DEVELOP_BRANCH" "$ORIGIN/$DEVELOP_BRANCH"
	fi

	# create branch
	if ! git checkout -b "$BRANCH" "$BASE"; then
		die "Could not create feature branch '$BRANCH'"
	fi

	# push it back up to remote repo
	hubflow_push_latest_changes_to_origin

	echo
	echo "Summary of actions:"
	echo "- A new branch '$BRANCH' was created, based on '$BASE'"
	echo "- The branch '$BRANCH' has been pushed up to '$ORIGIN/$BRANCH'"
	echo "- You are now on branch '$BRANCH'"
	echo ""
	echo "Now, start committing on your feature. When done, create a"
	echo "pull request on GitHub.  Once that has been merged, use:"
	echo ""
	echo "     git hf feature finish $NAME"
	echo
}

cmd_submit() {
	parse_args "$@"
    BASE=${2:-$DEVELOP_BRANCH}
	name_or_current

	# sanity checks
	require_branch "$BRANCH"
	require_clean_working_tree
    require_github_origin_repo
	require_remote_available
	hubflow_fetch_latest_changes_from_origin

	# push to origin
	hubflow_push_latest_changes_to_origin

	# pull request details
	PR_FILE="./COMMIT_MSG"
	PR_TITLE=
	PR_DESC=

	rm -f "$PR_FILE"

	# ask the user for a pull request description
	cat <<EOS > "$PR_FILE"

# Please enter the description for your pull request. Lines starting
# with '#' will be ignored, and an empty message aborts the request.
#
# The first line should be a short feature summary, no longer than
# 72 characters.
#
# The subsequent lines should be a longer description of the feature,
# including a summary of backwards-compatibility breaks, wrapped at
# 80 characters.
EOS
	${VISUAL:-${EDITOR:-vi}} "$PR_FILE"

	# extract pull request parameters from description
	if [ -r "$PR_FILE" ]; then
		PR_TITLE=$(head -n1 "$PR_FILE" | sed -e 's/^[[:space:]]+//' -e 's/"/\\"/g;' )
		PR_DESC=$(tail -n+2 "$PR_FILE" | grep -v '^#' | sed -e 's/"/\\"/g;' | tr '\n' '\000' | sed -e 's/\x00/\\n/g;' )
	fi

	# ensure there's an adequate description
	if [ -z "$PR_TITLE" ]; then
		die "Aborting submission due to empty summary."
	elif [ -z "$PR_DESC" ]; then
		warn "You have left the description empty; the review may decide to reject your"
		warn "pull request because of this."
	fi

	# submit pull request to GitHub and
    resp=$(github_post \
         "/repos/$GITHUB_ORIGIN/pulls" \
         "{\"title\":\"$PR_TITLE\",\"body\":\"$PR_DESC\",\"head\":\"$BRANCH\",\"base\":\"$BASE\"}")

	# did it succeed?
	if echo "$resp" | grep "Validation Failed" > /dev/null ; then
		# no, it did not
		if echo "$resp" | grep "pull request already exists" > /dev/null ; then
			die "A pull request already exists for this feature"
		elif echo "$resp" | grep "No commits between" > /dev/null ; then
			die "You need to make some commits for this feature before you can make a pull request"
		else
			warn "An unexpected error was returned from GitHub. Here is the raw response:"
			warn
			echo "$resp"
			exit 1
		fi
	fi

	# parse Pull Request URL from response
        PR_URL=$(echo $resp |
            awk -F"," '{for(i=1;i<=NF;i++){if($i~/html_url/){print $i"\n"}}}' |
            grep 'pull' |
            awk -F"\":" '{print $2}' |
            awk -F"\"" '{print $2}')

	if [ -z "$PR_URL" ]; then
		die "Failed to create Pull Request"
	fi

	echo
	echo "Summary of actions:"
	echo "- The branch '$BRANCH' was pushed to '$ORIGIN'"
	echo "- A Pull Request from '$BRANCH' to '$BASE' was created at '$PR_URL'"
	echo ""
	echo "Once the Pull Request has been accepted, cleanup the feature with:"
	echo ""
	echo "     git hf feature finish $NAME"
	echo
}

cmd_finish() {
	DEFINE_boolean fetch true "fetch from $ORIGIN before performing finish" F
	DEFINE_boolean rebase false "rebase instead of merge" r
	DEFINE_boolean keep false "keep branch after performing finish" k
	DEFINE_boolean force_delete false "force delete feature branch after finish" D
	DEFINE_boolean force_merge false "force merge of feature branch if not merged yet at origin" f
	parse_args "$@"
	expand_nameprefix_arg_or_current

	# sanity checks
	require_branch "$BRANCH"
	require_clean_working_tree
	require_remote_available

	# update local repo with remote changes first, if asked
	if flag fetch; then
		# fetch and merge the latest changes from origin
		hubflow_merge_latest_changes_from_origin
	fi

	if has "$ORIGIN/$BRANCH" $(git_remote_branches); then
		require_branches_equal "$BRANCH" "$ORIGIN/$BRANCH"
	fi
	if has "$ORIGIN/$DEVELOP_BRANCH" $(git_remote_branches); then
		require_branches_equal "$DEVELOP_BRANCH" "$ORIGIN/$DEVELOP_BRANCH"
	fi

	# make sure that the feature branch has been merged into develop
	if noflag force_merge ; then
		if [[ $(git rev-list -n2 "$DEVELOP_BRANCH..$BRANCH") ]] ; then
			echo
			echo "Feature branch has not yet been merged into $ORIGIN/$DEVELOP_BRANCH."
			echo "Please raise a pull-request via GitHub first, or use the -f flag."
			exit 1
		fi
	fi

	# if the user wants to rebase, do that first
	if flag rebase; then
		if ! git hf feature rebase "$NAME" "$DEVELOP_BRANCH"; then
			warn "Finish was aborted due to conflicts during rebase."
			warn "Please finish the rebase manually now."
			warn "When finished, re-run:"
			warn "    git hf feature finish '$NAME' '$DEVELOP_BRANCH'"
			exit 1
		fi
	fi

	# merge into the develop branch
	# the merge_helper will not return if there is a merge conflict
	# we want to remain on the DEVELOP_BRANCH after the merge is complete
	hubflow_local_merge_helper "$BRANCH" "$DEVELOP_BRANCH" no_checkout_afterwards

	# make sure the merge worked
	if [[ $(git rev-list -n2 "$DEVELOP_BRANCH..$BRANCH") ]] ; then
		die "feature merge failed"
	fi

	# when no merge conflict is detected, just clean up the feature branch
	# delete branch
	if flag fetch; then
		git push "$ORIGIN" ":refs/heads/$BRANCH"
	fi

	# switch to the develop branch
	hubflow_change_branch "$DEVELOP_BRANCH"

	# if we merged locally, push those changes up to origin
	hubflow_push_latest_changes_to_origin

	# delete the local branch if it is no longer needed
	if noflag keep; then
		if flag force_delete; then
			git branch -D "$BRANCH"
		else
			git branch -d "$BRANCH"
		fi
	fi

	echo
	echo "Summary of actions:"
	if flag fetch ; then
		echo "- The latest changes from '$ORIGIN' were merged into '$MASTER_BRANCH' and '$DEVELOP_BRANCH'"
	fi
	echo "- The feature branch '$BRANCH' was merged into '$DEVELOP_BRANCH'"
	if flag keep; then
		echo "- Feature branch '$BRANCH' is still available"
	else
		echo "- Feature branch '$BRANCH' has been removed"
	fi
	if flag fetch; then
		echo "- Feature branch '$ORIGIN/$BRANCH' has been removed"
	fi
	echo "- You are now on branch '$DEVELOP_BRANCH'"
}

cmd_diff() {
	parse_args "$@"

	if [ "$NAME" != "" ]; then
		expand_nameprefix_arg
		BASE=$(git merge-base "$DEVELOP_BRANCH" "$BRANCH")
		git diff "$BASE..$BRANCH"
	else
		if ! git_current_branch | grep -q "^$PREFIX"; then
			die "Not on a feature branch. Name one explicitly."
		fi

		BASE=$(git merge-base "$DEVELOP_BRANCH" HEAD)
		git diff "$BASE"
	fi
}

cmd_checkout() {
	parse_args "$@"

	if [ "$NAME" != "" ]; then
		expand_nameprefix_arg
		git checkout "$BRANCH"
	else
		die "Name a feature branch explicitly."
	fi
}

cmd_co() {
	# Alias for checkout
	cmd_checkout "$@"
}

cmd_rebase() {
	DEFINE_boolean interactive false 'do an interactive rebase' i
	parse_args "$@"
	expand_nameprefix_arg_or_current
	warn "Will try to rebase '$NAME'..."
	require_clean_working_tree
	require_branch "$BRANCH"

	git checkout -q "$BRANCH"
	local OPTS=
	if flag interactive; then
		OPTS="$OPTS -i"
	fi
	git rebase $OPTS "$DEVELOP_BRANCH"
}

avoid_accidental_cross_branch_action() {
	local current_branch=$(git_current_branch)
	if [ "$BRANCH" != "$current_branch" ]; then
		warn "Trying to pull from '$BRANCH' while currently on branch '$current_branch'."
		warn "To avoid unintended merges, hubflow aborted."
		return 1
	fi
	return 0
}

cmd_pull() {
	git hf pull "$@"
}

cmd_push() {
	git hf push "$@"
}

cmd_cancel() {
	DEFINE_boolean fetch true "fetch from $ORIGIN before performing cancel" F
	DEFINE_boolean push true "push to $ORIGIN after performing cancel" p
	DEFINE_boolean keep false "keep branch after performing cancel" k
	DEFINE_boolean force false "safety feature; cannot cancel a feature without this flag" f

	parse_args "$@"
	name_or_current

	# has the user chosen the force flag?
	if noflag force ; then
		warn "To prevent you accidentally cancelling a feature, you _must_ use the -f flag"
		warn "with this command"
		exit 1
	fi

	# sanity checks
	require_branch "$BRANCH"
	require_clean_working_tree
	if flag push ; then
		git push "$ORIGIN" "$BRANCH" || die "Could not push feature branch up to $ORIGIN"
	fi

	# delete the remote branch
	if flag push ; then
		git push "$ORIGIN" :"$BRANCH" || \
			die "Could not delete the remote $BRANCH in $ORIGIN."
	fi

	# delete the local branch
	if noflag keep ; then
		if [ "$BRANCH" = "$(git_current_branch)" ]; then
			git checkout "$DEVELOP_BRANCH"
		fi

		git branch -D "$BRANCH"
	fi

	echo
	echo "Summary of actions:"
	if flag push ; then
		echo "- Latest objects have been fetched from '$ORIGIN'"
	fi
	if flag push ; then
		echo "- Feature branch '$BRANCH' in '$ORIGIN' has been deleted."
	fi
	if flag keep ; then
		echo "- Feature branch '$BRANCH' is still available locally"
	else
		echo "- Feature branch '$BRANCH' has been deleted locally"
	fi
	echo
}
