#!/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"

. ${COGITO_LIB}cg-Xlib


reverse=
if [ "$1" = "-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 | {
	victim=
	redzone=
	origmode=
	newmode=

	while read sign file attrs; do
		if [ "$sign $file" = "diff --git" ]; then
			redzone=1
			origmode=
			newmode=
			continue
		fi
		if [ "$redzone" ] && (echo $sign | grep -q '^[nod]'); then
			mode=$(echo "$sign $file $attrs" | awk '
				/^deleted file mode [0-9]+/ {print "-"$4}
				/^old mode [0-9]+/ {print "-"$3}
				/^new file mode [0-9]+/ {print "+"$4}
				/^new mode [0-9]+/ {print "+"$3}
			')
			if [ "${mode:0:1}" = "-" ]; then
				origmode=${mode:1}
			elif [ "${mode:0:1}" = "+" ]; then
				newmode=${mode:1}
			fi
			continue
		fi
		case $sign in
		"---")
			victim=$file
			mode=$(echo $attrs | sed 's/.*mode:[0-7]*\([0-7]\{3\}\).*/\1/')
			if [ "$mode" ] && [ "$mode" != "$attrs" ]; then
				origmode=$mode
			elif ! [ "$redzone" ]; then
				origmode=
			fi
			;;
		"+++")
			if [ "$file" = "/dev/null" ]; then
				torm=$(echo "$victim" | sed 's/[^\/]*\///') #-p1
				if ! [ "$reverse" ]; then
					(git-ls-files | fgrep -qx "$torm") && echo -ne "rm\0$torm\0"
					continue
				else
					(git-ls-files | fgrep -qx "$torm") || echo -ne "add\0$torm\0"
				fi
			elif [ "$victim" = "/dev/null" ]; then
				toadd=$(echo "$file" | 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"
					continue
				fi
			fi
			mode=$(echo $attrs | sed 's/.*mode:[0-7]*\([0-7]\{3\}\).*/\1/')
			if [ "$mode" ] && [ "$mode" != "$attrs" ]; then
				newmode=$mode
			elif ! [ "$redzone" ]; then
				newmode=
			fi
			if [ "$origmode" != "$newmode" ]; then
				if ! [ "$reverse" ]; then
					tocm=$(echo "$file" | sed 's/[^\/]*\///') #-p1
					mode="$newmode"
				else
					tocm=$(echo "$victim" | sed 's/[^\/]*\///') #-p1
					mode="$oldmode"
				fi
				echo -ne "cm\0 $mode\000$tocm\0"
			fi
			;;
		esac
	done
} >$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
