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) 101ifexpr"$oldrev":'0*$'>/dev/null 102then 103 change_type="create" 104else 105ifexpr"$newrev":'0*$'>/dev/null 106then 107 change_type="delete" 108else 109 change_type="update" 110fi 111fi 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) 116case"$change_type"in 117 create|update) 118rev="$newrev" 119 rev_type="$newrev_type" 120;; 121 delete) 122rev="$oldrev" 123 rev_type="$oldrev_type" 124;; 125esac 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 133case"$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 144if[-n"$announcerecipients"];then 145 recipients="$announcerecipients" 146fi 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/} 157echo>&2"*** Push-update of tracking branch,$refname" 158echo>&2"*** - no email generated." 159return1 160;; 161*) 162# Anything else (is there anything else?) 163echo>&2"*** Unknown type of update to$refname($rev_type)" 164echo>&2"*** - no email generated" 165return1 166;; 167esac 168 169# Check if we've got anyone to send to 170if[-z"$recipients"];then 171case"$refname_type"in 172"annotated tag") 173 config_name="hooks.announcelist" 174;; 175*) 176 config_name="hooks.mailinglist" 177;; 178esac 179echo>&2"***$config_nameis not set so no email will be sent" 180echo>&2"*** for$refnameupdate$oldrev->$newrev" 181return1 182fi 183 184return0 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) 211if[-z"$describe"];then 212 describe=$rev 213fi 214 215 generate_email_header 216 217# Call the correct body generation function 218 fn_name=general 219case"$refname_type"in 220"tracking branch"|branch) 221 fn_name=branch 222;; 223"annotated tag") 224 fn_name=atag 225;; 226esac 227 228if[-z"$maxlines"];then 229 generate_${change_type}_${fn_name}_email 230else 231 generate_${change_type}_${fn_name}_email | limit_lines $maxlines 232fi 233 234 generate_email_footer 235} 236 237generate_email_header() 238{ 239# --- Email (all stdout will be the email) 240# Generate header 241cat<<-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_refnamehas been${change_type}d 258 EOF 259} 260 261generate_email_footer() 262{ 263 SPACE=" " 264cat<<-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 281echo" at$newrev($newrev_type)" 282echo"" 283 284echo$LOGBEGIN 285 show_new_revisions 286echo$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# Get's 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="" 370rev="" 371forrevin$(git rev-list $newrev..$oldrev) 372do 373 revtype=$(git cat-file -t "$rev") 374echo" discards$rev($revtype)" 375done 376if[-z"$rev"];then 377 fast_forward=1 378fi 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 385forrevin$(git rev-list $oldrev..$newrev) 386do 387 revtype=$(git cat-file -t "$rev") 388echo" via$rev($revtype)" 389done 390 391if["$fast_forward"];then 392echo" from$oldrev($oldrev_type)" 393else 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 404echo"" 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="" 410if["$baserev"="$newrev"];then 411echo"This update discarded existing revisions and left the branch pointing at" 412echo"a previous point in the repository history." 413echo"" 414echo" * -- * -- N ($newrev)" 415echo"\\" 416echo" O -- O -- O ($oldrev)" 417echo"" 418echo"The removed revisions are not necessarily gone - if another reference" 419echo"still refers to them they will stay in the repository." 420 rewind_only=1 421else 422echo"This update added new revisions after undoing existing revisions. That is" 423echo"to say, the old revision is not a strict subset of the new revision. This" 424echo"situation occurs when you --force push a change and generate a repository" 425echo"containing something like this:" 426echo"" 427echo" * -- * -- B -- O -- O -- O ($oldrev)" 428echo"\\" 429echo" N -- N -- N ($newrev)" 430echo"" 431echo"When this happens we assume that you've already had alert emails for all" 432echo"of the O revisions, and so we here report only the revisions in the N" 433echo"branch from the common base, B." 434fi 435fi 436 437echo"" 438if[-z"$rewind_only"];then 439echo"Those revisions listed above that are new to this repository have" 440echo"not appeared on any other notification email; so we list those" 441echo"revisions in full, below." 442 443echo"" 444echo$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 451echo$LOGEND 452else 453echo"No new revisions were added by this update." 454fi 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. 463echo"" 464echo"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{ 473echo" was$oldrev" 474echo"" 475echo$LOGBEGIN 476 git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev 477echo$LOGEND 478} 479 480# --------------- Annotated tags 481 482# 483# Called for the creation of an annotated tag 484# 485generate_create_atag_email() 486{ 487echo" 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{ 498echo" to$newrev($newrev_type)" 499echo" 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 511eval $(git for-each-ref --shell --format=' 512 tagobject=%(*objectname) 513 tagtype=%(*objecttype) 514 tagger=%(taggername) 515 tagged=%(taggerdate)'$refname 516) 517 518echo" tagging$tagobject($tagtype)" 519case"$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 527if[-n"$prevtag"];then 528echo" replaces$prevtag" 529fi 530;; 531*) 532echo" length$(git cat-file -s $tagobject)bytes" 533;; 534esac 535echo" tagged by$tagger" 536echo" on$tagged" 537 538echo"" 539echo$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 545echo"" 546case"$tagtype"in 547 commit) 548# Only commit tags make sense to have rev-list operations 549# performed on them 550if[-n"$prevtag"];then 551# Show changes since the previous release 552 git shortlog "$prevtag..$newrev" 553else 554# No previous tag, show all the changes since time 555# began 556 git shortlog $newrev 557fi 558;; 559*) 560# XXX: Is there anything useful we can do for non-commit 561# objects? 562;; 563esac 564 565echo$LOGEND 566} 567 568# 569# Called for the deletion of an annotated tag 570# 571generate_delete_atag_email() 572{ 573echo" was$oldrev" 574echo"" 575echo$LOGBEGIN 576 git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev 577echo$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{ 588echo" 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{ 599echo" to$newrev($newrev_type)" 600echo" 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 619echo"" 620if["$newrev_type"="commit"];then 621echo$LOGBEGIN 622 git diff-tree -s --always --encoding=UTF-8 --pretty=medium $newrev 623echo$LOGEND 624else 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 629echo"$newrevis a$newrev_type, and is$(git cat-file -s $newrev)bytes long." 630fi 631} 632 633# 634# Called for the deletion of any other type of reference 635# 636generate_delete_general_email() 637{ 638echo" was$oldrev" 639echo"" 640echo$LOGBEGIN 641 git diff-tree -s --always --encoding=UTF-8 --pretty=oneline $oldrev 642echo$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. 661if["$change_type"= create ] 662then 663# Show all revisions exclusive to this (new) branch. 664 revspec=$newrev 665else 666# Branch update; show revisions not part of $oldrev. 667 revspec=$oldrev..$newrev 668fi 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 "...$skippedlines 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 731ifexpr"$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 754whileread oldrev newrev refname 755do 756 prep_for_email $oldrev $newrev $refname||continue 757 generate_email $maxlines| send_mail 758done 759fi