| 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 | # Config |
| 27 | # ------ |
| 28 | # hooks.mailinglist |
| 29 | # This is the list that all pushes will go to; leave it blank to not send |
| 30 | # emails for every ref update. |
| 31 | # hooks.announcelist |
| 32 | # This is the list that all pushes of annotated tags will go to. Leave it |
| 33 | # blank to default to the mailinglist field. The announce emails lists |
| 34 | # the short log summary of the changes since the last annotated tag. |
| 35 | # hooks.envelopesender |
| 36 | # If set then the -f option is passed to sendmail to allow the envelope |
| 37 | # sender address to be set |
| 38 | # hooks.emailprefix |
| 39 | # All emails have their subjects prefixed with this prefix, or "[SCM]" |
| 40 | # if emailprefix is unset, to aid filtering |
| 41 | # |
| 42 | # Notes |
| 43 | # ----- |
| 44 | # All emails include the headers "X-Git-Refname", "X-Git-Oldrev", |
| 45 | # "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and |
| 46 | # give information for debugging. |
| 47 | # |
| 48 | |
| 49 | # ---------------------------- Functions |
| 50 | |
| 51 | # |
| 52 | # Top level email generation function. This decides what type of update |
| 53 | # this is and calls the appropriate body-generation routine after outputting |
| 54 | # the common header |
| 55 | # |
| 56 | # Note this function doesn't actually generate any email output, that is |
| 57 | # taken care of by the functions it calls: |
| 58 | # - generate_email_header |
| 59 | # - generate_create_XXXX_email |
| 60 | # - generate_update_XXXX_email |
| 61 | # - generate_delete_XXXX_email |
| 62 | # - generate_email_footer |
| 63 | # |
| 64 | generate_email() |
| 65 | { |
| 66 | # --- Arguments |
| 67 | oldrev=$(git rev-parse $1) |
| 68 | newrev=$(git rev-parse $2) |
| 69 | refname="$3" |
| 70 | |
| 71 | # --- Interpret |
| 72 | # 0000->1234 (create) |
| 73 | # 1234->2345 (update) |
| 74 | # 2345->0000 (delete) |
| 75 | if expr "$oldrev" : '0*$' >/dev/null |
| 76 | then |
| 77 | change_type="create" |
| 78 | else |
| 79 | if expr "$newrev" : '0*$' >/dev/null |
| 80 | then |
| 81 | change_type="delete" |
| 82 | else |
| 83 | change_type="update" |
| 84 | fi |
| 85 | fi |
| 86 | |
| 87 | # --- Get the revision types |
| 88 | newrev_type=$(git cat-file -t $newrev 2> /dev/null) |
| 89 | oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null) |
| 90 | case "$change_type" in |
| 91 | create|update) |
| 92 | rev="$newrev" |
| 93 | rev_type="$newrev_type" |
| 94 | ;; |
| 95 | delete) |
| 96 | rev="$oldrev" |
| 97 | rev_type="$oldrev_type" |
| 98 | ;; |
| 99 | esac |
| 100 | |
| 101 | # The revision type tells us what type the commit is, combined with |
| 102 | # the location of the ref we can decide between |
| 103 | # - working branch |
| 104 | # - tracking branch |
| 105 | # - unannoted tag |
| 106 | # - annotated tag |
| 107 | case "$refname","$rev_type" in |
| 108 | refs/tags/*,commit) |
| 109 | # un-annotated tag |
| 110 | refname_type="tag" |
| 111 | short_refname=${refname##refs/tags/} |
| 112 | ;; |
| 113 | refs/tags/*,tag) |
| 114 | # annotated tag |
| 115 | refname_type="annotated tag" |
| 116 | short_refname=${refname##refs/tags/} |
| 117 | # change recipients |
| 118 | if [ -n "$announcerecipients" ]; then |
| 119 | recipients="$announcerecipients" |
| 120 | fi |
| 121 | ;; |
| 122 | refs/heads/*,commit) |
| 123 | # branch |
| 124 | refname_type="branch" |
| 125 | short_refname=${refname##refs/heads/} |
| 126 | ;; |
| 127 | refs/remotes/*,commit) |
| 128 | # tracking branch |
| 129 | refname_type="tracking branch" |
| 130 | short_refname=${refname##refs/remotes/} |
| 131 | echo >&2 "*** Push-update of tracking branch, $refname" |
| 132 | echo >&2 "*** - no email generated." |
| 133 | exit 0 |
| 134 | ;; |
| 135 | *) |
| 136 | # Anything else (is there anything else?) |
| 137 | echo >&2 "*** Unknown type of update to $refname ($rev_type)" |
| 138 | echo >&2 "*** - no email generated" |
| 139 | exit 1 |
| 140 | ;; |
| 141 | esac |
| 142 | |
| 143 | # Check if we've got anyone to send to |
| 144 | if [ -z "$recipients" ]; then |
| 145 | case "$refname_type" in |
| 146 | "annotated tag") |
| 147 | config_name="hooks.announcelist" |
| 148 | ;; |
| 149 | *) |
| 150 | config_name="hooks.mailinglist" |
| 151 | ;; |
| 152 | esac |
| 153 | echo >&2 "*** $config_name is not set so no email will be sent" |
| 154 | echo >&2 "*** for $refname update $oldrev->$newrev" |
| 155 | exit 0 |
| 156 | fi |
| 157 | |
| 158 | # Email parameters |
| 159 | # The email subject will contain the best description of the ref |
| 160 | # that we can build from the parameters |
| 161 | describe=$(git describe $rev 2>/dev/null) |
| 162 | if [ -z "$describe" ]; then |
| 163 | describe=$rev |
| 164 | fi |
| 165 | |
| 166 | # Call the correct body generation function |
| 167 | fn_name=general |
| 168 | case "$refname_type" in |
| 169 | "tracking branch"|branch) |
| 170 | fn_name=branch |
| 171 | ;; |
| 172 | "annotated tag") |
| 173 | fn_name=atag |
| 174 | ;; |
| 175 | esac |
| 176 | |
| 177 | case "$fn_name" in |
| 178 | branch) |
| 179 | split="" |
| 180 | revs=$(list_${change_type}_branch_revs 2>/dev/null) |
| 181 | if [ "$(echo "$revs" | wc -l)" -gt 1 ] |
| 182 | then |
| 183 | split=1 |
| 184 | fi |
| 185 | |
| 186 | if [ -z "$split" -a "$change_type" = "update" ] |
| 187 | then |
| 188 | { |
| 189 | set_update_branch_subject |
| 190 | generate_email_header |
| 191 | print_change_info $newrev |
| 192 | summarize_branch_revs |
| 193 | generate_email_footer |
| 194 | } | $send_mail |
| 195 | else |
| 196 | { |
| 197 | set_update_branch_subject |
| 198 | generate_email_header |
| 199 | generate_${change_type}_branch_email |
| 200 | summarize_branch_revs |
| 201 | generate_email_footer |
| 202 | } | $send_mail |
| 203 | |
| 204 | for rev in $revs |
| 205 | do |
| 206 | newrev=$rev |
| 207 | { |
| 208 | set_update_branch_subject |
| 209 | generate_email_header |
| 210 | print_change_info $newrev |
| 211 | summarize_branch_revs |
| 212 | generate_email_footer |
| 213 | } | $send_mail |
| 214 | oldrev=$rev |
| 215 | done |
| 216 | fi |
| 217 | |
| 218 | ;; |
| 219 | |
| 220 | *) |
| 221 | { |
| 222 | generate_email_header |
| 223 | generate_${change_type}_${fn_name}_email |
| 224 | generate_email_footer |
| 225 | } | $send_mail |
| 226 | ;; |
| 227 | esac |
| 228 | } |
| 229 | |
| 230 | generate_email_header() |
| 231 | { |
| 232 | # --- Email (all stdout will be the email) |
| 233 | # Generate header |
| 234 | print_change_type=" ${change_type}d" |
| 235 | case "$change_type" in |
| 236 | "update") |
| 237 | print_change_type="" |
| 238 | ;; |
| 239 | esac |
| 240 | |
| 241 | cat <<-EOF |
| 242 | To: $recipients |
| 243 | Subject: ${emailprefix}$projectdesc $refname_type $short_refname${print_change_type}: $detail$describe |
| 244 | X-Git-Refname: $refname |
| 245 | X-Git-Reftype: $refname_type |
| 246 | X-Git-Oldrev: $oldrev |
| 247 | X-Git-Newrev: $newrev |
| 248 | |
| 249 | EOF |
| 250 | } |
| 251 | |
| 252 | generate_email_footer() |
| 253 | { |
| 254 | SPACE=" " |
| 255 | cat <<-EOF |
| 256 | |
| 257 | |
| 258 | --${SPACE} |
| 259 | $projectdesc |
| 260 | EOF |
| 261 | } |
| 262 | |
| 263 | # --------------- Branches |
| 264 | |
| 265 | # |
| 266 | # Called for the creation of a branch |
| 267 | # |
| 268 | generate_create_branch_email() |
| 269 | { |
| 270 | # This is a new branch and so oldrev is not valid |
| 271 | echo " at $newrev ($newrev_type)" |
| 272 | } |
| 273 | |
| 274 | list_create_branch_revs() |
| 275 | { |
| 276 | git rev-parse --not --branches | grep -v $(git rev-parse $refname) | |
| 277 | git rev-list --reverse --stdin $newrev |
| 278 | } |
| 279 | |
| 280 | # |
| 281 | # Called for the change of a pre-existing branch |
| 282 | # |
| 283 | generate_update_branch_email() |
| 284 | { |
| 285 | # XXX only valid for fast forwards |
| 286 | |
| 287 | # List all the revisions from baserev to newrev in a kind of |
| 288 | # "table-of-contents"; note this list can include revisions that |
| 289 | # have already had notification emails and is present to show the |
| 290 | # full detail of the change from rolling back the old revision to |
| 291 | # the base revision and then forward to the new revision |
| 292 | for rev in $(git rev-list $oldrev..$newrev) |
| 293 | do |
| 294 | revtype=$(git cat-file -t "$rev") |
| 295 | echo " via $rev ($revtype)" |
| 296 | done |
| 297 | |
| 298 | echo " from $oldrev ($oldrev_type)" |
| 299 | |
| 300 | echo "" |
| 301 | } |
| 302 | |
| 303 | set_update_branch_subject() |
| 304 | { |
| 305 | detail="$(git diff-tree -r --name-only $oldrev..$newrev | tr '\n' ' ') " |
| 306 | } |
| 307 | |
| 308 | list_update_branch_revs() |
| 309 | { |
| 310 | git rev-parse --not --branches | grep -v $(git rev-parse $refname) | |
| 311 | git rev-list --reverse --stdin $oldrev..$newrev |
| 312 | } |
| 313 | |
| 314 | summarize_branch_revs() |
| 315 | { |
| 316 | echo "Summary of changes:" |
| 317 | git diff-tree --stat --summary --find-copies-harder $oldrev..$newrev |
| 318 | } |
| 319 | |
| 320 | print_change_info() |
| 321 | { |
| 322 | git rev-list -n 1 --pretty $1 |
| 323 | } |
| 324 | |
| 325 | # |
| 326 | # Called for the deletion of a branch |
| 327 | # |
| 328 | generate_delete_branch_email() |
| 329 | { |
| 330 | echo " was $oldrev" |
| 331 | echo "" |
| 332 | echo $LOGEND |
| 333 | git show -s --pretty=oneline $oldrev |
| 334 | echo $LOGEND |
| 335 | } |
| 336 | |
| 337 | # --------------- Annotated tags |
| 338 | |
| 339 | # |
| 340 | # Called for the creation of an annotated tag |
| 341 | # |
| 342 | generate_create_atag_email() |
| 343 | { |
| 344 | echo " at $newrev ($newrev_type)" |
| 345 | |
| 346 | generate_atag_email |
| 347 | } |
| 348 | |
| 349 | # |
| 350 | # Called for the update of an annotated tag (this is probably a rare event |
| 351 | # and may not even be allowed) |
| 352 | # |
| 353 | generate_update_atag_email() |
| 354 | { |
| 355 | echo " to $newrev ($newrev_type)" |
| 356 | echo " from $oldrev (which is now obsolete)" |
| 357 | |
| 358 | generate_atag_email |
| 359 | } |
| 360 | |
| 361 | # |
| 362 | # Called when an annotated tag is created or changed |
| 363 | # |
| 364 | generate_atag_email() |
| 365 | { |
| 366 | # Use git for-each-ref to pull out the individual fields from the |
| 367 | # tag |
| 368 | eval $(git for-each-ref --shell --format=' |
| 369 | tagobject=%(*objectname) |
| 370 | tagtype=%(*objecttype) |
| 371 | tagger=%(taggername) |
| 372 | tagged=%(taggerdate)' $refname |
| 373 | ) |
| 374 | |
| 375 | echo " tagging $tagobject ($tagtype)" |
| 376 | case "$tagtype" in |
| 377 | commit) |
| 378 | |
| 379 | # If the tagged object is a commit, then we assume this is a |
| 380 | # release, and so we calculate which tag this tag is |
| 381 | # replacing |
| 382 | prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null) |
| 383 | |
| 384 | if [ -n "$prevtag" ]; then |
| 385 | echo " replaces $prevtag" |
| 386 | fi |
| 387 | ;; |
| 388 | *) |
| 389 | echo " length $(git cat-file -s $tagobject) bytes" |
| 390 | ;; |
| 391 | esac |
| 392 | echo " tagged by $tagger" |
| 393 | echo " on $tagged" |
| 394 | |
| 395 | echo "" |
| 396 | echo $LOGBEGIN |
| 397 | |
| 398 | # Show the content of the tag message; this might contain a change |
| 399 | # log or release notes so is worth displaying. |
| 400 | git cat-file tag $newrev | sed -e '1,/^$/d' |
| 401 | |
| 402 | echo "" |
| 403 | case "$tagtype" in |
| 404 | commit) |
| 405 | # Only commit tags make sense to have rev-list operations |
| 406 | # performed on them |
| 407 | if [ -n "$prevtag" ]; then |
| 408 | # Show changes since the previous release |
| 409 | git rev-list --pretty=short "$prevtag..$newrev" | git shortlog |
| 410 | else |
| 411 | # No previous tag, show all the changes since time |
| 412 | # began |
| 413 | git rev-list --pretty=short $newrev | git shortlog |
| 414 | fi |
| 415 | ;; |
| 416 | *) |
| 417 | # XXX: Is there anything useful we can do for non-commit |
| 418 | # objects? |
| 419 | ;; |
| 420 | esac |
| 421 | |
| 422 | echo $LOGEND |
| 423 | } |
| 424 | |
| 425 | # |
| 426 | # Called for the deletion of an annotated tag |
| 427 | # |
| 428 | generate_delete_atag_email() |
| 429 | { |
| 430 | echo " was $oldrev" |
| 431 | echo "" |
| 432 | echo $LOGEND |
| 433 | git show -s --pretty=oneline $oldrev |
| 434 | echo $LOGEND |
| 435 | } |
| 436 | |
| 437 | # --------------- General references |
| 438 | |
| 439 | # |
| 440 | # Called when any other type of reference is created (most likely a |
| 441 | # non-annotated tag) |
| 442 | # |
| 443 | generate_create_general_email() |
| 444 | { |
| 445 | echo " at $newrev ($newrev_type)" |
| 446 | |
| 447 | generate_general_email |
| 448 | } |
| 449 | |
| 450 | # |
| 451 | # Called when any other type of reference is updated (most likely a |
| 452 | # non-annotated tag) |
| 453 | # |
| 454 | generate_update_general_email() |
| 455 | { |
| 456 | echo " to $newrev ($newrev_type)" |
| 457 | echo " from $oldrev" |
| 458 | |
| 459 | generate_general_email |
| 460 | } |
| 461 | |
| 462 | # |
| 463 | # Called for creation or update of any other type of reference |
| 464 | # |
| 465 | generate_general_email() |
| 466 | { |
| 467 | # Unannotated tags are more about marking a point than releasing a |
| 468 | # version; therefore we don't do the shortlog summary that we do for |
| 469 | # annotated tags above - we simply show that the point has been |
| 470 | # marked, and print the log message for the marked point for |
| 471 | # reference purposes |
| 472 | # |
| 473 | # Note this section also catches any other reference type (although |
| 474 | # there aren't any) and deals with them in the same way. |
| 475 | |
| 476 | echo "" |
| 477 | if [ "$newrev_type" = "commit" ]; then |
| 478 | echo $LOGBEGIN |
| 479 | git show --no-color --root -s --pretty=medium $newrev |
| 480 | echo $LOGEND |
| 481 | else |
| 482 | # What can we do here? The tag marks an object that is not |
| 483 | # a commit, so there is no log for us to display. It's |
| 484 | # probably not wise to output git cat-file as it could be a |
| 485 | # binary blob. We'll just say how big it is |
| 486 | echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long." |
| 487 | fi |
| 488 | } |
| 489 | |
| 490 | # |
| 491 | # Called for the deletion of any other type of reference |
| 492 | # |
| 493 | generate_delete_general_email() |
| 494 | { |
| 495 | echo " was $oldrev" |
| 496 | echo "" |
| 497 | echo $LOGEND |
| 498 | git show -s --pretty=oneline $oldrev |
| 499 | echo $LOGEND |
| 500 | } |
| 501 | |
| 502 | send_mail() |
| 503 | { |
| 504 | if [ -n "$envelopesender" ]; then |
| 505 | /usr/sbin/sendmail -t -f "$envelopesender" |
| 506 | else |
| 507 | /usr/sbin/sendmail -t |
| 508 | fi |
| 509 | } |
| 510 | |
| 511 | # ---------------------------- main() |
| 512 | |
| 513 | # --- Constants |
| 514 | LOGBEGIN="- Log -----------------------------------------------------------------" |
| 515 | LOGEND="-----------------------------------------------------------------------" |
| 516 | |
| 517 | # --- Config |
| 518 | # Set GIT_DIR either from the working directory, or from the environment |
| 519 | # variable. |
| 520 | GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) |
| 521 | if [ -z "$GIT_DIR" ]; then |
| 522 | echo >&2 "fatal: post-receive: GIT_DIR not set" |
| 523 | exit 1 |
| 524 | fi |
| 525 | |
| 526 | projectdesc=$(sed -ne '1p' "$GIT_DIR/description") |
| 527 | # Check if the description is unchanged from it's default, and shorten it to |
| 528 | # a more manageable length if it is |
| 529 | if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null |
| 530 | then |
| 531 | projectdesc="UNNAMED PROJECT" |
| 532 | fi |
| 533 | |
| 534 | recipients=$(git config hooks.mailinglist) |
| 535 | announcerecipients=$(git config hooks.announcelist) |
| 536 | envelopesender=$(git config hooks.envelopesender) |
| 537 | emailprefix=$(git config hooks.emailprefix || echo '[SCM] ') |
| 538 | |
| 539 | # --- Main loop |
| 540 | # Allow dual mode: run from the command line just like the update hook, or |
| 541 | # if no arguments are given then run as a hook script |
| 542 | if [ -n "$1" -a -n "$2" -a -n "$3" ]; then |
| 543 | # Output to the terminal in command line mode - if someone wanted to |
| 544 | # resend an email; they could redirect the output to sendmail |
| 545 | # themselves |
| 546 | send_mail=cat |
| 547 | generate_email $2 $3 $1 |
| 548 | else |
| 549 | while read oldrev newrev refname |
| 550 | do |
| 551 | send_mail=send_mail |
| 552 | generate_email $oldrev $newrev $refname |
| 553 | done |
| 554 | fi |