#!/usr/bin/env bash
#
# Apply a diff generated by cg-diff.
# Copyright (c) Petr Baudis, 2005
#
# This is basically just a smart patch wrapper. It handles stuff like
# mode changes, removal of files vs. zero-size files etc.
#
# For now the script can process the old-style cg-diff patches (those
# having the "(mode: 0123)" stuff on the +++ and --- lines) as well
# as the new-style git-diff patches (those with the "diff --git" lines).
#
# OPTIONS
# -------
# -R::
#	Applies the patch in reverse (therefore effectively unapplies it)
#
# Takes the diff on stdin.

USAGE="cg-patch [-R] < patch on stdin"
_git_requires_root=1

. ${COGITO_LIB}cg-Xlib || exit 1

redzone_reset()
{
	redzone=
	origmode=
	newmode=
	op=
}

redzone_border()
{
	[ "$redzone" ] || return

	if [ "$op" = "delete" ]; then
		torm=$(echo "$file1" | sed 's/[^\/]*\///') #-p1
		if ! [ "$reverse" ]; then
			(git-ls-files | fgrep -qx "$torm") && echo -ne "rm\0$torm\0"
			redzone_reset
			return
		else
			(git-ls-files | fgrep -qx "$torm") || echo -ne "add\0$torm\0"
		fi
	elif [ "$op" = "add" ]; then
		toadd=$(echo "$file2" | sed 's/^[^\/]*\///') #-p1
		if ! [ "$reverse" ]; then
			(git-ls-files | fgrep -qx "$toadd") || echo -ne "add\0$toadd\0"
		else
			(git-ls-files | fgrep -qx "$toadd") && echo -ne "rm\0$toadd\0"
			redzone_reset
			return
		fi
	fi

	if [ "$origmode" != "$newmode" ]; then
		if ! [ "$reverse" ]; then
			tocm=$(echo "$file2" | sed 's/[^\/]*\///') #-p1
			mode="$newmode"
		else
			tocm=$(echo "$file1" | sed 's/[^\/]*\///') #-p1
			mode="$origmode"
		fi
		echo -ne "cm\0 $mode\000$tocm\0"
	fi

	redzone_reset
}


reverse=
if [ "${ARGS[0]}" = "-R" ]; then
	reverse=1
	shift
fi


gonefile=$(mktemp -t gitapply.XXXXXX)
todo=$(mktemp -t gitapply.XXXXXX)
patchfifo=$(mktemp -t gitapply.XXXXXX)
rm $patchfifo && mkfifo -m 600 $patchfifo

git-ls-files --deleted >$gonefile

# patch file removal behaviour cannot be sensibly controlled, so we
# just handle it all ourselves.
patch_args="-p1 -N"
[ "$reverse" ] && patch_args="$patch_args -R"
patch $patch_args <$patchfifo &

tee $patchfifo | {
	redzone_reset

	while read -r line; do
		if [ "${line:0:10}" = "diff --git" ]; then
			redzone_border

			cmd="$(echo "$line" | sed 's/^diff --git //')"
			file1="$(bash -c 'echo $1' padding $cmd)"
			file2="$(bash -c 'echo $2' padding $cmd)"

			redzone=1
			continue
		fi

		if [ "$redzone" ] && [ "${line#[nod]}" != "$line" ]; then
			mode=$(echo "$line" | awk '
				/^deleted file mode [0-9]+/ {print "D-"$4}
				/^old mode [0-9]+/ {print "-"$3}
				/^new file mode [0-9]+/ {print "A+"$4}
				/^new mode [0-9]+/ {print "+"$3}
			')
			if [ "${mode:0:1}" = "D" ]; then
				op=delete
				mode=${mode:1}
			elif [ "${mode:0:1}" = "A" ]; then
				op=add
				mode=${mode:1}
			fi
			if [ "${mode:0:1}" = "-" ]; then
				origmode=${mode:1}
			elif [ "${mode:0:1}" = "+" ]; then
				newmode=${mode:1}
			fi
			continue
		fi
	done
	redzone_border
} >$todo

wait

# Now we just recreate all the supposedly deleted files and
# kill only those who really are gone.
#
# This is done on the assumption that we are never going to have
# too many files deleted in the first place anyway.

touchfiles="$(git-ls-files --deleted | join -v 2 $gonefile -)"
[ "$touchfiles" ] && touch $touchfiles

cat $todo | xargs -0 bash -c '
while [ "$1" ]; do
	op="$1"; shift;
	case "$op" in
	"add") cg-add "$1"; shift;;
	"rm")  rm -- "$1" && cg-rm "$1"; shift;;
	"cm")
		mode=$1; shift
		# $mode contains leading space due to echo braindamage
		if [ "${mode:(-3):1}" = "7" ]; then
			mask=$(printf %o $((8#777&~8#$(umask))))
		else
			mask=$(printf %o $((8#666&~8#$(umask))))
		fi
		chmod "$mask" "$1"; shift;;
	esac
done
' padding

rm $patchfifo $todo $gonefile
