#!/bin/sh # # Copyright (c) 2007 Andy Parkins # # An example hook script to mail out commit update information. This hook # sends emails listing new revisions to the repository introduced by the # change being reported. The rule is that (for branch updates) each commit # will appear on one email and one email only. # # This hook is stored in the contrib/hooks directory. Your distribution # will have put this somewhere standard. You should make this script # executable then link to it in the repository you would like to use it in. # For example, on debian the hook is stored in # /usr/share/doc/git-core/contrib/hooks/post-receive-email: # # chmod a+x post-receive-email # cd /path/to/your/repository.git # ln -sf /usr/share/doc/git-core/contrib/hooks/post-receive-email hooks/post-receive # # This hook script assumes it is enabled on the central repository of a # project, with all users pushing only to it and not between each other. It # will still work if you don't operate in that style, but it would become # possible for the email to be from someone other than the person doing the # push. # # To help with debugging and use on pre-v1.5.1 git servers, this script will # also obey the interface of hooks/update, taking its arguments on the # command line. Unfortunately, hooks/update is called once for each ref. # To avoid firing one email per ref, this script just prints its output to # the screen when used in this mode. The output can then be redirected if # wanted. # # Config # ------ # hooks.mailinglist # This is the list that all pushes will go to; leave it blank to not send # emails for every ref update. # hooks.announcelist # This is the list that all pushes of annotated tags will go to. Leave it # blank to default to the mailinglist field. The announce emails lists # the short log summary of the changes since the last annotated tag. # hooks.envelopesender # If set then the -f option is passed to sendmail to allow the envelope # sender address to be set # hooks.emailprefix # All emails have their subjects prefixed with this prefix, or "[SCM]" # if emailprefix is unset, to aid filtering # hooks.showrev # The shell command used to format each revision in the email, with # "%s" replaced with the commit id. Defaults to "git rev-list -1 # --pretty %s", displaying the commit id, author, date and log # message. To list full patches separated by a blank line, you # could set this to "git show -C %s; echo". # To list a gitweb/cgit URL *and* a full patch for each change set, use this: # "t=%s; printf 'http://.../?id=%%s' \$t; echo;echo; git show -C \$t; echo" # Be careful if "..." contains things that will be expanded by shell "eval" # or printf. # hooks.emailmaxlines # The maximum number of lines that should be included in the generated # email body. If not specified, there is no limit. # Lines beyond the limit are suppressed and counted, and a final # line is added indicating the number of suppressed lines. # # Notes # ----- # All emails include the headers "X-Git-Refname", "X-Git-Oldrev", # "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and # give information for debugging. # # Exit code # --------- # The script uses the following exit code: # 0 : the script has been executed as expected, no errors have been encountered # 124 : the script has taken too much time to execute and a timeout has been triggered # ---------------------------- Functions # # Function to prepare for email generation. This decides what type # of update this is and whether an email should even be generated. # prep_for_email() { # --- Arguments oldrev=$(git rev-parse $1) newrev=$(git rev-parse $2) refname="$3" maxlines=$4 # --- Interpret # 0000->1234 (create) # 1234->2345 (update) # 2345->0000 (delete) if expr "$oldrev" : '0*$' >/dev/null then change_type="create" else if expr "$newrev" : '0*$' >/dev/null then change_type="delete" else change_type="update" fi fi # --- Get the revision types newrev_type=$(git cat-file -t $newrev 2> /dev/null) oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null) case "$change_type" in create|update) rev="$newrev" rev_type="$newrev_type" ;; delete) rev="$oldrev" rev_type="$oldrev_type" ;; esac # The revision type tells us what type the commit is, combined with # the location of the ref we can decide between # - working branch # - tracking branch # - unannoted tag # - annotated tag case "$refname","$rev_type" in refs/tags/*,commit) # un-annotated tag refname_type="tag" short_refname=${refname##refs/tags/} ;; refs/tags/*,tag) # annotated tag refname_type="annotated tag" short_refname=${refname##refs/tags/} ;; 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/} echo >&2 "*** Push-update of tracking branch, $refname" echo >&2 "*** - no email generated." return 1 ;; *) # Anything else (is there anything else?) echo >&2 "*** Unknown type of update to $refname ($rev_type)" echo >&2 "*** - no email generated" return 1 ;; esac return 0 } # # Top level email generation function. This calls the appropriate # body-generation routine after outputting the common header. # # Note this function doesn't actually generate any email output, that is # taken care of by the functions it calls: # - generate_email_header # - generate_create_XXXX_email # - generate_update_XXXX_email # - generate_delete_XXXX_email # - generate_email_footer # # Note also that this function cannot 'exit' from the script; when this # function is running (in hook script mode), the send_mail() function # is already executing in another process, connected via a pipe, and # if this function exits without, whatever has been generated to that # point will be sent as an email... even if nothing has been generated. # generate_email() { # Email parameters # The email subject will contain the best description of the ref # that we can build from the parameters describe=$(git describe $rev 2>/dev/null) if [ -z "$describe" ]; then describe=$rev fi # Author list (unique, separted by comma) authors="" if [ ${change_type} = "update" ]; then authors=$(git log --pretty='format:%an' $oldrev..$newrev | sort -u | tr "\n" "," | sed -e 's/,$//' -e 's/,/, /g') authors="by $authors" fi generate_email_header # Call the correct body generation function fn_name=general case "$refname_type" in "tracking branch"|branch) fn_name=branch ;; "annotated tag") fn_name=atag ;; esac if [ -z "$maxlines" ]; then generate_${change_type}_${fn_name}_email else generate_${change_type}_${fn_name}_email | limit_lines $maxlines fi generate_email_footer } generate_email_header() { # --- Email (all stdout will be the email) # Generate header cat <<-EOF ${emailprefix}$refname_type $short_refname ${change_type}d $authors X-Git-Refname:$refname X-Git-Reftype:$refname_type X-Git-Oldrev:$oldrev X-Git-Newrev:$newrev The $refname_type, $short_refname has been ${change_type}d EOF } generate_email_footer() { SPACE=" " cat <<-EOF --${SPACE} This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "$projectdesc". EOF } # --------------- Branches # # Called for the creation of a branch # generate_create_branch_email() { # This is a new branch and so oldrev is not valid echo " at $newrev ($newrev_type)" echo "" echo $LOGBEGIN show_new_revisions echo $LOGEND } # # Called for the change of a pre-existing branch # generate_update_branch_email() { timeout "$MAX_COMMAND_EXECUTION_TIME" \ "$(dirname $0)/post-receive-email-generate-update-branch-email" \ "$change_type" "$newrev" "$oldrev" "$refname" "$custom_showrev" "$oldrev_type" "$LOGBEGIN" "$LOGEND" if [ "$?" -eq "$TIMEOUT_EXIT_CODE" ]; then echo "The summary of changes in the updated branch has taken too much time to be generated and was truncated" exit_status=${TIMEOUT_EXIT_CODE} fi } # # Called for the deletion of a branch # generate_delete_branch_email() { echo " was $oldrev" echo "" echo $LOGEND git show -s --pretty=oneline $oldrev echo $LOGEND } # --------------- Annotated tags # # Called for the creation of an annotated tag # generate_create_atag_email() { echo " at $newrev ($newrev_type)" generate_atag_email } # # Called for the update of an annotated tag (this is probably a rare event # and may not even be allowed) # generate_update_atag_email() { echo " to $newrev ($newrev_type)" echo " from $oldrev (which is now obsolete)" generate_atag_email } # # Called when an annotated tag is created or changed # generate_atag_email() { # Use git for-each-ref to pull out the individual fields from the # tag eval $(git for-each-ref --shell --format=' tagobject=%(*objectname) tagtype=%(*objecttype) tagger=%(taggername) tagged=%(taggerdate)' $refname ) echo " tagging $tagobject ($tagtype)" case "$tagtype" in commit) # If the tagged object is a commit, then we assume this is a # release, and so we calculate which tag this tag is # replacing prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null) if [ -n "$prevtag" ]; then echo " replaces $prevtag" fi ;; *) echo " length $(git cat-file -s $tagobject) bytes" ;; esac echo " tagged by $tagger" echo " on $tagged" echo "" echo $LOGBEGIN # Show the content of the tag message; this might contain a change # log or release notes so is worth displaying. git cat-file tag $newrev | sed -e '1,/^$/d' echo "" case "$tagtype" in commit) # Only commit tags make sense to have rev-list operations # performed on them if [ -n "$prevtag" ]; then # Show changes since the previous release git rev-list --pretty=short "$prevtag..$newrev" | git shortlog else # No previous tag, show all the changes since time # began git rev-list --pretty=short $newrev | git shortlog fi ;; *) # XXX: Is there anything useful we can do for non-commit # objects? ;; esac echo $LOGEND } # # Called for the deletion of an annotated tag # generate_delete_atag_email() { echo " was $oldrev" echo "" echo $LOGEND git show -s --pretty=oneline $oldrev echo $LOGEND } # --------------- General references # # Called when any other type of reference is created (most likely a # non-annotated tag) # generate_create_general_email() { echo " at $newrev ($newrev_type)" generate_general_email } # # Called when any other type of reference is updated (most likely a # non-annotated tag) # generate_update_general_email() { echo " to $newrev ($newrev_type)" echo " from $oldrev" generate_general_email } # # Called for creation or update of any other type of reference # generate_general_email() { # 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. echo "" if [ "$newrev_type" = "commit" ]; then echo $LOGBEGIN git show --no-color --root -s --pretty=medium $newrev echo $LOGEND else # What can we do here? The tag marks an object that is not # a commit, so there is no log for us to display. It's # probably not wise to output git cat-file as it could be a # binary blob. We'll just say how big it is echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long." fi } # # Called for the deletion of any other type of reference # generate_delete_general_email() { echo " was $oldrev" echo "" echo $LOGEND git show -s --pretty=oneline $oldrev echo $LOGEND } # --------------- Miscellaneous utilities # # Show new revisions as the user would like to see them in the email. # show_new_revisions() { timeout "$MAX_COMMAND_EXECUTION_TIME" \ "$(dirname $0)/post-receive-email-show-new-revision" "$change_type" "$newrev" "$oldrev" "$refname" "$custom_showrev" if [ "$?" -eq "$TIMEOUT_EXIT_CODE" ]; then echo "The list of the new revisions has taken too much time to be generated and was truncated" exit_status=${TIMEOUT_EXIT_CODE} fi } limit_lines() { lines=0 skipped=0 while IFS="" read -r line; do lines=$((lines + 1)) if [ $lines -gt $1 ]; then skipped=$((skipped + 1)) else printf "%s\n" "$line" fi done if [ $skipped -ne 0 ]; then echo "... $skipped lines suppressed ..." fi } send_mail() { if [ -n "$envelopesender" ]; then /usr/sbin/sendmail -t -f "$envelopesender" else /usr/sbin/sendmail -t fi } # ---------------------------- main() # --- Constants LOGBEGIN="- Log -----------------------------------------------------------------" LOGEND="-----------------------------------------------------------------------" MAX_COMMAND_EXECUTION_TIME='5s' TIMEOUT_EXIT_CODE=124 # --- Config # Set GIT_DIR either from the working directory, or from the environment # variable. GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) if [ -z "$GIT_DIR" ]; then echo >&2 "fatal: post-receive: GIT_DIR not set" exit 1 fi projectdesc=$(sed -ne '1p' "$GIT_DIR/description") # Check if the description is unchanged from it's default, and shorten it to # a more manageable length if it is if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null then projectdesc="UNNAMED PROJECT" fi envelopesender=$(git config hooks.envelopesender) emailprefix=$(git config hooks.emailprefix || echo '[SCM] ') custom_showrev=$(git config hooks.showrev) maxlines=$(git config hooks.emailmaxlines) exit_status=0 # Expect 3 parameters: oldrev newrev refname if [ -n "$1" -a -n "$2" -a -n "$3" ]; then prep_for_email $1 $2 $3 || continue generate_email $maxlines fi exit ${exit_status}