#!/usr/pkg/bin/bash
#
# Merge a branch to the current branch
# Copyright (c) Petr Baudis, 2005
#
# Takes a parameter identifying the branch to be merged, defaulting
# to 'origin'.
#
# This command merges all changes currently in the given branch to your
# current branch. This can produce a merging commit on your branch sticking
# the two branch together (so-called 'tree merge'). However in case there
# are no changes in your branch that wouldn't be in the remote branch, no
# merge commit is done and commit pointer of your branch is just updated
# to the last commit on the remote branch (so-called 'fast-forward merge').
#
# In case of conflicts being generated by the merge, you have to examine
# the tree (cg-merge will tell you which files contain commits; the commits
# are denoted by rcsmerge-like markers <<<<, ====, and >>>>) and then do
# `cg-commit` yourself. `cg-commit` will know that you are committing a merge
# and will record it properly.
#
# Note that when you are merging remote branches, `cg-merge` will use them
# in the state they are currently at in your repository. If you want to
# fetch the latest changes from the remote repository, use `cg-fetch`. If you
# want to fetch the changes and then merge them to your branch, use the command
# `cg-update`.
#
# Also note that if you have local changes in your tree that you did not
# commit, cg-merge will always preserve them when fast-forwarding. When doing
# a tree merge, it will preserve them if they don't conflict with the merged
# changes, and report an error otherwise. In short, it should do the Right
# Thing (tm), never lose your local changes and never let them mix up with
# the merge.
#
# OPTIONS
# -------
# -b BASE_COMMIT:: Specify the base commit for the merge
#	Parameter specifies the base commit for the merge. Otherwise, the
#	least common ancestor is automatically selected.
#
# -j:: Join current branch with BRANCH_NAME
#	Join the current branch and BRANCH_NAME together. This makes sense
#	when the branches have no common history, meaning they are actually
#	not branches related at all as far as GIT is concerned. Merging such
#	branches might be a user error and you well may be doing something
#	you do not want; but equally likely, you may actually WANT to join
#	the projects together, which is what this option does.
#
# -n:: Disable autocommitting
#	Parameter specifies that you want to have tree merge never
#	autocommitted, but want to review and commit it manually. This will
#	basically make cg-merge always behave like there were conflicts
#	during the merge.
#
# --squash:: Use "squash" merge to record pending commits as a single merge commit
#	"Squash" merge - condense all the to-be-merged commits to a single
#	merge commit. This means "throw away history of the branch I'm
#	merging", essentially like in CVS or SVN, with the same problem -
#	re-merging with that branch later will cause trouble. This is not
#	recommended unless you actually really want to flatten the history
#	of the merged branch, e.g. when merging topical branches to your
#	mainline (you want to have the logical change you developed in
#	a branch as a single "do it" commit instead of a sequence of
#	"do it I", "fix it", "do it II", "fix it II", "fix it III" commits
#	like you would get with a regular merge).
#
# -v:: Enable verbosity
#	Display more verbose output - most notably list all the files
#	touched by the merged changes.
#
# HOOKS
# -----
# '.git/hooks/merge-pre' BRANCH BASE CURHEAD MERGEDHEAD MERGETYPE::
#	If the file exists and is executable it will be executed right
#	before the merge itself happens. The merge is cancelled if the script
#	returns non-zero exit code.
#	 - MERGETYPE is either "forward", "squash", or "tree".
#
# '.git/hooks/merge-post' BRANCH BASE CURHEAD MERGEDHEAD MERGETYPE STATUS::
#	If the file exists and is executable it will be executed after
#	the merge is done.
#	 - MERGETYPE is either "forward", "squash", or "tree".
#	 - For 'forward', the STATUS is always "ok", while for "squash"
#	   and "tree" the STATUS can be "localchanges", "conflicts",
#	   "nocommit", or "ok".

# Developer's documentation:
#
# ENVIRONMENT
# -----------
# _cg_orig_head::
#	The original commit ID of the to-be-merged branch, if cg-merge
#	is called right after fetch. This is used to do better decision
#	about whether to fast-forward or tree-merge.

# Testsuite: Largely covered (t92xx testsuite family, incomplete coverage;
# missing: hooks, -n, -b, --squash)

USAGE="cg-merge [-n] [-b BASE_COMMIT] [-j] [--squash] [-v] [BRANCH_NAME]"
_git_requires_root=1

. "${COGITO_LIB:-/usr/pkg/lib/cogito/}"cg-Xlib || exit 1


prehook()
{
	if [ -x "$_git/hooks/merge-pre" ]; then
		"$_git/hooks/merge-pre" "$branchname" "$base" "$head" "$branch" "$@" || die "merge cancelled by hook"
	fi
}

posthook()
{
	if [ -x "$_git/hooks/merge-post" ]; then
		"$_git/hooks/merge-post" "$branchname" "$base" "$head" "$branch" "$@"
	fi
}


head="$(cg-object-id -c)" || exit 1


careful=
base=
join=
squash=
verbose=
while optparse; do
	if optparse -n; then
		careful=1
	elif optparse -c; then
		warn "cg-merge -c is deprecated, cg-merge -n is the new flag name"
		careful=1
	elif optparse -b=; then
		base="$(cg-object-id -c "$OPTARG")" || exit 1
	elif optparse -j; then
		join=1
	# -s reserved to strategy
	elif optparse --squash; then
		squash=1
	elif optparse -v; then
		verbose=1
	else
		optfail
	fi
