]>
Commit | Line | Data |
---|---|---|
4557e0de AP |
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 | # | |
60 | generate_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 | ||
175 | generate_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 | ||
196 | generate_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 | # | |
212 | generate_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 | # | |
231 | generate_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 | # | |
375 | generate_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 | # | |
389 | generate_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 | # | |
400 | generate_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 | # | |
411 | generate_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 | # | |
470 | generate_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 | # | |
485 | generate_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 | # | |
496 | generate_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 | # | |
507 | generate_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 | # | |
534 | generate_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 | |
546 | EMAILPREFIX="[SCM] " | |
547 | LOGBEGIN="- Log -----------------------------------------------------------------" | |
548 | LOGEND="-----------------------------------------------------------------------" | |
549 | ||
550 | # --- Config | |
551 | # Set GIT_DIR either from the working directory, or from the environment | |
552 | # variable. | |
553 | GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) | |
554 | if [ -z "$GIT_DIR" ]; then | |
555 | echo >&2 "fatal: post-receive: GIT_DIR not set" | |
556 | exit 1 | |
557 | fi | |
558 | ||
559 | projectdesc=$(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 | |
562 | if expr "$projectdesc" : "Unnamed repository.*$" >/dev/null | |
563 | then | |
564 | projectdesc="UNNAMED PROJECT" | |
565 | fi | |
566 | ||
567 | recipients=$(git repo-config hooks.mailinglist) | |
568 | announcerecipients=$(git repo-config hooks.announcelist) | |
569 | envelopesender=$(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 | |
574 | if [ -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 | |
578 | else | |
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 | |
588 | fi |