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