The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/bin/sh
#
# An example hook script to mail out commit update information.
# It can also blocks tags that aren't annotated.
# Called by git-receive-pack with arguments: refname sha1-old sha1-new
#
# To enable this hook, make this file executable by "chmod +x update".
#
# Config
# ------
# hooks.mailinglist
#   This is the list that all pushes will go to; leave it blank to not send
#   emails frequently.  The log email will list every log entry in full between
#   the old ref value and the new ref value.
# hooks.announcelist
#   This is the list that all pushes of annotated tags will go to.  Leave it
#   blank to just use the mailinglist field.  The announce emails list the
#   short log summary of the changes since the last annotated tag
# hooks.allowunannotated
#   This boolean sets whether unannotated tags will be allowed into the
#   repository.  By default they won't be.
#
# Notes
# -----
# All emails have their subjects prefixed with "[SCM]" to aid filtering.
# All emails include the headers "X-Git-Refname", "X-Git-Oldrev",
# "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and info.

# --- Constants
EMAILPREFIX="[SCM] "
LOGBEGIN="- Log -----------------------------------------------------------------"
LOGEND="-----------------------------------------------------------------------"
DATEFORMAT="%F %R %z"

# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"

# --- Safety check
if [ -z "$GIT_DIR" ]; then
	echo "Don't run this script from the command line." >&2
	echo " (if you want, you could supply GIT_DIR then run" >&2
	echo "  $0 <ref> <oldrev> <newrev>)" >&2
	exit 1
fi

if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
	echo "Usage: $0 <ref> <oldrev> <newrev>" >&2
	exit 1
fi

# --- Config
projectdesc=$(cat $GIT_DIR/description)
recipients=$(git-repo-config hooks.mailinglist)
announcerecipients=$(git-repo-config hooks.announcelist)
allowunannotated=$(git-repo-config --bool hooks.allowunannotated)

# --- Check types
newrev_type=$(git-cat-file -t "$newrev")

