#!/bin/sh
#
# spatchcache: a poor-man's "ccache"-alike for "spatch" in git.git
#
# This caching command relies on the peculiarities of the Makefile
# driving "spatch" in git.git, in particular if we invoke:
#
#	make
#	# See "spatchCache.cacheWhenStderr" for why "--very-quiet" is
#	# used
#	make coccicheck SPATCH_FLAGS=--very-quiet
#
# We can with COMPUTE_HEADER_DEPENDENCIES (auto-detected as true with
# "gcc" and "clang") write e.g. a .depend/grep.o.d for grep.c, when we
# compile grep.o.
#
# The .depend/grep.o.d will have the full header dependency tree of
# grep.c, and we can thus cache the output of "spatch" by:
#
#	1. Hashing all of those files
#	2. Hashing our source file, and the *.cocci rule we're
#	   applying
#	3. Running spatch, if suggests no changes (by far the common
#	   case) we invoke "spatchCache.getCmd" and
#	   "spatchCache.setCmd" with a hash SHA-256 to ask "does this
#	   ID have no changes" or "say that ID had no changes>
#	4. If no "spatchCache.{set,get}Cmd" is specified we'll use
#	   "redis-cli" and maintain a SET called "spatch-cache". Set
#	   appropriate redis memory policies to keep it from growing
#	   out of control.
#
# This along with the general incremental "make" support for
# "tools/coccinelle" makes it viable to (re-)run coccicheck
# e.g. when merging integration branches.
#
# Note that the "--very-quiet" flag is currently critical. The cache
# will refuse to cache anything that has output on STDERR (which might
# be errors from spatch), but see spatchCache.cacheWhenStderr below.
#
# The STDERR (and exit code) could in principle be cached (as with
# ccache), but then the simple structure in the Redis cache would need
# to change, so just supply "--very-quiet" for now.
#
# To use this, simply set SPATCH to
# tools/coccinelle/spatchcache. Then optionally set:
#
#	[spatchCache]
#		# Optional: path to a custom spatch
#		spatch = ~/g/coccicheck/spatch.opt
#
# As well as this trace config (debug implies trace):
#
#		cacheWhenStderr = true
#		trace = false
#		debug = false
#
# The ".depend/grep.o.d" can also be customized, as a string that will
# be eval'd, it has access to a "$dirname" and "$basename":
#
#	[spatchCache]
#		dependFormat = "$dirname/.depend/${basename%.c}.o.d"
#
# Setting "trace" to "true" allows for seeing when we have a cache HIT
# or MISS. To debug whether the cache is working do that, and run e.g.:
#
#	redis-cli FLUSHALL
#	<make && make coccicheck, as above>
#	grep -hore HIT -e MISS -e SET -e NOCACHE -e CANTCACHE .build/tools/coccinelle | sort | uniq -c
#	    600 CANTCACHE
#	   7365 MISS
#	   7365 SET
#
# A subsequent "make cocciclean && make coccicheck" should then have
# all "HIT"'s and "CANTCACHE"'s.
#
# The "spatchCache.cacheWhenStderr" option is critical when using
# spatchCache.{trace,debug} to debug whether something is set in the
# cache, as we'll write to the spatch logs in .build/* we'd otherwise
# always emit a NOCACHE.
#
# Reading the config can make the command much slower, to work around
# this the config can be set in the environment, with environment
# variable name corresponding to the config key. "default" can be used
# to use whatever's the script default, e.g. setting
# spatchCache.cacheWhenStderr=true and deferring to the defaults for
# the rest is:
#
#	export GIT_CONTRIB_SPATCHCACHE_DEBUG=default
#	export GIT_CONTRIB_SPATCHCACHE_TRACE=default
#	export GIT_CONTRIB_SPATCHCACHE_CACHEWHENSTDERR=true
#	export GIT_CONTRIB_SPATCHCACHE_SPATCH=default
#	export GIT_CONTRIB_SPATCHCACHE_DEPENDFORMAT=default
#	export GIT_CONTRIB_SPATCHCACHE_SETCMD=default
#	export GIT_CONTRIB_SPATCHCACHE_GETCMD=default

set -e

env_or_config () {
	env="$1"
	shift
	if test "$env" = "default"
	then
		# Avoid expensive "git config" invocation
		return
	elif test -n "$env"
	then
		echo "$env"
	else
		git config $@ || :
	fi
}

## Our own configuration & options
debug=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_DEBUG" --bool "spatchCache.debug")
if test "$debug" != "true"
then
	debug=
fi
if test -n "$debug"
then
	set -x
