#!/bin/sh
#
# This program uses merge tools to stage and compare commits
#
# Copyright (c) 2008 David Aguilar
#
# Adapted from git-mergetool.sh
# Copyright (c) 2006 Theodore Y. Ts'o
#
# This file is licensed under the GPL v2, or a later version
#
USAGE='
[--tool=tool] [--no-prompt]
[--commit=ref] [--start=ref --end=ref]
[--] [file to view] ...'
SUBDIRECTORY_OK=Yes
OPTIONS_SPEC=
. git-sh-setup
require_work_tree
cd_to_toplevel

keep_backup_mode="$(git config --bool merge.keepBackup || echo true)"
PREFIX=.git-difftool."$$"."$(date +%N)".
FILEDIR=

keep_backup () {
	# whether to keep the .orig file
	test "$keep_backup_mode" = "true"
}

parse_arg () {
	# parses --foo=BAR
	expr "z$1" : 'z-[^=]*=\(.*\)'
}

index_present () {
	#  does the index contain staged changes?
	test -n "$index_mode"
}

modified_present () {
	# are there changes in the requested comparison?
	test -n "$modified_mode"
}

commitish_present () {
	# are we comparing against an arbitrary commit?
	test -n "$commitish"
}

should_prompt () {
	# are we running interactively?
	! test -n "$no_prompt"
}

use_rev_range () {
	# are we using --start=REF and --end=REF
	test -n "$start" && test -n "$end"
}

merge_dir_missing () {
	# does dirname($MERGED) exist in the work tree?
	test -n "$non_existant_dir"
}

base_present () {
	# whether we have 3 things to compare (index, worktree, other)
	index_present && modified_present && ! use_rev_range
}

modified_files () {
	# returns the appopriate list of differences based on the mode
	if use_rev_range; then
		git diff --name-only "$start".."$end" -- "$@" 2>/dev/null
	elif commitish_present; then
		git diff --name-only "$commitish" -- "$@" 2>/dev/null
	else
		git diff --name-only -- "$@" 2>/dev/null
	fi
}

staged_files() {
	# returns any staged changes
	git diff --name-only --cached "$@" 2>/dev/null
}

cleanup_temp_files () {
	# removes temporary files
	if test -n "$MERGED"; then
		if keep_backup && test "$MERGED" -nt "$BACKUP"; then
			test -f "$BACKUP" && mv -- "$BACKUP" "$MERGED.orig"
			rm -f -- "$LOCAL" "$REMOTE" "$BASE"
		else
			rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP"
		fi
		test -n "$MERGEDIR" && rmdir -p "$MERGEDIR"
	fi
}

sigint_handler () {
	echo
	cleanup_temp_files
	exit 1
}

