#!/bin/sh

. girar-sh-functions

PROG=girar-update

arg_refname="$1"
arg_oldrev="$2"
arg_newrev="$3"

# Safety checks
[ -n "$GIT_DIR" ] || fatal 'Environment variable GIT_DIR not set'
if [ -z "$arg_refname" -o -z "$arg_oldrev" -o -z "$arg_newrev" ]; then
	echo >&2 "Usage: $PROG <ref> <oldrev> <newrev>"
	exit 1
fi

# check ref name
case "$arg_refname" in
refs/heads/*)
	ref_style=head
	ref_name="${1#refs/heads/}"
	;;
refs/tags/*)
	ref_style=tag
	ref_name="${1#refs/tags/}"
	;;
refs/remotes/*)
	ref_style=remote
	ref_name="${1#refs/remotes/}"
	;;
*)
	fatal "Unrecognized ref name: $arg_refname"
	;;
esac

enable -f /usr/lib/bash/lockf lockf || exit 1
builtin lockf -v "$GIT_DIR" || exit 1

if expr "$arg_newrev" : '0*$' >/dev/null; then
	if expr "$arg_oldrev" : '0*$' >/dev/null; then
		fatal "Non-existent ref name: $arg_refname"
	fi
	ref_type="$(git cat-file -t "$arg_oldrev")" || exit 1
else
	ref_type="$(git cat-file -t "$arg_newrev")" || exit 1
	if [ "$arg_refname" = 'refs/heads/origin' ]; then
		fatal "$arg_refname is not designed for publication and therefore is not allowed in this repo"
	fi
fi

# Only allow annotated tags in a shared repo
case "$arg_refname,$ref_type" in
refs/tags/*,tag)
	#echo >&2 "### Pushing $ref_style \`$ref_name' to the masses"
	;;
refs/tags/*,*)
	echo >&2 "*** Un-annotated ${ref_style}s are not allowed in this repo"
	echo >&2 "*** Use \`git tag [ -a | -s ]' for tags you want to propagate."
	exit 1
	;;
esac

DIFF_MAX=32768
DIFF_SIZE=
date_format="%F %T %z"
git_dir="$(readlink -ev "$GIT_DIR")" || exit 1
girar_home="$(readlink -ev "$GIRAR_HOME")" || exit 1
git_dir="$GIRAR_HOME${git_dir#$girar_home}"
project_name="${git_dir##*/}"
project_name="${project_name%.git}"
project_type="${git_dir%/*}"
project_type="${project_type##*/}"
project="$project_type/$project_name"
charset="$(head -n1 $GIT_DIR/charset 2>/dev/null)"
[ -n "$charset" ] ||
	charset='utf-8'

check_etc_packages()
{
	[ "$project" = "etc/packages" -a "$1" = "refs/heads/master" ] ||
		return
}

check_etc_private()
{
	[ "$project" = "etc/private" -a "$1" = "refs/heads/master" ] ||
		return
}

validate_distribution_subscription()
{
	local where="$1"; shift
	local p="$1"; shift
	local t="$1"; shift
	local n="$1"; shift
	local rest="$1"; shift

	[ -z "$rest" ] ||
		fatal "$where: unexpected field: $rest"

	[ "$p" = '*' ] ||
	printf %s "$p" |egrep -qs "$project_name_regexp" ||
	printf %s "${p%\*}" |egrep -qs "$project_name_regexp" ||
		fatal "$where: invalid package pattern: $p"
	case "$t" in
		\*|head|tag|remote) ;;
		*) fatal "$where: invalid reftype pattern: $t" ;;
	esac
	[ "$n" = '*' ] ||
	git check-ref-format "refs/$n" ||
		fatal "$where: invalid refname pattern: $n"
}

validate_email_list()
{
	local where="$1"; shift
	local addr="$1"; shift
	local a="$(printf %s "$addr" |tr , ' ')"
	set -- ${a}
	for a; do
		[ -n "${a##*@*}" ] || continue
		cut -f1 -- "/etc/girar/aliases" |
			fgrep -xqs "${GIRAR_USER_PREFIX}$a:" ||
			fatal "$where: invalid email list: user $a not found."
	done
}