case "$refname","$newrev_type" in
	refs/tags/*,commit)
		# un-annotated tag
		refname_type="tag"
		short_refname=${refname##refs/tags/}
		if [ $allowunannotated != "true" ]; then
			echo "*** The un-annotated tag, $short_refname is not allowed in this repository" >&2
			echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
			exit 1
		fi
		;;
	refs/tags/*,tag)
		# annotated tag
		refname_type="annotated tag"
		short_refname=${refname##refs/tags/}
		# change recipients
		if [ -n "$announcerecipients" ]; then
			recipients="$announcerecipients"
		fi
		;;
	refs/heads/*,commit)
		# branch
		refname_type="branch"
		short_refname=${refname##refs/heads/}
		;;
	refs/remotes/*,commit)
		# tracking branch
		refname_type="tracking branch"
		short_refname=${refname##refs/remotes/}
		# Should this even be allowed?
		echo "*** Push-update of tracking branch, $refname.  No email generated." >&2
		exit 0
		;;
	*)
		# Anything else (is there anything else?)
		echo "*** Update hook: unknown type of update, \"$newrev_type\", to ref $refname" >&2
		exit 1
		;;
esac

# Check if we've got anyone to send to
if [ -z "$recipients" ]; then
	# If the email isn't sent, then at least give the user some idea of what command
	# would generate the email at a later date
	echo "*** No recipients found - no email will be sent, but the push will continue" >&2
	echo "*** for $0 $1 $2 $3" >&2
	exit 0
fi

# --- Email parameters
committer=$(git show --pretty=full -s $newrev | grep "^Commit: " | sed -e "s/^Commit: //")
describe=$(git describe $newrev 2>/dev/null)
if [ -z "$describe" ]; then
	describe=$newrev
fi

# --- Email (all stdout will be the email)
(
# Generate header
cat <<-EOF
From: $committer
To: $recipients
Subject: ${EMAILPREFIX}$projectdesc $refname_type, $short_refname now at $describe
X-Git-Refname: $refname
X-Git-Reftype: $refname_type
X-Git-Oldrev: $oldrev
X-Git-Newrev: $newrev

Hello,

This is an automated email from the git hooks/update script, it was
generated because a ref change was pushed to the repository.

Updating $refname_type, $short_refname,
EOF

case "$refname_type" in
	"tracking branch"|branch)
		if expr "$oldrev" : '0*$' >/dev/null
		then
			# If the old reference is "0000..0000" then this is a new branch
			# and so oldrev is not valid
			echo "  as a new  $refname_type"
		    echo "        to  $newrev ($newrev_type)"
			echo ""
			echo $LOGBEGIN
			# This shows all log entries that are not already covered by
			# another ref - i.e. commits that are now accessible from this
			# ref that were previously not accessible
			git-rev-parse --not --all | git-rev-list --stdin --pretty $newref
			echo $LOGEND
		else
			# oldrev is valid
			oldrev_type=$(git-cat-file -t "$oldrev")

			# Now the problem is for cases like this:
			#   * --- * --- * --- * (oldrev)
			#          \
			#           * --- * --- * (newrev)
			# i.e. there is no guarantee that newrev is a strict subset
			# of oldrev - (would have required a force, but that's allowed).
			# So, we can't simply say rev-list $oldrev..$newrev.  Instead
			# we find the common base of the two revs and list from there
			baserev=$(git-merge-base $oldrev $newrev)

			# Commit with a parent
			for rev in $(git-rev-list $newrev ^$baserev)
			do
				revtype=$(git-cat-file -t "$rev")
				echo "       via  $rev ($revtype)"
			done
			if [ "$baserev" = "$oldrev" ]; then
				echo "      from  $oldrev ($oldrev_type)"
			else
				echo "  based on  $baserev"
				echo "      from  $oldrev ($oldrev_type)"
				echo ""
				echo "This ref update crossed a branch point; i.e. the old rev is not a strict subset"
				echo "of the new rev.  This occurs, when you --force push a change in a situation"
				echo "like this:"
				echo ""
				echo " * -- * -- B -- O -- O -- O ($oldrev)"
				echo "            \\"
				echo "             N -- N -- N ($newrev)"
				echo ""
				echo "Therefore, we assume that you've already had alert emails for all of the O"
				echo "revisions, and now give you all the revisions in the N branch from the common"
				echo "base, B ($baserev), up to the new revision."
			fi
			echo ""
			echo $LOGBEGIN
			git-rev-list --pretty $newrev ^$baserev
			echo $LOGEND
			echo ""
			echo "Diffstat:"
			git-diff-tree --no-color --stat -M -C --find-copies-harder $newrev ^$baserev
		fi
		;;
	"annotated tag")
		# Should we allow changes to annotated tags?
		if expr "$oldrev" : '0*$' >/dev/null
		then
			# If the old reference is "0000..0000" then this is a new atag
			# and so oldrev is not valid
			echo "        to  $newrev ($newrev_type)"
		else
			echo "        to  $newrev ($newrev_type)"
			echo "      from  $oldrev"
		fi

		# If this tag succeeds another, then show which tag it replaces
		prevtag=$(git describe $newrev^ 2>/dev/null | sed 's/-g.*//')
		if [ -n "$prevtag" ]; then
			echo "  replaces  $prevtag"
		fi

		# Read the tag details
		eval $(git cat-file tag $newrev | \
			sed -n '4s/tagger \([^>]*>\)[^0-9]*\([0-9]*\).*/tagger="\1" ts="\2"/p')
		tagged=$(date --date="1970-01-01 00:00:00 +0000 $ts seconds" +"$DATEFORMAT")

		echo " tagged by  $tagger"
		echo "        on  $tagged"

		echo ""
		echo $LOGBEGIN
		echo ""

		if [ -n "$prevtag" ]; then
			git rev-list --pretty=short "$prevtag..$newrev" | git shortlog
		else
			git rev-list --pretty=short $newrev | git shortlog
		fi

		echo $LOGEND
		echo ""
		;;
	*)
		# By default, unannotated tags aren't allowed in; if
		# they are though, it's debatable whether we would even want an
		# email to be generated; however, I don't want to add another config
		# option just for that.
		#
		# Unannotated tags are more about marking a point than releasing
		# a version; therefore we don't do the shortlog summary that we
		# do for annotated tags above - we simply show that the point has
		# been marked, and print the log message for the marked point for
		# reference purposes
		#
		# Note this section also catches any other reference type (although
		# there aren't any) and deals with them in the same way.
		if expr "$oldrev" : '0*$' >/dev/null
		then
			# If the old reference is "0000..0000" then this is a new tag
			# and so oldrev is not valid
			echo "  as a new  $refname_type"
			echo "        to  $newrev ($newrev_type)"
		else
			echo "        to  $newrev ($newrev_type)"
			echo "      from  $oldrev"
		fi
		echo ""
		echo $LOGBEGIN
		git-show --no-color --root -s $newrev
		echo $LOGEND
		echo ""
		;;
esac

# Footer
cat <<-EOF

hooks/update
---
Git Source Code Management System
$0 $1 \\
  $2 \\
  $3
EOF
#) | cat >&2
) | /usr/sbin/sendmail -t

# --- Finished
exit 0