git-multimail: update to version 1.0.0

This commit contains the squashed changes from the upstream git-multimail repository since the last code drop. Highlights: * Fix encoding of non-ASCII email addresses in email headers. * Fix backwards-compatibility bugs for older Python 2.x versions. * Fix a backwards-compatibility bug for Git 1.7.1. * Add an option commitDiffOpts to customize logs for revisions. * Pass "-oi" to sendmail by default to prevent premature termination on a line containing only ".". * Stagger email "Date:" values in an attempt to help mail clients thread the emails in the right order. * If a mailing list setting is missing, just skip sending the corresponding email (with a warning) instead of failing. * Add a X-Git-Host header that can be used for email filtering. * Allow the sender's fully-qualified domain name to be configured. * Minor documentation improvements. * Add a CHANGES file. Contributions-by: Raphaël Hertzog <hertzog@debian.org> Contributions-by: Eric Berberich <eric.berberich@gmail.com> Contributions-by: Michiel Holtkamp <git@elfstone.nl> Contributions-by: Malte Swart <mswart@devtation.de> Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michael Haggerty committed Apr 7, 2014 at 17:20 UTC b513f71f6043c05c3918e670b332dfa7ec6b2661
5 files changed +249 -56
contrib/hooks/multimail/CHANGES new
+33
@@ -0,0 +1,33 @@
1 +Release 1.0.0
2 +=============
3 +
4 +* Fix encoding of non-ASCII email addresses in email headers.
5 +
6 +* Fix backwards-compatibility bugs for older Python 2.x versions.
7 +
8 +* Fix a backwards-compatibility bug for Git 1.7.1.
9 +
10 +* Add an option commitDiffOpts to customize logs for revisions.
11 +
12 +* Pass "-oi" to sendmail by default to prevent premature termination
13 + on a line containing only ".".
14 +
15 +* Stagger email "Date:" values in an attempt to help mail clients
16 + thread the emails in the right order.
17 +
18 +* If a mailing list setting is missing, just skip sending the
19 + corresponding email (with a warning) instead of failing.
20 +
21 +* Add a X-Git-Host header that can be used for email filtering.
22 +
23 +* Allow the sender's fully-qualified domain name to be configured.
24 +
25 +* Minor documentation improvements.
26 +
27 +* Add this CHANGES file.
28 +
29 +
30 +Release 0.9.0
31 +=============
32 +
33 +* Initial release.
contrib/hooks/multimail/README
+30 -16
@@ -91,9 +91,10 @@ Requirements
91 been tested; if you do so, please report your results.)
92
93 * To send emails using the default configuration, a standard sendmail
94 - program must be located at '/usr/sbin/sendmail' and configured
95 - correctly to send emails. If this is not the case, see the
96 - multimailhook.mailer configuration variable below for how to
94 + program must be located at '/usr/sbin/sendmail' or
95 + '/usr/lib/sendmail' and must be configured correctly to send emails.
96 + If this is not the case, set multimailhook.sendmailCommand, or see
97 + the multimailhook.mailer configuration variable below for how to
98 configure git-multimail to send emails via an SMTP server.
99
100
@@ -169,7 +170,7 @@ multimailhook.repoName
170 for gitolite repositories, or otherwise to derive this value from
171 the repository path name.
172
172 -multimailhook.mailinglist
173 +multimailhook.mailingList
174
175 The list of email addresses to which notification emails should be
176 sent, as RFC 2822 email addresses separated by commas. This
@@ -184,26 +185,29 @@ multimailhook.refchangeList
185 reference changes should be sent, as RFC 2822 email addresses
186 separated by commas. This configuration option can be
187 multivalued. The default is the value in
187 - multimailhook.mailinglist. Set this value to the empty string to
188 - prevent reference change emails from being sent.
188 + multimailhook.mailingList. Set this value to the empty string to
189 + prevent reference change emails from being sent even if
190 + multimailhook.mailingList is set.
191
192 multimailhook.announceList
193
194 The list of email addresses to which emails about new annotated
195 tags should be sent, as RFC 2822 email addresses separated by
196 commas. This configuration option can be multivalued. The
195 - default is the value in multimailhook.refchangelist or
196 - multimailhook.mailinglist. Set this value to the empty string to
197 - prevent annotated tag announcement emails from being sent.
197 + default is the value in multimailhook.refchangeList or
198 + multimailhook.mailingList. Set this value to the empty string to
199 + prevent annotated tag announcement emails from being sent even if
200 + one of the other values is set.
201
202 multimailhook.commitList
203
204 The list of email addresses to which emails about individual new
205 commits should be sent, as RFC 2822 email addresses separated by
206 commas. This configuration option can be multivalued. The
204 - default is the value in multimailhook.mailinglist. Set this value
207 + default is the value in multimailhook.mailingList. Set this value
208 to the empty string to prevent notification emails about
206 - individual commits from being sent.
209 + individual commits from being sent even if
210 + multimailhook.mailingList is set.
211
212 multimailhook.announceShortlog
213
@@ -237,10 +241,11 @@ multimailhook.mailer
241 quoting is allowed in the value of this setting, but remember that
242 Git requires double-quotes to be escaped; e.g.,
243
240 - git config multimailhook.sendmailcommand '/usr/sbin/sendmail -t -F \"Git Repo\"'
244 + git config multimailhook.sendmailcommand '/usr/sbin/sendmail -oi -t -F \"Git Repo\"'
245
242 - Default is '/usr/sbin/sendmail -t' or '/usr/lib/sendmail
243 - -t' (depending on which file is present and executable).
246 + Default is '/usr/sbin/sendmail -oi -t' or
247 + '/usr/lib/sendmail -oi -t' (depending on which file is
248 + present and executable).
249
250 multimailhook.envelopeSender
251
@@ -344,6 +349,14 @@ multimailhook.logOpts
349 [multimailhook]
350 logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\"
351
352 +multimailhook.commitLogOpts
353 +
354 + Options passed to "git log" to generate additional info for
355 + revision change emails. For example, adding --ignore-all-spaces
356 + will suppress whitespace changes. The default options are "-C
357 + --stat -p --cc". Shell quoting is allowed; see
358 + multimailhook.logOpts for details.
359 +
360 multimailhook.emailDomain
361
362 Domain name appended to the username of the person doing the push
@@ -381,8 +394,8 @@ Email filtering aids
394
395 All emails include extra headers to enable fine tuned filtering and
396 give information for debugging. All emails include the headers
384 -"X-Git-Repo", "X-Git-Refname", and "X-Git-Reftype". ReferenceChange
385 -emails also include headers "X-Git-Oldrev" and "X-Git-Newrev";
397 +"X-Git-Host", "X-Git-Repo", "X-Git-Refname", and "X-Git-Reftype".
398 +ReferenceChange emails also include headers "X-Git-Oldrev" and "X-Git-Newrev";
399 Revision emails also include header "X-Git-Rev".
400
401
@@ -463,6 +476,7 @@ The git-multimail project itself is currently hosted on GitHub:
476 We use the GitHub issue tracker to keep track of bugs and feature
477 requests, and GitHub pull requests to exchange patches (though, if you
478 prefer, you can send patches via the Git mailing list with cc to me).
479 +Please sign off your patches as per the Git project practice.
480
481 Please note that although a copy of git-multimail will probably be
482 distributed in the "contrib" section of the main Git project,
contrib/hooks/multimail/README.Git
+2 -2
@@ -6,10 +6,10 @@ website:
6 https://github.com/mhagger/git-multimail
7
8 The version in this directory was obtained from the upstream project
9 -on 2013-07-14 and consists of the "git-multimail" subdirectory from
9 +on 2014-04-07 and consists of the "git-multimail" subdirectory from
10 revision
11
12 - 1a5cb09c698a74d15a715a86b09ead5f56bf4b06
12 + 1b32653bafc4f902535b9fc1cd9cae911325b870
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/git_multimail.py
+182 -36
@@ -1,6 +1,6 @@
1 #! /usr/bin/env python2
2
3 -# Copyright (c) 2012,2013 Michael Haggerty
3 +# Copyright (c) 2012-2014 Michael Haggerty and others
4 # Derived from contrib/hooks/post-receive-email, which is
5 # Copyright (c) 2007 Andy Parkins
6 # and also includes contributions by other authors.
@@ -49,21 +49,25 @@ import sys
49 import os
50 import re
51 import bisect
52 +import socket
53 import subprocess
54 import shlex
55 import optparse
56 import smtplib
57 +import time
58
59 try:
60 from email.utils import make_msgid
61 from email.utils import getaddresses
62 from email.utils import formataddr
63 + from email.utils import formatdate
64 from email.header import Header
65 except ImportError:
66 # Prior to Python 2.5, the email module used different names:
67 from email.Utils import make_msgid
68 from email.Utils import getaddresses
69 from email.Utils import formataddr
70 + from email.Utils import formatdate
71 from email.Header import Header
72
73
@@ -73,6 +77,7 @@ ZEROS = '0' * 40
77 LOGBEGIN = '- Log -----------------------------------------------------------------\n'
78 LOGEND = '-----------------------------------------------------------------------\n'
79
80 +ADDR_HEADERS = set(['from', 'to', 'cc', 'bcc', 'reply-to', 'sender'])
81
82 # It is assumed in many places that the encoding is uniformly UTF-8,
83 # so changing these constants is unsupported. But define them here
@@ -95,6 +100,7 @@ REF_DELETED_SUBJECT_TEMPLATE = (
100 )
101
102 REFCHANGE_HEADER_TEMPLATE = """\
103 +Date: %(send_date)s
104 To: %(recipients)s
105 Subject: %(subject)s
106 MIME-Version: 1.0
@@ -103,6 +109,7 @@ Content-Transfer-Encoding: 8bit
109 Message-ID: %(msgid)s
110 From: %(fromaddr)s
111 Reply-To: %(reply_to)s
112 +X-Git-Host: %(fqdn)s
113 X-Git-Repo: %(repo_shortname)s
114 X-Git-Refname: %(refname)s
115 X-Git-Reftype: %(refname_type)s
@@ -221,6 +228,7 @@ how to provide full information about this reference change.
228
229
230 REVISION_HEADER_TEMPLATE = """\
231 +Date: %(send_date)s
232 To: %(recipients)s
233 Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s
234 MIME-Version: 1.0
@@ -230,6 +238,7 @@ From: %(fromaddr)s
238 Reply-To: %(reply_to)s
239 In-Reply-To: %(reply_to_msgid)s
240 References: %(reply_to_msgid)s
241 +X-Git-Host: %(fqdn)s
242 X-Git-Repo: %(repo_shortname)s
243 X-Git-Refname: %(refname)s
244 X-Git-Reftype: %(refname_type)s
@@ -263,13 +272,43 @@ class ConfigurationException(Exception):
272 pass
273
274
275 +# The "git" program (this could be changed to include a full path):
276 +GIT_EXECUTABLE = 'git'
277 +
278 +
279 +# How "git" should be invoked (including global arguments), as a list
280 +# of words. This variable is usually initialized automatically by
281 +# read_git_output() via choose_git_command(), but if a value is set
282 +# here then it will be used unconditionally.
283 +GIT_CMD = None
284 +
285 +
286 +def choose_git_command():
287 + """Decide how to invoke git, and record the choice in GIT_CMD."""
288 +
289 + global GIT_CMD
290 +
291 + if GIT_CMD is None:
292 + try:
293 + # Check to see whether the "-c" option is accepted (it was
294 + # only added in Git 1.7.2). We don't actually use the
295 + # output of "git --version", though if we needed more
296 + # specific version information this would be the place to
297 + # do it.
298 + cmd = [GIT_EXECUTABLE, '-c', 'foo.bar=baz', '--version']
299 + read_output(cmd)
300 + GIT_CMD = [GIT_EXECUTABLE, '-c', 'i18n.logoutputencoding=%s' % (ENCODING,)]
301 + except CommandError:
302 + GIT_CMD = [GIT_EXECUTABLE]
303 +
304 +
305 def read_git_output(args, input=None, keepends=False, **kw):
306 """Read the output of a Git command."""
307
269 - return read_output(
270 - ['git', '-c', 'i18n.logoutputencoding=%s' % (ENCODING,)] + args,
271 - input=input, keepends=keepends, **kw
272 - )
308 + if GIT_CMD is None:
309 + choose_git_command()
310 +
311 + return read_output(GIT_CMD + args, input=input, keepends=keepends, **kw)
312
313
314 def read_output(cmd, input=None, keepends=False, **kw):
@@ -297,6 +336,31 @@ def read_git_lines(args, keepends=False, **kw):
336 return read_git_output(args, keepends=True, **kw).splitlines(keepends)
337
338
339 +def header_encode(text, header_name=None):
340 + """Encode and line-wrap the value of an email header field."""
341 +
342 + try:
343 + if isinstance(text, str):
344 + text = text.decode(ENCODING, 'replace')
345 + return Header(text, header_name=header_name).encode()
346 + except UnicodeEncodeError:
347 + return Header(text, header_name=header_name, charset=CHARSET,
348 + errors='replace').encode()
349 +
350 +
351 +def addr_header_encode(text, header_name=None):
352 + """Encode and line-wrap the value of an email header field containing
353 + email addresses."""
354 +
355 + return Header(
356 + ', '.join(
357 + formataddr((header_encode(name), emailaddr))
358 + for name, emailaddr in getaddresses([text])
359 + ),
360 + header_name=header_name
361 + ).encode()
362 +
363 +
364 class Config(object):
365 def __init__(self, section, git_config=None):
366 """Represent a section of the git configuration.
@@ -578,11 +642,11 @@ class Change(object):
642 % (e.args[0], line,)
643 )
644 else:
581 - try:
582 - h = Header(value, header_name=name)
583 - except UnicodeDecodeError:
584 - h = Header(value, header_name=name, charset=CHARSET, errors='replace')
585 - for splitline in ('%s: %s\n' % (name, h.encode(),)).splitlines(True):
645 + if name.lower() in ADDR_HEADERS:
646 + value = addr_header_encode(value, name)
647 + else:
648 + value = header_encode(value, name)
649 + for splitline in ('%s: %s\n' % (name, value)).splitlines(True):
650 yield splitline
651
652 def generate_email_header(self):
@@ -616,15 +680,19 @@ class Change(object):
680
681 raise NotImplementedError()
682
619 - def generate_email(self, push, body_filter=None):
683 + def generate_email(self, push, body_filter=None, extra_header_values={}):
684 """Generate an email describing this change.
685
686 Iterate over the lines (including the header lines) of an
687 email describing this change. If body_filter is not None,
688 then use it to filter the lines that are intended for the
625 - email body."""
689 + email body.
690 +
691 + The extra_header_values field is received as a dict and not as
692 + **kwargs, to allow passing other keyword arguments in the
693 + future (e.g. passing extra values to generate_email_intro()"""
694
627 - for line in self.generate_email_header():
695 + for line in self.generate_email_header(**extra_header_values):
696 yield line
697 yield '\n'
698 for line in self.generate_email_intro():
@@ -680,8 +748,10 @@ class Revision(Change):
748
749 return values
750
683 - def generate_email_header(self):
684 - for line in self.expand_header_lines(REVISION_HEADER_TEMPLATE):
751 + def generate_email_header(self, **extra_values):
752 + for line in self.expand_header_lines(
753 + REVISION_HEADER_TEMPLATE, **extra_values
754 + ):
755 yield line
756
757 def generate_email_intro(self):
@@ -692,11 +762,7 @@ class Revision(Change):
762 """Show this revision."""
763
764 return read_git_lines(
695 - [
696 - 'log', '-C',
697 - '--stat', '-p', '--cc',
698 - '-1', self.rev.sha1,
699 - ],
765 + ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],
766 keepends=True,
767 )
768
@@ -800,6 +866,7 @@ class ReferenceChange(Change):
866 self.msgid = make_msgid()
867 self.diffopts = environment.diffopts
868 self.logopts = environment.logopts
869 + self.commitlogopts = environment.commitlogopts
870 self.showlog = environment.refchange_showlog
871
872 def _compute_values(self):
@@ -835,9 +902,12 @@ class ReferenceChange(Change):
902 }[self.change_type]
903 return self.expand(template)
904
838 - def generate_email_header(self):
905 + def generate_email_header(self, **extra_values):
906 + if 'subject' not in extra_values:
907 + extra_values['subject'] = self.get_subject()
908 +
909 for line in self.expand_header_lines(
840 - REFCHANGE_HEADER_TEMPLATE, subject=self.get_subject(),
910 + REFCHANGE_HEADER_TEMPLATE, **extra_values
911 ):
912 yield line
913
@@ -1273,7 +1343,7 @@ class Mailer(object):
1343
1344
1345 class SendMailer(Mailer):
1276 - """Send emails using 'sendmail -t'."""
1346 + """Send emails using 'sendmail -oi -t'."""
1347
1348 SENDMAIL_CANDIDATES = [
1349 '/usr/sbin/sendmail',
@@ -1302,7 +1372,7 @@ class SendMailer(Mailer):
1372 if command:
1373 self.command = command[:]
1374 else:
1305 - self.command = [self.find_sendmail(), '-t']
1375 + self.command = [self.find_sendmail(), '-oi', '-t']
1376
1377 if envelopesender:
1378 self.command.extend(['-f', envelopesender])
@@ -1495,6 +1565,12 @@ class Environment(object):
1565 'git log' when generating the detailed log for a set of
1566 commits (see refchange_showlog)
1567
1568 + commitlogopts (list of strings)
1569 +
1570 + The options that should be passed to 'git log' for each
1571 + commit mail. The value should be a list of strings
1572 + representing words to be passed to the command.
1573 +
1574 """
1575
1576 REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$')
@@ -1506,6 +1582,7 @@ class Environment(object):
1582 self.diffopts = ['--stat', '--summary', '--find-copies-harder']
1583 self.logopts = []
1584 self.refchange_showlog = False
1585 + self.commitlogopts = ['-C', '--stat', '-p', '--cc']
1586
1587 self.COMPUTED_KEYS = [
1588 'administrator',
@@ -1672,6 +1749,10 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
1749 if logopts is not None:
1750 self.logopts = shlex.split(logopts)
1751
1752 + commitlogopts = config.get('commitlogopts')
1753 + if commitlogopts is not None:
1754 + self.commitlogopts = shlex.split(commitlogopts)
1755 +
1756 reply_to = config.get('replyTo')
1757 self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to)
1758 if (
@@ -1829,6 +1910,47 @@ class ConfigMaxlinesEnvironmentMixin(
1910 )
1911
1912
1913 +class FQDNEnvironmentMixin(Environment):
1914 + """A mixin that sets the host's FQDN to its constructor argument."""
1915 +
1916 + def __init__(self, fqdn, **kw):
1917 + super(FQDNEnvironmentMixin, self).__init__(**kw)
1918 + self.COMPUTED_KEYS += ['fqdn']
1919 + self.__fqdn = fqdn
1920 +
1921 + def get_fqdn(self):
1922 + """Return the fully-qualified domain name for this host.
1923 +
1924 + Return None if it is unavailable or unwanted."""
1925 +
1926 + return self.__fqdn
1927 +
1928 +
1929 +class ConfigFQDNEnvironmentMixin(
1930 + ConfigEnvironmentMixin,
1931 + FQDNEnvironmentMixin,
1932 + ):
1933 + """Read the FQDN from the config."""
1934 +
1935 + def __init__(self, config, **kw):
1936 + fqdn = config.get('fqdn')
1937 + super(ConfigFQDNEnvironmentMixin, self).__init__(
1938 + config=config,
1939 + fqdn=fqdn,
1940 + **kw
1941 + )
1942 +
1943 +
1944 +class ComputeFQDNEnvironmentMixin(FQDNEnvironmentMixin):
1945 + """Get the FQDN by calling socket.getfqdn()."""
1946 +
1947 + def __init__(self, **kw):
1948 + super(ComputeFQDNEnvironmentMixin, self).__init__(
1949 + fqdn=socket.getfqdn(),
1950 + **kw
1951 + )
1952 +
1953 +
1954 class PusherDomainEnvironmentMixin(ConfigEnvironmentMixin):
1955 """Deduce pusher_email from pusher by appending an emaildomain."""
1956
@@ -1861,6 +1983,10 @@ class StaticRecipientsEnvironmentMixin(Environment):
1983 # actual *contents* of the change being reported, we only
1984 # choose based on the *type* of the change. Therefore we can
1985 # compute them once and for all:
1986 + if not (refchange_recipients
1987 + or announce_recipients
1988 + or revision_recipients):
1989 + raise ConfigurationException('No email recipients configured!')
1990 self.__refchange_recipients = refchange_recipients
1991 self.__announce_recipients = announce_recipients
1992 self.__revision_recipients = revision_recipients
@@ -1911,17 +2037,8 @@ class ConfigRecipientsEnvironmentMixin(
2037 retval = config.get_recipients(name)
2038 if retval is not None:
2039 return retval
1914 - if len(names) == 1:
1915 - hint = 'Please set "%s.%s"' % (config.section, name)
2040 else:
1917 - hint = (
1918 - 'Please set one of the following:\n "%s"'
1919 - % ('"\n "'.join('%s.%s' % (config.section, name) for name in names))
1920 - )
1921 -
1922 - raise ConfigurationException(
1923 - 'The list of recipients for %s is not configured.\n%s' % (names[0], hint)
1924 - )
2041 + return ''
2042
2043
2044 class ProjectdescEnvironmentMixin(Environment):
@@ -1956,6 +2073,7 @@ class GenericEnvironmentMixin(Environment):
2073 class GenericEnvironment(
2074 ProjectdescEnvironmentMixin,
2075 ConfigMaxlinesEnvironmentMixin,
2076 + ComputeFQDNEnvironmentMixin,
2077 ConfigFilterLinesEnvironmentMixin,
2078 ConfigRecipientsEnvironmentMixin,
2079 PusherDomainEnvironmentMixin,
@@ -1980,9 +2098,27 @@ class GitoliteEnvironmentMixin(Environment):
2098 return self.osenv.get('GL_USER', 'unknown user')
2099
2100
2101 +class IncrementalDateTime(object):
2102 + """Simple wrapper to give incremental date/times.
2103 +
2104 + Each call will result in a date/time a second later than the
2105 + previous call. This can be used to falsify email headers, to
2106 + increase the likelihood that email clients sort the emails
2107 + correctly."""
2108 +
2109 + def __init__(self):
2110 + self.time = time.time()
2111 +
2112 + def next(self):
2113 + formatted = formatdate(self.time, True)
2114 + self.time += 1
2115 + return formatted
2116 +
2117 +
2118 class GitoliteEnvironment(
2119 ProjectdescEnvironmentMixin,
2120 ConfigMaxlinesEnvironmentMixin,
2121 + ComputeFQDNEnvironmentMixin,
2122 ConfigFilterLinesEnvironmentMixin,
2123 ConfigRecipientsEnvironmentMixin,
2124 PusherDomainEnvironmentMixin,
@@ -2187,6 +2323,7 @@ class Push(object):
2323 # guarantee that one (and only one) email is generated for
2324 # each new commit.
2325 unhandled_sha1s = set(self.get_new_commits())
2326 + send_date = IncrementalDateTime()
2327 for change in self.changes:
2328 # Check if we've got anyone to send to
2329 if not change.recipients:
@@ -2197,7 +2334,11 @@ class Push(object):
2334 )
2335 else:
2336 sys.stderr.write('Sending notification emails to: %s\n' % (change.recipients,))
2200 - mailer.send(change.generate_email(self, body_filter), change.recipients)
2337 + extra_values = {'send_date' : send_date.next()}
2338 + mailer.send(
2339 + change.generate_email(self, body_filter, extra_values),
2340 + change.recipients,
2341 + )
2342
2343 sha1s = []
2344 for sha1 in reversed(list(self.get_new_commits(change))):
@@ -2217,7 +2358,11 @@ class Push(object):
2358 for (num, sha1) in enumerate(sha1s):
2359 rev = Revision(change, GitObject(sha1), num=num+1, tot=len(sha1s))
2360 if rev.recipients:
2220 - mailer.send(rev.generate_email(self, body_filter), rev.recipients)
2361 + extra_values = {'send_date' : send_date.next()}
2362 + mailer.send(
2363 + rev.generate_email(self, body_filter, extra_values),
2364 + rev.recipients,
2365 + )
2366
2367 # Consistency check:
2368 if unhandled_sha1s:
@@ -2288,6 +2433,7 @@ def choose_environment(config, osenv=None, env=None, recipients=None):
2433 environment_mixins = [
2434 ProjectdescEnvironmentMixin,
2435 ConfigMaxlinesEnvironmentMixin,
2436 + ComputeFQDNEnvironmentMixin,
2437 ConfigFilterLinesEnvironmentMixin,
2438 PusherDomainEnvironmentMixin,
2439 ConfigOptionsEnvironmentMixin,
contrib/hooks/multimail/post-receive
+2 -2
@@ -66,10 +66,10 @@ mailer = git_multimail.choose_mailer(config, environment)
66 # Alternatively, you may hardcode the mailer using code like one of
67 # the following:
68
69 -# Use "/usr/sbin/sendmail -t" to send emails. The envelopesender
69 +# Use "/usr/sbin/sendmail -oi -t" to send emails. The envelopesender
70 # argument is optional:
71 #mailer = git_multimail.SendMailer(
72 -# command=['/usr/sbin/sendmail', '-t'],
72 +# command=['/usr/sbin/sendmail', '-oi', '-t'],
73 # envelopesender='git-repo@example.com',
74 # )
75