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__':