merge_file () {
	# prepares temporary files and launches the appropriate merge tool
	MERGED="$1"

	modified_mode=$(modified_files "$MERGED")
	index_mode=$(staged_files "$MERGED")

	if ! modified_present && use_rev_range; then
		echo "$MERGED: no changes between '$start' and '$end'."
		exit 1
	elif ! modified_present && ! index_present; then
		if ! test -f "$MERGED"; then
			echo "$MERGED: file not found"
		else
			echo "$MERGED: file is unchanged."
		fi
		exit 1
	fi

	# handle comparing a file that doesn't exist in the current checkout
	MERGEDIR=$(dirname "$MERGED")
	non_existant_dir=
	test -d "$MERGEDIR" || non_existant_dir=true
	if merge_dir_missing; then
		mkdir -p -- "$MERGEDIR"
	else
		MERGEDIR=
	fi

	ext="$$$(expr "$MERGED" : '.*\(\.[^/]*\)$')"
	BACKUP="./$MERGED.BACKUP.$ext"
	test -f "$MERGED" && cp -- "$MERGED" "$BACKUP"

	if use_rev_range; then
		# we're comparing two arbitrary commits
		BASE="./$MERGED.CURRENT.$ext"
		LOCAL="./$MERGED.START.$ext"
		REMOTE="./$MERGED.END.$ext"
		base=current
		local=start
		remote=end
		touch "$BASE"
		touch "$LOCAL"
		touch "$REMOTE"

		# detects renames.. sweet!
		oldname=$(git diff --follow "$start".."$end" -- "$MERGED" |
			head -n9 |
			grep '^rename from' |
			sed -e 's/rename from //')
		startname="$MERGED"
		test -n "$oldname" && startname="$oldname"

		if ! git show "$start":"$startname" > "$LOCAL" 2>/dev/null; then
			if should_prompt; then
				printf "\nWarning: "
				printf "'$startname' does not exist at $start.\n"
			fi
		else
			cp -- "$LOCAL" "$BASE"
		fi
		if ! git show "$end":"$MERGED" > "$REMOTE" 2>/dev/null; then
			if should_prompt; then
				printf "\nWarning: "
				printf "'$MERGED' does not exist at $end.\n"
			fi
		else
			! test -f "$BASE" && cp -- "$REMOTE" "$BASE"
		fi

		# $BASE could be used by custom mergetool commands
		if test -f "$MERGED"; then
			cp -- "$MERGED" "$BASE"
		fi
	else
		# We're either comparing against the index or an
		# arbitrary commit.
		# The goal is to re-use existing mergetool.$tool.cmd
		# configurations so we provide $BASE $LOCAL and $REMOTE
		if commitish_present; then
			HEAD=OTHER
			local=Other
		else
			HEAD=HEAD
			local=Index
		fi
		base=${commitish-HEAD}
		remote=Current
		BASE="./$MERGED.$HEAD.$ext"
		LOCAL="./$MERGED.INDEX.$ext"
		REMOTE="./$MERGED.CURRENT.$ext"
		touch "$BASE"
		touch "$LOCAL"
		touch "$REMOTE"
		if ! git show "$base":"$MERGED" > "$BASE" 2>&1; then
			printf "\nWarning: "
			printf "'$MERGED' does not exist at $base.\n"
		else
			cp "$BASE" "$LOCAL"
			if commitish_present; then
				rm -f "$LOCAL"
				LOCAL="$BASE"
			fi
		fi
		# If changes are present in the index use them as $LOCAL
		# but only if $MERGED exists at $base
		if ! commitish_present; then
			git checkout-index --prefix="$PREFIX" "$MERGED"
			if test -f "$PREFIX""$MERGED"; then
				mv -- "$PREFIX""$MERGED" "$LOCAL"
				tmpdir=$(dirname "$PREFIX""$MERGED")
				test -d "$tmpdir" &&
				test "$tmpdir" != "." &&
				rmdir -p "$tmpdir"
			else
				index_mode=
			fi
		else
			index_mode=
		fi
		if test -f "$MERGED"; then
			cp -- "$MERGED" "$REMOTE"
		fi
	fi

	# ensure that we clean up after ourselves
	trap sigint_handler SIGINT

	if should_prompt; then
		printf "\nEditing: '$MERGED'\n"
		printf "Hit return to launch '%s': " "$merge_tool"
		read ans
	fi

	case "$merge_tool" in
	kdiff3)
		basename=$(basename "$MERGED")
		if base_present; then
		(
			"$merge_tool_path" --auto \
				--L1 "($base) $basename" \
				--L2 "($local) $basename" \
				--L3 "($remote) $basename" \
				-o "$MERGED" "$BASE" "$LOCAL" "$REMOTE" \
				> /dev/null 2>&1
		)
		else
		(
			"$merge_tool_path" --auto \
				--L1 "($local) $basename" \
				--L2 "($remote) $basename" \
				-o "$MERGED" "$LOCAL" "$REMOTE" \
				> /dev/null 2>&1
		)
		fi
		;;

	tkdiff)
		if base_present; then
			"$merge_tool_path" \
				-a "$BASE" \
				-o "$MERGED" "$LOCAL" "$REMOTE"
		else
			"$merge_tool_path" \
				-o "$MERGED" "$LOCAL" "$REMOTE"
		fi
		;;

	meld)
		"$merge_tool_path" "$LOCAL" "$MERGED"
		;;

	vimdiff)
		if base_present; then
			"$merge_tool_path" "$BASE" "$LOCAL" "$MERGED"
		else
			"$merge_tool_path" "$LOCAL" "$MERGED"
		fi
		;;

	gvimdiff)
		if base_present; then
			"$merge_tool_path" -f "$BASE" "$LOCAL" "$MERGED"
		else
			"$merge_tool_path" -f "$LOCAL" "$MERGED"
		fi
		;;

	xxdiff)
		if base_present; then
			"$merge_tool_path" -X --show-merged-pane \
				-R 'Accel.SaveAsMerged: "Ctrl-S"' \
				-R 'Accel.Search: "Ctrl+F"' \
				-R 'Accel.SearchForward: "Ctrl-G"' \
				--merged-file "$MERGED" \
				"$BASE" "$LOCAL" "$REMOTE"
		else
			"$merge_tool_path" -X --show-merged-pane \
				-R 'Accel.SaveAsMerged: "Ctrl-S"' \
				-R 'Accel.Search: "Ctrl+F"' \
				-R 'Accel.SearchForward: "Ctrl-G"' \
				--merged-file "$MERGED" \
				"$LOCAL" "$REMOTE"
		fi
		;;

	opendiff)
		if base_present; then
			"$merge_tool_path" "$LOCAL" "$REMOTE" \
				-ancestor "$BASE" \
				-merge "$MERGED" | cat
		else
			"$merge_tool_path" "$LOCAL" "$REMOTE" \
				-merge "$MERGED" | cat
		fi
		;;

	ecmerge)
		if base_present; then
			"$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" \
				--default --mode=merge3 --to="$MERGED"
		else
			"$merge_tool_path" "$LOCAL" "$REMOTE" \
				--default --mode=merge2 --to="$MERGED"
		fi
		;;

	emerge)
		if base_present; then
			"$merge_tool_path" \
				-f emerge-files-with-ancestor-command \
				"$LOCAL" "$REMOTE" "$BASE" \
				"$(basename "$MERGED")"
		else
			"$merge_tool_path" -f emerge-files-command \
				"$LOCAL" "$REMOTE" "$(basename "$MERGED")"
		fi
		;;
	*)
		if test -n "$merge_tool_cmd"; then
			if test "$merge_tool_trust_exit_code" = "false"; then
				( eval $merge_tool_cmd )
			else
				( eval $merge_tool_cmd )
			fi
		fi
		;;
	esac
	cleanup_temp_files
}

