1#!/bin/sh 2# 3# Copyright (c) 2007 Andy Parkins 4# 5# An example hook script to mail out commit update information. This hook sends emails 6# listing new revisions to the repository introduced by the change being reported. The 7# rule is that (for branch updates) each commit will appear on one email and one email 8# only. 9# 10# This hook is stored in the contrib/hooks directory. Your distribution will have put 11# this somewhere standard. You should make this script executable then link to it in 12# the repository you would like to use it in. For example, on debian the hook is stored 13# in /usr/share/doc/git-core/contrib/hooks/post-receive-email: 14# 15# chmod a+x post-receive-email 16# cd /path/to/your/repository.git 17# ln -sf /usr/share/doc/git-core/contrib/hooks/post-receive-email hooks/post-receive 18# 19# This hook script assumes it is enabled on the central repository of a project, with 20# all users pushing only to it and not between each other. It will still work if you 21# don't operate in that style, but it would become possible for the email to be from 22# someone other than the person doing the push. 23# 24# Config 25# ------ 26# hooks.mailinglist 27# This is the list that all pushes will go to; leave it blank to not send 28# emails for every ref update. 29# hooks.announcelist 30# This is the list that all pushes of annotated tags will go to. Leave it 31# blank to default to the mailinglist field. The announce emails lists the 32# short log summary of the changes since the last annotated tag. 33# hook.envelopesender 34# If set then the -f option is passed to sendmail to allow the envelope sender 35# address to be set 36# 37# Notes 38# ----- 39# All emails have their subjects prefixed with "[SCM]" to aid filtering. 40# All emails include the headers "X-Git-Refname", "X-Git-Oldrev", 41# "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and 42# give information for debugging. 43# 44 45# ---------------------------- Functions 46 47# 48# Top level email generation function. This decides what type of update 49# this is and calls the appropriate body-generation routine after outputting 50# the common header 51# 52# Note this function doesn't actually generate any email output, that is taken 53# care of by the functions it calls: 54# - generate_email_header 55# - generate_create_XXXX_email 56# - generate_update_XXXX_email 57# - generate_delete_XXXX_email 58# - generate_email_footer 59# 60generate_email() 61{ 62# --- Arguments 63 oldrev=$(git rev-parse $1) 64 newrev=$(git rev-parse $2) 65 refname="$3" 66 67# --- Interpret 68# 0000->1234 (create) 69# 1234->2345 (update) 70# 2345->0000 (delete) 71ifexpr"$oldrev":'0*$'>/dev/null 72then 73 change_type="create" 74else 75ifexpr"$newrev":'0*$'>/dev/null 76then 77 change_type="delete" 78else 79 change_type="update" 80fi 81fi 82 83# --- Get the revision types 84 newrev_type=$(git cat-file -t $newrev 2> /dev/null) 85 oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null) 86case"$change_type"in 87 create|update) 88rev="$newrev" 89 rev_type="$newrev_type" 90;; 91 delete) 92rev="$oldrev" 93 rev_type="$oldrev_type" 94;; 95esac 96 97# The revision type tells us what type the commit is, combined with 98# the location of the ref we can decide between 99# - working branch 100# - tracking branch 101# - unannoted tag 102# - annotated tag 103case"$refname","$rev_type"in 104 refs/tags/*,commit) 105# un-annotated tag 106 refname_type="tag" 107 short_refname=${refname##refs/tags/} 108;; 109 refs/tags/*,tag) 110# annotated tag 111 refname_type="annotated tag" 112 short_refname=${refname##refs/tags/} 113# change recipients 114if[-n"$announcerecipients"];then 115 recipients="$announcerecipients" 116fi 117;; 118 refs/heads/*,commit) 119# branch 120 refname_type="branch" 121 short_refname=${refname##refs/heads/} 122;; 123 refs/remotes/*,commit) 124# tracking branch 125 refname_type="tracking branch" 126 short_refname=${refname##refs/remotes/} 127echo>&2"*** Push-update of tracking branch,$refname" 128echo>&2"*** - no email generated." 129exit0 130;; 131*) 132# Anything else (is there anything else?) 133echo>&2"*** Unknown type of update to$refname($rev_type)" 134echo>&2"*** - no email generated" 135exit1 136;; 137esac 138 139# Check if we've got anyone to send to 140if[-z"$recipients"];then 141echo>&2"*** hooks.recipients is not set so no email will be sent" 142echo>&2"*** for$refnameupdate$oldrev->$newrev" 143exit0 144fi 145 146# Email parameters 147# The committer will be obtained from the latest existing rev; so 148# for a deletion it will be the oldrev, for the others, then newrev 149 committer=$(git show --pretty=full -s$rev|sed-ne"s/^Commit: //p"| 150sed-ne's/\(.*\) </"\1" </p') 151# The email subject will contain the best description of the ref 152# that we can build from the parameters 153 describe=$(git describe $rev 2>/dev/null) 154if[-z"$describe"];then 155 describe=$rev 156fi 157 158 generate_email_header 159 160# Call the correct body generation function 161 fn_name=general 162case"$refname_type"in 163"tracking branch"|branch) 164 fn_name=branch 165;; 166"annotated tag") 167 fn_name=atag 168;; 169esac 170 generate_${change_type}_${fn_name}_email 171 172 generate_email_footer 173} 174 175generate_email_header() 176{ 177# --- Email (all stdout will be the email) 178# Generate header 179cat<<-EOF 180 From:$committer 181 To:$recipients 182 Subject:${EMAILPREFIX}$projectdesc$refname_type,$short_refname,${change_type}d.$describe 183 X-Git-Refname:$refname 184 X-Git-Reftype:$refname_type 185 X-Git-Oldrev:$oldrev 186 X-Git-Newrev:$newrev 187 188 This is an automated email from the git hooks/post-receive script. It was 189 generated because a ref change was pushed to the repository containing 190 the project "$projectdesc". 191 192 The$refname_type,$short_refnamehas been${change_type}d 193 EOF 194} 195 196generate_email_footer() 197{ 198cat<<-EOF 199 200 201 hooks/post-receive 202 -- 203$projectdesc 204 EOF 205} 206 207# --------------- Branches 208 209# 210# Called for the creation of a branch 211# 212generate_create_branch_email() 213{ 214# This is a new branch and so oldrev is not valid 215echo" at$newrev($newrev_type)" 216echo"" 217 218echo$LOGBEGIN 219# This shows all log entries that are not already covered by 220# another ref - i.e. commits that are now accessible from this 221# ref that were previously not accessible (see generate_update_branch_email 222# for the explanation of this command) 223 git rev-parse --not --branches|grep-v$(git rev-parse $refname)| 224 git rev-list --pretty --stdin$newrev 225echo$LOGEND 226} 227 228# 229# Called for the change of a pre-existing branch 230# 231generate_update_branch_email() 232{ 233# Consider this: 234# 1 --- 2 --- O --- X --- 3 --- 4 --- N 235# 236# O is $oldrev for $refname 237# N is $newrev for $refname 238# X is a revision pointed to by some other ref, for which we may 239# assume that an email has already been generated. 240# In this case we want to issue an email containing only revisions 241# 3, 4, and N. Given (almost) by 242# 243# git-rev-list N ^O --not --all 244# 245# The reason for the "almost", is that the "--not --all" will take 246# precedence over the "N", and effectively will translate to 247# 248# git-rev-list N ^O ^X ^N 249# 250# So, we need to build up the list more carefully. git-rev-parse will 251# generate a list of revs that may be fed into git-rev-list. We can get 252# it to make the "--not --all" part and then filter out the "^N" with: 253# 254# git-rev-parse --not --all | grep -v N 255# 256# Then, using the --stdin switch to git-rev-list we have effectively 257# manufactured 258# 259# git-rev-list N ^O ^X 260# 261# This leaves a problem when someone else updates the repository 262# while this script is running. Their new value of the ref we're working 263# on would be included in the "--not --all" output; and as our $newrev 264# would be an ancestor of that commit, it would exclude all of our 265# commits. What we really want is to exclude the current value of 266# $refname from the --not list, rather than N itself. So: 267# 268# git-rev-parse --not --all | grep -v $(git-rev-parse $refname) 269# 270# Get's us to something pretty safe (apart from the small time between 271# refname being read, and git-rev-parse running - for that, I give up) 272# 273# 274# Next problem, consider this: 275# * --- B --- * --- O ($oldrev) 276# \ 277# * --- X --- * --- N ($newrev) 278# 279# That is to say, there is no guarantee that oldrev is a strict subset of 280# newrev (it would have required a --force, but that's allowed). So, we 281# can't simply say rev-list $oldrev..$newrev. Instead we find the common 282# base of the two revs and list from there. 283# 284# As above, we need to take into account the presence of X; if another 285# branch is already in the repository and points at some of the revisions 286# that we are about to output - we don't want them. The solution is as 287# before: git-rev-parse output filtered. 288# 289# Finally, tags: 290# 1 --- 2 --- O --- T --- 3 --- 4 --- N 291# 292# Tags pushed into the repository generate nice shortlog emails that 293# summarise the commits between them and the previous tag. However, 294# those emails don't include the full commit messages that we output 295# for a branch update. Therefore we still want to output revisions 296# that have been output on a tag email. 297# 298# Luckily, git-rev-parse includes just the tool. Instead of using "--all" 299# we use "--branches"; this has the added benefit that "remotes/" will 300# be ignored as well. 301 302# List all of the revisions that were removed by this update, in a fast forward 303# update, this list will be empty, because rev-list O ^N is empty. For a non 304# fast forward, O ^N is the list of removed revisions 305 fastforward="" 306rev="" 307forrevin$(git rev-list $newrev..$oldrev) 308do 309 revtype=$(git cat-file -t "$rev") 310echo" discards$rev($revtype)" 311done 312if[-z"$rev"];then 313 fast_forward=1 314fi 315 316# List all the revisions from baserev to newrev in a kind of 317# "table-of-contents"; note this list can include revisions that have 318# already had notification emails and is present to show the full detail 319# of the change from rolling back the old revision to the base revision and 320# then forward to the new revision 321forrevin$(git rev-list $oldrev..$newrev) 322do 323 revtype=$(git cat-file -t "$rev") 324echo" via$rev($revtype)" 325done 326 327if[-z"$fastforward"];then 328echo" from$oldrev($oldrev_type)" 329else 330echo"" 331echo"This update added new revisions after undoing old revisions. That is to" 332echo"say, the old revision is not a strict subset of the new revision. This" 333echo"situation occurs when you --force push a change and generate a" 334echo"repository containing something like this:" 335echo"" 336echo" * -- * -- B -- O -- O -- O ($oldrev)" 337echo"\\" 338echo" N -- N -- N ($newrev)" 339echo"" 340echo"When this happens we assume that you've already had alert emails for all" 341echo"of the O revisions, and so we here report only the revisions in the N" 342echo"branch from the common base, B." 343fi 344 345echo"" 346echo"Those revisions listed above that are new to this repository have" 347echo"not appeared on any other notification email; so we list those" 348echo"revisions in full, below." 349 350echo"" 351echo$LOGBEGIN 352 git rev-parse --not --branches|grep-v$(git rev-parse $refname)| 353 git rev-list --pretty --stdin$oldrev..$newrev 354 355# XXX: Need a way of detecting whether git rev-list actually outputted 356# anything, so that we can issue a "no new revisions added by this 357# update" message 358 359echo$LOGEND 360 361# The diffstat is shown from the old revision to the new revision. This 362# is to show the truth of what happened in this change. There's no point 363# showing the stat from the base to the new revision because the base 364# is effectively a random revision at this point - the user will be 365# interested in what this revision changed - including the undoing of 366# previous revisions in the case of non-fast forward updates. 367echo"" 368echo"Summary of changes:" 369 git diff-tree --stat --summary --find-copies-harder$oldrev..$newrev 370} 371 372# 373# Called for the deletion of a branch 374# 375generate_delete_branch_email() 376{ 377echo" was$oldrev" 378echo"" 379echo$LOGEND 380 git show -s --pretty=oneline $oldrev 381echo$LOGEND 382} 383 384# --------------- Annotated tags 385 386# 387# Called for the creation of an annotated tag 388# 389generate_create_atag_email() 390{ 391echo" at$newrev($newrev_type)" 392 393 generate_atag_email 394} 395 396# 397# Called for the update of an annotated tag (this is probably a rare event 398# and may not even be allowed) 399# 400generate_update_atag_email() 401{ 402echo" to$newrev($newrev_type)" 403echo" from$oldrev(which is now obsolete)" 404 405 generate_atag_email 406} 407 408# 409# Called when an annotated tag is created or changed 410# 411generate_atag_email() 412{ 413# Use git-for-each-ref to pull out the individual fields from the tag 414eval $(git for-each-ref --shell --format=' 415 tagobject=%(*objectname) 416 tagtype=%(*objecttype) 417 tagger=%(taggername) 418 tagged=%(taggerdate)'$refname 419) 420 421echo" tagging$tagobject($tagtype)" 422case"$tagtype"in 423 commit) 424# If the tagged object is a commit, then we assume this is a 425# release, and so we calculate which tag this tag is replacing 426 prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null) 427 428if[-n"$prevtag"];then 429echo" replaces$prevtag" 430fi 431;; 432*) 433echo" length$(git cat-file -s $tagobject)bytes" 434;; 435esac 436echo" tagged by$tagger" 437echo" on$tagged" 438 439echo"" 440echo$LOGBEGIN 441 442# Show the content of the tag message; this might contain a change log 443# or release notes so is worth displaying. 444 git cat-file tag $newrev|sed-e'1,/^$/d' 445 446echo"" 447case"$tagtype"in 448 commit) 449# Only commit tags make sense to have rev-list operations performed 450# on them 451if[-n"$prevtag"];then 452# Show changes since the previous release 453 git rev-list --pretty=short "$prevtag..$newrev"| git shortlog 454else 455# No previous tag, show all the changes since time began 456 git rev-list --pretty=short $newrev| git shortlog 457fi 458;; 459*) 460# XXX: Is there anything useful we can do for non-commit objects? 461;; 462esac 463 464echo$LOGEND 465} 466 467# 468# Called for the deletion of an annotated tag 469# 470generate_delete_atag_email() 471{ 472echo" was$oldrev" 473echo"" 474echo$LOGEND 475 git show -s --pretty=oneline $oldrev 476echo$LOGEND 477} 478 479# --------------- General references 480 481# 482# Called when any other type of reference is created (most likely a 483# non-annotated tag) 484# 485generate_create_general_email() 486{ 487echo" at$newrev($newrev_type)" 488 489 generate_general_email 490} 491 492# 493# Called when any other type of reference is updated (most likely a 494# non-annotated tag) 495# 496generate_update_general_email() 497{ 498echo" to$newrev($newrev_type)" 499echo" from$oldrev" 500 501 generate_general_email 502} 503 504# 505# Called for creation or update of any other type of reference 506# 507generate_general_email() 508{ 509# Unannotated tags are more about marking a point than releasing a version; 510# therefore we don't do the shortlog summary that we do for annotated tags 511# above - we simply show that the point has been marked, and print the log 512# message for the marked point for reference purposes 513# 514# Note this section also catches any other reference type (although there 515# aren't any) and deals with them in the same way. 516 517echo"" 518if["$newrev_type"="commit"];then 519echo$LOGBEGIN 520 git show --no-color --root -s$newrev 521echo$LOGEND 522else 523# What can we do here? The tag marks an object that is not a commit, 524# so there is no log for us to display. It's probably not wise to 525# output git-cat-file as it could be a binary blob. We'll just say how 526# big it is 527echo"$newrevis a$newrev_type, and is$(git cat-file -s $newrev)bytes long." 528fi 529} 530 531# 532# Called for the deletion of any other type of reference 533# 534generate_delete_general_email() 535{ 536echo" was$oldrev" 537echo"" 538echo$LOGEND 539 git show -s --pretty=oneline $oldrev 540echo$LOGEND 541} 542 543# ---------------------------- main() 544 545# --- Constants 546EMAILPREFIX="[SCM] " 547LOGBEGIN="- Log -----------------------------------------------------------------" 548LOGEND="-----------------------------------------------------------------------" 549 550# --- Config 551# Set GIT_DIR either from the working directory, or from the environment 552# variable. 553GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) 554if[-z"$GIT_DIR"];then 555echo>&2"fatal: post-receive: GIT_DIR not set" 556exit1 557fi 558 559projectdesc=$(sed -e '1p' "$GIT_DIR/description") 560# Check if the description is unchanged from it's default, and shorten it to a 561# more manageable length if it is 562ifexpr"$projectdesc":"Unnamed repository.*$">/dev/null 563then 564 projectdesc="UNNAMED PROJECT" 565fi 566 567recipients=$(git repo-config hooks.mailinglist) 568announcerecipients=$(git repo-config hooks.announcelist) 569envelopesender=$(git-repo-config hooks.envelopesender) 570 571# --- Main loop 572# Allow dual mode: run from the command line just like the update hook, or if 573# no arguments are given then run as a hook script 574if[-n"$1"-a -n"$2"-a -n"$3"];then 575# Output to the terminal in command line mode - if someone wanted to 576# resend an email; they could redirect the output to sendmail themselves 577 PAGER= generate_email $2 $3 $1 578else 579if[-n"$envelopesender"];then 580 envelopesender="-f '$envelopesender'" 581fi 582 583whileread oldrev newrev refname 584do 585 generate_email $oldrev $newrev $refname| 586/usr/sbin/sendmail -t$envelopesender 587done 588fi