git-multimail: update to release 1.5.0

Changes are described in CHANGES. Contributions-by: Matthieu Moy <git@matthieu-moy.fr> Contributions-by: William Stewart <william.stewart@booking.com> Contributions-by: Ville Skyttä <ville.skytta@iki.fi> Contributions-by: Dirk Olmes <dirk.olmes@codedo.de> Contributions-by: Björn Kautler <Bjoern@Kautler.net> Contributions-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org> Contributions-by: Gareth Pye <garethp@gpsatsys.com.au> Contributions-by: David Lazar <lazard@csail.mit.edu> Signed-off-by: Matthieu Moy <git@matthieu-moy.fr> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Matthieu Moy committed Jan 7, 2019 at 18:48 UTC 99177b34db1d473e8f90544cf0bf83f47308e9ad
8 files changed +281 -57
contrib/hooks/multimail/CHANGES
+56
@@ -1,3 +1,59 @@
1 +Release 1.5.0
2 +=============
3 +
4 +Backward-incompatible change
5 +----------------------------
6 +
7 +The name of classes for environment was misnamed as `*Environement`.
8 +It is now `*Environment`.
9 +
10 +New features
11 +------------
12 +
13 +* A Thread-Index header is now added to each email sent (except for
14 + combined emails where it would not make sense), so that MS Outlook
15 + properly groups messages by threads even though they have a
16 + different subject line. Unfortunately, even adding this header the
17 + threading still seems to be unreliable, but it is unclear whether
18 + this is an issue on our side or on MS Outlook's side (see discussion
19 + here: https://github.com/git-multimail/git-multimail/pull/194).
20 +
21 +* A new variable multimailhook.ExcludeMergeRevisions was added to send
22 + notification emails only for non-merge commits.
23 +
24 +* For gitolite environment, it is now possible to specify the mail map
25 + in a separate file in addition to gitolite.conf, using the variable
26 + multimailhook.MailaddressMap.
27 +
28 +Internal changes
29 +----------------
30 +
31 +* The testsuite now uses GIT_PRINT_SHA1_ELLIPSIS where needed for
32 + compatibility with recent Git versions. Only tests are affected.
33 +
34 +* We don't try to install pyflakes in the continuous integration job
35 + for old Python versions where it's no longer available.
36 +
37 +* Stop using the deprecated cgi.escape in Python 3.
38 +
39 +* New flake8 warnings have been fixed.
40 +
41 +* Python 3.6 is now tested against on Travis-CI.
42 +
43 +* A bunch of lgtm.com warnings have been fixed.
44 +
45 +Bug fixes
46 +---------
47 +
48 +* SMTPMailer logs in only once now. It used to re-login for each email
49 + sent which triggered errors for some SMTP servers.
50 +
51 +* migrate-mailhook-config was broken by internal refactoring, it
52 + should now work again.
53 +
54 +This version was tested with Python 2.6 to 3.7. It was tested with Git
55 +1.7.10.406.gdc801, 2.15.1 and 2.20.1.98.gecbdaf0.
56 +
57 Release 1.4.0
58 =============
59
contrib/hooks/multimail/CONTRIBUTING.rst
+25 -3
@@ -4,9 +4,8 @@ Contributing
4 git-multimail is an open-source project, built by volunteers. We would
5 welcome your help!
6
7 -The current maintainers are Matthieu Moy
8 -<matthieu.moy@grenoble-inp.fr> and Michael Haggerty
9 -<mhagger@alum.mit.edu>.
7 +The current maintainers are `Matthieu Moy <http://matthieu-moy.fr>`__ and
8 +`Michael Haggerty <https://github.com/mhagger>`__.
9
10 Please note that although a copy of git-multimail is distributed in
11 the "contrib" section of the main Git project, development takes place
@@ -33,6 +32,29 @@ mailing list`_.
32 Please CC emails regarding git-multimail to the maintainers so that we
33 don't overlook them.
34
35 +Help needed: testers/maintainer for specific environments/OS
36 +------------------------------------------------------------
37 +
38 +The current maintainer uses and tests git-multimail on Linux with the
39 +Generic environment. More testers, or better contributors are needed
40 +to test git-multimail on other real-life setups:
41 +
42 +* Mac OS X, Windows: git-multimail is currently not supported on these
43 + platforms. But since we have no external dependencies and try to
44 + write code as portable as possible, it is possible that
45 + git-multimail already runs there and if not, it is likely that it
46 + could be ported easily.
47 +
48 + Patches to improve support for Windows and OS X are welcome.
49 + Ideally, there would be a sub-maintainer for each OS who would test
50 + at least once before each release (around twice a year).
51 +
52 +* Gerrit, Stash, Gitolite environments: although the testsuite
53 + contains tests for these environments, a tester/maintainer for each
54 + environment would be welcome to test and report failure (or success)
55 + on real-life environments periodically (here also, feedback before
56 + each release would be highly appreciated).
57 +
58
59 .. _`git-multimail repository on GitHub`: https://github.com/git-multimail/git-multimail
60 .. _`Git mailing list`: git@vger.kernel.org
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 August 17 2016 and consists of the "git-multimail" subdirectory from
9 +on January 07 2019 and consists of the "git-multimail" subdirectory from
10 revision
11
12 - 07b1cb6bfd7be156c62e1afa17cae13b850a869f refs/tags/1.4.0
12 + 04e80e6c40be465cc62b6c246f0fcb8fd2cfd454 refs/tags/1.5.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/README.rst renamed
+32 -6
@@ -1,4 +1,4 @@
1 -git-multimail version 1.4.0
1 +git-multimail version 1.5.0
2 ===========================
3
4 .. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master
@@ -20,8 +20,8 @@ GPLv2 (see the COPYING file for details).
20
21 Please note: although, as a convenience, git-multimail may be
22 distributed along with the main Git project, development of
23 -git-multimail takes place in its own, separate project. See section
24 -"Getting involved" below for more information.
23 +git-multimail takes place in its own, separate project. Please, read
24 +`<CONTRIBUTING.rst>`__ for more information.
25
26
27 By default, for each push received by the repository, git-multimail:
@@ -89,6 +89,10 @@ Requirements
89 the multimailhook.mailer configuration variable below for how to
90 configure git-multimail to send emails via an SMTP server.
91
92 +* git-multimail is currently tested only on Linux. It may or may not
93 + work on other platforms such as Windows and Mac OS. See
94 + `<CONTRIBUTING.rst>`__ to improve the situation.
95 +
96
97 Invocation
98 ----------
@@ -369,7 +373,7 @@ multimailhook.mailer
373 unset, then the value of multimailhook.from is used.
374
375 multimailhook.smtpServerTimeout
372 - Timeout in seconds.
376 + Timeout in seconds. Default is 10.
377
378 multimailhook.smtpEncryption
379 Set the security type. Allowed values: ``none``, ``ssl``, ``tls`` (starttls).
@@ -419,8 +423,20 @@ multimailhook.from, multimailhook.fromCommit, multimailhook.fromRefchange
423 If config values are unset, the value of the From: header is
424 determined as follows:
425
422 - 1. (gitolite environment only) Parse gitolite.conf, looking for a
423 - block of comments that looks like this::
426 + 1. (gitolite environment only)
427 + 1.a) If ``multimailhook.MailaddressMap`` is set, and is a path
428 + to an existing file (if relative, it is considered relative to
429 + the place where ``gitolite.conf`` is located), then this file
430 + should contain lines like::
431 +
432 + username Firstname Lastname <email@example.com>
433 +
434 + git-multimail will then look for a line where ``$GL_USER``
435 + matches the ``username`` part, and use the rest of the line for
436 + the ``From:`` header.
437 +
438 + 1.b) Parse gitolite.conf, looking for a block of comments that
439 + looks like this::
440
441 # BEGIN USER EMAILS
442 # username Firstname Lastname <email@example.com>
@@ -436,6 +452,11 @@ multimailhook.from, multimailhook.fromCommit, multimailhook.fromRefchange
452
453 3. Use the value of multimailhook.envelopeSender.
454
455 +multimailhook.MailaddressMap
456 + (gitolite environment only)
457 + File to look for a ``From:`` address based on the user doing the
458 + push. Defaults to unset. See ``multimailhook.from`` for details.
459 +
460 multimailhook.administrator
461 The name and/or email address of the administrator of the Git
462 repository; used in FOOTER_TEMPLATE. Default is
@@ -484,6 +505,11 @@ multimailhook.maxCommitEmails
505 mailbombing, for example on an initial push. To disable commit
506 emails limit, set this option to 0. The default is 500.
507
508 +multimailhook.excludeMergeRevisions
509 + When sending out revision emails, do not consider merge commits (the
510 + functional equivalent of `rev-list --no-merges`).
511 + The default is `false` (send merge commit emails).
512 +
513 multimailhook.emailStrictUTF8
514 If this boolean option is set to `true`, then the main part of the
515 email body is forced to be valid UTF-8. Any characters that are
contrib/hooks/multimail/doc/gitolite.rst
+9
@@ -46,6 +46,15 @@ and add::
46 config multimailhook.mailingList = # Where emails should be sent
47 config multimailhook.from = # From address to use
48
49 +Note that by default, gitolite forbids ``<`` and ``>`` in variable
50 +values (for security/paranoia reasons, see
51 +`compensating for UNSAFE_PATT
52 +<http://gitolite.com/gitolite/git-config/index.html#compensating-for-unsafe95patt>`__
53 +in gitolite's documentation for explanations and a way to disable
54 +this). As a consequence, you will not be able to use ``First Last
55 +<First.Last@example.com>`` as recipient email, but specifying
56 +``First.Last@example.com`` alone works.
57 +
58 Obviously, you can customize all parameters on a per-repository basis by
59 adding these ``config multimailhook.*`` lines in the section
60 corresponding to a repository or set of repositories.
contrib/hooks/multimail/git_multimail.py
+148 -40
@@ -1,6 +1,6 @@
1 #! /usr/bin/env python
2
3 -__version__ = '1.4.0'
3 +__version__ = '1.5.0'
4
5 # Copyright (c) 2015-2016 Matthieu Moy and others
6 # Copyright (c) 2012-2014 Michael Haggerty and others
@@ -64,7 +64,9 @@ except ImportError:
64 # Python < 2.6 do not have ssl, but that's OK if we don't use it.
65 pass
66 import time
67 -import cgi
67 +
68 +import uuid
69 +import base64
70
71 PYTHON3 = sys.version_info >= (3, 0)
72
@@ -73,7 +75,7 @@ if sys.version_info <= (2, 5):
75 for element in iterable:
76 if not element:
77 return False
76 - return True
78 + return True
79
80
81 def is_ascii(s):
@@ -108,6 +110,12 @@ if PYTHON3:
110 return out.decode(sys.getdefaultencoding())
111 except UnicodeEncodeError:
112 return out.decode(ENCODING)
113 +
114 + import html
115 +
116 + def html_escape(s):
117 + return html.escape(s)
118 +
119 else:
120 def is_string(s):
121 try:
@@ -130,6 +138,10 @@ else:
138 def next(it):
139 return it.next()
140
141 + import cgi
142 +
143 + def html_escape(s):
144 + return cgi.escape(s, True)
145
146 try:
147 from email.charset import Charset
@@ -190,6 +202,7 @@ Content-Transfer-Encoding: 8bit
202 Message-ID: %(msgid)s
203 From: %(fromaddr)s
204 Reply-To: %(reply_to)s
205 +Thread-Index: %(thread_index)s
206 X-Git-Host: %(fqdn)s
207 X-Git-Repo: %(repo_shortname)s
208 X-Git-Refname: %(refname)s
@@ -322,6 +335,7 @@ From: %(fromaddr)s
335 Reply-To: %(reply_to)s
336 In-Reply-To: %(reply_to_msgid)s
337 References: %(reply_to_msgid)s
338 +Thread-Index: %(thread_index)s
339 X-Git-Host: %(fqdn)s
340 X-Git-Repo: %(repo_shortname)s
341 X-Git-Refname: %(refname)s
@@ -763,6 +777,9 @@ class GitObject(object):
777 def __eq__(self, other):
778 return isinstance(other, GitObject) and self.sha1 == other.sha1
779
780 + def __ne__(self, other):
781 + return not self == other
782 +
783 def __hash__(self):
784 return hash(self.sha1)
785
@@ -852,7 +869,7 @@ class Change(object):
869 if html_escape_val:
870 for k in values:
871 if is_string(values[k]):
855 - values[k] = cgi.escape(values[k], True)
872 + values[k] = html_escape(values[k])
873 for line in template.splitlines(True):
874 yield line % values
875
@@ -909,7 +926,7 @@ class Change(object):
926
927 raise NotImplementedError()
928
912 - def generate_email_body(self):
929 + def generate_email_body(self, push):
930 """Generate the main part of the email body, a line at a time.
931
932 The text in the body might be truncated after a specified
@@ -936,7 +953,7 @@ class Change(object):
953 yield "<pre style='margin:0'>\n"
954
955 for line in lines:
939 - yield cgi.escape(line)
956 + yield html_escape(line)
957
958 yield '</pre>\n'
959 else:
@@ -1011,7 +1028,7 @@ class Change(object):
1028 fgcolor = '404040'
1029
1030 # Chop the trailing LF, we don't want it inside <pre>.
1014 - line = cgi.escape(line[:-1])
1031 + line = html_escape(line[:-1])
1032
1033 if bgcolor or fgcolor:
1034 style = 'display:block; white-space:pre;'
@@ -1060,6 +1077,10 @@ class Revision(Change):
1077 self.author = read_git_output(['log', '--no-walk', '--format=%aN <%aE>', self.rev.sha1])
1078 self.recipients = self.environment.get_revision_recipients(self)
1079
1080 + # -s is short for --no-patch, but -s works on older git's (e.g. 1.7)
1081 + self.parents = read_git_lines(['show', '-s', '--format=%P',
1082 + self.rev.sha1])[0].split()
1083 +
1084 self.cc_recipients = ''
1085 if self.environment.get_scancommitforcc():
1086 self.cc_recipients = ', '.join(to.strip() for to in self._cc_recipients())
@@ -1090,6 +1111,7 @@ class Revision(Change):
1111 oneline = oneline[:max_subject_length - 6] + ' [...]'
1112
1113 values['rev'] = self.rev.sha1
1114 + values['parents'] = ' '.join(self.parents)
1115 values['rev_short'] = self.rev.short
1116 values['change_type'] = self.change_type
1117 values['refname'] = self.refname
@@ -1097,6 +1119,7 @@ class Revision(Change):
1119 values['short_refname'] = self.reference_change.short_refname
1120 values['refname_type'] = self.reference_change.refname_type
1121 values['reply_to_msgid'] = self.reference_change.msgid
1122 + values['thread_index'] = self.reference_change.thread_index
1123 values['num'] = self.num
1124 values['tot'] = self.tot
1125 values['recipients'] = self.recipients
@@ -1244,6 +1267,23 @@ class ReferenceChange(Change):
1267 old=old, new=new, rev=rev,
1268 )
1269
1270 + @staticmethod
1271 + def make_thread_index():
1272 + """Return a string appropriate for the Thread-Index header,
1273 + needed by MS Outlook to get threading right.
1274 +
1275 + The format is (base64-encoded):
1276 + - 1 byte must be 1
1277 + - 5 bytes encode a date (hardcoded here)
1278 + - 16 bytes for a globally unique identifier
1279 +
1280 + FIXME: Unfortunately, even with the Thread-Index field, MS
1281 + Outlook doesn't seem to do the threading reliably (see
1282 + https://github.com/git-multimail/git-multimail/pull/194).
1283 + """
1284 + thread_index = b'\x01\x00\x00\x12\x34\x56' + uuid.uuid4().bytes
1285 + return base64.standard_b64encode(thread_index).decode('ascii')
1286 +
1287 def __init__(self, environment, refname, short_refname, old, new, rev):
1288 Change.__init__(self, environment)
1289 self.change_type = {
@@ -1257,6 +1297,7 @@ class ReferenceChange(Change):
1297 self.new = new
1298 self.rev = rev
1299 self.msgid = make_msgid()
1300 + self.thread_index = self.make_thread_index()
1301 self.diffopts = environment.diffopts
1302 self.graphopts = environment.graphopts
1303 self.logopts = environment.logopts
@@ -1276,6 +1317,7 @@ class ReferenceChange(Change):
1317 values['refname'] = self.refname
1318 values['short_refname'] = self.short_refname
1319 values['msgid'] = self.msgid
1320 + values['thread_index'] = self.thread_index
1321 values['recipients'] = self.recipients
1322 values['oldrev'] = str(self.old)
1323 values['oldrev_short'] = self.old.short
@@ -1941,6 +1983,9 @@ class Mailer(object):
1983 def __init__(self, environment):
1984 self.environment = environment
1985
1986 + def close(self):
1987 + pass
1988 +
1989 def send(self, lines, to_addrs):
1990 """Send an email consisting of lines.
1991
@@ -2054,6 +2099,7 @@ class SMTPMailer(Mailer):
2099 self.username = smtpuser
2100 self.password = smtppass
2101 self.smtpcacerts = smtpcacerts
2102 + self.loggedin = False
2103 try:
2104 def call(klass, server, timeout):
2105 try:
@@ -2130,20 +2176,30 @@ class SMTPMailer(Mailer):
2176 % (self.smtpserver, sys.exc_info()[1]))
2177 sys.exit(1)
2178
2133 - def __del__(self):
2179 + def close(self):
2180 if hasattr(self, 'smtp'):
2181 self.smtp.quit()
2182 del self.smtp
2183
2184 + def __del__(self):
2185 + self.close()
2186 +
2187 def send(self, lines, to_addrs):
2188 try:
2189 if self.username or self.password:
2141 - self.smtp.login(self.username, self.password)
2190 + if not self.loggedin:
2191 + self.smtp.login(self.username, self.password)
2192 + self.loggedin = True
2193 msg = ''.join(lines)
2194 # turn comma-separated list into Python list if needed.
2195 if is_string(to_addrs):
2196 to_addrs = [email for (name, email) in getaddresses([to_addrs])]
2197 self.smtp.sendmail(self.envelopesender, to_addrs, msg)
2198 + except socket.timeout:
2199 + self.environment.get_logger().error(
2200 + '*** Error sending email ***\n'
2201 + '*** SMTP server timed out (timeout is %s)\n'
2202 + % self.smtpservertimeout)
2203 except smtplib.SMTPResponseException:
2204 err = sys.exc_info()[1]
2205 self.environment.get_logger().error(
@@ -2171,7 +2227,8 @@ class OutputMailer(Mailer):
2227
2228 SEPARATOR = '=' * 75 + '\n'
2229
2174 - def __init__(self, f):
2230 + def __init__(self, f, environment=None):
2231 + super(OutputMailer, self).__init__(environment=environment)
2232 self.f = f
2233
2234 def send(self, lines, to_addrs):
@@ -2382,6 +2439,7 @@ class Environment(object):
2439 self.html_in_footer = False
2440 self.commitBrowseURL = None
2441 self.maxcommitemails = 500
2442 + self.excludemergerevisions = False
2443 self.diffopts = ['--stat', '--summary', '--find-copies-harder']
2444 self.graphopts = ['--oneline', '--decorate']
2445 self.logopts = []
@@ -2621,6 +2679,8 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2679
2680 self.commitBrowseURL = config.get('commitBrowseURL')
2681
2682 + self.excludemergerevisions = config.get('excludeMergeRevisions')
2683 +
2684 maxcommitemails = config.get('maxcommitemails')
2685 if maxcommitemails is not None:
2686 try:
@@ -3152,7 +3212,10 @@ class GitoliteEnvironmentHighPrecMixin(Environment):
3212 return self.osenv.get('GL_USER', 'unknown user')
3213
3214
3155 -class GitoliteEnvironmentLowPrecMixin(Environment):
3215 +class GitoliteEnvironmentLowPrecMixin(
3216 + ConfigEnvironmentMixin,
3217 + Environment):
3218 +
3219 def get_repo_shortname(self):
3220 # The gitolite environment variable $GL_REPO is a pretty good
3221 # repo_shortname (though it's probably not as good as a value
@@ -3162,6 +3225,16 @@ class GitoliteEnvironmentLowPrecMixin(Environment):
3225 super(GitoliteEnvironmentLowPrecMixin, self).get_repo_shortname()
3226 )
3227
3228 + @staticmethod
3229 + def _compile_regex(re_template):
3230 + return (
3231 + re.compile(re_template % x)
3232 + for x in (
3233 + r'BEGIN\s+USER\s+EMAILS',
3234 + r'([^\s]+)\s+(.*)',
3235 + r'END\s+USER\s+EMAILS',
3236 + ))
3237 +
3238 def get_fromaddr(self, change=None):
3239 GL_USER = self.osenv.get('GL_USER')
3240 if GL_USER is not None:
@@ -3174,18 +3247,42 @@ class GitoliteEnvironmentLowPrecMixin(Environment):
3247 GL_CONF = self.osenv.get(
3248 'GL_CONF',
3249 os.path.join(GL_ADMINDIR, 'conf', 'gitolite.conf'))
3250 +
3251 + mailaddress_map = self.config.get('MailaddressMap')
3252 + # If relative, consider relative to GL_CONF:
3253 + if mailaddress_map:
3254 + mailaddress_map = os.path.join(os.path.dirname(GL_CONF),
3255 + mailaddress_map)
3256 + if os.path.isfile(mailaddress_map):
3257 + f = open(mailaddress_map, 'rU')
3258 + try:
3259 + # Leading '#' is optional
3260 + re_begin, re_user, re_end = self._compile_regex(
3261 + r'^(?:\s*#)?\s*%s\s*$')
3262 + for l in f:
3263 + l = l.rstrip('\n')
3264 + if re_begin.match(l) or re_end.match(l):
3265 + continue # Ignore these lines
3266 + m = re_user.match(l)
3267 + if m:
3268 + if m.group(1) == GL_USER:
3269 + return m.group(2)
3270 + else:
3271 + continue # Not this user, but not an error
3272 + raise ConfigurationException(
3273 + "Syntax error in mail address map.\n"
3274 + "Check file {}.\n"
3275 + "Line: {}".format(mailaddress_map, l))
3276 +
3277 + finally:
3278 + f.close()
3279 +
3280 if os.path.isfile(GL_CONF):
3281 f = open(GL_CONF, 'rU')
3282 try:
3283 in_user_emails_section = False
3181 - re_template = r'^\s*#\s*%s\s*$'
3182 - re_begin, re_user, re_end = (
3183 - re.compile(re_template % x)
3184 - for x in (
3185 - r'BEGIN\s+USER\s+EMAILS',
3186 - re.escape(GL_USER) + r'\s+(.*)',
3187 - r'END\s+USER\s+EMAILS',
3188 - ))
3284 + re_begin, re_user, re_end = self._compile_regex(
3285 + r'^\s*#\s*%s\s*$')
3286 for l in f:
3287 l = l.rstrip('\n')
3288 if not in_user_emails_section:
@@ -3195,8 +3292,8 @@ class GitoliteEnvironmentLowPrecMixin(Environment):
3292 if re_end.match(l):
3293 break
3294 m = re_user.match(l)
3198 - if m:
3199 - return m.group(1)
3295 + if m and m.group(1) == GL_USER:
3296 + return m.group(2)
3297 finally:
3298 f.close()
3299 return super(GitoliteEnvironmentLowPrecMixin, self).get_fromaddr(change)
@@ -3228,7 +3325,7 @@ class StashEnvironmentHighPrecMixin(Environment):
3325 self.__repo = repo
3326
3327 def get_pusher(self):
3231 - return re.match('(.*?)\s*<', self.__user).group(1)
3328 + return re.match(r'(.*?)\s*<', self.__user).group(1)
3329
3330 def get_pusher_email(self):
3331 return self.__user
@@ -3262,7 +3359,7 @@ class GerritEnvironmentHighPrecMixin(Environment):
3359 if self.__submitter.find('<') != -1:
3360 # Submitter has a configured email, we transformed
3361 # __submitter into an RFC 2822 string already.
3265 - return re.match('(.*?)\s*<', self.__submitter).group(1)
3362 + return re.match(r'(.*?)\s*<', self.__submitter).group(1)
3363 else:
3364 # Submitter has no configured email, it's just his name.
3365 return self.__submitter
@@ -3615,6 +3712,9 @@ class Push(object):
3712
3713 for (num, sha1) in enumerate(sha1s):
3714 rev = Revision(change, GitObject(sha1), num=num + 1, tot=len(sha1s))
3715 + if len(rev.parents) > 1 and change.environment.excludemergerevisions:
3716 + # skipping a merge commit
3717 + continue
3718 if not rev.recipients and rev.cc_recipients:
3719 change.environment.log_msg('*** Replacing Cc: with To:')
3720 rev.recipients = rev.cc_recipients
@@ -3664,11 +3764,14 @@ def run_as_post_receive_hook(environment, mailer):
3764 changes.append(
3765 ReferenceChange.create(environment, oldrev, newrev, refname)
3766 )
3667 - if changes:
3668 - push = Push(environment, changes)
3767 + if not changes:
3768 + mailer.close()
3769 + return
3770 + push = Push(environment, changes)
3771 + try:
3772 push.send_emails(mailer, body_filter=environment.filter_body)
3670 - if hasattr(mailer, '__del__'):
3671 - mailer.__del__()
3773 + finally:
3774 + mailer.close()
3775
3776
3777 def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
@@ -3687,10 +3790,14 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=
3790 refname,
3791 ),
3792 ]
3793 + if not changes:
3794 + mailer.close()
3795 + return
3796 push = Push(environment, changes, force_send)
3691 - push.send_emails(mailer, body_filter=environment.filter_body)
3692 - if hasattr(mailer, '__del__'):
3693 - mailer.__del__()
3797 + try:
3798 + push.send_emails(mailer, body_filter=environment.filter_body)
3799 + finally:
3800 + mailer.close()
3801
3802
3803 def check_ref_filter(environment):
@@ -3860,7 +3967,7 @@ def build_environment_klass(env_name):
3967 low_prec_mixin = known_env['lowprec']
3968 environment_mixins.append(low_prec_mixin)
3969 environment_mixins.append(Environment)
3863 - klass_name = env_name.capitalize() + 'Environement'
3970 + klass_name = env_name.capitalize() + 'Environment'
3971 environment_klass = type(
3972 klass_name,
3973 tuple(environment_mixins),
@@ -4057,21 +4164,21 @@ class Logger(object):
4164 environment, 'git_multimail.error', environment.error_log_file, logging.ERROR)
4165 self.loggers.append(error_log_file)
4166
4060 - def info(self, msg):
4167 + def info(self, msg, *args, **kwargs):
4168 for l in self.loggers:
4062 - l.info(msg)
4169 + l.info(msg, *args, **kwargs)
4170
4064 - def debug(self, msg):
4171 + def debug(self, msg, *args, **kwargs):
4172 for l in self.loggers:
4066 - l.debug(msg)
4173 + l.debug(msg, *args, **kwargs)
4174
4068 - def warning(self, msg):
4175 + def warning(self, msg, *args, **kwargs):
4176 for l in self.loggers:
4070 - l.warning(msg)
4177 + l.warning(msg, *args, **kwargs)
4178
4072 - def error(self, msg):
4179 + def error(self, msg, *args, **kwargs):
4180 for l in self.loggers:
4074 - l.error(msg)
4181 + l.error(msg, *args, **kwargs)
4182
4183
4184 def main(args):
@@ -4189,7 +4296,7 @@ def main(args):
4296 show_env(environment, sys.stderr)
4297
4298 if options.stdout or environment.stdout:
4192 - mailer = OutputMailer(sys.stdout)
4299 + mailer = OutputMailer(sys.stdout, environment)
4300 else:
4301 mailer = choose_mailer(config, environment)
4302
@@ -4234,5 +4341,6 @@ def main(args):
4341 sys.stderr.write(msg)
4342 sys.exit(1)
4343
4344 +
4345 if __name__ == '__main__':
4346 main(sys.argv[1:])
contrib/hooks/multimail/migrate-mailhook-config
+8 -5
@@ -110,11 +110,12 @@ def is_section_empty(section, local):
110
111 try:
112 read_output(
113 - ['git', 'config']
114 - + local_option
115 - + ['--get-regexp', '^%s\.' % (section,)]
113 + ['git', 'config'] +
114 + local_option +
115 + ['--get-regexp', '^%s\.' % (section,)]
116 )
117 - except CommandError, e:
117 + except CommandError:
118 + t, e, traceback = sys.exc_info()
119 if e.retcode == 1:
120 # This means that no settings were found.
121 return True
@@ -188,7 +189,9 @@ def migrate_config(strict=False, retain=False, overwrite=False):
189 sys.stderr.write(
190 '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
191 )
191 - new.set_recipients(name, old.get_recipients(name))
192 + old_recipients = old.get_all(name, default=None)
193 + old_recipients = ', '.join(o.strip() for o in old_recipients)
194 + new.set_recipients(name, old_recipients)
195
196 if strict:
197 sys.stderr.write(
contrib/hooks/multimail/post-receive.example
+1 -1
@@ -30,7 +30,6 @@ script's behavior could be changed or customized.
30 """
31
32 import sys
33 -import os
33
34 # If necessary, add the path to the directory containing
35 # git_multimail.py to the Python path as follows. (This is not
@@ -86,6 +85,7 @@ mailer = git_multimail.choose_mailer(config, environment)
85
86 # Use Python's smtplib to send emails. Both arguments are required.
87 #mailer = git_multimail.SMTPMailer(
88 +# environment=environment,
89 # envelopesender='git-repo@example.com',
90 # # The smtpserver argument can also include a port number; e.g.,
91 # # smtpserver='mail.example.com:25'