#!/bin/sh
#
# An example hook script to mail out commit update information.
-# It also blocks tags that aren't annotated.
+# 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:
-# (1) change the recipient e-mail address
-# (2) make this file executable by "chmod +x update".
+# 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.
-project=$(cat $GIT_DIR/description)
-recipients="commit-list@somewhere.com commit-list@somewhereelse.com"
-
-ref_type=$(git cat-file -t "$3")
-
-# Only allow annotated tags in a shared repo
-# Remove this code to treat dumb tags the same as everything else
-case "$1","$ref_type" in
-refs/tags/*,commit)
- echo "*** Un-annotated tags are not allowed in this repo" >&2
- echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate."
- exit 1;;
-refs/tags/*,tag)
- echo "### Pushing version '${1##refs/tags/}' to the masses" >&2
- # recipients="release-announce@somwehere.com announce@somewhereelse.com"
- ;;
-esac
+# --- 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)
-# set this to 'cat' to get a very detailed listing.
-# short only kicks in when an annotated tag is added
-short='git shortlog'
-
-# see 'date --help' for info on how to write this
-# The default is a human-readable iso8601-like format with minute
-# precision ('2006-01-25 15:58 +0100' for example)
-date_format="%F %R %z"
-
-(if expr "$2" : '0*$' >/dev/null
-then
- # new ref
- case "$1" in
- refs/tags/*)
- # a pushed and annotated tag (usually) means a new version
- tag="${1##refs/tags/}"
- if [ "$ref_type" = tag ]; then
- eval $(git cat-file tag $3 | \
- sed -n '4s/tagger \([^>]*>\)[^0-9]*\([0-9]*\).*/tagger="\1" ts="\2"/p')
- date=$(date --date="1970-01-01 00:00:00 $ts seconds" +"$date_format")
- echo "Tag '$tag' created by $tagger at $date"
- git cat-file tag $3 | sed -n '5,$p'
- echo
+# --- 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
- prev=$(git describe "$3^" | sed 's/-g.*//')
- # the first tag in a repo will yield no $prev
- if [ -z "$prev" ]; then
- echo "Changes since the dawn of time:"
- git rev-list --pretty $3 | $short
- else
- echo "Changes since $prev:"
- git rev-list --pretty $prev..$3 | $short
- echo ---
- git diff $prev..$3 | diffstat -p1
- echo ---
+ ;;
+ 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
- refs/heads/*)
- branch="${1##refs/heads/}"
- echo "New branch '$branch' available with the following commits:"
- git-rev-list --pretty "$3" $(git-rev-parse --not --all)
+# --- 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-parse --not --all | git-rev-list --stdin $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-parse --not --all |
+ git-rev-list --stdin --pretty $newrev ^$baserev
+ echo $LOGEND
+ echo ""
+ echo "Diffstat:"
+ git-diff-tree --no-color --stat -M -C --find-copies-harder $newrev ^$baserev
+ fi
;;
- esac
-else
- base=$(git-merge-base "$2" "$3")
- case "$base" in
- "$2")
- git diff "$3" "^$base" | diffstat -p1
- echo
- echo "New commits:"
+ "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 ""
;;
*)
- echo "Rebased ref, commits from common ancestor:"
+ # 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
- git-rev-list --pretty "$3" "^$base"
-fi) |
-mail -s "$project: Changes to '${1##refs/heads/}'" $recipients
+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