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