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