validate_distribution()
{
	[ -s "$workdir/distribution" ] || return 0
	local p t n a rest line=0
	while read -r p t n a rest; do
		line="$((1+$line))"
		[ -n "$a" ] ||
			fatal "email-distribution:$line: insufficient fields."
		validate_distribution_subscription \
			"email-distribution:$line" \
			"$p" "$t" "$n" "$rest"
		validate_email_list "email-distribution:$line" "$a"
	done <"$workdir/distribution"
}

validate_subscription()
{
	[ -s "$workdir/subscription" ] || return 0
	local u p t n rest line=0
	while read -r u p t n rest; do
		line="$((1+$line))"
		[ -n "$n" ] ||
			fatal "email-subscription:$line: insufficient fields."
		validate_distribution_subscription \
			"email-subscription:$line" \
			"$p" "$t" "$n" "$rest"
		[ "$u" = '*' ] ||
			{
				printf %s "$u" |egrep -qs '^[a-z][a-z_0-9]+$' &&
				[ -d "$GIRAR_HOME/$u" ]
			} ||
			fatal "$where: invalid user pattern: $u"
	done <"$workdir/subscription"
}

check_email_update()
{
	local n="$1"; shift
	rm -f -- "$workdir/$n"
	if git diff-tree --name-only "$2" "$3" -- email-"$n" |
	   fgrep -qs email-"$n"; then
		git cat-file blob "$3:email-$n" 2>/dev/null |
			egrep '^[[:space:]]*[^#]' >"$workdir/$n"
	fi
}

handle_etc_packages()
{
	check_etc_packages "$@" || return 0

	check_email_update distribution "$@"
	validate_distribution

	check_email_update subscription "$@"
	validate_subscription

	for n in distribution subscription; do
		if [ -r "$workdir/$n" ]; then
			cp "$workdir/$n" "/var/lib/girar/email/packages/$GIRAR_USER/" &&
				msg_info "email $n configuration updated."
		fi
	done
}

handle_etc_private()
{
	check_etc_private "$@" || return 0

	check_email_update distribution "$@"
	validate_distribution

	[ ! -s "$workdir/distribution" ] ||
		cp "$workdir/distribution" "/var/lib/girar/email/private/$GIRAR_USER/"
}

size_of_file()
{
	stat -c %s -- "$@"
}

diff_is_too_large()
{
	DIFF_SIZE="$(size_of_file "$DIFF")"
	[ "$DIFF_SIZE" -ge "$DIFF_MAX" ]
}

show_large_info()
{
	local text="$1"; shift

	if diff_is_too_large; then
		echo "$text is too large ($DIFF_SIZE bytes)."
	else
		echo "$text follows:"
		cat "$DIFF"
	fi
}

show_changes_statistics()
{
	local next="$1"; shift
	local prev="$1"; shift

	git diff -M --stat "$next" "^$prev" >"$DIFF"
	show_large_info "Changes statistics since $1"
	echo
}

email_new_head()
{
	git rev-parse --not --all |
		git rev-list --stdin --pretty "$3" >"$DIFF"
	if diff_is_too_large; then
		echo "New $ref_style \`$ref_name' is available with too many changes ($DIFF_SIZE bytes)."
	elif [ "$DIFF_SIZE" -eq 0 ]; then
		echo "New $ref_style \`$ref_name' is available without new commits."
	else
		echo "New $ref_style \`$ref_name' is available with the following commits:"
		cat "$DIFF"
	fi
}

email_new_tag()
{
	# a pushed and annotated tag (usually) means a new version
	if [ "$ref_type" = tag ]; then
		local tag="$workdir/tag"
		git cat-file tag "$3" >"$tag"
		local tagger ts date
		tagger="$(sed -ne '4s/^tagger \([^>]\+>\).*/\1/p' <"$tag")"
		ts="$(sed -ne '4s/^tagger [^>]\+>[^0-9]*\([0-9]\+\).*/\1/p' <"$tag")"
		date="$(date --date="1970-01-01 00:00:00 +0000 $ts seconds" +"$date_format")"
		echo "Tag \`$ref_name' created by $tagger at $date"

		git cat-file tag "$3" |sed -n '5,$p' >"$DIFF"
		if diff_is_too_large; then
			echo "with very long message ($DIFF_SIZE bytes)."
		else
			echo "with the following message:"
			cat "$DIFF"
		fi

		echo =======
		echo
	fi

	local prev size
	prev="$(git describe --abbrev=0 "$3^" 2>/dev/null| sed 's/-g[0-9a-f]\{7\}$//')"
	# the first tag in a repo will yield no $prev
	if [ -z "$prev" ]; then
		git rev-list --pretty "$3" >"$DIFF"
		show_large_info "Changelog since the dawn of time"
	else
		show_changes_statistics "$3" "$prev" "\`$prev'"

		git rev-list --pretty "$3" "^$prev" >"$DIFF"
		show_large_info "Changelog since \`$prev'"

		git diff -M "$3" "^$prev" >"$DIFF"
		show_large_info "Full diff since \`$prev'"
	fi
}