done

branchname="${ARGS[0]}"
[ "$branchname" ] || branchname="$(choose_origin branches "what to merge?")" || exit 1
branch=$(cg-object-id -c "$branchname") || exit 1

[ "$base" ] || base="$(git-merge-base --all "$head" "$branch")"
if [ ! "$join" ]; then
	[ "$base" ] || die "unable to automatically determine merge base (consider cg-merge -j)"
	baselist=($base)
	if [ "${#baselist[@]}" -gt "1" ]; then
		echo "Multiple merge base candidates, please select one manually (by running cg-merge -b BASE [BRANCH]):"
		echo "${baselist[*]}" | tr ' ' '\n'
		echo
		conservative_merge_base "${baselist[@]}" # -> _cg_baselist
		echo -n "The most conservative base (but likely a lot of conflicts): "
		echo "${_cg_baselist[*]}"
		exit 3
	fi >&2

else
	[ "$base" ] && die "joining branches with common history is something I refuse to do"
	index="$(mktemp -t gitmerge.XXXXXX)" || exit $?
	GIT_INDEX_FILE="$index" git-read-tree
	base="$(GIT_INDEX_FILE="$index" git-write-tree)"
	rm "$index"
fi


[ -s "$_git/blocked" ] && die "merge blocked: $(cat "$_git/blocked")"

if [ -s "$_git/merging" ] && /usr/bin/grep -q "$branch" "$_git/merging"; then
	echo "Branch already merged in the working tree." >&2
	exit 0
fi

if [ "$base" = "$branch" ]; then
	echo "Branch already fully merged." >&2
	exit 0
fi

if { [ "$head" = "$base" ] || [ "$head" = "$_cg_orig_head" ]; } && [ ! "$squash" ] && [ ! -s "$_git/merging" ]; then
	# No need to do explicit merge with a merge commit; just bring
	# the HEAD forward.

	echo "Fast-forwarding $base -> $branch" >&2
	echo -e "\ton top of $head ..." >&2

	[ "$verbose" ] && git-diff-tree --abbrev -r "$(cg-object-id -t "$head")" "$(cg-object-id -t "$branch")"

	prehook forward
	tree_timewarp "forward" "yes, rollback (or rather rollforth) the tree!" "$head" "$branch"
	posthook forward ok

	exit 0
fi


git-update-index --refresh >/dev/null

if [ ! "$squash" ]; then
	[ -s "$_git/squashing" ] && die "cannot combine squashing and non-squashing merges"

	echo "Merging $base -> $branch" >&2
	echo -e "\tto $head ..." >&2

	mergetype="tree"
else
	echo "Squashing $base -> $branch" >&2
	echo -e "\ton top of $head ..." >&2

	mergetype="squash"
fi

[ "$verbose" ] && git-diff-tree --abbrev -r "$(cg-object-id -t "$base")" "$(cg-object-id -t "$branch")"

prehook "$mergetype"

git-diff-index --name-only "$(cg-object-id -t $head)" >>"$_git/commit-ignore"
# Don't keep around useless empty files
[ -s "$_git/commit-ignore" ] || rm "$_git/commit-ignore"

if ! git-read-tree -u -m "$(cg-object-id -t "$base")" "$(cg-object-id -t "$head")" "$(cg-object-id -t "$branch")"; then
	echo "cg-merge: git-read-tree failed (merge likely blocked by local changes)" >&2
	posthook "$mergetype" localchanges
	rm -f "$_git/commit-ignore"
	exit 1
fi

echo "$base" >>"$_git/merge-base"
echo "$branch" >>"$_git/merging"
echo "$branchname" >>"$_git/merging-sym"
[ "$squash" ] && echo "$branch" >>"$_git/squashing"

if ! git-merge-index -o -q "${COGITO_LIB:-/usr/pkg/lib/cogito/}"cg-Xmergefile -a || [ "$careful" ]; then
	echo >&2
	if [ ! "$careful" ]; then
		echo "	Conflicts during merge. Do cg-commit after resolving them." >&2
	else
		echo "	Do cg-commit after reviewing the merge." >&2
	fi
	if [ -s "$_git/commit-ignore" ]; then
		echo "	cg-reset will cancel the merge (but also your pending local changes!)." >&2
		echo >&2
		echo "	These files contained local modifications and won't be automatically chosen for committing:" >&2
		cat "$_git/commit-ignore" >&2
	else
		echo "	cg-reset will cancel the merge." >&2
		echo >&2
	fi
	posthook "$mergetype" conflicts
	exit 2
fi

echo
readtree=
if ! cg-commit -C; then
	readtree=1
	echo "cg-merge: COMMIT FAILED, retry manually" >&2
	if [ -s "$_git/commit-ignore" ]; then
		echo "	cg-reset will cancel the merge (but also your pending local changes!)." >&2
	else
		echo "	cg-reset will cancel the merge." >&2
	fi
	posthook "$mergetype" nocommit
fi

[ "$readtree" ] && git-read-tree -m HEAD
# update_index here is safe because no tree<->index desyncs could've
# survived the read-tree above
update_index

posthook "$mergetype" ok
