1#!/bin/sh 2# 3# Copyright (c) 2007 Andy Parkins 4# 5# An example hook script to mail out commit update information. 6# 7# NOTE: This script is no longer under active development. There 8# is another script, git-multimail, which is more capable and 9# configurable and is largely backwards-compatible with this script; 10# please see "contrib/hooks/multimail/". For instructions on how to 11# migrate from post-receive-email to git-multimail, please see 12# "README.migrate-from-post-receive-email" in that directory. 13# 14# This hook sends emails listing new revisions to the repository 15# introduced by the change being reported. The rule is that (for 16# branch updates) each commit will appear on one email and one email 17# only. 18# 19# This hook is stored in the contrib/hooks directory. Your distribution 20# will have put this somewhere standard. You should make this script 21# executable then link to it in the repository you would like to use it in. 22# For example, on debian the hook is stored in 23# /usr/share/git-core/contrib/hooks/post-receive-email: 24# 25# cd /path/to/your/repository.git 26# ln -sf /usr/share/git-core/contrib/hooks/post-receive-email hooks/post-receive 27# 28# This hook script assumes it is enabled on the central repository of a 29# project, with all users pushing only to it and not between each other. It 30# will still work if you don't operate in that style, but it would become 31# possible for the email to be from someone other than the person doing the 32# push. 33# 34# To help with debugging and use on pre-v1.5.1 git servers, this script will 35# also obey the interface of hooks/update, taking its arguments on the 36# command line. Unfortunately, hooks/update is called once for each ref. 37# To avoid firing one email per ref, this script just prints its output to 38# the screen when used in this mode. The output can then be redirected if 39# wanted. 40# 41# Config 42# ------ 43# hooks.mailinglist 44# This is the list that all pushes will go to; leave it blank to not send 45# emails for every ref update. 46# hooks.announcelist 47# This is the list that all pushes of annotated tags will go to. Leave it 48# blank to default to the mailinglist field. The announce emails lists 49# the short log summary of the changes since the last annotated tag. 50# hooks.envelopesender 51# If set then the -f option is passed to sendmail to allow the envelope 52# sender address to be set 53# hooks.emailprefix 54# All emails have their subjects prefixed with this prefix, or "[SCM]" 55# if emailprefix is unset, to aid filtering 56# hooks.showrev 57# The shell command used to format each revision in the email, with 58# "%s" replaced with the commit id. Defaults to "git rev-list -1 59# --pretty %s", displaying the commit id, author, date and log 60# message. To list full patches separated by a blank line, you 61# could set this to "git show -C %s; echo". 62# To list a gitweb/cgit URL *and* a full patch for each change set, use this: 63# "t=%s; printf 'http://.../?id=%%s' \$t; echo;echo; git show -C \$t; echo" 64# Be careful if "..." contains things that will be expanded by shell "eval" 65# or printf. 66# hooks.emailmaxlines 67# The maximum number of lines that should be included in the generated 68# email body. If not specified, there is no limit. 69# Lines beyond the limit are suppressed and counted, and a final 70# line is added indicating the number of suppressed lines. 71# hooks.diffopts 72# Alternate options for the git diff-tree invocation that shows changes. 73# Default is "--stat --summary --find-copies-harder". Add -p to those 74# options to include a unified diff of changes in addition to the usual 75# summary output. 76# 77# Notes 78# ----- 79# All emails include the headers "X-Git-Refname", "X-Git-Oldrev", 80# "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and 81# give information for debugging. 82# 83 84# ---------------------------- Functions 85 86# 87# Function to prepare for email generation. This decides what type 88# of update this is and whether an email should even be generated. 89# 90prep_for_email() 91{ 92 # --- Arguments 93 oldrev=$(git rev-parse $1) 94 newrev=$(git rev-parse $2) 95 refname="$3" 96 97 # --- Interpret 98 # 0000->1234 (create) 99 # 1234->2345 (update) 100 # 2345->0000 (delete) 101 if expr "$oldrev" : '0*$' >/dev/null 102 then 103 change_type="create" 104 else 105 if expr "$newrev" : '0*$' >/dev/null 106 then 107 change_type="delete" 108 else 109 change_type="update" 110 fi 111 fi 112 113 # --- Get the revision types 114 newrev_type=$(git cat-file -t $newrev 2> /dev/null) 115 oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null) 116 case "$change_type" in 117 create|update) 118 rev="$newrev" 119 rev_type="$newrev_type" 120 ;; 121 delete) 122 rev="$oldrev" 123 rev_type="$oldrev_type" 124 ;; 125 esac 126 127 # The revision type tells us what type the commit is, combined with 128 # the location of the ref we can decide between 129 # - working branch 130 # - tracking branch 131 # - unannoted tag 132 # - annotated tag 133 case "$refname","$rev_type" in 134 refs/tags/*,commit) 135 # un-annotated tag 136 refname_type="tag" 137 short_refname=${refname##refs/tags/} 138 ;; 139 refs/tags/*,tag) 140 # annotated tag 141 refname_type="annotated tag" 142 short_refname=${refname##refs/tags/} 143 # change recipients 144 if [ -n "$announcerecipients" ]; then 145 recipients="$announcerecipients" 146 fi 147 ;; 148 refs/heads/*,commit) 149 # branch 150 refname_type="branch" 151 short_refname=${refname##refs/heads/} 152 ;; 153 refs/remotes/*,commit) 154 # tracking branch 155 refname_type="tracking branch" 156 short_refname=${refname##refs/remotes/} 157 echo >&2 "*** Push-update of tracking branch, $refname" 158 echo >&2 "*** - no email generated." 159 return 1 160 ;; 161 *) 162 # Anything else (is there anything else?) 163 echo >&2 "*** Unknown type of update to $refname ($rev_type)" 164 echo >&2 "*** - no email generated" 165 return 1 166 ;; 167 esac 168 169 # Check if we've got anyone to send to 170 if [ -z "$recipients" ]; then 171 case "$refname_type" in 172 "annotated tag") 173 config_name="hooks.announcelist" 174 ;; 175 *) 176 config_name="hooks.mailinglist" 177 ;; 178 esac 179 echo >&2 "*** $config_name is not set so no email will be sent" 180 echo >&2 "*** for $refname update $oldrev->$newrev" 181 return 1 182 fi 183 184 return 0 185} 186 187# 188# Top level email generation function. This calls the appropriate 189# body-generation routine after outputting the common header. 190# 191# Note this function doesn't actually generate any email output, that is 192# taken care of by the functions it calls: 193# - generate_email_header 194# - generate_create_XXXX_email 195# - generate_update_XXXX_email 196# - generate_delete_XXXX_email 197# - generate_email_footer 198# 199# Note also that this function cannot 'exit' from the script; when this 200# function is running (in hook script mode), the send_mail() function 201# is already executing in another process, connected via a pipe, and 202# if this function exits without, whatever has been generated to that 203# point will be sent as an email... even if nothing has been generated. 204# 205generate_email() 206{ 207 # Email parameters 208 # The email subject will contain the best description of the ref 209 # that we can build from the parameters 210 describe=$(git describe $rev 2>/dev/null) 211 if [ -z "$describe" ]; then 212 describe=$rev 213 fi 214 215 generate_email_header 216 217 # Call the correct body generation function 218 fn_name=general 219 case "$refname_type" in 220 "tracking branch"|branch) 221 fn_name=branch 222 ;; 223 "annotated tag") 224 fn_name=atag 225 ;; 226 esac 227 228 if [ -z "$maxlines" ]; then 229 generate_${change_type}_${fn_name}_email 230 else 231 generate_${change_type}_${fn_name}_email | limit_lines $maxlines 232 fi 233 234 generate_email_footer 235} 236 237generate_email_header() 238{ 239 # --- Email (all stdout will be the email) 240 # Generate header 241 cat <<-EOF 242 To: $recipients 243 Subject: ${emailprefix}$projectdesc $refname_type $short_refname ${change_type}d. $describe 244 MIME-Version: 1.0 245 Content-Type: text/plain; charset=utf-8 246 Content-Transfer-Encoding: 8bit 247 X-Git-Refname: $refname 248 X-Git-Reftype: $refname_type 249 X-Git-Oldrev: $oldrev 250 X-Git-Newrev: $newrev 251 Auto-Submitted: auto-generated 252 253 This is an automated email from the git hooks/post-receive script. It was 254 generated because a ref change was pushed to the repository containing 255 the project "$projectdesc". 256 257 The $refname_type, $short_refname has been ${change_type}d 258 EOF 259} 260 261generate_email_footer() 262{ 263 SPACE=" " 264 cat <<-EOF 265 266 267 hooks/post-receive 268 --${SPACE} 269 $projectdesc 270 EOF 271} 272 273# --------------- Branches 274 275# 276# Called for the creation of a branch 277# 278generate_create_branch_email() 279{ 280 # This is a new branch and so oldrev is not valid 281 echo " at $newrev ($newrev_type)" 282 echo "" 283 284 echo $LOGBEGIN 285 show_new_revisions 286 echo $LOGEND 287} 288 289# 290# Called for the change of a pre-existing branch 291# 292generate_update_branch_email() 293{ 294 # Consider this: 295 # 1 --- 2 --- O --- X --- 3 --- 4 --- N 296 # 297 # O is $oldrev for $refname 298 # N is $newrev for $refname 299 # X is a revision pointed to by some other ref, for which we may 300 # assume that an email has already been generated. 301 # In this case we want to issue an email containing only revisions 302 # 3, 4, and N. Given (almost) by 303 # 304 # git rev-list N ^O --not --all 305 # 306 # The reason for the "almost", is that the "--not --all" will take 307 # precedence over the "N", and effectively will translate to 308 # 309 # git rev-list N ^O ^X ^N 310 # 311 # So, we need to build up the list more carefully. git rev-parse 312 # will generate a list of revs that may be fed into git rev-list. 313 # We can get it to make the "--not --all" part and then filter out 314 # the "^N" with: 315 # 316 # git rev-parse --not --all | grep -v N 317 # 318 # Then, using the --stdin switch to git rev-list we have effectively 319 # manufactured 320 # 321 # git rev-list N ^O ^X 322 # 323 # This leaves a problem when someone else updates the repository 324 # while this script is running. Their new value of the ref we're 325 # working on would be included in the "--not --all" output; and as 326 # our $newrev would be an ancestor of that commit, it would exclude 327 # all of our commits. What we really want is to exclude the current 328 # value of $refname from the --not list, rather than N itself. So: 329 # 330 # git rev-parse --not --all | grep -v $(git rev-parse $refname) 331 # 332 # Gets us to something pretty safe (apart from the small time 333 # between refname being read, and git rev-parse running - for that, 334 # I give up) 335 # 336 # 337 # Next problem, consider this: 338 # * --- B --- * --- O ($oldrev) 339 # \ 340 # * --- X --- * --- N ($newrev) 341 # 342 # That is to say, there is no guarantee that oldrev is a strict 343 # subset of newrev (it would have required a --force, but that's 344 # allowed). So, we can't simply say rev-list $oldrev..$newrev. 345 # Instead we find the common base of the two revs and list from 346 # there. 347 # 348 # As above, we need to take into account the presence of X; if 349 # another branch is already in the repository and points at some of 350 # the revisions that we are about to output - we don't want them. 351 # The solution is as before: git rev-parse output filtered. 352 # 353 # Finally, tags: 1 --- 2 --- O --- T --- 3 --- 4 --- N 354 # 355 # Tags pushed into the repository generate nice shortlog emails that 356 # summarise the commits between them and the previous tag. However, 357 # those emails don't include the full commit messages that we output 358 # for a branch update. Therefore we still want to output revisions 359 # that have been output on a tag email. 360 # 361 # Luckily, git rev-parse includes just the tool. Instead of using 362 # "--all" we use "--branches"; this has the added benefit that 363 # "remotes/" will be ignored as well. 364 365 # List all of the revisions that were removed by this update, in a 366 # fast-forward update, this list will be empty, because rev-list O 367 # ^N is empty. For a non-fast-forward, O ^N is the list of removed 368 # revisions 369 fast_forward="" 370 rev="" 371 for rev in $(git rev-list $newrev..$oldrev) 372 do 373 revtype=$(git cat-file -t "$rev") 374 echo " discards $rev ($revtype)" 375 done 376 if [ -z "$rev" ]; then 377 fast_forward=1 378 fi 379 380 # List all the revisions from baserev to newrev in a kind of 381 # "table-of-contents"; note this list can include revisions that 382 # have already had notification emails and is present to show the 383 # full detail of the change from rolling back the old revision to 384 # the base revision and then forward to the new revision 385 for rev in $(git rev-list $oldrev..$newrev) 386 do 387 revtype=$(git cat-file -t "$rev") 388 echo " via $rev ($revtype)" 389 done 390 391 if [ "$fast_forward" ]; then 392 echo " from $oldrev ($oldrev_type)" 393 else 394 # 1. Existing revisions were removed. In this case newrev 395 # is a subset of oldrev - this is the reverse of a 396 # fast-forward, a rewind 397 # 2. New revisions were added on top of an old revision, 398 # this is a rewind and addition. 399 400 # (1) certainly happened, (2) possibly. When (2) hasn't 401 # happened, we set a flag to indicate that no log printout 402 # is required. 403 404 echo "" 405 406 # Find the common ancestor of the old and new revisions and 407 # compare it with newrev 408 baserev=$(git merge-base $oldrev $newrev) 409 rewind_only="" 410 if [ "$baserev" = "$newrev" ]; then 411 echo "This update discarded existing revisions and left the branch pointing at" 412 echo "a previous point in the repository history." 413 echo "" 414 echo " * -- * -- N ($newrev)" 415 echo " \\" 416 echo " O -- O -- O ($oldrev)" 417 echo "" 418 echo "The removed revisions are not necessarily gone - if another reference" 419 echo "still refers to them they will stay in the repository." 420 rewind_only=1 421 else 422 echo "This update added new revisions after undoing existing revisions. That is" 423 echo "to say, the old revision is not a strict subset of the new revision. This" 424 echo "situation occurs when you --force push a change and generate a repository" 425 echo "containing something like this:" 426 echo "" 427 echo " * -- * -- B -- O -- O -- O ($oldrev)" 428 echo " \\" 429 echo " N -- N -- N ($newrev)" 430 echo "" 431 echo "When this happens we assume that you've already had alert emails for all" 432 echo "of the O revisions, and so we here report only the revisions in the N" 433 echo "branch from the common base, B." 434 fi 435 fi 436 437 echo "" 438 if [ -z "$rewind_only" ]; then 439 echo "Those revisions listed above that are new to this repository have" 440 echo "not appeared on any other notification email; so we list those" 441 echo "revisions in full, below." 442 443 echo "" 444 echo $LOGBEGIN 445 show_new_revisions 446 447 # XXX: Need a way of detecting whether git rev-list actually 448 # outputted anything, so that we can issue a "no new 449 # revisions added by this update" message 450 451 echo $LOGEND 452 else 453 echo "No new revisions were added by this update." 454 fi 455 456 # The diffstat is shown from the old revision to the new revision. 457 # This is to show the truth of what happened in this change. 458 # There's no point showing the stat from the base to the new 459 # revision because the base is effectively a random revision at this 460 # point - the user will be interested in what this revision changed 461 # - including the undoing of previous revisions in the case of 462 # non-fast-forward updates. 463 echo "" 464 echo "Summary of changes:" 465 git diff-tree $diffopts $oldrev..$newrev 466} 467 468# 469# Called for the deletion of a branch 470# 471generate_delete_branch_email() 472{ 473 echo " was $oldrev" 474 echo "" 475 echo $LOGBEGIN 476 git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev 477 echo $LOGEND 478} 479 480# --------------- Annotated tags 481 482# 483# Called for the creation of an annotated tag 484# 485generate_create_atag_email() 486{ 487 echo " at $newrev ($newrev_type)" 488 489 generate_atag_email 490} 491 492# 493# Called for the update of an annotated tag (this is probably a rare event 494# and may not even be allowed) 495# 496generate_update_atag_email() 497{ 498 echo " to $newrev ($newrev_type)" 499 echo " from $oldrev (which is now obsolete)" 500 501 generate_atag_email 502} 503 504# 505# Called when an annotated tag is created or changed 506# 507generate_atag_email() 508{ 509 # Use git for-each-ref to pull out the individual fields from the 510 # tag 511 eval $(git for-each-ref --shell --format=' 512 tagobject=%(*objectname) 513 tagtype=%(*objecttype) 514 tagger=%(taggername) 515 tagged=%(taggerdate)' $refname 516 ) 517 518 echo " tagging $tagobject ($tagtype)" 519 case "$tagtype" in 520 commit) 521 522 # If the tagged object is a commit, then we assume this is a 523 # release, and so we calculate which tag this tag is 524 # replacing 525 prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null) 526 527 if [ -n "$prevtag" ]; then 528 echo " replaces $prevtag" 529 fi 530 ;; 531 *) 532 echo " length $(git cat-file -s $tagobject) bytes" 533 ;; 534 esac 535 echo " tagged by $tagger" 536 echo " on $tagged" 537 538 echo "" 539 echo $LOGBEGIN 540 541 # Show the content of the tag message; this might contain a change 542 # log or release notes so is worth displaying. 543 git cat-file tag $newrev | sed -e '1,/^$/d' 544 545 echo "" 546 case "$tagtype" in 547 commit) 548 # Only commit tags make sense to have rev-list operations 549 # performed on them 550 if [ -n "$prevtag" ]; then 551 # Show changes since the previous release 552 git shortlog "$prevtag..$newrev" 553 else 554 # No previous tag, show all the changes since time 555 # began 556 git shortlog $newrev 557 fi 558 ;; 559 *) 560 # XXX: Is there anything useful we can do for non-commit 561 # objects? 562 ;; 563 esac 564 565 echo $LOGEND 566} 567 568# 569# Called for the deletion of an annotated tag 570# 571generate_delete_atag_email() 572{ 573 echo " was $oldrev" 574 echo "" 575 echo $LOGBEGIN 576 git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev 577 echo $LOGEND 578} 579 580# --------------- General references 581 582# 583# Called when any other type of reference is created (most likely a 584# non-annotated tag) 585# 586generate_create_general_email() 587{ 588 echo " at $newrev ($newrev_type)" 589 590 generate_general_email 591} 592 593# 594# Called when any other type of reference is updated (most likely a 595# non-annotated tag) 596# 597generate_update_general_email() 598{ 599 echo " to $newrev ($newrev_type)" 600 echo " from $oldrev" 601 602 generate_general_email 603} 604 605# 606# Called for creation or update of any other type of reference 607# 608generate_general_email() 609{ 610 # Unannotated tags are more about marking a point than releasing a 611 # version; therefore we don't do the shortlog summary that we do for 612 # annotated tags above - we simply show that the point has been 613 # marked, and print the log message for the marked point for 614 # reference purposes 615 # 616 # Note this section also catches any other reference type (although 617 # there aren't any) and deals with them in the same way. 618 619 echo "" 620 if [ "$newrev_type" = "commit" ]; then 621 echo $LOGBEGIN 622 git diff-tree -s --always --encoding=UTF-8 --pretty=medium $newrev 623 echo $LOGEND 624 else 625 # What can we do here? The tag marks an object that is not 626 # a commit, so there is no log for us to display. It's 627 # probably not wise to output git cat-file as it could be a 628 # binary blob. We'll just say how big it is 629 echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long." 630 fi 631} 632 633# 634# Called for the deletion of any other type of reference 635# 636generate_delete_general_email() 637{ 638 echo " was $oldrev" 639 echo "" 640 echo $LOGBEGIN 641 git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev 642 echo $LOGEND 643} 644 645 646# --------------- Miscellaneous utilities 647 648# 649# Show new revisions as the user would like to see them in the email. 650# 651show_new_revisions() 652{ 653 # This shows all log entries that are not already covered by 654 # another ref - i.e. commits that are now accessible from this 655 # ref that were previously not accessible 656 # (see generate_update_branch_email for the explanation of this 657 # command) 658 659 # Revision range passed to rev-list differs for new vs. updated 660 # branches. 661 if [ "$change_type" = create ] 662 then 663 # Show all revisions exclusive to this (new) branch. 664 revspec=$newrev 665 else 666 # Branch update; show revisions not part of $oldrev. 667 revspec=$oldrev..$newrev 668 fi 669 670 other_branches=$(git for-each-ref --format='%(refname)' refs/heads/ | 671 grep -F -v $refname) 672 git rev-parse --not $other_branches | 673 if [ -z "$custom_showrev" ] 674 then 675 git rev-list --pretty --stdin $revspec 676 else 677 git rev-list --stdin $revspec | 678 while read onerev 679 do 680 eval $(printf "$custom_showrev" $onerev) 681 done 682 fi 683} 684 685 686limit_lines() 687{ 688 lines=0 689 skipped=0 690 while IFS="" read -r line; do 691 lines=$((lines + 1)) 692 if [ $lines -gt $1 ]; then 693 skipped=$((skipped + 1)) 694 else 695 printf "%s\n" "$line" 696 fi 697 done 698 if [ $skipped -ne 0 ]; then 699 echo "... $skipped lines suppressed ..." 700 fi 701} 702 703 704send_mail() 705{ 706 if [ -n "$envelopesender" ]; then 707 /usr/sbin/sendmail -t -f "$envelopesender" 708 else 709 /usr/sbin/sendmail -t 710 fi 711} 712 713# ---------------------------- main() 714 715# --- Constants 716LOGBEGIN="- Log -----------------------------------------------------------------" 717LOGEND="-----------------------------------------------------------------------" 718 719# --- Config 720# Set GIT_DIR either from the working directory, or from the environment 721# variable. 722GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) 723if [ -z "$GIT_DIR" ]; then 724 echo >&2 "fatal: post-receive: GIT_DIR not set" 725 exit 1 726fi 727 728projectdesc=$(sed -ne '1p' "$GIT_DIR/description" 2>/dev/null) 729# Check if the description is unchanged from it's default, and shorten it to 730# a more manageable length if it is 731if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null 732then 733 projectdesc="UNNAMED PROJECT" 734fi 735 736recipients=$(git config hooks.mailinglist) 737announcerecipients=$(git config hooks.announcelist) 738envelopesender=$(git config hooks.envelopesender) 739emailprefix=$(git config hooks.emailprefix || echo '[SCM] ') 740custom_showrev=$(git config hooks.showrev) 741maxlines=$(git config hooks.emailmaxlines) 742diffopts=$(git config hooks.diffopts) 743: ${diffopts:="--stat --summary --find-copies-harder"} 744 745# --- Main loop 746# Allow dual mode: run from the command line just like the update hook, or 747# if no arguments are given then run as a hook script 748if [ -n "$1" -a -n "$2" -a -n "$3" ]; then 749 # Output to the terminal in command line mode - if someone wanted to 750 # resend an email; they could redirect the output to sendmail 751 # themselves 752 prep_for_email $2 $3 $1 && PAGER= generate_email 753else 754 while read oldrev newrev refname 755 do 756 prep_for_email $oldrev $newrev $refname || continue 757 generate_email $maxlines | send_mail 758 done 759fi 760