#!/bin/bash
#
# A hook script to check the commit log message to ensure that it has
# a well-formed commit summary and body, a valid Signed-off-by: line,
# and a Gerrit Change-Id: line (added automatically if missing).
#
# Called by git-commit with one argument, the name of the file
# that has the commit message.  The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit.  The hook is allowed to edit the commit message file.
#
# Should be installed as .git/hooks/commit-msg.
#

ORIGINAL="$1"
REVISED="$(mktemp "$ORIGINAL.XXXXXX")"
SAVE="$(basename $ORIGINAL).$(date +%Y%m%d.%H%M%S)"
SIGNOFF="Signed-off-by"
CHANGEID="Change-Id"
WIDTH_SUM=64
WIDTH_REG=70

# Check for, and add if missing, a unique Change-Id
new_changeid() {
	NEWID=$({
			git var GIT_AUTHOR_IDENT
			git var GIT_COMMITTER_IDENT
			git write-tree
			git rev-parse HEAD 2>/dev/null
			grep -v "^$SIGNOFF" "$ORIGINAL" | git stripspace -s
		} | git hash-object --stdin)
	if [ -z "$NEWID" ]; then
		error "$0: git hash-object failed for $CHANGEID:"
		exit 1
	fi

	echo "$CHANGEID: I$NEWID"
	HAS_CHANGEID=true
}

error() {
	[ "$LINE" ] && echo "line $NUM: $LINE" 1>&2
	[ "$1" ] && echo "error: commit message $1" 1>&2
	[ "$2" ] && echo "$2" 1>&2

	HAS_ERROR=true
}

usage() {
	cat 1>&2 <<- EOF

	See http://wiki.whamcloud.com/display/PUB/Commit+Comments
	for full details.  An example valid commit comment is:

	LU-nnn component: short description of change under 64 columns

	The "component:" should be a lower-case single-word subsystem of the
	Lustre code that best encompasses the change being made.  Examples of
	components include modules like: llite, lov, lmv, osc, mdc, ldlm, lnet,
	ptlrpc, mds, oss, osd, ldiskfs, libcfs, socklnd, o2iblnd; functional
	subsystems like: recovery, quota, grant; and auxilliary areas like:
	build, tests, docs.  This list is not exhaustive, but is a guideline.

	The commit comment should contain a detailed explanation of the change
	being made.  This can be as long as you'd like.  Please give details
	of what problem was solved (including error messages or problems that
	were seen), a good high-level description of how it was solved, and
	which parts of the code were changed (including important functions
	that were changed, if this is useful to understand the patch, and
	for easier searching).  Wrap lines at/under $WIDTH_REG columns.

	$SIGNOFF: Your Real Name <your_email@domain.name>
	$CHANGEID: Ixxxx(added automatically if missing)xxxx

	EOF

	mv "$ORIGINAL" "$SAVE" &&
		echo "$0: saved original commit comment to $SAVE" 1>&2
}

export HAS_ERROR=false
export HAS_SUMMARY=false
export HAS_LAST_BLANK=false
export HAS_BODY=false
export HAS_SIGNOFF=false
case $(grep -c "^$CHANGEID:" "$ORIGINAL") in
0)	export HAS_CHANGEID=false ;;
1)	export HAS_CHANGEID=true ;;
*)	error "with multiple $CHANGEID: lines not allowed."
	export HAS_CHANGEID=true
	HAS_ERROR=true ;;
esac
export HAS_COMMENTS=false
export HAS_DIFF=false

export LINE=""
export NUM=1