email_update_ref()
{
	local base prev size
	base="$(git merge-base "$2" "$3")"
	prev="$(git describe "$base" 2>/dev/null)" || prev="$base"
	if [ -z "$prev" ]; then
		git rev-list --pretty "$3" >"$DIFF"
		show_large_info "Changelog since the dawn of time"
	else
		local since_text
		if [ "$base" = "$2" ]; then
			since_text="\`$prev'"
		else
			since_text="common ancestor \`$prev'"
		fi

		if [ "$base" = "$3" ]; then
			echo "Reset to $since_text."
			return 0
		fi

		show_changes_statistics "$3" "$base" "$since_text"

		git rev-list --pretty "$3" "^$base" >"$DIFF"
		show_large_info "Changelog since $since_text"

		git diff -M "$3" "^$base" >"$DIFF"
		show_large_info "Full diff since $since_text"
	fi
}

mailto="$(
	find-subscribers "$project_name" "$ref_style" "$ref_name" |
	tr -s '\n' , |
	sed -e 's/^,\+//' -e 's/,\+$//' -e 's/,/, /g'
)"
[ -n "$mailto" ] ||
	check_etc_packages "$@" ||
	check_etc_private "$@" ||
	exit 0

exit_handler()
{
	local rc=$?
	trap - EXIT
	rm -rf -- "$workdir"
	exit $rc
}
workdir="$(mktemp -dt git-update.XXXXXXXX)" || exit 1
trap exit_handler HUP PIPE INT QUIT TERM EXIT

if [ -n "$mailto" ]; then
	DIFF="$workdir/diff"
	MSG="$workdir/message"
	from_addr="${GIRAR_USER}@etersoft.ru ($(getent passwd "$USER" |cut -d: -f5))"
	case "$project_type" in
		packages)
			reply_addr="ALT Devel discussion list <devel@lists.etersoft.ru>"
			;;
		*)
			reply_addr="$from_addr"
			;;
	esac
	gitweb_url="http://git.etersoft.ru${git_dir%/*}/?p=${git_dir##*/};a=$ref_type;h=$arg_newrev"

	cat >"$MSG" <<__EOF
To: git-update-subscribers
Bcc: $mailto
From: $from_addr
Reply-To: $reply_addr
Subject: [git update] $project: ${1#refs/}
Content-Type: text/plain${charset:+; charset=$charset}
X-git-update-subscribers: $mailto
X-git-dir: $git_dir
X-git-description: $project
X-git-refstyle: $ref_style
X-git-refname: $arg_refname
X-git-oldrev: $arg_oldrev
X-git-newrev: $arg_newrev
X-git-URL: $gitweb_url

Update of $git_dir

__EOF
fi

if expr "$arg_oldrev" : '0*$' >/dev/null; then
	# new ref
	case "$arg_refname" in
	refs/heads/*|refs/remotes/*)
		# new head
		[ -z "$mailto" ] ||
			email_new_head "$@" >>"$MSG"
		;;
	refs/tags/*)
		# new tag
		[ -z "$mailto" ] ||
			email_new_tag "$@" >>"$MSG"
		;;
	esac
elif expr "$arg_newrev" : '0*$' >/dev/null; then
	# del ref
	[ -z "$mailto" ] ||
		echo "Reference to $ref_style \`$ref_name' removed." >>"$MSG"
else
	# update ref
	handle_etc_packages "$@"
	handle_etc_private "$@"
	[ -z "$mailto" ] ||
		email_update_ref "$@" >>"$MSG"
fi

if [ -n "$mailto" ]; then
	/usr/sbin/sendmail -i -t <"$MSG" &&
		msg_info "email notification about \`$arg_refname' update sent."
fi
exit 0