while test $# != 0
do
	case "$1" in
	-t|--tool*)
		case "$#,$1" in
		*,*=*)
			merge_tool=$(parse_arg "$1")
			shift
			;;
		1,*)
			usage
			;;
		*)
			shift
			merge_tool="$1"
			shift
			;;
		esac
		;;
	-c|--commit*)
		case "$#,$1" in
		*,*=*)
			commitish=$(parse_arg "$1")
			shift
			;;
		1,*)
			usage
			;;
		*)
			shift
			commitish="$1"
			shift
			;;
		esac
		;;
	-s|--start*)
		case "$#,$1" in
		*,*=*)
			start=$(parse_arg "$1")
			shift
			;;
		1,*)
			usage
			;;
		*)
			shift
			start="$1"
			shift
			;;
		esac
		;;
	-e|--end*)
		case "$#,$1" in
		*,*=*)
			end=$(parse_arg "$1")
			shift
			;;
		1,*)
			usage
			;;
		*)
			shift
			end="$1"
			shift
			;;
		esac
		;;
	--no-prompt)
		no_prompt=true
		shift
		;;
	--)
		shift
		break
		;;
	-*)
		usage
		;;
	*)
		break
		;;
	esac
done

valid_custom_tool() {
	merge_tool_cmd="$(git config mergetool.$1.cmd)"
	test -n "$merge_tool_cmd"
}

valid_tool() {
	case "$1" in
	kdiff3 | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
		;; # happy
	*)
		if ! valid_custom_tool "$1"
		then
			return 1
		fi
		;;
	esac
}

init_merge_tool_path() {
	merge_tool_path=$(git config mergetool."$1".path)
	if test -z "$merge_tool_path"; then
		case "$1" in
		emerge)
			merge_tool_path=emacs
			;;
		*)
			merge_tool_path="$1"
			;;
		esac
	fi
}


if test -z "$merge_tool"; then
	merge_tool=$(git config merge.tool)
	if test -n "$merge_tool" && ! valid_tool "$merge_tool"; then
		echo >&2 "git config option merge.tool set to unknown tool: $merge_tool"
		echo >&2 "Resetting to default..."
		unset merge_tool
	fi
fi

if test -z "$merge_tool"; then
	if test -n "$DISPLAY"; then
		merge_tool_candidates="kdiff3 tkdiff xxdiff meld gvimdiff"
		if test -n "$GNOME_DESKTOP_SESSION_ID"; then
			merge_tool_candidates="meld $merge_tool_candidates"
		fi
		if test "$KDE_FULL_SESSION" = "true"; then
			merge_tool_candidates="kdiff3 $merge_tool_candidates"
		fi
	fi

	if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
		merge_tool_candidates="$merge_tool_candidates emerge"
	fi

	if echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
		merge_tool_candidates="$merge_tool_candidates vimdiff"
	fi

	merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
	echo "merge tool candidates: $merge_tool_candidates"

	for i in $merge_tool_candidates
	do
		init_merge_tool_path $i
		if type "$merge_tool_path" > /dev/null 2>&1; then
			merge_tool=$i
			break
		fi
	done

	if test -z "$merge_tool" ; then
		echo "No known merge resolution program available."
		exit 1
	fi

else
	if ! valid_tool "$merge_tool"; then
		echo >&2 "Unknown merge tool $merge_tool"
		exit 1
	fi

	init_merge_tool_path "$merge_tool"

	if test -z "$merge_tool_cmd" && ! type "$merge_tool_path" > /dev/null 2>&1; then
		echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
		exit 1
	fi

	if ! test -z "$merge_tool_cmd"; then
		merge_tool_trust_exit_code="$(git config --bool mergetool.$merge_tool.trustExitCode || echo false)"
	fi
fi


if test $# -eq 0; then
	use_index=0
	files=$(modified_files)

	if test -z "$files"; then
		use_index=1
		files=$(staged_files)
	fi

	if test -z "$files"; then
		echo "No modified files exist."
		exit 1
	fi


	if test $use_index -eq 0; then
		modified_files |
		while IFS= read i
		do
			merge_file "$i" < /dev/tty > /dev/tty
		done
	elif ! use_rev_range; then
		staged_files |
		while IFS= read i
		do
			merge_file "$i" < /dev/tty > /dev/tty
		done
	else
		echo "Nothing to compare."
		exit 1
	fi
else
	while test $# -gt 0
	do
		merge_file "$1"
		shift
	done
fi
exit 0
