8d5d5c86f1430fc5d6c165770e45ba837163deb2
[git-central.git] / server / post-receive-email
1 #!/bin/sh
2 #
3 # Copyright (c) 2007 Andy Parkins
4 # Copyright (c) 2008 Stephen Haberman
5 #
6 # This hook sends emails listing new revisions to the repository introduced by
7 # the 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 script assumes it is enabled on the central repository of a
11 # project, with all users pushing only to it and not between each other.  It
12 # will still work if you don't operate in that style, but it would become
13 # possible for the email to be from someone other than the person doing the
14 # push.
15 #
16 # Config
17 # ------
18 # hooks.post-receive-email.mailinglist
19 #   This is the list that all pushes will go to; leave it blank to not send
20 #   emails for every ref update.
21 # hooks.post-receive-email.announcelist
22 #   This is the list that all pushes of annotated tags will go to.  Leave it
23 #   blank to default to the mailinglist field.  The announce emails lists
24 #   the short log summary of the changes since the last annotated tag.
25 # hooks.post-receive-email.envelopesender
26 #   If set then the -f option is passed to sendmail to allow the envelope
27 #   sender address to be set
28 # hooks.post-receive-email.sendmail
29 #   The path to sendmail, e.g. /usr/sbin/sendmail or /bin/msmtp
30 # USER_EMAIL
31 #   Environment variable that should be set by your repository-specific
32 #   post-receive hook. E.g. export USER_EMAIL=${USER}@example.com
33 #
34 # Notes
35 # -----
36 # All emails include the headers "X-Git-Refname", "X-Git-Oldrev",
37 # "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and
38 # give information for debugging.
39 #
40
41 # ---------------------------- Functions
42
43 . $(dirname $0)/functions
44
45 #
46 # Top level email generation function.  This decides what type of update
47 # this is and calls the appropriate body-generation routine after outputting
48 # the common header
49 #
50 # Note this function doesn't actually generate any email output, that is
51 # taken care of by the functions it calls:
52 #  - generate_email_header
53 #  - generate_create_XXXX_email
54 #  - generate_update_XXXX_email
55 #  - generate_delete_XXXX_email
56 #
57 generate_email()
58 {
59         # --- Arguments
60         oldrev=$(git rev-parse $1)
61         newrev=$(git rev-parse $2)
62         refname="$3"
63
64         set_change_type
65         set_rev_types
66         set_describe_tags
67
68         # The revision type tells us what type the commit is, combined with
69         # the location of the ref we can decide between
70         #  - working branch
71         #  - tracking branch
72         #  - unannoted tag
73         #  - annotated tag
74         case "$refname","$rev_type" in
75                 refs/tags/*,commit)
76                         # un-annotated tag
77                         refname_type="tag"
78                         function="ltag"
79                         short_refname=${refname##refs/tags/}
80                         ;;
81                 refs/tags/*,tag)
82                         # annotated tag
83                         refname_type="annotated tag"
84                         function="atag"
85                         short_refname=${refname##refs/tags/}
86                         # change recipients
87                         if [ -n "$announcerecipients" ]; then
88                                 recipients="$announcerecipients"
89                         fi
90                         ;;
91                 refs/heads/*,commit)
92                         # branch
93                         refname_type="branch"
94                         function="branch"
95                         short_refname=${refname##refs/heads/}
96                         ;;
97                 refs/remotes/*,commit)
98                         # tracking branch
99                         refname_type="tracking branch"
100                         short_refname=${refname##refs/remotes/}
101                         echo >&2 "*** Push-update of tracking branch, $refname"
102                         echo >&2 "***  - no email generated."
103                         exit 0
104                         ;;
105                 *)
106                         # Anything else (is there anything else?)
107                         echo >&2 "*** Unknown type of update to $refname ($rev_type)"
108                         echo >&2 "***  - no email generated"
109                         exit 1
110                         ;;
111         esac
112
113         # Check if we've got anyone to send to
114         if [ -z "$recipients" ]; then
115                 case "$refname_type" in
116                         "annotated tag")
117                                 config_name="hooks.post-receive-email.announcelist"
118                                 ;;
119                         *)
120                                 config_name="hooks.post-receive-email.mailinglist"
121                                 ;;
122                 esac
123                 echo >&2 "*** $config_name is not set so no email will be sent"
124                 echo >&2 "*** for $refname update $oldrev->$newrev"
125                 exit 0
126         fi
127
128         generate_email_header
129         generate_${change_type}_${function}_email
130 }
131
132 generate_email_header()
133 {
134         # --- Email (all stdout will be the email)
135         # Generate header
136         cat <<-EOF
137         From: $USER_EMAIL
138         To: $recipients
139         Subject: ${emailprefix} $short_refname $refname_type ${change_type}d. $describe_tags
140         X-Git-Refname: $refname
141         X-Git-Reftype: $refname_type
142         X-Git-Oldrev: $oldrev
143         X-Git-Newrev: $newrev
144
145         The $refname_type, $short_refname has been ${change_type}d
146         EOF
147 }
148
149
150 # --------------- Branches
151
152 #
153 # Called for the creation of a branch
154 #
155 generate_create_branch_email()
156 {
157         # This is a new branch and so oldrev is not valid
158         git rev-list --pretty=format:"        at %h %s" --no-walk "$newrev" | grep -vP "^commit"
159
160         set_new_commits
161
162         echo ""
163         echo $LOGBEGIN
164         echo "$new_commits" | git rev-list --reverse --stdin | while read commit ; do
165                 echo ""
166                 git rev-list --no-walk --pretty "$commit"
167                 git diff-tree --cc "$commit"
168                 echo ""
169                 echo $LOGEND
170         done
171
172         oldest_new=$(echo "$new_commits" | git rev-list --stdin | tail -n 1)
173         if [ "$oldest_new" != "" ] ; then
174                 echo ""
175                 echo "Summary of changes:"
176                 git diff-tree --stat $oldest_new^..$newrev
177         fi
178 }
179
180 #
181 # Called for the change of a pre-existing branch
182 #
183 generate_update_branch_email()
184 {
185         # List all of the revisions that were removed by this update (hopefully empty)
186         git rev-list --first-parent --pretty=format:"  discards %h %s" $newrev..$oldrev | grep -vP "^commit"
187
188         # List all of the revisions that were added by this update
189         git rev-list --first-parent --pretty=format:"       via %h %s" $oldrev..$newrev | grep -vP "^commit"
190
191         removed=$(git rev-list $newrev..$oldrev)
192         if [ "$removed" == "" ] ; then
193                 git rev-list --no-walk --pretty=format:"      from %h %s" $oldrev | grep -vP "^commit"
194         else
195                 # Must be rewind, could be rewind+addition
196                 echo ""
197
198                 # Find the common ancestor of the old and new revisions and compare it with newrev
199                 baserev=$(git merge-base $oldrev $newrev)
200                 rewind_only=""
201                 if [ "$baserev" = "$newrev" ]; then
202                         echo "This update discarded existing revisions and left the branch pointing at"
203                         echo "a previous point in the repository history."
204                         echo ""
205                         echo " * -- * -- N ($newrev)"
206                         echo "            \\"
207                         echo "             O -- O -- O ($oldrev)"
208                         echo ""
209                         echo "The removed revisions are not necessarilly gone - if another reference"
210                         echo "still refers to them they will stay in the repository."
211                         rewind_only=1
212                 else
213                         echo "This update added new revisions after undoing existing revisions.  That is"
214                         echo "to say, the old revision is not a strict subset of the new revision.  This"
215                         echo "situation occurs when you --force push a change and generate a repository"
216                         echo "containing something like this:"
217                         echo ""
218                         echo " * -- * -- B -- O -- O -- O ($oldrev)"
219                         echo "            \\"
220                         echo "             N -- N -- N ($newrev)"
221                         echo ""
222                         echo "When this happens we assume that you've already had alert emails for all"
223                         echo "of the O revisions, and so we here report only the revisions in the N"
224                         echo "branch from the common base, B."
225                 fi
226         fi
227
228         echo ""
229         if [ -z "$rewind_only" ]; then
230                 echo "Those revisions listed above that are new to this repository have"
231                 echo "not appeared on any other notification email; so we list those"
232                 echo "revisions in full, below."
233
234                 set_new_commits
235
236                 echo ""
237                 echo $LOGBEGIN
238                 echo "$new_commits" | git rev-list --reverse --stdin | while read commit ; do
239                         echo ""
240                         git rev-list --no-walk --pretty "$commit"
241                         git diff-tree --cc "$commit"
242                         echo ""
243                         echo $LOGEND
244                 done
245
246                 # XXX: Need a way of detecting whether git rev-list actually
247                 # outputted anything, so that we can issue a "no new
248                 # revisions added by this update" message
249         else
250                 echo "No new revisions were added by this update."
251         fi
252
253         # Show the diffstat which is what really happened (new commits/whatever aside)
254         echo ""
255         echo "Summary of changes:"
256         git diff-tree --stat --find-copies-harder $oldrev..$newrev
257 }
258
259 #
260 # Called for the deletion of a branch
261 #
262 generate_delete_branch_email()
263 {
264         echo "       was  $oldrev"
265         echo ""
266         echo $LOGEND
267         git show -s --pretty=oneline $oldrev
268         echo $LOGEND
269 }
270
271 # --------------- Annotated tags
272
273 #
274 # Called for the creation of an annotated tag
275 #
276 generate_create_atag_email()
277 {
278         echo "        at $newrev ($newrev_type)"
279         generate_atag_email
280 }
281
282 #
283 # Called for the update of an annotated tag (this is probably a rare event
284 # and may not even be allowed)
285 #
286 generate_update_atag_email()
287 {
288         echo "        to $newrev ($newrev_type)"
289         echo "      from $oldrev (which is now obsolete)"
290         generate_atag_email
291 }
292
293 #
294 # Called when an annotated tag is created or changed
295 #
296 generate_atag_email()
297 {
298         # Use git for-each-ref to pull out the individual fields from the
299         # tag
300         eval $(git for-each-ref --shell --format='
301         tagobject=%(*objectname)
302         tagtype=%(*objecttype)
303         tagger=%(taggername)
304         tagged=%(taggerdate)' $refname
305         )
306
307         echo "   tagging $tagobject ($tagtype)"
308         case "$tagtype" in
309         commit)
310                 # If the tagged object is a commit, then we assume this is a
311                 # release, and so we calculate which tag this tag is replacing
312                 prevtag=$(git describe --abbrev=0 $newrev^ 2>/dev/null)
313                 if [ -n "$prevtag" ]; then
314                         echo "  replaces $prevtag"
315                 fi
316                 ;;
317         *)
318                 echo "    length $(git cat-file -s $tagobject) bytes"
319                 ;;
320         esac
321         echo " tagged by $tagger"
322         echo "        on $tagged"
323
324         echo ""
325         echo $LOGBEGIN
326
327         # Show the content of the tag message; this might contain a change
328         # log or release notes so is worth displaying.
329         git cat-file tag $newrev | sed -e '1,/^$/d'
330
331         echo ""
332         case "$tagtype" in
333         commit)
334                 # Only commit tags make sense to have rev-list operations
335                 # performed on them
336                 if [ -n "$prevtag" ]; then
337                         # Show changes since the previous release
338                         git rev-list --pretty=short "$prevtag..$newrev" | git shortlog
339                 else
340                         # No previous tag, show all the changes since time
341                         # began
342                         git rev-list --pretty=short $newrev | git shortlog
343                 fi
344                 ;;
345         *)
346                 # XXX: Is there anything useful we can do for non-commit
347                 # objects?
348                 ;;
349         esac
350
351         echo $LOGEND
352 }
353
354 #
355 # Called for the deletion of an annotated tag
356 #
357 generate_delete_atag_email()
358 {
359         echo "       was $oldrev ($oldrev_type)"
360         echo ""
361         echo $LOGEND
362         git show -s --pretty=oneline $oldrev
363         echo $LOGEND
364 }
365
366 # --------------- General references
367
368 #
369 # Called when any other type of reference is created (most likely a
370 # non-annotated tag)
371 #
372 generate_create_ltag_email()
373 {
374         echo "        at $newrev ($newrev_type)"
375         generate_ltag_email
376 }
377
378 #
379 # Called when any other type of reference is updated (most likely a
380 # non-annotated tag)
381 #
382 generate_update_ltag_email()
383 {
384         echo "        to $newrev ($newrev_type)"
385         echo "      from $oldrev ($oldrev_type)"
386         generate_ltag_email
387 }
388
389 #
390 # Called for creation or update of any other type of reference
391 #
392 generate_ltag_email()
393 {
394         # Unannotated tags are more about marking a point than releasing a
395         # version; therefore we don't do the shortlog summary that we do for
396         # annotated tags above - we simply show that the point has been
397         # marked, and print the log message for the marked point for
398         # reference purposes
399         #
400         # Note this section also catches any other reference type (although
401         # there aren't any) and deals with them in the same way.
402
403         echo ""
404         if [ "$newrev_type" = "commit" ]; then
405                 echo $LOGBEGIN
406                 git show --no-color --root -s --pretty=medium $newrev
407                 echo $LOGEND
408         else
409                 # What can we do here?  The tag marks an object that is not
410                 # a commit, so there is no log for us to display.  It's
411                 # probably not wise to output git cat-file as it could be a
412                 # binary blob.  We'll just say how big it is
413                 echo "$newrev is a $newrev_type, and is $(git cat-file -s $newrev) bytes long."
414         fi
415 }
416
417 #
418 # Called for the deletion of any other type of reference
419 #
420 generate_delete_ltag_email()
421 {
422         echo "       was $oldrev ($oldrev_type)"
423         echo ""
424         echo $LOGEND
425         git show -s --pretty=oneline $oldrev
426         echo $LOGEND
427 }
428
429 send_mail()
430 {
431         if [ -n "$envelopesender" ] ; then
432                 $sendmail -t -f "$envelopesender"
433         else
434                 $sendmail -t
435         fi
436 }
437
438 # ---------------------------- main()
439
440 # --- Constants
441 LOGBEGIN="- Log -----------------------------------------------------------------"
442 LOGEND="-----------------------------------------------------------------------"
443
444 # --- Config
445 # Set GIT_DIR either from the working directory or the environment variable.
446 GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
447 if [ -z "$GIT_DIR" ]; then
448         echo >&2 "fatal: post-receive: GIT_DIR not set"
449         exit 1
450 fi
451
452 projectdesc=$(sed -ne '1p' "$GIT_DIR/description")
453 # Shorten the description if it's the default
454 if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null ; then
455         projectdesc="UNNAMED"
456 fi
457
458 recipients=$(git config hooks.post-receive-email.mailinglist)
459 announcerecipients=$(git config hooks.post-receive-email.announcelist)
460 envelopesender=$(git config hooks.post-receive-email.envelopesender)
461 emailprefix="[$projectdesc]"
462 debug=$(git config hooks.post-receive-email.debug)
463 sendmail=$(git config hooks.post-receive-email.sendmail)
464
465 # --- Main loop
466 # Allow dual mode: run from the command line just like the update hook, or
467 # if no arguments are given then run as a hook script
468 if [ -n "$1" -a -n "$2" -a -n "$3" ]; then
469         # Output to the terminal in command line mode - if someone wanted to
470         # resend an email; they could redirect the output to sendmail
471         # themselves
472         PAGER= generate_email $2 $3 $1
473 else
474         while read oldrev newrev refname
475         do
476                 if [ "$debug" == "true" ] ; then
477                         generate_email $oldrev $newrev $refname > "${refname//\//.}.out"
478                 else
479                         generate_email $oldrev $newrev $refname | send_mail
480                 fi
481         done
482 fi
483