git-multimail: update to release 1.4.0

Changes are described in CHANGES. Contributions-by: Matthieu Moy <Matthieu.Moy@imag.fr> Contributions-by: Irfan Adilovic <irfanadilovic@gmail.com> Signed-off-by: Matthieu Moy <Matthieu.Moy@imag.fr> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Matthieu Moy committed Aug 17, 2016 at 08:41 UTC 7c5543115edf58b3b9b188f6f1377660bec10629
6 files changed +725 -276
contrib/hooks/multimail/CHANGES
+59
@@ -1,3 +1,62 @@
1 +Release 1.4.0
2 +=============
3 +
4 +New features to troubleshoot a git-multimail installation
5 +---------------------------------------------------------
6 +
7 +* One can now perform a basic check of git-multimail's setup by
8 + running the hook with the environment variable
9 + GIT_MULTIMAIL_CHECK_SETUP set to a non-empty string. See
10 + doc/troubleshooting.rst for details.
11 +
12 +* A new log files system was added. See the multimailhook.logFile,
13 + multimailhook.errorLogFile and multimailhook.debugLogFile variables.
14 +
15 +* git_multimail.py can now be made more verbose using
16 + multimailhook.verbose.
17 +
18 +* A new option --check-ref-filter is now available to help debugging
19 + the refFilter* options.
20 +
21 +Formatting emails
22 +-----------------
23 +
24 +* Formatting of emails was made slightly more compact, to reduce the
25 + odds of having long subject lines truncated or wrapped in short list
26 + of commits.
27 +
28 +* multimailhook.emailPrefix may now use the '%(repo_shortname)s'
29 + placeholder for the repository's short name.
30 +
31 +* A new option multimailhook.subjectMaxLength is available to truncate
32 + overly long subject lines.
33 +
34 +Bug fixes and minor changes
35 +---------------------------
36 +
37 +* Options refFilterDoSendRegex and refFilterDontSendRegex were
38 + essentially broken. They should work now.
39 +
40 +* The behavior when both refFilter{Do,Dont}SendRegex and
41 + refFilter{Exclusion,Inclusion}Regex are set have been slightly
42 + changed. Exclusion/Inclusion is now strictly stronger than
43 + DoSend/DontSend.
44 +
45 +* The management of precedence when a setting can be computed in
46 + multiple ways has been considerably refactored and modified.
47 + multimailhook.from and multimailhook.reponame now have precedence
48 + over the environment-specific settings ($GL_REPO/$GL_USER for
49 + gitolite, --stash-user/repo for Stash, --submitter/--project for
50 + Gerrit).
51 +
52 +* The coverage of the testsuite has been considerably improved. All
53 + configuration variables now appear at least once in the testsuite.
54 +
55 +This version was tested with Python 2.6 to 3.5. It also mostly works
56 +with Python 2.4, but there is one known breakage in the testsuite
57 +related to non-ascii characters. It was tested with Git
58 +1.7.10.406.gdc801, 1.8.5.6, 2.1.4, and 2.10.0.rc0.1.g07c9292.
59 +
60 Release 1.3.1 (bugfix-only release)
61 ===================================
62
contrib/hooks/multimail/CONTRIBUTING.rst
+7 -2
@@ -4,8 +4,9 @@ Contributing
4 git-multimail is an open-source project, built by volunteers. We would
5 welcome your help!
6
7 -The current maintainers are Michael Haggerty <mhagger@alum.mit.edu>
8 -and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.
7 +The current maintainers are Matthieu Moy
8 +<matthieu.moy@grenoble-inp.fr> and Michael Haggerty
9 +<mhagger@alum.mit.edu>.
10
11 Please note that although a copy of git-multimail is distributed in
12 the "contrib" section of the main Git project, development takes place
@@ -22,6 +23,10 @@ to the maintainers). Please sign off your patches as per the `Git
23 project practice
24 <https://github.com/git/git/blob/master/Documentation/SubmittingPatches#L234>`__.
25
26 +Please vote for issues you would like to be addressed in priority
27 +(click "add your reaction" and then the "+1" thumbs-up button on the
28 +GitHub issue).
29 +
30 General discussion of git-multimail can take place on the main `Git
31 mailing list`_.
32
contrib/hooks/multimail/README
+65 -20
@@ -1,11 +1,11 @@
1 -git-multimail 1.3.1
2 -===================
1 +git-multimail version 1.4.0
2 +===========================
3
4 .. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master
5 :target: https://travis-ci.org/git-multimail/git-multimail
6
7 git-multimail is a tool for sending notification emails on pushes to a
8 -Git repository. It includes a Python module called git_multimail.py,
8 +Git repository. It includes a Python module called ``git_multimail.py``,
9 which can either be used as a hook script directly or can be imported
10 as a Python module into another script.
11
@@ -93,20 +93,20 @@ Requirements
93 Invocation
94 ----------
95
96 -git_multimail.py is designed to be used as a ``post-receive`` hook in a
96 +``git_multimail.py`` is designed to be used as a ``post-receive`` hook in a
97 Git repository (see githooks(5)). Link or copy it to
98 $GIT_DIR/hooks/post-receive within the repository for which email
99 notifications are desired. Usually it should be installed on the
100 central repository for a project, to which all commits are eventually
101 pushed.
102
103 -For use on pre-v1.5.1 Git servers, git_multimail.py can also work as
103 +For use on pre-v1.5.1 Git servers, ``git_multimail.py`` can also work as
104 an ``update`` hook, taking its arguments on the command line. To use
105 this script in this manner, link or copy it to $GIT_DIR/hooks/update.
106 Please note that the script is not completely reliable in this mode
107 -[2]_.
107 +[1]_.
108
109 -Alternatively, git_multimail.py can be imported as a Python module
109 +Alternatively, ``git_multimail.py`` can be imported as a Python module
110 into your own Python post-receive script. This method is a bit more
111 work, but allows the behavior of the hook to be customized using
112 arbitrary Python code. For example, you can use a custom environment
@@ -122,7 +122,7 @@ arbitrary Python code. For example, you can use a custom environment
122
123 Or you can change how emails are sent by writing your own Mailer
124 class. The ``post-receive`` script in this directory demonstrates how
125 -to use git_multimail.py as a Python module. (If you make interesting
125 +to use ``git_multimail.py`` as a Python module. (If you make interesting
126 changes of this type, please consider sharing them with the
127 community.)
128
@@ -151,7 +151,10 @@ multimailhook.environment
151 the repository name is derived from the repository's path.
152
153 gitolite
154 - the username of the pusher is read from $GL_USER, the repository
154 + Environment to use when ``git-multimail`` is ran as a gitolite_
155 + hook.
156 +
157 + The username of the pusher is read from $GL_USER, the repository
158 name is read from $GL_REPO, and the From: header value is
159 optionally read from gitolite.conf (see multimailhook.from).
160
@@ -444,7 +447,9 @@ multimailhook.emailPrefix
447 email filtering (though filtering based on the X-Git-* email
448 headers is probably more robust). Default is the short name of
449 the repository in square brackets; e.g., ``[myrepo]``. Set this
447 - value to the empty string to suppress the email prefix.
450 + value to the empty string to suppress the email prefix. You may
451 + use the placeholder ``%(repo_shortname)s`` for the short name of
452 + the repository.
453
454 multimailhook.emailMaxLines
455 The maximum number of lines that should be included in the body of
@@ -461,6 +466,17 @@ multimailhook.emailMaxLineLength
466 lines, the diffs are probably unreadable anyway. To disable line
467 truncation, set this option to 0.
468
469 +multimailhook.subjectMaxLength
470 + The maximum length of the subject line (i.e. the ``oneline`` field
471 + in templates, not including the prefix). Lines longer than this
472 + limit are truncated to this length with a trailing ``[...]`` added
473 + to indicate the missing text. This option The default is to use
474 + ``multimailhook.emailMaxLineLength``. This option avoids sending
475 + emails with overly long subject lines, but should not be needed if
476 + the commit messages follow the Git convention (one short subject
477 + line, then a blank line, then the message body). To disable line
478 + truncation, set this option to 0.
479 +
480 multimailhook.maxCommitEmails
481 The maximum number of commit emails to send for a given change.
482 When the number of patches is larger that this value, only the
@@ -474,12 +490,15 @@ multimailhook.emailStrictUTF8
490 not valid UTF-8 are converted to the Unicode replacement
491 character, U+FFFD. The default is `true`.
492
493 + This option is ineffective with Python 3, where non-UTF-8
494 + characters are unconditionally replaced.
495 +
496 multimailhook.diffOpts
497 Options passed to ``git diff-tree`` when generating the summary
498 information for ReferenceChange emails. Default is ``--stat
499 --summary --find-copies-harder``. Add -p to those options to
500 include a unified diff of changes in addition to the usual summary
482 - output. Shell quoting is allowed; see multimailhook.logOpts for
501 + output. Shell quoting is allowed; see ``multimailhook.logOpts`` for
502 details.
503
504 multimailhook.graphOpts
@@ -564,6 +583,8 @@ multimailhook.refFilterInclusionRegex, multimailhook.refFilterExclusionRegex, mu
583 the user-interface is not stable yet (in particular, the option
584 names may change). If you want to participate in stabilizing the
585 feature, please contact the maintainers and/or send pull-requests.
586 + If you are happy with the current shape of the feature, please
587 + report it too.
588
589 Regular expressions that can be used to limit refs for which email
590 updates will be sent. It is an error to specify both an inclusion
@@ -613,6 +634,32 @@ multimailhook.refFilterInclusionRegex, multimailhook.refFilterExclusionRegex, mu
634 [multimailhook]
635 refFilterExclusionRegex = ^refs/tags/|^refs/heads/master$
636
637 + ``refFilterInclusionRegex`` and ``refFilterExclusionRegex`` are
638 + strictly stronger than ``refFilterDoSendRegex`` and
639 + ``refFilterDontSendRegex``. In other words, adding a ref to a
640 + DoSend/DontSend regex has no effect if it is already excluded by a
641 + Exclusion/Inclusion regex.
642 +
643 +multimailhook.logFile, multimailhook.errorLogFile, multimailhook.debugLogFile
644 +
645 + When set, these variable designate path to files where
646 + git-multimail will log some messages. Normal messages and error
647 + messages are sent to ``logFile``, and error messages are also sent
648 + to ``errorLogFile``. Debug messages and all other messages are
649 + sent to ``debugLogFile``. The recommended way is to set only one
650 + of these variables, but it is also possible to set several of them
651 + (part of the information is then duplicated in several log files,
652 + for example errors are duplicated to all log files).
653 +
654 + Relative path are relative to the Git repository where the push is
655 + done.
656 +
657 +multimailhook.verbose
658 +
659 + Verbosity level of git-multimail on its standard output. By
660 + default, show only error and info messages. If set to true, show
661 + also debug messages.
662 +
663 Email filtering aids
664 --------------------
665
@@ -628,8 +675,8 @@ Customizing email contents
675
676 git-multimail mostly generates emails by expanding templates. The
677 templates can be customized. To avoid the need to edit
631 -git_multimail.py directly, the preferred way to change the templates
632 -is to write a separate Python script that imports git_multimail.py as
678 +``git_multimail.py`` directly, the preferred way to change the templates
679 +is to write a separate Python script that imports ``git_multimail.py`` as
680 a module, then replaces the templates in place. See the provided
681 post-receive script for an example of how this is done.
682
@@ -645,8 +692,8 @@ GenericEnvironment
692 a stand-alone Git repository.
693
694 GitoliteEnvironment
648 - a Git repository that is managed by gitolite
649 - [3]_. For such repositories, the identity of the pusher is read from
695 + a Git repository that is managed by gitolite_. For such
696 + repositories, the identity of the pusher is read from
697 environment variable $GL_USER, the name of the repository is read
698 from $GL_REPO (if it is not overridden by multimailhook.reponame),
699 and the From: header value is optionally read from gitolite.conf
@@ -662,7 +709,7 @@ option to the script.
709 If you need to customize the script in ways that are not supported by
710 the existing environments, you can define your own environment class
711 class using arbitrary Python code. To do so, you need to import
665 -git_multimail.py as a Python module, as demonstrated by the example
712 +``git_multimail.py`` as a Python module, as demonstrated by the example
713 post-receive script. Then implement your environment class; it should
714 usually inherit from one of the existing Environment classes and
715 possibly one or more of the EnvironmentMixin classes. Then set the
@@ -690,9 +737,7 @@ contribute to git-multimail.
737 Footnotes
738 ---------
739
693 -.. [1] http://www.python.org/dev/peps/pep-0394/
694 -
695 -.. [2] Because of the way information is passed to update hooks, the
740 +.. [1] Because of the way information is passed to update hooks, the
741 script's method of determining whether a commit has already
742 been seen does not work when it is used as an ``update`` script.
743 In particular, no notification email will be generated for a
@@ -700,4 +745,4 @@ Footnotes
745 push. A workaround is to use --force-send to force sending the
746 emails.
747
703 -.. [3] https://github.com/sitaramc/gitolite
748 +.. _gitolite: https://github.com/sitaramc/gitolite
contrib/hooks/multimail/README.Git
+2 -2
@@ -6,10 +6,10 @@ website:
6 https://github.com/git-multimail/git-multimail
7
8 The version in this directory was obtained from the upstream project
9 -on May 13 2016 and consists of the "git-multimail" subdirectory from
9 +on August 17 2016 and consists of the "git-multimail" subdirectory from
10 revision
11
12 - 3ce5470d4abf7251604cbf64e73a962e1b617f5e refs/tags/1.3.1
12 + 07b1cb6bfd7be156c62e1afa17cae13b850a869f refs/tags/1.4.0
13
14 Please see the README file in this directory for information about how
15 to report bugs or contribute to git-multimail.
contrib/hooks/multimail/doc/troubleshooting.rst
+34
@@ -1,6 +1,40 @@
1 Troubleshooting issues with git-multimail: a FAQ
2 ================================================
3
4 +How to check that git-multimail is properly set up?
5 +---------------------------------------------------
6 +
7 +Since version 1.4.0, git-multimail allows a simple self-checking of
8 +its configuration: run it with the environment variable
9 +``GIT_MULTIMAIL_CHECK_SETUP`` set to a non-empty string. You should
10 +get something like this::
11 +
12 + $ GIT_MULTIMAIL_CHECK_SETUP=true /home/moy/dev/git-multimail/git-multimail/git_multimail.py
13 + Environment values:
14 + administrator : 'the administrator of this repository'
15 + charset : 'utf-8'
16 + emailprefix : '[git-multimail] '
17 + fqdn : 'anie'
18 + projectdesc : 'UNNAMED PROJECT'
19 + pusher : 'moy'
20 + repo_path : '/home/moy/dev/git-multimail'
21 + repo_shortname : 'git-multimail'
22 +
23 + Now, checking that git-multimail's standard input is properly set ...
24 + Please type some text and then press Return
25 + foo
26 + You have just entered:
27 + foo
28 + git-multimail seems properly set up.
29 +
30 +If you forgot to set an important variable, you may get instead::
31 +
32 + $ GIT_MULTIMAIL_CHECK_SETUP=true /home/moy/dev/git-multimail/git-multimail/git_multimail.py
33 + No email recipients configured!
34 +
35 +Do not set ``$GIT_MULTIMAIL_CHECK_SETUP`` other than for testing your
36 +configuration: it would disable the hook completely.
37 +
38 Git is not using the right address in the From/To/Reply-To field
39 ----------------------------------------------------------------
40
contrib/hooks/multimail/git_multimail.py
+558 -252
@@ -1,8 +1,8 @@
1 #! /usr/bin/env python
2
3 -__version__ = '1.3.1'
3 +__version__ = '1.4.0'
4
5 -# Copyright (c) 2015 Matthieu Moy and others
5 +# Copyright (c) 2015-2016 Matthieu Moy and others
6 # Copyright (c) 2012-2014 Michael Haggerty and others
7 # Derived from contrib/hooks/post-receive-email, which is
8 # Copyright (c) 2007 Andy Parkins
@@ -56,6 +56,7 @@ import socket
56 import subprocess
57 import shlex
58 import optparse
59 +import logging
60 import smtplib
61 try:
62 import ssl
@@ -86,8 +87,8 @@ if PYTHON3:
87 def str_to_bytes(s):
88 return s.encode(ENCODING)
89
89 - def bytes_to_str(s):
90 - return s.decode(ENCODING)
90 + def bytes_to_str(s, errors='strict'):
91 + return s.decode(ENCODING, errors)
92
93 unicode = str
94
@@ -98,6 +99,15 @@ if PYTHON3:
99 f.buffer.write(msg.encode(sys.getdefaultencoding()))
100 except UnicodeEncodeError:
101 f.buffer.write(msg.encode(ENCODING))
102 +
103 + def read_line(f):
104 + # Try reading with the default encoding. If it fails,
105 + # try UTF-8.
106 + out = f.buffer.readline()
107 + try:
108 + return out.decode(sys.getdefaultencoding())
109 + except UnicodeEncodeError:
110 + return out.decode(ENCODING)
111 else:
112 def is_string(s):
113 try:
@@ -108,12 +118,15 @@ else:
118 def str_to_bytes(s):
119 return s
120
111 - def bytes_to_str(s):
121 + def bytes_to_str(s, errors='strict'):
122 return s
123
124 def write_str(f, msg):
125 f.write(msg)
126
127 + def read_line(f):
128 + return f.readline()
129 +
130 def next(it):
131 return it.next()
132
@@ -213,8 +226,8 @@ reference pointing at a previous point in the repository history.
226 \\
227 O -- O -- O (%(oldrev_short)s)
228
216 -Any revisions marked "omits" are not gone; other references still
217 -refer to them. Any revisions marked "discards" are gone forever.
229 +Any revisions marked "omit" are not gone; other references still
230 +refer to them. Any revisions marked "discard" are gone forever.
231 """
232
233
@@ -233,8 +246,8 @@ You should already have received notification emails for all of the O
246 revisions, and so the following emails describe only the N revisions
247 from the common base, B.
248
236 -Any revisions marked "omits" are not gone; other references still
237 -refer to them. Any revisions marked "discards" are gone forever.
249 +Any revisions marked "omit" are not gone; other references still
250 +refer to them. Any revisions marked "discard" are gone forever.
251 """
252
253
@@ -258,22 +271,22 @@ from the repository.
271 NEW_REVISIONS_TEMPLATE = """\
272 The %(tot)s revisions listed above as "new" are entirely new to this
273 repository and will be described in separate emails. The revisions
261 -listed as "adds" were already present in the repository and have only
274 +listed as "add" were already present in the repository and have only
275 been added to this reference.
276
277 """
278
279
280 TAG_CREATED_TEMPLATE = """\
268 - at %(newrev_short)-9s (%(newrev_type)s)
281 + at %(newrev_short)-8s (%(newrev_type)s)
282 """
283
284
285 TAG_UPDATED_TEMPLATE = """\
286 *** WARNING: tag %(short_refname)s was modified! ***
287
275 - from %(oldrev_short)-9s (%(oldrev_type)s)
276 - to %(newrev_short)-9s (%(newrev_type)s)
288 + from %(oldrev_short)-8s (%(oldrev_type)s)
289 + to %(newrev_short)-8s (%(newrev_type)s)
290 """
291
292
@@ -286,7 +299,7 @@ TAG_DELETED_TEMPLATE = """\
299 # The template used in summary tables. It looks best if this uses the
300 # same alignment as TAG_CREATED_TEMPLATE and TAG_UPDATED_TEMPLATE.
301 BRIEF_SUMMARY_TEMPLATE = """\
289 -%(action)10s %(rev_short)-9s %(text)s
302 +%(action)8s %(rev_short)-8s %(text)s
303 """
304
305
@@ -434,11 +447,16 @@ def read_output(cmd, input=None, keepends=False, **kw):
447 input = str_to_bytes(input)
448 else:
449 stdin = None
450 + errors = 'strict'
451 + if 'errors' in kw:
452 + errors = kw['errors']
453 + del kw['errors']
454 p = subprocess.Popen(
438 - cmd, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
455 + tuple(str_to_bytes(w) for w in cmd),
456 + stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
457 )
458 (out, err) = p.communicate(input)
441 - out = bytes_to_str(out)
459 + out = bytes_to_str(out, errors=errors)
460 retcode = p.wait()
461 if retcode:
462 raise CommandError(cmd, retcode)
@@ -1020,7 +1038,9 @@ class Change(object):
1038 for line in footer:
1039 yield line
1040
1023 - def get_alt_fromaddr(self):
1041 + def get_specific_fromaddr(self):
1042 + """For kinds of Changes which specify it, return the kind-specific
1043 + From address to use."""
1044 return None
1045
1046
@@ -1045,7 +1065,7 @@ class Revision(Change):
1065 self.cc_recipients = ', '.join(to.strip() for to in self._cc_recipients())
1066 if self.cc_recipients:
1067 self.environment.log_msg(
1048 - 'Add %s to CC for %s\n' % (self.cc_recipients, self.rev.sha1))
1068 + 'Add %s to CC for %s' % (self.cc_recipients, self.rev.sha1))
1069
1070 def _cc_recipients(self):
1071 cc_recipients = []
@@ -1065,6 +1085,10 @@ class Revision(Change):
1085 ['log', '--format=%s', '--no-walk', self.rev.sha1]
1086 )
1087
1088 + max_subject_length = self.environment.get_max_subject_length()
1089 + if max_subject_length > 0 and len(oneline) > max_subject_length:
1090 + oneline = oneline[:max_subject_length - 6] + ' [...]'
1091 +
1092 values['rev'] = self.rev.sha1
1093 values['rev_short'] = self.rev.short
1094 values['change_type'] = self.change_type
@@ -1121,7 +1145,7 @@ class Revision(Change):
1145 for line in read_git_lines(
1146 ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],
1147 keepends=True,
1124 - ):
1148 + errors='replace'):
1149 if line.startswith('Date: ') and self.environment.date_substitute:
1150 yield self.environment.date_substitute + line[len('Date: '):]
1151 else:
@@ -1135,7 +1159,7 @@ class Revision(Change):
1159 self._contains_diff()
1160 return Change.generate_email(self, push, body_filter, extra_header_values)
1161
1138 - def get_alt_fromaddr(self):
1162 + def get_specific_fromaddr(self):
1163 return self.environment.from_commit
1164
1165
@@ -1193,7 +1217,7 @@ class ReferenceChange(Change):
1217 # Tracking branch:
1218 environment.log_warning(
1219 '*** Push-update of tracking branch %r\n'
1196 - '*** - incomplete email generated.\n'
1220 + '*** - incomplete email generated.'
1221 % (refname,)
1222 )
1223 klass = OtherReferenceChange
@@ -1201,7 +1225,7 @@ class ReferenceChange(Change):
1225 # Some other reference namespace:
1226 environment.log_warning(
1227 '*** Push-update of strange reference %r\n'
1204 - '*** - incomplete email generated.\n'
1228 + '*** - incomplete email generated.'
1229 % (refname,)
1230 )
1231 klass = OtherReferenceChange
@@ -1209,7 +1233,7 @@ class ReferenceChange(Change):
1233 # Anything else (is there anything else?)
1234 environment.log_warning(
1235 '*** Unknown type of update to %r (%s)\n'
1212 - '*** - incomplete email generated.\n'
1236 + '*** - incomplete email generated.'
1237 % (refname, rev.type,)
1238 )
1239 klass = OtherReferenceChange
@@ -1446,9 +1470,9 @@ class ReferenceChange(Change):
1470 if discards and adds:
1471 for (sha1, subject) in discards:
1472 if sha1 in discarded_commits:
1449 - action = 'discards'
1473 + action = 'discard'
1474 else:
1451 - action = 'omits'
1475 + action = 'omit'
1476 yield self.expand(
1477 BRIEF_SUMMARY_TEMPLATE, action=action,
1478 rev_short=sha1, text=subject,
@@ -1457,7 +1481,7 @@ class ReferenceChange(Change):
1481 if sha1 in new_commits:
1482 action = 'new'
1483 else:
1460 - action = 'adds'
1484 + action = 'add'
1485 yield self.expand(
1486 BRIEF_SUMMARY_TEMPLATE, action=action,
1487 rev_short=sha1, text=subject,
@@ -1469,9 +1493,9 @@ class ReferenceChange(Change):
1493 elif discards:
1494 for (sha1, subject) in discards:
1495 if sha1 in discarded_commits:
1472 - action = 'discards'
1496 + action = 'discard'
1497 else:
1474 - action = 'omits'
1498 + action = 'omit'
1499 yield self.expand(
1500 BRIEF_SUMMARY_TEMPLATE, action=action,
1501 rev_short=sha1, text=subject,
@@ -1490,7 +1514,7 @@ class ReferenceChange(Change):
1514 if sha1 in new_commits:
1515 action = 'new'
1516 else:
1493 - action = 'adds'
1517 + action = 'add'
1518 yield self.expand(
1519 BRIEF_SUMMARY_TEMPLATE, action=action,
1520 rev_short=sha1, text=subject,
@@ -1543,7 +1567,7 @@ class ReferenceChange(Change):
1567 for r in discarded_revisions:
1568 (sha1, subject) = r.rev.get_summary()
1569 yield r.expand(
1546 - BRIEF_SUMMARY_TEMPLATE, action='discards', text=subject,
1570 + BRIEF_SUMMARY_TEMPLATE, action='discard', text=subject,
1571 )
1572 for line in self.generate_revision_change_graph(push):
1573 yield line
@@ -1581,7 +1605,7 @@ class ReferenceChange(Change):
1605 )
1606 yield '\n'
1607
1584 - def get_alt_fromaddr(self):
1608 + def get_specific_fromaddr(self):
1609 return self.environment.from_refchange
1610
1611
@@ -1791,13 +1815,13 @@ class AnnotatedTagChange(ReferenceChange):
1815 except CommandError:
1816 prevtag = None
1817 if prevtag:
1794 - yield ' replaces %s\n' % (prevtag,)
1818 + yield ' replaces %s\n' % (prevtag,)
1819 else:
1820 prevtag = None
1797 - yield ' length %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),)
1821 + yield ' length %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),)
1822
1799 - yield ' tagged by %s\n' % (tagger,)
1800 - yield ' on %s\n' % (tagged,)
1823 + yield ' by %s\n' % (tagger,)
1824 + yield ' on %s\n' % (tagged,)
1825 yield '\n'
1826
1827 # Show the content of the tag message; this might contain a
@@ -1914,6 +1938,9 @@ class OtherReferenceChange(ReferenceChange):
1938 class Mailer(object):
1939 """An object that can send emails."""
1940
1941 + def __init__(self, environment):
1942 + self.environment = environment
1943 +
1944 def send(self, lines, to_addrs):
1945 """Send an email consisting of lines.
1946
@@ -1948,14 +1975,14 @@ class SendMailer(Mailer):
1975 'Try setting multimailhook.sendmailCommand.'
1976 )
1977
1951 - def __init__(self, command=None, envelopesender=None):
1978 + def __init__(self, environment, command=None, envelopesender=None):
1979 """Construct a SendMailer instance.
1980
1981 command should be the command and arguments used to invoke
1982 sendmail, as a list of strings. If an envelopesender is
1983 provided, it will also be passed to the command, via '-f
1984 envelopesender'."""
1958 -
1985 + super(SendMailer, self).__init__(environment)
1986 if command:
1987 self.command = command[:]
1988 else:
@@ -1968,7 +1995,7 @@ class SendMailer(Mailer):
1995 try:
1996 p = subprocess.Popen(self.command, stdin=subprocess.PIPE)
1997 except OSError:
1971 - sys.stderr.write(
1998 + self.environment.get_logger().error(
1999 '*** Cannot execute command: %s\n' % ' '.join(self.command) +
2000 '*** %s\n' % sys.exc_info()[1] +
2001 '*** Try setting multimailhook.mailer to "smtp"\n' +
@@ -1979,15 +2006,16 @@ class SendMailer(Mailer):
2006 lines = (str_to_bytes(line) for line in lines)
2007 p.stdin.writelines(lines)
2008 except Exception:
1982 - sys.stderr.write(
2009 + self.environment.get_logger().error(
2010 '*** Error while generating commit email\n'
2011 '*** - mail sending aborted.\n'
2012 )
1986 - try:
2013 + if hasattr(p, 'terminate'):
2014 # subprocess.terminate() is not available in Python 2.4
2015 p.terminate()
1989 - except AttributeError:
1990 - pass
2016 + else:
2017 + import signal
2018 + os.kill(p.pid, signal.SIGTERM)
2019 raise
2020 else:
2021 p.stdin.close()
@@ -1999,14 +2027,16 @@ class SendMailer(Mailer):
2027 class SMTPMailer(Mailer):
2028 """Send emails using Python's smtplib."""
2029
2002 - def __init__(self, envelopesender, smtpserver,
2030 + def __init__(self, environment,
2031 + envelopesender, smtpserver,
2032 smtpservertimeout=10.0, smtpserverdebuglevel=0,
2033 smtpencryption='none',
2034 smtpuser='', smtppass='',
2035 smtpcacerts=''
2036 ):
2037 + super(SMTPMailer, self).__init__(environment)
2038 if not envelopesender:
2009 - sys.stderr.write(
2039 + self.environment.get_logger().error(
2040 'fatal: git_multimail: cannot use SMTPMailer without a sender address.\n'
2041 'please set either multimailhook.envelopeSender or user.email\n'
2042 )
@@ -2041,7 +2071,7 @@ class SMTPMailer(Mailer):
2071 self.smtp = call(smtplib.SMTP_SSL, self.smtpserver, timeout=self.smtpservertimeout)
2072 elif self.security == 'tls':
2073 if 'ssl' not in sys.modules:
2044 - sys.stderr.write(
2074 + self.environment.get_logger().error(
2075 '*** Your Python version does not have the ssl library installed\n'
2076 '*** smtpEncryption=tls is not available.\n'
2077 '*** Either upgrade Python to 2.6 or later\n'
@@ -2071,7 +2101,7 @@ class SMTPMailer(Mailer):
2101 self.smtp.sock,
2102 cert_reqs=ssl.CERT_NONE
2103 )
2074 - sys.stderr.write(
2104 + self.environment.get_logger().error(
2105 '*** Warning, the server certificat is not verified (smtp) ***\n'
2106 '*** set the option smtpCACerts ***\n'
2107 )
@@ -2094,10 +2124,10 @@ class SMTPMailer(Mailer):
2124 % self.smtpserverdebuglevel)
2125 self.smtp.set_debuglevel(self.smtpserverdebuglevel)
2126 except Exception:
2097 - sys.stderr.write(
2127 + self.environment.get_logger().error(
2128 '*** Error establishing SMTP connection to %s ***\n'
2099 - % self.smtpserver)
2100 - sys.stderr.write('*** %s\n' % sys.exc_info()[1])
2129 + '*** %s\n'
2130 + % (self.smtpserver, sys.exc_info()[1]))
2131 sys.exit(1)
2132
2133 def __del__(self):
@@ -2115,10 +2145,11 @@ class SMTPMailer(Mailer):
2145 to_addrs = [email for (name, email) in getaddresses([to_addrs])]
2146 self.smtp.sendmail(self.envelopesender, to_addrs, msg)
2147 except smtplib.SMTPResponseException:
2118 - sys.stderr.write('*** Error sending email ***\n')
2148 err = sys.exc_info()[1]
2120 - sys.stderr.write('*** Error %d: %s\n' % (err.smtp_code,
2121 - bytes_to_str(err.smtp_error)))
2149 + self.environment.get_logger().error(
2150 + '*** Error sending email ***\n'
2151 + '*** Error %d: %s\n'
2152 + % (err.smtp_code, bytes_to_str(err.smtp_error)))
2153 try:
2154 smtp = self.smtp
2155 # delete the field before quit() so that in case of
@@ -2126,9 +2157,10 @@ class SMTPMailer(Mailer):
2157 del self.smtp
2158 smtp.quit()
2159 except:
2129 - sys.stderr.write('*** Error closing the SMTP connection ***\n')
2130 - sys.stderr.write('*** Exiting anyway ... ***\n')
2131 - sys.stderr.write('*** %s\n' % sys.exc_info()[1])
2160 + self.environment.get_logger().error(
2161 + '*** Error closing the SMTP connection ***\n'
2162 + '*** Exiting anyway ... ***\n'
2163 + '*** %s\n' % sys.exc_info()[1])
2164 sys.exit(1)
2165
2166
@@ -2250,6 +2282,11 @@ class Environment(object):
2282 to send and when computing what commits are considered new
2283 to the repository. Default is "^refs/notes/".
2284
2285 + get_max_subject_length()
2286 +
2287 + Return an int giving the maximal length for the subject
2288 + (git log --oneline).
2289 +
2290 They should also define the following attributes:
2291
2292 announce_show_shortlog (bool)
@@ -2324,6 +2361,15 @@ class Environment(object):
2361 multimailhook.fromRefchange and multimailhook.fromCommit
2362 by ConfigEnvironmentMixin.
2363
2364 + log_file, error_log_file, debug_log_file (string)
2365 +
2366 + Name of a file to which logs should be sent.
2367 +
2368 + verbose (int)
2369 +
2370 + How verbose the system should be.
2371 + - 0 (default): show info, errors, ...
2372 + - 1 : show basic debug info
2373 """
2374
2375 REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$')
@@ -2346,6 +2392,7 @@ class Environment(object):
2392 self.quiet = False
2393 self.stdout = False
2394 self.combine_when_single_commit = True
2395 + self.logger = None
2396
2397 self.COMPUTED_KEYS = [
2398 'administrator',
@@ -2360,6 +2407,12 @@ class Environment(object):
2407
2408 self._values = None
2409
2410 + def get_logger(self):
2411 + """Get (possibly creates) the logger associated to this environment."""
2412 + if self.logger is None:
2413 + self.logger = Logger(self)
2414 + return self.logger
2415 +
2416 def get_repo_shortname(self):
2417 """Use the last part of the repo path, with ".git" stripped off if present."""
2418
@@ -2467,6 +2520,11 @@ class Environment(object):
2520 # which we simply do not have right now.
2521 return "^refs/notes/"
2522
2523 + def get_max_subject_length(self):
2524 + """Return the maximal subject line (git log --oneline) length.
2525 + Longer subject lines will be truncated."""
2526 + raise NotImplementedError()
2527 +
2528 def filter_body(self, lines):
2529 """Filter the lines intended for an email body.
2530
@@ -2482,19 +2540,22 @@ class Environment(object):
2540 """Write the string msg on a log file or on stderr.
2541
2542 Sends the text to stderr by default, override to change the behavior."""
2485 - write_str(sys.stderr, msg)
2543 + self.get_logger().info(msg)
2544
2545 def log_warning(self, msg):
2546 """Write the string msg on a log file or on stderr.
2547
2548 Sends the text to stderr by default, override to change the behavior."""
2491 - write_str(sys.stderr, msg)
2549 + self.get_logger().warning(msg)
2550
2551 def log_error(self, msg):
2552 """Write the string msg on a log file or on stderr.
2553
2554 Sends the text to stderr by default, override to change the behavior."""
2497 - write_str(sys.stderr, msg)
2555 + self.get_logger().error(msg)
2556 +
2557 + def check(self):
2558 + pass
2559
2560
2561 class ConfigEnvironmentMixin(Environment):
@@ -2613,6 +2674,14 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2674 if combine is not None:
2675 self.combine_when_single_commit = combine
2676
2677 + self.log_file = config.get('logFile', default=None)
2678 + self.error_log_file = config.get('errorLogFile', default=None)
2679 + self.debug_log_file = config.get('debugLogFile', default=None)
2680 + if config.get_bool('Verbose', default=False):
2681 + self.verbose = 1
2682 + else:
2683 + self.verbose = 0
2684 +
2685 def get_administrator(self):
2686 return (
2687 self.config.get('administrator') or
@@ -2631,11 +2700,21 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2700 if emailprefix is not None:
2701 emailprefix = emailprefix.strip()
2702 if emailprefix:
2634 - return emailprefix + ' '
2635 - else:
2636 - return ''
2703 + emailprefix += ' '
2704 else:
2638 - return '[%s] ' % (self.get_repo_shortname(),)
2705 + emailprefix = '[%(repo_shortname)s] '
2706 + short_name = self.get_repo_shortname()
2707 + try:
2708 + return emailprefix % {'repo_shortname': short_name}
2709 + except:
2710 + self.get_logger().error(
2711 + '*** Invalid multimailhook.emailPrefix: %s\n' % emailprefix +
2712 + '*** %s\n' % sys.exc_info()[1] +
2713 + "*** Only the '%(repo_shortname)s' placeholder is allowed\n"
2714 + )
2715 + raise ConfigurationException(
2716 + '"%s" is not an allowed setting for emailPrefix' % emailprefix
2717 + )
2718
2719 def get_sender(self):
2720 return self.config.get('envelopesender')
@@ -2656,9 +2735,9 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2735 def get_fromaddr(self, change=None):
2736 fromaddr = self.config.get('from')
2737 if change:
2659 - alt_fromaddr = change.get_alt_fromaddr()
2660 - if alt_fromaddr:
2661 - fromaddr = alt_fromaddr
2738 + specific_fromaddr = change.get_specific_fromaddr()
2739 + if specific_fromaddr:
2740 + fromaddr = specific_fromaddr
2741 if fromaddr:
2742 fromaddr = self.process_addr(fromaddr, change)
2743 if fromaddr:
@@ -2684,7 +2763,7 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2763 class FilterLinesEnvironmentMixin(Environment):
2764 """Handle encoding and maximum line length of body lines.
2765
2687 - emailmaxlinelength (int or None)
2766 + email_max_line_length (int or None)
2767
2768 The maximum length of any single line in the email body.
2769 Longer lines are truncated at that length with ' [...]'
@@ -2699,10 +2778,13 @@ class FilterLinesEnvironmentMixin(Environment):
2778
2779 """
2780
2702 - def __init__(self, strict_utf8=True, emailmaxlinelength=500, **kw):
2781 + def __init__(self, strict_utf8=True,
2782 + email_max_line_length=500, max_subject_length=500,
2783 + **kw):
2784 super(FilterLinesEnvironmentMixin, self).__init__(**kw)
2785 self.__strict_utf8 = strict_utf8
2705 - self.__emailmaxlinelength = emailmaxlinelength
2786 + self.__email_max_line_length = email_max_line_length
2787 + self.__max_subject_length = max_subject_length
2788
2789 def filter_body(self, lines):
2790 lines = super(FilterLinesEnvironmentMixin, self).filter_body(lines)
@@ -2711,15 +2793,18 @@ class FilterLinesEnvironmentMixin(Environment):
2793 lines = (line.decode(ENCODING, 'replace') for line in lines)
2794 # Limit the line length in Unicode-space to avoid
2795 # splitting characters:
2714 - if self.__emailmaxlinelength:
2715 - lines = limit_linelength(lines, self.__emailmaxlinelength)
2796 + if self.__email_max_line_length > 0:
2797 + lines = limit_linelength(lines, self.__email_max_line_length)
2798 if not PYTHON3:
2799 lines = (line.encode(ENCODING, 'replace') for line in lines)
2718 - elif self.__emailmaxlinelength:
2719 - lines = limit_linelength(lines, self.__emailmaxlinelength)
2800 + elif self.__email_max_line_length:
2801 + lines = limit_linelength(lines, self.__email_max_line_length)
2802
2803 return lines
2804
2805 + def get_max_subject_length(self):
2806 + return self.__max_subject_length
2807 +
2808
2809 class ConfigFilterLinesEnvironmentMixin(
2810 ConfigEnvironmentMixin,
@@ -2732,9 +2817,13 @@ class ConfigFilterLinesEnvironmentMixin(
2817 if strict_utf8 is not None:
2818 kw['strict_utf8'] = strict_utf8
2819
2735 - emailmaxlinelength = config.get('emailmaxlinelength')
2736 - if emailmaxlinelength is not None:
2737 - kw['emailmaxlinelength'] = int(emailmaxlinelength)
2820 + email_max_line_length = config.get('emailmaxlinelength')
2821 + if email_max_line_length is not None:
2822 + kw['email_max_line_length'] = int(email_max_line_length)
2823 +
2824 + max_subject_length = config.get('subjectMaxLength', default=email_max_line_length)
2825 + if max_subject_length is not None:
2826 + kw['max_subject_length'] = int(max_subject_length)
2827
2828 super(ConfigFilterLinesEnvironmentMixin, self).__init__(
2829 config=config, **kw
@@ -2750,7 +2839,7 @@ class MaxlinesEnvironmentMixin(Environment):
2839
2840 def filter_body(self, lines):
2841 lines = super(MaxlinesEnvironmentMixin, self).filter_body(lines)
2753 - if self.__emailmaxlines:
2842 + if self.__emailmaxlines > 0:
2843 lines = limit_lines(lines, self.__emailmaxlines)
2844 return lines
2845
@@ -2843,25 +2932,64 @@ class StaticRecipientsEnvironmentMixin(Environment):
2932 # actual *contents* of the change being reported, we only
2933 # choose based on the *type* of the change. Therefore we can
2934 # compute them once and for all:
2846 - if not (refchange_recipients or
2847 - announce_recipients or
2848 - revision_recipients or
2849 - scancommitforcc):
2850 - raise ConfigurationException('No email recipients configured!')
2935 self.__refchange_recipients = refchange_recipients
2936 self.__announce_recipients = announce_recipients
2937 self.__revision_recipients = revision_recipients
2938
2939 + def check(self):
2940 + if not (self.get_refchange_recipients(None) or
2941 + self.get_announce_recipients(None) or
2942 + self.get_revision_recipients(None) or
2943 + self.get_scancommitforcc()):
2944 + raise ConfigurationException('No email recipients configured!')
2945 + super(StaticRecipientsEnvironmentMixin, self).check()
2946 +
2947 def get_refchange_recipients(self, refchange):
2948 + if self.__refchange_recipients is None:
2949 + return super(StaticRecipientsEnvironmentMixin,
2950 + self).get_refchange_recipients(refchange)
2951 return self.__refchange_recipients
2952
2953 def get_announce_recipients(self, annotated_tag_change):
2954 + if self.__announce_recipients is None:
2955 + return super(StaticRecipientsEnvironmentMixin,
2956 + self).get_refchange_recipients(annotated_tag_change)
2957 return self.__announce_recipients
2958
2959 def get_revision_recipients(self, revision):
2960 + if self.__revision_recipients is None:
2961 + return super(StaticRecipientsEnvironmentMixin,
2962 + self).get_refchange_recipients(revision)
2963 return self.__revision_recipients
2964
2965
2966 +class CLIRecipientsEnvironmentMixin(Environment):
2967 + """Mixin storing recipients information comming from the
2968 + command-line."""
2969 +
2970 + def __init__(self, cli_recipients=None, **kw):
2971 + super(CLIRecipientsEnvironmentMixin, self).__init__(**kw)
2972 + self.__cli_recipients = cli_recipients
2973 +
2974 + def get_refchange_recipients(self, refchange):
2975 + if self.__cli_recipients is None:
2976 + return super(CLIRecipientsEnvironmentMixin,
2977 + self).get_refchange_recipients(refchange)
2978 + return self.__cli_recipients
2979 +
2980 + def get_announce_recipients(self, annotated_tag_change):
2981 + if self.__cli_recipients is None:
2982 + return super(CLIRecipientsEnvironmentMixin,
2983 + self).get_announce_recipients(annotated_tag_change)
2984 + return self.__cli_recipients
2985 +
2986 + def get_revision_recipients(self, revision):
2987 + if self.__cli_recipients is None:
2988 + return super(CLIRecipientsEnvironmentMixin,
2989 + self).get_revision_recipients(revision)
2990 + return self.__cli_recipients
2991 +
2992 +
2993 class ConfigRecipientsEnvironmentMixin(
2994 ConfigEnvironmentMixin,
2995 StaticRecipientsEnvironmentMixin
@@ -2935,24 +3063,20 @@ class StaticRefFilterEnvironmentMixin(Environment):
3063 if ref_filter_do_send_regex and ref_filter_dont_send_regex:
3064 raise ConfigurationException(
3065 "Cannot specify both a ref doSend and dontSend regex.")
2938 - if ref_filter_do_send_regex or ref_filter_dont_send_regex:
2939 - self.__is_do_send_filter = bool(ref_filter_do_send_regex)
2940 - if ref_filter_incl_regex:
2941 - ref_filter_send_regex = ref_filter_incl_regex
2942 - elif ref_filter_excl_regex:
2943 - ref_filter_send_regex = ref_filter_excl_regex
2944 - else:
2945 - ref_filter_send_regex = '.*'
2946 - self.__is_do_send_filter = True
2947 - try:
2948 - self.__send_compiled_regex = re.compile(ref_filter_send_regex)
2949 - except Exception:
2950 - raise ConfigurationException(
2951 - 'Invalid Ref Filter Regex "%s": %s' %
2952 - (ref_filter_send_regex, sys.exc_info()[1]))
3066 + self.__is_do_send_filter = bool(ref_filter_do_send_regex)
3067 + if ref_filter_do_send_regex:
3068 + ref_filter_send_regex = ref_filter_do_send_regex
3069 + elif ref_filter_dont_send_regex:
3070 + ref_filter_send_regex = ref_filter_dont_send_regex
3071 else:
2954 - self.__send_compiled_regex = self.__compiled_regex
2955 - self.__is_do_send_filter = self.__is_inclusion_filter
3072 + ref_filter_send_regex = '.*'
3073 + self.__is_do_send_filter = True
3074 + try:
3075 + self.__send_compiled_regex = re.compile(ref_filter_send_regex)
3076 + except Exception:
3077 + raise ConfigurationException(
3078 + 'Invalid Ref Filter Regex "%s": %s' %
3079 + (ref_filter_send_regex, sys.exc_info()[1]))
3080
3081 def get_ref_filter_regex(self, send_filter=False):
3082 if send_filter:
@@ -3023,34 +3147,21 @@ class GenericEnvironmentMixin(Environment):
3147 return self.osenv.get('USER', self.osenv.get('USERNAME', 'unknown user'))
3148
3149
3026 -class GenericEnvironment(
3027 - ProjectdescEnvironmentMixin,
3028 - ConfigMaxlinesEnvironmentMixin,
3029 - ComputeFQDNEnvironmentMixin,
3030 - ConfigFilterLinesEnvironmentMixin,
3031 - ConfigRecipientsEnvironmentMixin,
3032 - ConfigRefFilterEnvironmentMixin,
3033 - PusherDomainEnvironmentMixin,
3034 - ConfigOptionsEnvironmentMixin,
3035 - GenericEnvironmentMixin,
3036 - Environment,
3037 - ):
3038 - pass
3150 +class GitoliteEnvironmentHighPrecMixin(Environment):
3151 + def get_pusher(self):
3152 + return self.osenv.get('GL_USER', 'unknown user')
3153
3154
3041 -class GitoliteEnvironmentMixin(Environment):
3155 +class GitoliteEnvironmentLowPrecMixin(Environment):
3156 def get_repo_shortname(self):
3157 # The gitolite environment variable $GL_REPO is a pretty good
3158 # repo_shortname (though it's probably not as good as a value
3159 # the user might have explicitly put in his config).
3160 return (
3161 self.osenv.get('GL_REPO', None) or
3048 - super(GitoliteEnvironmentMixin, self).get_repo_shortname()
3162 + super(GitoliteEnvironmentLowPrecMixin, self).get_repo_shortname()
3163 )
3164
3051 - def get_pusher(self):
3052 - return self.osenv.get('GL_USER', 'unknown user')
3053 -
3165 def get_fromaddr(self, change=None):
3166 GL_USER = self.osenv.get('GL_USER')
3167 if GL_USER is not None:
@@ -3088,7 +3199,7 @@ class GitoliteEnvironmentMixin(Environment):
3199 return m.group(1)
3200 finally:
3201 f.close()
3091 - return super(GitoliteEnvironmentMixin, self).get_fromaddr(change)
3202 + return super(GitoliteEnvironmentLowPrecMixin, self).get_fromaddr(change)
3203
3204
3205 class IncrementalDateTime(object):
@@ -3109,67 +3220,43 @@ class IncrementalDateTime(object):
3220 return formatted
3221
3222
3112 -class GitoliteEnvironment(
3113 - ProjectdescEnvironmentMixin,
3114 - ConfigMaxlinesEnvironmentMixin,
3115 - ComputeFQDNEnvironmentMixin,
3116 - ConfigFilterLinesEnvironmentMixin,
3117 - ConfigRecipientsEnvironmentMixin,
3118 - ConfigRefFilterEnvironmentMixin,
3119 - PusherDomainEnvironmentMixin,
3120 - ConfigOptionsEnvironmentMixin,
3121 - GitoliteEnvironmentMixin,
3122 - Environment,
3123 - ):
3124 - pass
3125 -
3126 -
3127 -class StashEnvironmentMixin(Environment):
3223 +class StashEnvironmentHighPrecMixin(Environment):
3224 def __init__(self, user=None, repo=None, **kw):
3129 - super(StashEnvironmentMixin, self).__init__(**kw)
3225 + super(StashEnvironmentHighPrecMixin,
3226 + self).__init__(user=user, repo=repo, **kw)
3227 self.__user = user
3228 self.__repo = repo
3229
3133 - def get_repo_shortname(self):
3134 - return self.__repo
3135 -
3230 def get_pusher(self):
3231 return re.match('(.*?)\s*<', self.__user).group(1)
3232
3233 def get_pusher_email(self):
3234 return self.__user
3235
3142 - def get_fromaddr(self, change=None):
3143 - return self.__user
3236
3237 +class StashEnvironmentLowPrecMixin(Environment):
3238 + def __init__(self, user=None, repo=None, **kw):
3239 + super(StashEnvironmentLowPrecMixin, self).__init__(**kw)
3240 + self.__repo = repo
3241 + self.__user = user
3242
3146 -class StashEnvironment(
3147 - StashEnvironmentMixin,
3148 - ProjectdescEnvironmentMixin,
3149 - ConfigMaxlinesEnvironmentMixin,
3150 - ComputeFQDNEnvironmentMixin,
3151 - ConfigFilterLinesEnvironmentMixin,
3152 - ConfigRecipientsEnvironmentMixin,
3153 - ConfigRefFilterEnvironmentMixin,
3154 - PusherDomainEnvironmentMixin,
3155 - ConfigOptionsEnvironmentMixin,
3156 - Environment,
3157 - ):
3158 - pass
3243 + def get_repo_shortname(self):
3244 + return self.__repo
3245 +
3246 + def get_fromaddr(self, change=None):
3247 + return self.__user
3248
3249
3161 -class GerritEnvironmentMixin(Environment):
3250 +class GerritEnvironmentHighPrecMixin(Environment):
3251 def __init__(self, project=None, submitter=None, update_method=None, **kw):
3163 - super(GerritEnvironmentMixin, self).__init__(**kw)
3252 + super(GerritEnvironmentHighPrecMixin,
3253 + self).__init__(submitter=submitter, project=project, **kw)
3254 self.__project = project
3255 self.__submitter = submitter
3256 self.__update_method = update_method
3257 "Make an 'update_method' value available for templates."
3258 self.COMPUTED_KEYS += ['update_method']
3259
3170 - def get_repo_shortname(self):
3171 - return self.__project
3172 -
3260 def get_pusher(self):
3261 if self.__submitter:
3262 if self.__submitter.find('<') != -1:
@@ -3192,16 +3279,10 @@ class GerritEnvironmentMixin(Environment):
3279 if self.__submitter:
3280 return self.__submitter
3281 else:
3195 - return super(GerritEnvironmentMixin, self).get_pusher_email()
3196 -
3197 - def get_fromaddr(self, change=None):
3198 - if self.__submitter and self.__submitter.find('<') != -1:
3199 - return self.__submitter
3200 - else:
3201 - return super(GerritEnvironmentMixin, self).get_fromaddr(change)
3282 + return super(GerritEnvironmentHighPrecMixin, self).get_pusher_email()
3283
3284 def get_default_ref_ignore_regex(self):
3204 - default = super(GerritEnvironmentMixin, self).get_default_ref_ignore_regex()
3285 + default = super(GerritEnvironmentHighPrecMixin, self).get_default_ref_ignore_regex()
3286 return default + '|^refs/changes/|^refs/cache-automerge/|^refs/meta/'
3287
3288 def get_revision_recipients(self, revision):
@@ -3214,25 +3295,26 @@ class GerritEnvironmentMixin(Environment):
3295 if committer == 'Gerrit Code Review':
3296 return []
3297 else:
3217 - return super(GerritEnvironmentMixin, self).get_revision_recipients(revision)
3298 + return super(GerritEnvironmentHighPrecMixin, self).get_revision_recipients(revision)
3299
3300 def get_update_method(self):
3301 return self.__update_method
3302
3303
3223 -class GerritEnvironment(
3224 - GerritEnvironmentMixin,
3225 - ProjectdescEnvironmentMixin,
3226 - ConfigMaxlinesEnvironmentMixin,
3227 - ComputeFQDNEnvironmentMixin,
3228 - ConfigFilterLinesEnvironmentMixin,
3229 - ConfigRecipientsEnvironmentMixin,
3230 - ConfigRefFilterEnvironmentMixin,
3231 - PusherDomainEnvironmentMixin,
3232 - ConfigOptionsEnvironmentMixin,
3233 - Environment,
3234 - ):
3235 - pass
3304 +class GerritEnvironmentLowPrecMixin(Environment):
3305 + def __init__(self, project=None, submitter=None, **kw):
3306 + super(GerritEnvironmentLowPrecMixin, self).__init__(**kw)
3307 + self.__project = project
3308 + self.__submitter = submitter
3309 +
3310 + def get_repo_shortname(self):
3311 + return self.__project
3312 +
3313 + def get_fromaddr(self, change=None):
3314 + if self.__submitter and self.__submitter.find('<') != -1:
3315 + return self.__submitter
3316 + else:
3317 + return super(GerritEnvironmentLowPrecMixin, self).get_fromaddr(change)
3318
3319
3320 class Push(object):
@@ -3498,13 +3580,13 @@ class Push(object):
3580 if not change.recipients:
3581 change.environment.log_warning(
3582 '*** no recipients configured so no email will be sent\n'
3501 - '*** for %r update %s->%s\n'
3583 + '*** for %r update %s->%s'
3584 % (change.refname, change.old.sha1, change.new.sha1,)
3585 )
3586 else:
3587 if not change.environment.quiet:
3588 change.environment.log_msg(
3507 - 'Sending notification emails to: %s\n' % (change.recipients,))
3589 + 'Sending notification emails to: %s' % (change.recipients,))
3590 extra_values = {'send_date': next(send_date)}
3591
3592 rev = change.send_single_combined_email(sha1s)
@@ -3527,14 +3609,14 @@ class Push(object):
3609 change.environment.log_warning(
3610 '*** Too many new commits (%d), not sending commit emails.\n' % len(sha1s) +
3611 '*** Try setting multimailhook.maxCommitEmails to a greater value\n' +
3530 - '*** Currently, multimailhook.maxCommitEmails=%d\n' % max_emails
3612 + '*** Currently, multimailhook.maxCommitEmails=%d' % max_emails
3613 )
3614 return
3615
3616 for (num, sha1) in enumerate(sha1s):
3617 rev = Revision(change, GitObject(sha1), num=num + 1, tot=len(sha1s))
3618 if not rev.recipients and rev.cc_recipients:
3537 - change.environment.log_msg('*** Replacing Cc: with To:\n')
3619 + change.environment.log_msg('*** Replacing Cc: with To:')
3620 rev.recipients = rev.cc_recipients
3621 rev.cc_recipients = None
3622 if rev.recipients:
@@ -3548,7 +3630,7 @@ class Push(object):
3630 if unhandled_sha1s:
3631 change.environment.log_error(
3632 'ERROR: No emails were sent for the following new commits:\n'
3551 - ' %s\n'
3633 + ' %s'
3634 % ('\n '.join(sorted(unhandled_sha1s)),)
3635 )
3636
@@ -3562,12 +3644,23 @@ def include_ref(refname, ref_filter_regex, is_inclusion_filter):
3644
3645
3646 def run_as_post_receive_hook(environment, mailer):
3565 - ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
3647 + environment.check()
3648 + send_filter_regex, send_is_inclusion_filter = environment.get_ref_filter_regex(True)
3649 + ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(False)
3650 changes = []
3567 - for line in sys.stdin:
3651 + while True:
3652 + line = read_line(sys.stdin)
3653 + if line == '':
3654 + break
3655 (oldrev, newrev, refname) = line.strip().split(' ', 2)
3656 + environment.get_logger().debug(
3657 + "run_as_post_receive_hook: oldrev=%s, newrev=%s, refname=%s" %
3658 + (oldrev, newrev, refname))
3659 +
3660 if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
3661 continue
3662 + if not include_ref(refname, send_filter_regex, send_is_inclusion_filter):
3663 + continue
3664 changes.append(
3665 ReferenceChange.create(environment, oldrev, newrev, refname)
3666 )
@@ -3579,9 +3672,13 @@ def run_as_post_receive_hook(environment, mailer):
3672
3673
3674 def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
3582 - ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
3675 + environment.check()
3676 + send_filter_regex, send_is_inclusion_filter = environment.get_ref_filter_regex(True)
3677 + ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(False)
3678 if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
3679 return
3680 + if not include_ref(refname, send_filter_regex, send_is_inclusion_filter):
3681 + return
3682 changes = [
3683 ReferenceChange.create(
3684 environment,
@@ -3596,6 +3693,75 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=
3693 mailer.__del__()
3694
3695
3696 +def check_ref_filter(environment):
3697 + send_filter_regex, send_is_inclusion = environment.get_ref_filter_regex(True)
3698 + ref_filter_regex, ref_is_inclusion = environment.get_ref_filter_regex(False)
3699 +
3700 + def inc_exc_lusion(b):
3701 + if b:
3702 + return 'inclusion'
3703 + else:
3704 + return 'exclusion'
3705 +
3706 + if send_filter_regex:
3707 + sys.stdout.write("DoSend/DontSend filter regex (" +
3708 + (inc_exc_lusion(send_is_inclusion)) +
3709 + '): ' + send_filter_regex.pattern +
3710 + '\n')
3711 + if send_filter_regex:
3712 + sys.stdout.write("Include/Exclude filter regex (" +
3713 + (inc_exc_lusion(ref_is_inclusion)) +
3714 + '): ' + ref_filter_regex.pattern +
3715 + '\n')
3716 + sys.stdout.write(os.linesep)
3717 +
3718 + sys.stdout.write(
3719 + "Refs marked as EXCLUDE are excluded by either refFilterInclusionRegex\n"
3720 + "or refFilterExclusionRegex. No emails will be sent for commits included\n"
3721 + "in these refs.\n"
3722 + "Refs marked as DONT-SEND are excluded by either refFilterDoSendRegex or\n"
3723 + "refFilterDontSendRegex, but not by either refFilterInclusionRegex or\n"
3724 + "refFilterExclusionRegex. Emails will be sent for commits included in these\n"
3725 + "refs only when the commit reaches a ref which isn't excluded.\n"
3726 + "Refs marked as DO-SEND are not excluded by any filter. Emails will\n"
3727 + "be sent normally for commits included in these refs.\n")
3728 +
3729 + sys.stdout.write(os.linesep)
3730 +
3731 + for refname in read_git_lines(['for-each-ref', '--format', '%(refname)']):
3732 + sys.stdout.write(refname)
3733 + if not include_ref(refname, ref_filter_regex, ref_is_inclusion):
3734 + sys.stdout.write(' EXCLUDE')
3735 + elif not include_ref(refname, send_filter_regex, send_is_inclusion):
3736 + sys.stdout.write(' DONT-SEND')
3737 + else:
3738 + sys.stdout.write(' DO-SEND')
3739 +
3740 + sys.stdout.write(os.linesep)
3741 +
3742 +
3743 +def show_env(environment, out):
3744 + out.write('Environment values:\n')
3745 + for (k, v) in sorted(environment.get_values().items()):
3746 + if k: # Don't show the {'' : ''} pair.
3747 + out.write(' %s : %r\n' % (k, v))
3748 + out.write('\n')
3749 + # Flush to avoid interleaving with further log output
3750 + out.flush()
3751 +
3752 +
3753 +def check_setup(environment):
3754 + environment.check()
3755 + show_env(environment, sys.stdout)
3756 + sys.stdout.write("Now, checking that git-multimail's standard input "
3757 + "is properly set ..." + os.linesep)
3758 + sys.stdout.write("Please type some text and then press Return" + os.linesep)
3759 + stdin = sys.stdin.readline()
3760 + sys.stdout.write("You have just entered:" + os.linesep)
3761 + sys.stdout.write(stdin)
3762 + sys.stdout.write("git-multimail seems properly set up." + os.linesep)
3763 +
3764 +
3765 def choose_mailer(config, environment):
3766 mailer = config.get('mailer', default='sendmail')
3767
@@ -3608,6 +3774,7 @@ def choose_mailer(config, environment):
3774 smtppass = config.get('smtppass', default='')
3775 smtpcacerts = config.get('smtpcacerts', default='')
3776 mailer = SMTPMailer(
3777 + environment,
3778 envelopesender=(environment.get_sender() or environment.get_fromaddr()),
3779 smtpserver=smtpserver, smtpservertimeout=smtpservertimeout,
3780 smtpserverdebuglevel=smtpserverdebuglevel,
@@ -3620,43 +3787,41 @@ def choose_mailer(config, environment):
3787 command = config.get('sendmailcommand')
3788 if command:
3789 command = shlex.split(command)
3623 - mailer = SendMailer(command=command, envelopesender=environment.get_sender())
3790 + mailer = SendMailer(environment,
3791 + command=command, envelopesender=environment.get_sender())
3792 else:
3793 environment.log_error(
3794 'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer +
3627 - 'please use one of "smtp" or "sendmail".\n'
3795 + 'please use one of "smtp" or "sendmail".'
3796 )
3797 sys.exit(1)
3798 return mailer
3799
3800
3801 KNOWN_ENVIRONMENTS = {
3634 - 'generic': GenericEnvironmentMixin,
3635 - 'gitolite': GitoliteEnvironmentMixin,
3636 - 'stash': StashEnvironmentMixin,
3637 - 'gerrit': GerritEnvironmentMixin,
3802 + 'generic': {'highprec': GenericEnvironmentMixin},
3803 + 'gitolite': {'highprec': GitoliteEnvironmentHighPrecMixin,
3804 + 'lowprec': GitoliteEnvironmentLowPrecMixin},
3805 + 'stash': {'highprec': StashEnvironmentHighPrecMixin,
3806 + 'lowprec': StashEnvironmentLowPrecMixin},
3807 + 'gerrit': {'highprec': GerritEnvironmentHighPrecMixin,
3808 + 'lowprec': GerritEnvironmentLowPrecMixin},
3809 }
3810
3811
3812 def choose_environment(config, osenv=None, env=None, recipients=None,
3813 hook_info=None):
3814 + env_name = choose_environment_name(config, env, osenv)
3815 + environment_klass = build_environment_klass(env_name)
3816 + env = build_environment(environment_klass, env_name, config,
3817 + osenv, recipients, hook_info)
3818 + return env
3819 +
3820 +
3821 +def choose_environment_name(config, env, osenv):
3822 if not osenv:
3823 osenv = os.environ
3824
3646 - environment_mixins = [
3647 - ConfigRefFilterEnvironmentMixin,
3648 - ProjectdescEnvironmentMixin,
3649 - ConfigMaxlinesEnvironmentMixin,
3650 - ComputeFQDNEnvironmentMixin,
3651 - ConfigFilterLinesEnvironmentMixin,
3652 - PusherDomainEnvironmentMixin,
3653 - ConfigOptionsEnvironmentMixin,
3654 - ]
3655 - environment_kw = {
3656 - 'osenv': osenv,
3657 - 'config': config,
3658 - }
3659 -
3825 if not env:
3826 env = config.get('environment')
3827
@@ -3665,8 +3830,58 @@ def choose_environment(config, osenv=None, env=None, recipients=None,
3830 env = 'gitolite'
3831 else:
3832 env = 'generic'
3833 + return env
3834 +
3835 +
3836 +COMMON_ENVIRONMENT_MIXINS = [
3837 + ConfigRecipientsEnvironmentMixin,
3838 + CLIRecipientsEnvironmentMixin,
3839 + ConfigRefFilterEnvironmentMixin,
3840 + ProjectdescEnvironmentMixin,
3841 + ConfigMaxlinesEnvironmentMixin,
3842 + ComputeFQDNEnvironmentMixin,
3843 + ConfigFilterLinesEnvironmentMixin,
3844 + PusherDomainEnvironmentMixin,
3845 + ConfigOptionsEnvironmentMixin,
3846 + ]
3847 +
3848 +
3849 +def build_environment_klass(env_name):
3850 + if 'class' in KNOWN_ENVIRONMENTS[env_name]:
3851 + return KNOWN_ENVIRONMENTS[env_name]['class']
3852 +
3853 + environment_mixins = []
3854 + known_env = KNOWN_ENVIRONMENTS[env_name]
3855 + if 'highprec' in known_env:
3856 + high_prec_mixin = known_env['highprec']
3857 + environment_mixins.append(high_prec_mixin)
3858 + environment_mixins = environment_mixins + COMMON_ENVIRONMENT_MIXINS
3859 + if 'lowprec' in known_env:
3860 + low_prec_mixin = known_env['lowprec']
3861 + environment_mixins.append(low_prec_mixin)
3862 + environment_mixins.append(Environment)
3863 + klass_name = env_name.capitalize() + 'Environement'
3864 + environment_klass = type(
3865 + klass_name,
3866 + tuple(environment_mixins),
3867 + {},
3868 + )
3869 + KNOWN_ENVIRONMENTS[env_name]['class'] = environment_klass
3870 + return environment_klass
3871 +
3872
3669 - environment_mixins.insert(0, KNOWN_ENVIRONMENTS[env])
3873 +GerritEnvironment = build_environment_klass('gerrit')
3874 +StashEnvironment = build_environment_klass('stash')
3875 +GitoliteEnvironment = build_environment_klass('gitolite')
3876 +GenericEnvironment = build_environment_klass('generic')
3877 +
3878 +
3879 +def build_environment(environment_klass, env, config,
3880 + osenv, recipients, hook_info):
3881 + environment_kw = {
3882 + 'osenv': osenv,
3883 + 'config': config,
3884 + }
3885
3886 if env == 'stash':
3887 environment_kw['user'] = hook_info['stash_user']
@@ -3676,20 +3891,8 @@ def choose_environment(config, osenv=None, env=None, recipients=None,
3891 environment_kw['submitter'] = hook_info['submitter']
3892 environment_kw['update_method'] = hook_info['update_method']
3893
3679 - if recipients:
3680 - environment_mixins.insert(0, StaticRecipientsEnvironmentMixin)
3681 - environment_kw['refchange_recipients'] = recipients
3682 - environment_kw['announce_recipients'] = recipients
3683 - environment_kw['revision_recipients'] = recipients
3684 - environment_kw['scancommitforcc'] = config.get('scancommitforcc')
3685 - else:
3686 - environment_mixins.insert(0, ConfigRecipientsEnvironmentMixin)
3894 + environment_kw['cli_recipients'] = recipients
3895
3688 - environment_klass = type(
3689 - 'EffectiveEnvironment',
3690 - tuple(environment_mixins) + (Environment,),
3691 - {},
3692 - )
3896 return environment_klass(**environment_kw)
3897
3898
@@ -3710,7 +3913,8 @@ def get_version():
3913 return __version__
3914
3915
3713 -def compute_gerrit_options(options, args, required_gerrit_options):
3916 +def compute_gerrit_options(options, args, required_gerrit_options,
3917 + raw_refname):
3918 if None in required_gerrit_options:
3919 raise SystemExit("Error: Specify all of --oldrev, --newrev, --refname, "
3920 "and --project; or none of them.")
@@ -3727,24 +3931,11 @@ def compute_gerrit_options(options, args, required_gerrit_options):
3931 # Gerrit oddly omits 'refs/heads/' in the refname when calling
3932 # ref-updated hook; put it back.
3933 git_dir = get_git_dir()
3730 - if (not os.path.exists(os.path.join(git_dir, options.refname)) and
3934 + if (not os.path.exists(os.path.join(git_dir, raw_refname)) and
3935 os.path.exists(os.path.join(git_dir, 'refs', 'heads',
3732 - options.refname))):
3936 + raw_refname))):
3937 options.refname = 'refs/heads/' + options.refname
3938
3735 - # Convert each string option unicode for Python3.
3736 - if PYTHON3:
3737 - opts = ['environment', 'recipients', 'oldrev', 'newrev', 'refname',
3738 - 'project', 'submitter', 'stash-user', 'stash-repo']
3739 - for opt in opts:
3740 - if not hasattr(options, opt):
3741 - continue
3742 - obj = getattr(options, opt)
3743 - if obj:
3744 - enc = obj.encode('utf-8', 'surrogateescape')
3745 - dec = enc.decode('utf-8', 'replace')
3746 - setattr(options, opt, dec)
3747 -
3939 # New revisions can appear in a gerrit repository either due to someone
3940 # pushing directly (in which case options.submitter will be set), or they
3941 # can press "Submit this patchset" in the web UI for some CR (in which
@@ -3784,6 +3975,20 @@ def compute_gerrit_options(options, args, required_gerrit_options):
3975
3976
3977 def check_hook_specific_args(options, args):
3978 + raw_refname = options.refname
3979 + # Convert each string option unicode for Python3.
3980 + if PYTHON3:
3981 + opts = ['environment', 'recipients', 'oldrev', 'newrev', 'refname',
3982 + 'project', 'submitter', 'stash_user', 'stash_repo']
3983 + for opt in opts:
3984 + if not hasattr(options, opt):
3985 + continue
3986 + obj = getattr(options, opt)
3987 + if obj:
3988 + enc = obj.encode('utf-8', 'surrogateescape')
3989 + dec = enc.decode('utf-8', 'replace')
3990 + setattr(options, opt, dec)
3991 +
3992 # First check for stash arguments
3993 if (options.stash_user is None) != (options.stash_repo is None):
3994 raise SystemExit("Error: Specify both of --stash-user and "
@@ -3797,12 +4002,78 @@ def check_hook_specific_args(options, args):
4002 required_gerrit_options = (options.oldrev, options.newrev, options.refname,
4003 options.project)
4004 if required_gerrit_options != (None,) * 4:
3800 - return compute_gerrit_options(options, args, required_gerrit_options)
4005 + return compute_gerrit_options(options, args, required_gerrit_options,
4006 + raw_refname)
4007
4008 # No special options in use, just return what we started with
4009 return options, args, {}
4010
4011
4012 +class Logger(object):
4013 + def parse_verbose(self, verbose):
4014 + if verbose > 0:
4015 + return logging.DEBUG
4016 + else:
4017 + return logging.INFO
4018 +
4019 + def create_log_file(self, environment, name, path, verbosity):
4020 + log_file = logging.getLogger(name)
4021 + file_handler = logging.FileHandler(path)
4022 + log_fmt = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s")
4023 + file_handler.setFormatter(log_fmt)
4024 + log_file.addHandler(file_handler)
4025 + log_file.setLevel(verbosity)
4026 + return log_file
4027 +
4028 + def __init__(self, environment):
4029 + self.environment = environment
4030 + self.loggers = []
4031 + stderr_log = logging.getLogger('git_multimail.stderr')
4032 +
4033 + class EncodedStderr(object):
4034 + def write(self, x):
4035 + write_str(sys.stderr, x)
4036 +
4037 + def flush(self):
4038 + sys.stderr.flush()
4039 +
4040 + stderr_handler = logging.StreamHandler(EncodedStderr())
4041 + stderr_log.addHandler(stderr_handler)
4042 + stderr_log.setLevel(self.parse_verbose(environment.verbose))
4043 + self.loggers.append(stderr_log)
4044 +
4045 + if environment.debug_log_file is not None:
4046 + debug_log_file = self.create_log_file(
4047 + environment, 'git_multimail.debug', environment.debug_log_file, logging.DEBUG)
4048 + self.loggers.append(debug_log_file)
4049 +
4050 + if environment.log_file is not None:
4051 + log_file = self.create_log_file(
4052 + environment, 'git_multimail.file', environment.log_file, logging.INFO)
4053 + self.loggers.append(log_file)
4054 +
4055 + if environment.error_log_file is not None:
4056 + error_log_file = self.create_log_file(
4057 + environment, 'git_multimail.error', environment.error_log_file, logging.ERROR)
4058 + self.loggers.append(error_log_file)
4059 +
4060 + def info(self, msg):
4061 + for l in self.loggers:
4062 + l.info(msg)
4063 +
4064 + def debug(self, msg):
4065 + for l in self.loggers:
4066 + l.debug(msg)
4067 +
4068 + def warning(self, msg):
4069 + for l in self.loggers:
4070 + l.warning(msg)
4071 +
4072 + def error(self, msg):
4073 + for l in self.loggers:
4074 + l.error(msg)
4075 +
4076 +
4077 def main(args):
4078 parser = optparse.OptionParser(
4079 description=__doc__,
@@ -3829,7 +4100,7 @@ def main(args):
4100 '--show-env', action='store_true', default=False,
4101 help=(
4102 'Write to stderr the values determined for the environment '
3832 - '(intended for debugging purposes).'
4103 + '(intended for debugging purposes), then proceed normally.'
4104 ),
4105 )
4106 parser.add_option(
@@ -3854,6 +4125,22 @@ def main(args):
4125 "Display git-multimail's version"
4126 ),
4127 )
4128 +
4129 + parser.add_option(
4130 + '--python-version', action='store_true', default=False,
4131 + help=(
4132 + "Display the version of Python used by git-multimail"
4133 + ),
4134 + )
4135 +
4136 + parser.add_option(
4137 + '--check-ref-filter', action='store_true', default=False,
4138 + help=(
4139 + 'List refs and show information on how git-multimail '
4140 + 'will process them.'
4141 + )
4142 + )
4143 +
4144 # The following options permit this script to be run as a gerrit
4145 # ref-updated hook. See e.g.
4146 # code.google.com/p/gerrit/source/browse/Documentation/config-hooks.txt
@@ -3880,11 +4167,16 @@ def main(args):
4167 sys.stdout.write('git-multimail version ' + get_version() + '\n')
4168 return
4169
4170 + if options.python_version:
4171 + sys.stdout.write('Python version ' + sys.version + '\n')
4172 + return
4173 +
4174 if options.c:
4175 Config.add_config_parameters(options.c)
4176
4177 config = Config('multimailhook')
4178
4179 + environment = None
4180 try:
4181 environment = choose_environment(
4182 config, osenv=os.environ,
@@ -3894,38 +4186,52 @@ def main(args):
4186 )
4187
4188 if options.show_env:
3897 - sys.stderr.write('Environment values:\n')
3898 - for (k, v) in sorted(environment.get_values().items()):
3899 - sys.stderr.write(' %s : %r\n' % (k, v))
3900 - sys.stderr.write('\n')
4189 + show_env(environment, sys.stderr)
4190
4191 if options.stdout or environment.stdout:
4192 mailer = OutputMailer(sys.stdout)
4193 else:
4194 mailer = choose_mailer(config, environment)
4195
4196 + must_check_setup = os.environ.get('GIT_MULTIMAIL_CHECK_SETUP')
4197 + if must_check_setup == '':
4198 + must_check_setup = False
4199 + if options.check_ref_filter:
4200 + check_ref_filter(environment)
4201 + elif must_check_setup:
4202 + check_setup(environment)
4203 # Dual mode: if arguments were specified on the command line, run
4204 # like an update hook; otherwise, run as a post-receive hook.
3909 - if args:
4205 + elif args:
4206 if len(args) != 3:
4207 parser.error('Need zero or three non-option arguments')
4208 (refname, oldrev, newrev) = args
4209 + environment.get_logger().debug(
4210 + "run_as_update_hook: refname=%s, oldrev=%s, newrev=%s, force_send=%s" %
4211 + (refname, oldrev, newrev, options.force_send))
4212 run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send)
4213 else:
4214 run_as_post_receive_hook(environment, mailer)
4215 except ConfigurationException:
4216 sys.exit(sys.exc_info()[1])
4217 + except SystemExit:
4218 + raise
4219 except Exception:
4220 t, e, tb = sys.exc_info()
4221 import traceback
3921 - sys.stdout.write('\n')
3922 - sys.stdout.write('Exception \'' + t.__name__ +
3923 - '\' raised. Please report this as a bug to\n')
3924 - sys.stdout.write('https://github.com/git-multimail/git-multimail/issues\n')
3925 - sys.stdout.write('with the information below:\n\n')
3926 - sys.stdout.write('git-multimail version ' + get_version() + '\n')
3927 - sys.stdout.write('Python version ' + sys.version + '\n')
3928 - traceback.print_exc(file=sys.stdout)
4222 + sys.stderr.write('\n') # Avoid mixing message with previous output
4223 + msg = (
4224 + 'Exception \'' + t.__name__ +
4225 + '\' raised. Please report this as a bug to\n'
4226 + 'https://github.com/git-multimail/git-multimail/issues\n'
4227 + 'with the information below:\n\n'
4228 + 'git-multimail version ' + get_version() + '\n'
4229 + 'Python version ' + sys.version + '\n' +
4230 + traceback.format_exc())
4231 + try:
4232 + environment.get_logger().error(msg)
4233 + except:
4234 + sys.stderr.write(msg)
4235 sys.exit(1)
4236
4237 if __name__ == '__main__':