IFS="" # don't eat whitespace, to preserve message formatting
while read LINE; do
	WIDTH=$(($(echo $LINE | wc -c) - 1)) # -1 for end-of-line character

	case "$LINE" in
	$SIGNOFF*)
		# Signed-off-by: First Last <email@host.domain>
		HAS_SOB=$(echo "$LINE" | grep "^$SIGNOFF: .* .* <.*@[^.]*\..*>")
		if [ -z "$HAS_SOB" ]; then
			error "missing valid commit summary line to show" \
			      "agreement with code submission requirements."
		elif ! $HAS_SUMMARY; then
			error "missing summary before $SIGNOFF:."
		elif ! $HAS_LAST_BLANK; then
			error "missing blank line before $SIGNOFF:."
		elif ! $HAS_BODY; then
			error "missing useful comments before $SIGNOFF:."
		else
			HAS_SIGNOFF=true # allow multiple signoff lines
		fi
		echo $LINE
		! $HAS_CHANGEID && new_changeid
		;;
	$CHANGEID*)
		# Change-Id: I762ab50568f25527176ae54e92c446cf06112097
		HAS_ID=$(echo "$LINE" | grep "^$CHANGEID: I[0-9a-fA-F]\{40\}")
		if [ -z "$HAS_ID" ]; then
			error "has invalid $CHANGEID: line for Gerrit tracking"
		elif ! $HAS_SUMMARY; then
			error "missing summary before $CHANGEID:."
		elif ! $HAS_BODY; then
			error "missing useful comments before $CHANGEID:."

		# $CHANGEID used to come before $SIGNOFF in old commits.
		# Allow this to continue for some time, until they are gone.
		# elif ! $HAS_SIGNOFF; then
		#	error "missing $SIGNOFF: before $CHANGEID:."

		# $HAS_CHANGEID was already checked and set above
		#elif $HAS_CHANGEID; then
		#	error "does not allow multiple $CHANGEID: lines."
		#else
		#	HAS_CHANGEID=true
		fi
		echo $LINE
		;;
	"")
		[ $HAS_SUMMARY -a ! $HAS_BODY ] && HAS_LAST_BLANK=true
		[ $HAS_BODY ] && HAS_LAST_BLANK=true
		echo $LINE
		;;
	diff*|index*)
		# Beginning of uncommented diffstat from "commit -v".  If
		# there are diff and index lines, skip the rest of the input.
		# diff --git a/build/commit-msg b/build/commit-msg
		# index 80a3442..acb4c50 100755
		DIFF=$(echo "$LINE" | grep -- "^diff --git a/")
		[ "$DIFF" ] && HAS_DIFF=true && continue
		INDEX=$(echo "$LINE" | grep -- "^index [0-9a-fA-F]\{6,\}\.\.")
		[ $HAS_DIFF -a "$INDEX" ] && break || HAS_DIFF=false
		;;
	\#*)
		HAS_COMMENTS=true
		continue
		;;
	*)	if [ $NUM -eq 1 ]; then
			FMT="^[A-Z]\{2,5\}-[0-9]\{1,5\} [-a-z0-9]\{2,9\}: "
			HAS_JIRA_COMPONENT=$(echo "$LINE" | grep "$FMT")
			if [ -z "$HAS_JIRA_COMPONENT" ]; then
				FMT="^[A-Z]\{2,5\}-[0-9]\{1,5\} "
				HAS_JIRA=$(echo "$LINE" | grep "$FMT")
				if [ "$HAS_JIRA" ]; then
					error "has no component in summary."
				else
					error "missing JIRA ticket number."
				fi
			elif [ $WIDTH -gt $WIDTH_SUM ]; then
				error "summary longer than $WIDTH_SUM columns."
			else
				HAS_SUMMARY=true
			fi
		elif $HAS_SIGNOFF; then
			error "trailing garbage after $SIGNOFF: line."
		elif [ $WIDTH -gt $WIDTH_REG ]; then
			error "has line longer than $WIDTH_REG columns."
		elif ! $HAS_BODY && ! $HAS_LAST_BLANK; then
			error "has no blank line after summary."
		else
			HAS_BODY=true
		fi
		HAS_LAST_BLANK=false
		HAS_DIFF=false
		echo $LINE
		;;
	esac

	NUM=$((NUM + 1))
done < "$ORIGINAL" > "$REVISED"

[ $NUM -eq 1 ] && exit 1 # empty file

LINE=""
if ! $HAS_SUMMARY; then
	error "missing commit summary line."
elif ! $HAS_BODY; then
	error "missing commit description."
elif ! $HAS_SIGNOFF; then
	error "missing valid $SIGNOFF: line."
elif ! $HAS_CHANGEID; then
	error "missing valid $CHANGEID: line."
fi
if $HAS_ERROR; then
	usage
	rm "$REVISED"
	exit 1
fi

mv "$REVISED" "$ORIGINAL"
