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