| 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 | # The email subject will contain the best description of the ref |
| 159 | # that we can build from the parameters |
| 160 | set_describe $rev |
| 161 | |
| 162 | # Call the correct body generation function |
| 163 | fn_name=general |
| 164 | case "$refname_type" in |
| 165 | "tracking branch"|branch) |
| 166 | fn_name=branch |
| 167 | ;; |
| 168 | "annotated tag") |
| 169 | fn_name=atag |
| 170 | ;; |
| 171 | esac |
| 172 | |
| 173 | case "$fn_name" in |
| 174 | branch) |
| 175 | split="" |
| 176 | revs=$(list_${change_type}_branch_revs 2>/dev/null) |
| 177 | if [ "$(echo "$revs" | wc -l)" -gt 1 ] |
| 178 | then |
| 179 | split=1 |
| 180 | fi |
| 181 | |
| 182 | if [ "$change_type" != "update" ] |
| 183 | then |
| 184 | set_update_branch_subject |
| 185 | { |
| 186 | generate_email_header |
| 187 | generate_${change_type}_branch_email |
| 188 | generate_email_footer |
| 189 | } | $send_mail |
| 190 | fi |
| 191 | |
| 192 | for rev in $revs |
| 193 | do |
| 194 | change_type=update |
| 195 | oldrev= |
| 196 | newrev=$rev |
| 197 | |
| 198 | # set_update_branch_subject will return false if the |
| 199 | # commit is boring (no change). Then we don't even |
| 200 | # send a mail. |
| 201 | set_update_branch_subject || continue |
| 202 | { |
| 203 | generate_email_header |
| 204 | print_change_info $newrev |
| 205 | summarize_branch_revs |
| 206 | generate_email_footer |
| 207 | } | $send_mail |
| 208 | done |
| 209 | ;; |
| 210 | |
| 211 | *) |
| 212 | { |
| 213 | generate_email_header |
| 214 | generate_${change_type}_${fn_name}_email |
| 215 | generate_email_footer |
| 216 | } | $send_mail |
| 217 | ;; |
| 218 | esac |
| 219 | } |
| 220 | |
| 221 | set_describe() |
| 222 | { |
| 223 | describe=$(git describe --abbrev=4 $1 2>/dev/null || git rev-parse --short $1) |
| 224 | describe=$(echo "$describe" | sed -e 's/-/./g;s/^v//;') |
| 225 | } |
| 226 | |
| 227 | generate_email_header() |
| 228 | { |
| 229 | # --- Email (all stdout will be the email) |
| 230 | # Generate header |
| 231 | print_change_type=" ${change_type}d" |
| 232 | case "$change_type" in |
| 233 | "update") |
| 234 | print_change_type="" |
| 235 | ;; |
| 236 | esac |
| 237 | |
| 238 | print_refname="$refname_type " |
| 239 | case "$refname_type" in |
| 240 | "branch") |
| 241 | print_refname="" |
| 242 | ;; |
| 243 | esac |
| 244 | |
| 245 | cat <<-EOF |
| 246 | To: $recipients |
| 247 | Subject: ${emailprefix}$describe $print_refname$short_refname${print_change_type}$detail |
| 248 | X-Git-Refname: $refname |
| 249 | X-Git-Reftype: $refname_type |
| 250 | X-Git-Newrev: $newrev |
| 251 | |
| 252 | EOF |
| 253 | } |
| 254 | |
| 255 | generate_email_footer() |
| 256 | { |
| 257 | SPACE=" " |
| 258 | cat <<-EOF |
| 259 | |
| 260 | --${SPACE} |
| 261 | $projectdesc |
| 262 | EOF |
| 263 | } |
| 264 | |
| 265 | # --------------- Branches |
| 266 | |
| 267 | # |
| 268 | # Called for the creation of a branch |
| 269 | # |
| 270 | generate_create_branch_email() |
| 271 | { |
| 272 | # This is a new branch and so oldrev is not valid |
| 273 | echo " at $newrev ($newrev_type)" |
| 274 | echo "" |
| 275 | } |
| 276 | |
| 277 | list_create_branch_revs() |
| 278 | { |
| 279 | # We want to list all revs that are reachable now, but |
| 280 | # were not before. |
| 281 | # All revs that were reachable before are git rev-parse --branches. |
| 282 | # However, this includes $refname. The naive | grep -v $branchtip |
| 283 | # will not work, because another branch might already have been on |
| 284 | # $branchtip. In this case we shouldn't list any rev. |
| 285 | # As git rev-parse --branches might list a given rev multiple times |
| 286 | # if there are multiple branches at this rev, we simply drop this |
| 287 | # $branchtip rev once, and pass all subsequent ones through. |
| 288 | |
| 289 | branchtip=$(git rev-parse $refname) |
| 290 | |
| 291 | git rev-parse --not --branches | sed -e "1,/$branchtip/{/$branchtip/d;}" | |
| 292 | git rev-list --reverse --stdin $newrev |
| 293 | } |
| 294 | |
| 295 | # |
| 296 | # Called for the change of a pre-existing branch |
| 297 | # |
| 298 | generate_update_branch_email() |
| 299 | { |
| 300 | # XXX only valid for fast forwards |
| 301 | |
| 302 | # List all the revisions from baserev to newrev in a kind of |
| 303 | # "table-of-contents"; note this list can include revisions that |
| 304 | # have already had notification emails and is present to show the |
| 305 | # full detail of the change from rolling back the old revision to |
| 306 | # the base revision and then forward to the new revision |
| 307 | for rev in $(git rev-list $oldrev..$newrev) |
| 308 | do |
| 309 | revtype=$(git cat-file -t "$rev") |
| 310 | echo " via $rev ($revtype)" |
| 311 | done |
| 312 | |
| 313 | echo " from $oldrev ($oldrev_type)" |
| 314 | |
| 315 | echo "" |
| 316 | echo $LOGBEGIN |
| 317 | } |
| 318 | |
| 319 | skip_diff_tree_parent() |
| 320 | { |
| 321 | if [ -n "$oldrev" ] |
| 322 | then |
| 323 | cat |
| 324 | else |
| 325 | tail +2 |
| 326 | fi |
| 327 | } |
| 328 | |
| 329 | set_update_branch_subject() |
| 330 | { |
| 331 | set_describe $newrev |
| 332 | |
| 333 | detail=$(git diff-tree -r -c --name-only $oldrev${oldrev:+..}$newrev 2>/dev/null | |
| 334 | skip_diff_tree_parent | |
| 335 | sed -E -e 's#/([^/]*)$# \1#' | |
| 336 | awk 'pos > 74 { |
| 337 | printf "\n"; |
| 338 | pos = 0; |
| 339 | } |
| 340 | $1 != lastdir { |
| 341 | printf " %s", $1; |
| 342 | pos += length($1) + 1; |
| 343 | lastdir = $1; |
| 344 | } |
| 345 | $2 != "" { |
| 346 | printf " %s", $2; |
| 347 | pos += length($2) + 1; |
| 348 | }') |
| 349 | [ -n "$(git diff-tree -r --name-only --cc $oldrev${oldrev:+..}$newrev 2>/dev/null | |
| 350 | skip_diff_tree_parent)" ] |
| 351 | } |
| 352 | |
| 353 | list_update_branch_revs() |
| 354 | { |
| 355 | git rev-parse --not --branches | grep -v $(git rev-parse $refname) | |
| 356 | git rev-list --reverse --stdin $oldrev..$newrev |
| 357 | } |
| 358 | |
| 359 | summarize_branch_revs() |
| 360 | { |
| 361 | echo "Summary of changes:" |
| 362 | git diff-tree --stat --summary --find-copies-harder $oldrev${oldrev:+..}$newrev | skip_diff_tree_parent |
| 363 | |
| 364 | if [ -n "$gitweburl" ] |
| 365 | then |
| 366 | echo "" |
| 367 | echo "$gitweburl/$reponame/commitdiff/$oldrev${oldrev:+..}$newrev" |
| 368 | fi |
| 369 | echo $LOGEND |
| 370 | } |
| 371 | |
| 372 | print_change_info() |
| 373 | { |
| 374 | echo $LOGBEGIN |
| 375 | git rev-list -n 1 --pretty $1 |
| 376 | } |
| 377 | |
| 378 | # |
| 379 | # Called for the deletion of a branch |
| 380 | # |
| 381 | generate_delete_branch_email() |
| 382 | { |
| 383 | echo " was $oldrev" |
| 384 | echo "" |
| 385 | echo $LOGEND |
| 386 | git show -s --pretty=oneline $oldrev |
| 387 | echo $LOGEND |
| 388 | } |
| 389 | |
| 390 | # --------------- Annotated tags |
| 391 | |
| 392 | # |
| 393 | # Called for the creation of an annotated tag |
| 394 | # |
| 395 | generate_create_atag_email() |
| 396 | { |
| 397 | echo " at $newrev ($newrev_type)" |
| 398 | |
| 399 | generate_atag_email |
| 400 | } |
| 401 | |
| 402 | # |
| 403 | # Called for the update of an annotated tag (this is probably a rare event |
| 404 | # and may not even be allowed) |
| 405 | # |
| 406 | generate_update_atag_email() |
| 407 | { |
| 408 | echo " to $newrev ($newrev_type)" |
| 409 | echo " from $oldrev (which is now obsolete)" |
| 410 | |
| 411 | generate_atag_email |
| 412 | } |
| 413 | |
| 414 | # |
| 415 | # Called when an annotated tag is created or changed |
| 416 | # |
| 417 | generate_atag_email() |
| 418 | { |
| 419 | # Use git for-each-ref to pull out the individual fields from the |
| 420 | # tag |
| 421 | eval $(git for-each-ref --shell --format=' |
| 422 | tagobject=%(*objectname) |
| 423 | tagtype=%(*objecttype) |
| 424 | tagger=%(taggername) |
| 425 | tagged=%(taggerdate)' $refname |
| 426 | ) |
| 427 | |
| 428 | echo " tagging $tagobject ($tagtype)" |
| 429 | case "$tagtype" in |
| 430 | commit) |
| 431 | |
| 432 | # If the tagged object is a commit, then we assume this is a |
| 433 | # release, and so we calculate which tag this tag is |
| 434 | # replacing |
| 435 | prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null) |
| 436 | |
| 437 | if [ -n "$prevtag" ]; then |
| 438 | echo " replaces $prevtag" |
| 439 | fi |
| 440 | ;; |
| 441 | *) |
| 442 | echo " length $(git cat-file -s $tagobject) bytes" |
| 443 | ;; |
| 444 | esac |
| 445 | echo " tagged by $tagger" |
| 446 | echo " on $tagged" |
| 447 | |
| 448 | echo "" |
| 449 | echo $LOGBEGIN |
| 450 | |
| 451 | # Show the content of the tag message; this might contain a change |
| 452 | # log or release notes so is worth displaying. |
| 453 | git cat-file tag $newrev | sed -e '1,/^$/d' |
| 454 | |
| 455 | echo "" |
| 456 | case "$tagtype" in |
| 457 | commit) |
| 458 | # Only commit tags make sense to have rev-list operations |
| 459 | # performed on them |
| 460 | if [ -n "$prevtag" ]; then |
| 461 | # Show changes since the previous release |
| 462 | git rev-list --pretty=short "$prevtag..$newrev" | git shortlog |
| 463 | else |
| 464 | # No previous tag, show all the changes since time |
| 465 | # began, but only summarize due to the possibly large size. |
| 466 | git rev-list --pretty=short $newrev | git shortlog -s |
| 467 | fi |
| 468 | ;; |
| 469 | *) |
| 470 | # XXX: Is there anything useful we can do for non-commit |
| 471 | # objects? |
| 472 | ;; |
| 473 | esac |
| 474 | |
| 475 | echo $LOGEND |
| 476 | } |
| 477 | |
| 478 | # |
| 479 | # Called for the deletion of an annotated tag |
| 480 | # |
| 481 | generate_delete_atag_email() |
| 482 | { |
| 483 | echo " was $oldrev" |
| 484 | echo "" |
| 485 | echo $LOGEND |
| 486 | git show -s --pretty=oneline $oldrev |
| 487 | echo $LOGEND |
| 488 | } |
| 489 | |
| 490 | # --------------- General references |
| 491 | |
| 492 | # |
| 493 | # Called when any other type of reference is created (most likely a |
| 494 | # non-annotated tag) |
| 495 | # |
| 496 | generate_create_general_email() |
| 497 | { |
| 498 | echo " at $newrev ($newrev_type)" |
| 499 | |
| 500 | generate_general_email |
| 501 | } |
| 502 | |
| 503 | # |
| 504 | # Called when any other type of reference is updated (most likely a |
| 505 | # non-annotated tag) |
| 506 | # |
| 507 | generate_update_general_email() |
| 508 | { |
| 509 | echo " to $newrev ($newrev_type)" |
| 510 | echo " from $oldrev" |
| 511 | |
| 512 | generate_general_email |
| 513 | } |
| 514 | |
| 515 | # |
| 516 | # Called for creation or update of any other type of reference |
| 517 | # |
| 518 | generate_general_email() |
| 519 | { |
| 520 | # Unannotated tags are more about marking a point than releasing a |
| 521 | # version; therefore we don't do the shortlog summary that we do for |
| 522 | # annotated tags above - we simply show that the point has been |
| 523 | # marked, and print the log message for the marked point for |
| 524 | # reference purposes |
| 525 | # |
| 526 | # Note this section also catches any other reference type (although |
| 527 | # there aren't any) and deals with them in the same way. |
| 528 | |
| 529 | echo "" |
| 530 | if [ "$newrev_type" = "commit" ]; then |
| 531 | echo $LOGBEGIN |
| 532 | git show --no-color --root -s --pretty=medium $newrev |
| 533 | echo $LOGEND |
| 534 | else |
| 535 | # What can we do here? The tag marks an object that is not |
| 536 | # a commit, so there is no log for us to display. It's |
| 537 | # probably not wise to output git cat-file as it could be a |
| 538 | # binary blob. We'll just say how big it is |
| 539 | echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long." |
| 540 | fi |
| 541 | } |
| 542 | |
| 543 | # |
| 544 | # Called for the deletion of any other type of reference |
| 545 | # |
| 546 | generate_delete_general_email() |
| 547 | { |
| 548 | echo " was $oldrev" |
| 549 | echo "" |
| 550 | echo $LOGEND |
| 551 | git show -s --pretty=oneline $oldrev |
| 552 | echo $LOGEND |
| 553 | } |
| 554 | |
| 555 | send_mail() |
| 556 | { |
| 557 | if [ -n "$envelopesender" ]; then |
| 558 | /usr/sbin/sendmail -t -f "$envelopesender" |
| 559 | else |
| 560 | /usr/sbin/sendmail -t |
| 561 | fi |
| 562 | } |
| 563 | |
| 564 | # ---------------------------- main() |
| 565 | |
| 566 | # --- Constants |
| 567 | LOGBEGIN="" |
| 568 | LOGEND="" |
| 569 | |
| 570 | # --- Config |
| 571 | # Set GIT_DIR either from the working directory, or from the environment |
| 572 | # variable. |
| 573 | GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) |
| 574 | if [ -z "$GIT_DIR" ]; then |
| 575 | echo >&2 "fatal: post-receive: GIT_DIR not set" |
| 576 | exit 1 |
| 577 | fi |
| 578 | |
| 579 | projectdesc=$(sed -ne '1p' "$GIT_DIR/description") |
| 580 | # Check if the description is unchanged from it's default, and shorten it to |
| 581 | # a more manageable length if it is |
| 582 | if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null |
| 583 | then |
| 584 | projectdesc="UNNAMED PROJECT" |
| 585 | fi |
| 586 | |
| 587 | recipients=$(git config hooks.mailinglist) |
| 588 | announcerecipients=$(git config hooks.announcelist) |
| 589 | envelopesender=$(git config hooks.envelopesender) |
| 590 | emailprefix=$(git config hooks.emailprefix || echo '[SCM] ') |
| 591 | gitweburl=$(git config hooks.gitweburl) |
| 592 | reponame=$(git config hooks.reponame || basename $(realpath $GIT_DIR)) |
| 593 | |
| 594 | # --- Main loop |
| 595 | # Allow dual mode: run from the command line just like the update hook, or |
| 596 | # if no arguments are given then run as a hook script |
| 597 | if [ -n "$1" -a -n "$2" -a -n "$3" ]; then |
| 598 | # Output to the terminal in command line mode - if someone wanted to |
| 599 | # resend an email; they could redirect the output to sendmail |
| 600 | # themselves |
| 601 | send_mail=cat |
| 602 | generate_email $2 $3 $1 |
| 603 | else |
| 604 | while read oldrev newrev refname |
| 605 | do |
| 606 | send_mail=send_mail |
| 607 | generate_email $oldrev $newrev $refname |
| 608 | done |
| 609 | fi |