fi

trace=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_TRACE" --bool "spatchCache.trace")
if test "$trace" != "true"
then
	trace=
fi
if test -n "$debug"
then
	# debug implies trace
	trace=true
fi

cacheWhenStderr=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_CACHEWHENSTDERR" --bool "spatchCache.cacheWhenStderr")
if test "$cacheWhenStderr" != "true"
then
	cacheWhenStderr=
fi

trace_it () {
	if test -z "$trace"
	then
		return
	fi
	echo "$@" >&2
}

spatch=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_SPATCH" --path "spatchCache.spatch")
if test -n "$spatch"
then
	if test -n "$debug"
	then
		trace_it "custom spatchCache.spatch='$spatch'"
	fi
else
	spatch=spatch
fi

dependFormat='$dirname/.depend/${basename%.c}.o.d'
dependFormatCfg=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_DEPENDFORMAT" "spatchCache.dependFormat")
if test -n "$dependFormatCfg"
then
	dependFormat="$dependFormatCfg"
fi

set=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_SETCMD" "spatchCache.setCmd")
get=$(env_or_config "$GIT_CONTRIB_SPATCHCACHE_GETCMD" "spatchCache.getCmd")

## Parse spatch()-like command-line for caching info
arg_sp=
arg_file=
args="$@"
spatch_opts() {
	while test $# != 0
	do
		arg_file="$1"
		case "$1" in
		--sp-file)
			arg_sp="$2"
			;;
		esac
		shift
	done
}
spatch_opts "$@"
if ! test -f "$arg_file"
then
	arg_file=
fi

hash_for_cache() {
	# Parameters that should affect the cache
	echo "args=$args"
	echo "config spatchCache.spatch=$spatch"
	echo "config spatchCache.debug=$debug"
	echo "config spatchCache.trace=$trace"
	echo "config spatchCache.cacheWhenStderr=$cacheWhenStderr"
	echo

	# Our target file and its dependencies
	git hash-object "$1" "$2" $(grep -E -o '^[^:]+:$' "$3" | tr -d ':')
}

# Sanity checks
if ! test -f "$arg_sp" && ! test -f "$arg_file"
then
	echo $0: no idea how to cache "$@" >&2
	exit 128
fi

# Main logic
dirname=$(dirname "$arg_file")
basename=$(basename "$arg_file")
eval "dep=$dependFormat"

if ! test -f "$dep"
then
	trace_it "$0: CANTCACHE have no '$dep' for '$arg_file'!"
	exec "$spatch" "$@"
fi

if test -n "$debug"
then
	trace_it "$0: The full cache input for '$arg_sp' '$arg_file' '$dep'"
	hash_for_cache "$arg_sp" "$arg_file" "$dep" >&2
fi
sum=$(hash_for_cache "$arg_sp" "$arg_file" "$dep" | git hash-object --stdin)

trace_it "$0: processing '$arg_file' with '$arg_sp' rule, and got hash '$sum' for it + '$dep'"

getret=
if test -z "$get"
then
	if test $(redis-cli SISMEMBER spatch-cache "$sum") = 1
	then
		getret=0
	else
		getret=1
	fi
else
	$set "$sum"
	getret=$?
fi

if test "$getret" = 0
then
	trace_it "$0: HIT for '$arg_file' with '$arg_sp'"
	exit 0
else
	trace_it "$0: MISS: for '$arg_file' with '$arg_sp'"
fi

out="$(mktemp)"
err="$(mktemp)"

set +e
"$spatch" "$@" >"$out" 2>>"$err"
ret=$?
cat "$out"
cat "$err" >&2
set -e

nocache=
if test $ret != 0
then
	nocache="exited non-zero: $ret"
elif test -s "$out"
then
	nocache="had patch output"
elif test -z "$cacheWhenStderr" && test -s "$err"
then
	nocache="had stderr (use --very-quiet or spatchCache.cacheWhenStderr=true?)"
fi

if test -n "$nocache"
then
	trace_it "$0: NOCACHE ($nocache): for '$arg_file' with '$arg_sp'"
	exit "$ret"
fi

trace_it "$0: SET: for '$arg_file' with '$arg_sp'"

setret=
if test -z "$set"
then
	if test $(redis-cli SADD spatch-cache "$sum") = 1
	then
		setret=0
	else
		setret=1
	fi
else
	"$set" "$sum"
	setret=$?
fi

if test "$setret" != 0
then
	echo "FAILED to set '$sum' in cache!" >&2
	exit 128
fi

exit "$ret"
