git-multimail: update to release 1.2.0
The changes are described in CHANGES. Contributions-by: Matthieu Moy <Matthieu.Moy@imag.fr> Contributions-by: Elijah Newren <newren@palantir.com> Contributions-by: Edward d'Auvergne <edward@nmr-relax.com> Contributions-by: Vadim Zeitlin <vadim@zeitlins.org> Contributions-by: Paul Sokolovsky <paul.sokolovsky@linaro.org> Contributions-by: Michael Haggerty <mhagger@alum.mit.edu> Contributions-by: Elijah Newren <newren@gmail.com> Contributions-by: Richard Hansen <rhansen@rhansen.org> Contributions-by: Job Snijders <job@instituut.net> Signed-off-by: Matthieu Moy <Matthieu.Moy@imag.fr> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Matthieu Moy committed
Oct 11, 2015 at 20:43 UTC
4b1fd356b8f8ca464203174a47ab5fa6a5cbc26b
9 files changed
+1218
-226
contrib/hooks/multimail/CHANGES
+41
@@ -1,3 +1,44 @@
1
+Release 1.2.0
2
+=============
3
+
4
+* It is now possible to exclude some refs (e.g. exclude some branches
5
+ or tags). See refFilterDoSendRegex, refFilterDontSendRegex,
6
+ refFilterInclusionRegex and refFilterExclusionRegex.
7
+
8
+* New commitEmailFormat option which can be set to "html" to generate
9
+ simple colorized diffs using HTML for the commit emails.
10
+
11
+* git-multimail can now be ran as a Gerrit ref-updated hook, or from
12
+ Atlassian BitBucket Server (formerly known as Atlassian Stash).
13
+
14
+* The From: field is now more customizeable. It can be set
15
+ independently for refchange emails and commit emails (see
16
+ fromCommit, fromRefChange). The special values pusher and author can
17
+ be used in these configuration variable.
18
+
19
+* A new command-line option, --version, was added. The version is also
20
+ available in the X-Git-Multimail-Version header of sent emails.
21
+
22
+* Set X-Git-NotificationType header to differentiate the various types
23
+ of notifications. Current values are: diff, ref_changed_plus_diff,
24
+ ref_changed.
25
+
26
+* Preliminary support for Python 3. The testsuite passes with Python 3,
27
+ but it has not received as much testing as the Python 2 version yet.
28
+
29
+* Several encoding-related fixes. UTF-8 characters work in more
30
+ situations (but non-ascii characters in email address are still not
31
+ supported).
32
+
33
+* The testsuite and its documentation has been greatly improved.
34
+
35
+Plus all the bugfixes from version 1.1.1.
36
+
37
+This version has been tested with Python 2.4 and 2.6 to 3.5, and Git
38
+v1.7.10-406-gdc801e7, git-1.8.2.3 and 2.6.0. Git versions prior to
39
+v1.7.10-406-gdc801e7 probably work, but cannot run the testsuite
40
+properly.
41
+
42
Release 1.1.1 (bugfix-only release)
43
===================================
44
contrib/hooks/multimail/CONTRIBUTING.rst
new
+30
@@ -0,0 +1,30 @@
1
+git-multimail is an open-source project, built by volunteers. We would
2
+welcome your help!
3
+
4
+The current maintainers are Michael Haggerty <mhagger@alum.mit.edu>
5
+and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.
6
+
7
+Please note that although a copy of git-multimail is distributed in
8
+the "contrib" section of the main Git project, development takes place
9
+in a separate git-multimail repository on GitHub:
10
+
11
+ https://github.com/git-multimail/git-multimail
12
+
13
+Whenever enough changes to git-multimail have accumulated, a new
14
+code-drop of git-multimail will be submitted for inclusion in the Git
15
+project.
16
+
17
+We use the GitHub issue tracker to keep track of bugs and feature
18
+requests, and we use GitHub pull requests to exchange patches (though,
19
+if you prefer, you can send patches via the Git mailing list with CC
20
+to the maintainers). Please sign off your patches as per the `Git
21
+project practice
22
+<https://github.com/git/git/blob/master/Documentation/SubmittingPatches#L234>`__.
23
+
24
+General discussion of git-multimail can take place on the main Git
25
+mailing list,
26
+
27
+ git@vger.kernel.org
28
+
29
+Please CC emails regarding git-multimail to the maintainers so that we
30
+don't overlook them.
contrib/hooks/multimail/README
+175
-83
@@ -1,5 +1,5 @@
1
-git-multimail Version 1.1.1
2
-===========================
1
+git-multimail (version 1.2.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
@@ -53,11 +53,13 @@ By default, for each push received by the repository, git-multimail:
53
+ [git] 07/08: Merge branch 'mm/api-credentials-doc'
54
+ [git] 08/08: Git 1.7.11-rc2
55
56
- Each commit appears in exactly one commit email, the first time
57
- that it is pushed to the repository. If a commit is later merged
58
- into another branch, then a one-line summary of the commit is
59
- included in the reference change email (as usual), but no
60
- additional commit email is generated.
56
+ By default, each commit appears in exactly one commit email, the
57
+ first time that it is pushed to the repository. If a commit is later
58
+ merged into another branch, then a one-line summary of the commit
59
+ is included in the reference change email (as usual), but no
60
+ additional commit email is generated. See
61
+ `multimailhook.refFilter(Inclusion|Exclusion|DoSend|DontSend)Regex`
62
+ below to configure which branches and tags are watched by the hook.
63
64
By default, reference change emails have their "Reply-To" field set
65
to the person who pushed the change, and commit emails have their
@@ -73,21 +75,8 @@ Requirements
75
------------
76
77
* Python 2.x, version 2.4 or later. No non-standard Python modules
76
- are required. git-multimail does *not* currently work with Python
77
- 3.x.
78
-
79
- The example scripts invoke Python using the following shebang line
80
- (following PEP 394 [1]_)::
81
-
82
- #! /usr/bin/env python2
83
-
84
- If your system's Python2 interpreter is not in your PATH or is not
85
- called ``python2``, you can change the lines accordingly. Or you can
86
- invoke the Python interpreter explicitly, for example via a tiny
87
- shell script like::
88
-
89
- #! /bin/sh
90
- /usr/local/bin/python /path/to/git_multimail.py "$@"
78
+ are required. git-multimail has preliminary support for Python 3
79
+ (but it has been better tested with Python 2).
80
81
* The ``git`` command must be in your PATH. git-multimail is known to
82
work with Git versions back to 1.7.1. (Earlier versions have not
@@ -146,7 +135,9 @@ following ``git config`` settings:
135
136
multimailhook.environment
137
149
- This describes the general environment of the repository.
138
+ This describes the general environment of the repository. In most
139
+ cases, you do not need to specify a value for this variable:
140
+ `git-multimail` will autodetect which environment to use.
141
Currently supported values:
142
143
* generic
@@ -161,18 +152,57 @@ multimailhook.environment
152
optionally read from gitolite.conf (see multimailhook.from).
153
154
For more information about gitolite and git-multimail, read
164
- doc/gitolite.rst
155
+ `<doc/gitolite.rst>`__
156
+
157
+ * stash
158
+
159
+ Environment to use when ``git-multimail`` is ran as an Atlassian
160
+ BitBucket Server (formerly known as Atlassian Stash) hook.
161
+
162
+ **Warning:** this mode was provided by a third-party contributor
163
+ and never tested by the git-multimail maintainers. It is
164
+ provided as-is and may or may not work for you.
165
+
166
+ This value is automatically assumed when the stash-specific
167
+ flags (``--stash-user`` and ``--stash-repo``) are specified on
168
+ the command line. When this environment is active, the username
169
+ and repo come from these two command line flags, which must be
170
+ specified.
171
+
172
+ * gerrit
173
+
174
+ Environment to use when ``git-multimail`` is ran as a
175
+ ``ref-updated`` Gerrit hook.
176
+
177
+ This value is used when the gerrit-specific command line flags
178
+ (``--oldrev``, ``--newrev``, ``--refname``, ``--project``) for
179
+ gerrit's ref-updated hook are present. When this environment is
180
+ active, the username of the pusher is taken from the
181
+ ``--submitter`` argument if that command line option is passed,
182
+ otherwise 'Gerrit' is used. The repository name is taken from
183
+ the ``--project`` option on the command line, which must be passed.
184
+
185
+ For more information about gerrit and git-multimail, read
186
+ `<doc/gerrit.rst>`__
187
166
- If neither of these environments is suitable for your setup, then
167
- you can implement a Python class that inherits from Environment
168
- and instantiate it via a script that looks like the example
188
+ If none of these environments is suitable for your setup, then you
189
+ can implement a Python class that inherits from Environment and
190
+ instantiate it via a script that looks like the example
191
post-receive script.
192
193
The environment value can be specified on the command line using
172
- the --environment option. If it is not specified on the command
173
- line or by multimailhook.environment, then it defaults to
174
- ``gitolite`` if the environment contains variables $GL_USER and
175
- $GL_REPO; otherwise ``generic``.
194
+ the ``--environment`` option. If it is not specified on the
195
+ command line or by ``multimailhook.environment``, the value is
196
+ guessed as follows:
197
+
198
+ * If stash-specific (respectively gerrit-specific) command flags
199
+ are present on the command-line, then ``stash`` (respectively
200
+ ``gerrit``) is used.
201
+
202
+ * If the environment variables $GL_USER and $GL_REPO are set, then
203
+ ``gitolite`` is used.
204
+
205
+ * If none of the above apply, then ``generic`` is used.
206
207
multimailhook.repoName
208
@@ -196,8 +226,8 @@ multimailhook.refchangeList
226
reference changes should be sent, as RFC 2822 email addresses
227
separated by commas. This configuration option can be
228
multivalued. The default is the value in
199
- multimailhook.mailingList. Set this value to the empty string to
200
- prevent reference change emails from being sent even if
229
+ multimailhook.mailingList. Set this value to "none" (or the empty
230
+ string) to prevent reference change emails from being sent even if
231
multimailhook.mailingList is set.
232
233
multimailhook.announceList
@@ -206,9 +236,9 @@ multimailhook.announceList
236
tags should be sent, as RFC 2822 email addresses separated by
237
commas. This configuration option can be multivalued. The
238
default is the value in multimailhook.refchangeList or
209
- multimailhook.mailingList. Set this value to the empty string to
210
- prevent annotated tag announcement emails from being sent even if
211
- one of the other values is set.
239
+ multimailhook.mailingList. Set this value to "none" (or the empty
240
+ string) to prevent annotated tag announcement emails from being sent
241
+ even if one of the other values is set.
242
243
multimailhook.commitList
244
@@ -216,7 +246,7 @@ multimailhook.commitList
246
commits should be sent, as RFC 2822 email addresses separated by
247
commas. This configuration option can be multivalued. The
248
default is the value in multimailhook.mailingList. Set this value
219
- to the empty string to prevent notification emails about
249
+ to "none" (or the empty string) to prevent notification emails about
250
individual commits from being sent even if
251
multimailhook.mailingList is set.
252
@@ -230,6 +260,20 @@ multimailhook.announceShortlog
260
not so straightforward, then the shortlog might be confusing
261
rather than useful. Default is false.
262
263
+multimailhook.commitEmailFormat
264
+
265
+ The format of email messages for the individual commits, can be "text" or
266
+ "html". In the latter case, the emails will include diffs using colorized
267
+ HTML instead of plain text used by default. Note that this currently the
268
+ ref change emails are always sent in plain text.
269
+
270
+ Note that when using "html", the formatting is done by parsing the
271
+ output of ``git log`` with ``-p``. When using
272
+ ``multimailhook.commitLogOpts`` to specify a ``--format`` for
273
+ ``git log``, one may get false positive (e.g. lines in the body of
274
+ the message starting with ``+++`` or ``---`` colored in red or
275
+ green).
276
+
277
multimailhook.refchangeShowGraph
278
279
If this option is set to true, then summary emails about reference
@@ -305,7 +349,7 @@ multimailhook.mailer
349
350
* multimailhook.smtpEncryption
351
308
- Set the security type. Allowed values: none, ssl.
352
+ Set the security type. Allowed values: none, ssl, tls.
353
Default=none.
354
355
* multimailhook.smtpServerDebugLevel
@@ -313,9 +357,26 @@ multimailhook.mailer
357
Integer number. Set to greater than 0 to activate debugging.
358
359
multimailhook.from
360
+multimailhook.fromCommit
361
+multimailhook.fromRefchange
362
+
363
+ If set, use this value in the From: field of generated emails.
364
+ ``fromCommit`` is used for commit emails, ``fromRefchange`` is
365
+ used for refchange emails, and ``from`` is used as fall-back in
366
+ all cases.
367
+
368
+ The value for these variables can be either:
369
+
370
+ - An email address, which will be used directly.
371
+
372
+ - The value ``pusher``, in which case the pusher's address (if
373
+ available) will be used.
374
317
- If set, use this value in the From: field of generated emails. If
318
- unset, the value of the From: header is determined as follows:
375
+ - The value ``author`` (meaningful only for replyToCommit), in which
376
+ case the commit author's address will be used.
377
+
378
+ If config values are unset, the value of the From: header is
379
+ determined as follows:
380
381
1. (gitolite environment only) Parse gitolite.conf, looking for a
382
block of comments that looks like this::
@@ -425,6 +486,15 @@ multimailhook.commitLogOpts
486
--stat -p --cc``. Shell quoting is allowed; see
487
multimailhook.logOpts for details.
488
489
+multimailhook.dateSubstitute
490
+
491
+ String to use as a substitute for ``Date:`` in the output of ``git
492
+ log`` while formatting commit messages. This is usefull to avoid
493
+ emitting a line that can be interpreted by mailers as the start of
494
+ a cited message (Zimbra webmail in particular). Defaults to
495
+ ``CommitDate: ``. Set to an empty string or ``none`` to deactivate
496
+ the behavior.
497
+
498
multimailhook.emailDomain
499
500
Domain name appended to the username of the person doing the push
@@ -440,21 +510,13 @@ multimailhook.replyToRefchange
510
Addresses to use in the Reply-To: field for commit emails
511
(replyToCommit) and refchange emails (replyToRefchange).
512
multimailhook.replyTo is used as default when replyToCommit or
443
- replyToRefchange is not set. The value for these variables can be
444
- either:
445
-
446
- - An email address, which will be used directly.
447
-
448
- - The value `pusher`, in which case the pusher's address (if
449
- available) will be used. This is the default for refchange
450
- emails.
513
+ replyToRefchange is not set. The shortcuts ``pusher`` and
514
+ ``author`` are allowed with the same semantics as for
515
+ ``multimailhook.from``. In addition, the value ``none`` can be
516
+ used to omit the ``Reply-To:`` field.
517
452
- - The value `author` (meaningful only for replyToCommit), in which
453
- case the commit author's address will be used. This is the
454
- default for commit emails.
455
-
456
- - The value `none`, in which case the Reply-To: field will be
457
- omitted.
518
+ The default is ``pusher`` for refchange emails, and ``author`` for
519
+ commit emails.
520
521
multimailhook.quiet
522
@@ -478,6 +540,63 @@ multimailhook.combineWhenSingleCommit
540
single email.
541
Default: true
542
543
+multimailhook.refFilterInclusionRegex
544
+multimailhook.refFilterExclusionRegex
545
+multimailhook.refFilterDoSendRegex
546
+multimailhook.refFilterDontSendRegex
547
+
548
+ **Warning:** these options are experimental. They should work, but
549
+ the user-interface is not stable yet (in particular, the option
550
+ names may change). If you want to participate in stabilizing the
551
+ feature, please contact the maintainers and/or send pull-requests.
552
+
553
+ Regular expressions that can be used to limit refs for which email
554
+ updates will be sent. It is an error to specify both an inclusion
555
+ and an exclusion regex. If a ``refFilterInclusionRegex`` is
556
+ specified, emails will only be sent for refs which match this
557
+ regex. If a ``refFilterExclusionRegex`` regex is specified,
558
+ emails will be sent for all refs except those that match this
559
+ regex (or that match a predefined regex specific to the
560
+ environment, such as "^refs/notes" for most environments and
561
+ "^refs/notes|^refs/changes" for the gerrit environment).
562
+
563
+ The expressions are matched against the complete refname, and is
564
+ considered to match if any substring matches. For example, to
565
+ filter-out all tags, set ``refFilterExclusionRegex`` to
566
+ ``^refs/tags/`` (note the leading ``^`` but no trailing ``$``). If
567
+ you set ``refFilterExclusionRegex`` to ``master``, then any ref
568
+ containing ``master`` will be excluded (the ``master`` branch, but
569
+ also ``refs/tags/master`` or ``refs/heads/foo-master-bar``).
570
+
571
+ ``refFilterDoSendRegex`` and ``refFilterDontSendRegex`` are
572
+ analogous to ``refFilterInclusionRegex`` and
573
+ ``refFilterExclusionRegex`` with one difference: with
574
+ ``refFilterDoSendRegex`` and ``refFilterDontSendRegex``, commits
575
+ introduced by one excluded ref will not be considered as new when
576
+ they reach an included ref. Typically, if you add a branch ``foo``
577
+ to ``refFilterDontSendRegex``, push commits to this branch, and
578
+ later merge branch ``foo`` into ``master``, then the notification
579
+ email for ``master`` will contain a commit email only for the
580
+ merge commit. If you include ``foo`` in
581
+ ``refFilterExclusionRegex``, then at the time of merge, you will
582
+ receive one commit email per commit in the branch.
583
+
584
+ These variables can be multi-valued, like::
585
+
586
+ [multimailhook]
587
+ refFilterExclusionRegex = ^refs/tags/
588
+ refFilterExclusionRegex = ^refs/heads/master$
589
+
590
+ You can also provide a whitespace-separated list like::
591
+
592
+ [multimailhook]
593
+ refFilterExclusionRegex = ^refs/tags/ ^refs/heads/master$
594
+
595
+ Both examples exclude tags and the master branch, and are
596
+ equivalent to::
597
+
598
+ [multimailhook]
599
+ refFilterExclusionRegex = ^refs/tags/|^refs/heads/master$
600
601
Email filtering aids
602
--------------------
@@ -547,35 +666,8 @@ consider sharing them with the community!
666
Getting involved
667
----------------
668
550
-git-multimail is an open-source project, built by volunteers. We would
551
-welcome your help!
552
-
553
-The current maintainers are Michael Haggerty <mhagger@alum.mit.edu>
554
-and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.
555
-
556
-Please note that although a copy of git-multimail is distributed in
557
-the "contrib" section of the main Git project, development takes place
558
-in a separate git-multimail repository on GitHub:
559
-
560
- https://github.com/git-multimail/git-multimail
561
-
562
-Whenever enough changes to git-multimail have accumulated, a new
563
-code-drop of git-multimail will be submitted for inclusion in the Git
564
-project.
565
-
566
-We use the GitHub issue tracker to keep track of bugs and feature
567
-requests, and we use GitHub pull requests to exchange patches (though,
568
-if you prefer, you can send patches via the Git mailing list with CC
569
-to the maintainers). Please sign off your patches as per the Git
570
-project practice.
571
-
572
-General discussion of git-multimail can take place on the main Git
573
-mailing list,
574
-
575
- git@vger.kernel.org
576
-
577
-Please CC emails regarding git-multimail to the maintainers so that we
578
-don't overlook them.
669
+Please, read `<CONTRIBUTING.rst>`__ for instructions on how to
670
+contribute to git-multimail.
671
672
673
Footnotes
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 July 03 2015 and consists of the "git-multimail" subdirectory from
9
+on October 11 2015 and consists of the "git-multimail" subdirectory from
10
revision
11
12
- 6d6c9eb62a054143322cfaecde3949189c065b46 refs/tags/1.1.1
12
+ c0791b9ef5821a746fc3475c25765e640452eaae refs/tags/1.2.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/gerrit.rst
new
+56
@@ -0,0 +1,56 @@
1
+Setting up git-multimail on Gerrit
2
+==================================
3
+
4
+Gerrit has its own email-sending system, but you may prefer using
5
+``git-multimail`` instead. It supports Gerrit natively as a Gerrit
6
+``ref-updated`` hook (Warning: `Gerrit hooks
7
+<https://gerrit-review.googlesource.com/Documentation/config-hooks.html>`__
8
+are distinct from Git hooks). Setting up ``git-multimail`` on a Gerrit
9
+installation can be done following the instructions below.
10
+
11
+The explanations show an easy way to set up ``git-multimail``,
12
+but leave ``git-multimail`` installed and unconfigured for a while. If
13
+you run Gerrit on a production server, it is advised that you
14
+execute the step "Set up the hook" last to avoid confusing your users
15
+in the meantime.
16
+
17
+Set up the hook
18
+---------------
19
+
20
+Create a directory ``$site_path/hooks/`` if it does not exist (if you
21
+don't know what ``$site_path`` is, run ``gerrit.sh status`` and look
22
+for a ``GERRIT_SITE`` line). Either copy ``git_multimail.py`` to
23
+``$site_path/hooks/ref-updated`` or create a wrapper script like
24
+this::
25
+
26
+ #! /bin/sh
27
+ exec /path/to/git_multimail.py "$@"
28
+
29
+In both cases, make sure the file is named exactly
30
+``$site_path/hooks/ref-updated`` and is executable.
31
+
32
+(Alternatively, you may configure the ``[hooks]`` section of
33
+gerrit.config)
34
+
35
+Configuration
36
+-------------
37
+
38
+Log on the gerrit server and edit ``$site_path/git/$project/config``
39
+to configure ``git-multimail``.
40
+
41
+Troubleshooting
42
+---------------
43
+
44
+Warning: this will disable ``git-multimail`` during the debug, and
45
+could confuse your users. Don't run on a production server.
46
+
47
+To debug configuration issues with ``git-multimail``, you can add the
48
+``--stdout`` option when calling ``git_multimail.py`` like this::
49
+
50
+ #!/bin/sh
51
+ exec /path/to/git-multimail/git-multimail/git_multimail.py \
52
+ --stdout "$@" >> /tmp/log.txt
53
+
54
+and try pushing from a test repository. You should see the source of
55
+the email that would have been sent in the output of ``git push`` in
56
+the file ``/tmp/log.txt``.
contrib/hooks/multimail/doc/gitolite.rst
new
+109
@@ -0,0 +1,109 @@
1
+Setting up git-multimail on gitolite
2
+====================================
3
+
4
+``git-multimail`` supports gitolite 3 natively.
5
+The explanations below show an easy way to set up ``git-multimail``,
6
+but leave ``git-multimail`` installed and unconfigured for a while. If
7
+you run gitolite on a production server, it is advised that you
8
+execute the step "Set up the hook" last to avoid confusing your users
9
+in the meantime.
10
+
11
+Set up the hook
12
+---------------
13
+
14
+Log in as your gitolite user.
15
+
16
+Create a file ``.gitolite/hooks/common/post-receive`` on your gitolite
17
+account containing (adapt the path, obviously)::
18
+
19
+ #!/bin/sh
20
+ exec /path/to/git-multimail/git-multimail/git_multimail.py "$@"
21
+
22
+Make sure it's executable (``chmod +x``). Record the hook in
23
+gitolite::
24
+
25
+ gitolite setup
26
+
27
+Configuration
28
+-------------
29
+
30
+First, you have to allow the admin to set Git configuration variables.
31
+
32
+As gitolite user, edit the line containing ``GIT_CONFIG_KEYS`` in file
33
+``.gitolite.rc``, to make it look like::
34
+
35
+ GIT_CONFIG_KEYS => 'multimailhook\..*',
36
+
37
+You can now log out and return to your normal user.
38
+
39
+In the ``gitolite-admin`` clone, edit the file ``conf/gitolite.conf``
40
+and add::
41
+
42
+ repo @all
43
+ # Not strictly needed as git_multimail.py will chose gitolite if
44
+ # $GL_USER is set.
45
+ config multimailhook.environment = gitolite
46
+ config multimailhook.mailingList = # Where emails should be sent
47
+ config multimailhook.from = # From address to use
48
+
49
+Obviously, you can customize all parameters on a per-repository basis by
50
+adding these ``config multimailhook.*`` lines in the section
51
+corresponding to a repository or set of repositories.
52
+
53
+To activate ``git-multimail`` on a per-repository basis, do not set
54
+``multimailhook.mailingList`` in the ``@all`` section and set it only
55
+for repositories for which you want ``git-multimail``.
56
+
57
+Alternatively, you can set up the ``From:`` field on a per-user basis
58
+by adding a ``BEGIN USER EMAILS``/``END USER EMAILS`` section (see
59
+``../README``).
60
+
61
+Specificities of Gitolite for Configuration
62
+-------------------------------------------
63
+
64
+Empty configuration variables
65
+.............................
66
+
67
+With gitolite, the syntax ``config multimailhook.commitList = ""``
68
+unsets the variable instead of setting it to an empty string (see
69
+`here
70
+<http://gitolite.com/gitolite/git-config.html#an-important-warning-about-deleting-a-config-line>`__).
71
+As a result, there is no way to set a variable to the empty string.
72
+In all most places where an empty value is required, git-multimail
73
+now allows to specify special ``"none"`` value (case-sensitive) to
74
+mean the same.
75
+
76
+Alternatively, one can use ``" "`` (a single space) instead of ``""``.
77
+In most cases (in particular ``multimailhook.*List`` variables), this
78
+will be equivalent to an empty string.
79
+
80
+If you have a use-case where ``"none"`` is not an acceptable value and
81
+you need ``" "`` or ``""`` instead, please report it as a bug to
82
+git-multimail.
83
+
84
+Allowing Regular Expressions in Configuration
85
+.............................................
86
+
87
+gitolite has a mechanism to prevent unsafe configuration variable
88
+values, which prevent characters like ``|`` commonly used in regular
89
+expressions. If you do not need the safety feature of gitolite and
90
+need to use regular expressions in your configuration (e.g. for
91
+``multimailhook.refFilter*`` variables), set
92
+`UNSAFE_PATT
93
+<http://gitolite.com/gitolite/git-config.html#unsafe-patt>`__ to a
94
+less restrictive value.
95
+
96
+Troubleshooting
97
+---------------
98
+
99
+Warning: this will disable ``git-multimail`` during the debug, and
100
+could confuse your users. Don't run on a production server.
101
+
102
+To debug configuration issues with ``git-multimail``, you can add the
103
+``--stdout`` option when calling ``git_multimail.py`` like this::
104
+
105
+ #!/bin/sh
106
+ exec /path/to/git-multimail/git-multimail/git_multimail.py --stdout "$@"
107
+
108
+and try pushing from a test repository. You should see the source of
109
+the email that would have been sent in the output of ``git push``.
contrib/hooks/multimail/git_multimail.py
+800
-136
@@ -1,4 +1,6 @@
1
-#! /usr/bin/env python2
1
+#! /usr/bin/env python
2
+
3
+__version__ = '1.2.0'
4
5
# Copyright (c) 2015 Matthieu Moy and others
6
# Copyright (c) 2012-2014 Michael Haggerty and others
@@ -56,8 +58,54 @@ import shlex
58
import optparse
59
import smtplib
60
import time
61
+import cgi
62
+
63
+PYTHON3 = sys.version_info >= (3, 0)
64
+
65
+if sys.version_info <= (2, 5):
66
+ def all(iterable):
67
+ for element in iterable:
68
+ if not element:
69
+ return False
70
+ return True
71
+
72
+
73
+def is_ascii(s):
74
+ return all(ord(c) < 128 and ord(c) > 0 for c in s)
75
+
76
+
77
+if PYTHON3:
78
+ def str_to_bytes(s):
79
+ return s.encode(ENCODING)
80
+
81
+ def bytes_to_str(s):
82
+ return s.decode(ENCODING)
83
+
84
+ unicode = str
85
+
86
+ def write_str(f, msg):
87
+ # Try outputing with the default encoding. If it fails,
88
+ # try UTF-8.
89
+ try:
90
+ f.buffer.write(msg.encode(sys.getdefaultencoding()))
91
+ except UnicodeEncodeError:
92
+ f.buffer.write(msg.encode(ENCODING))
93
+else:
94
+ def str_to_bytes(s):
95
+ return s
96
+
97
+ def bytes_to_str(s):
98
+ return s
99
+
100
+ def write_str(f, msg):
101
+ f.write(msg)
102
+
103
+ def next(it):
104
+ return it.next()
105
+
106
107
try:
108
+ from email.charset import Charset
109
from email.utils import make_msgid
110
from email.utils import getaddresses
111
from email.utils import formataddr
@@ -65,6 +113,7 @@ try:
113
from email.header import Header
114
except ImportError:
115
# Prior to Python 2.5, the email module used different names:
116
+ from email.Charset import Charset
117
from email.Utils import make_msgid
118
from email.Utils import getaddresses
119
from email.Utils import formataddr
@@ -109,7 +158,7 @@ Date: %(send_date)s
158
To: %(recipients)s
159
Subject: %(subject)s
160
MIME-Version: 1.0
112
-Content-Type: text/plain; charset=%(charset)s
161
+Content-Type: text/%(contenttype)s; charset=%(charset)s
162
Content-Transfer-Encoding: 8bit
163
Message-ID: %(msgid)s
164
From: %(fromaddr)s
@@ -120,6 +169,8 @@ X-Git-Refname: %(refname)s
169
X-Git-Reftype: %(refname_type)s
170
X-Git-Oldrev: %(oldrev)s
171
X-Git-Newrev: %(newrev)s
172
+X-Git-NotificationType: ref_changed
173
+X-Git-Multimail-Version: %(multimail_version)s
174
Auto-Submitted: auto-generated
175
"""
176
@@ -238,7 +289,7 @@ To: %(recipients)s
289
Cc: %(cc_recipients)s
290
Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s
291
MIME-Version: 1.0
241
-Content-Type: text/plain; charset=%(charset)s
292
+Content-Type: text/%(contenttype)s; charset=%(charset)s
293
Content-Transfer-Encoding: 8bit
294
From: %(fromaddr)s
295
Reply-To: %(reply_to)s
@@ -249,6 +300,8 @@ X-Git-Repo: %(repo_shortname)s
300
X-Git-Refname: %(refname)s
301
X-Git-Reftype: %(refname_type)s
302
X-Git-Rev: %(rev)s
303
+X-Git-NotificationType: diff
304
+X-Git-Multimail-Version: %(multimail_version)s
305
Auto-Submitted: auto-generated
306
"""
307
@@ -270,7 +323,7 @@ Date: %(send_date)s
323
To: %(recipients)s
324
Subject: %(subject)s
325
MIME-Version: 1.0
273
-Content-Type: text/plain; charset=%(charset)s
326
+Content-Type: text/%(contenttype)s; charset=%(charset)s
327
Content-Transfer-Encoding: 8bit
328
Message-ID: %(msgid)s
329
From: %(fromaddr)s
@@ -282,6 +335,8 @@ X-Git-Reftype: %(refname_type)s
335
X-Git-Oldrev: %(oldrev)s
336
X-Git-Newrev: %(newrev)s
337
X-Git-Rev: %(rev)s
338
+X-Git-NotificationType: ref_changed_plus_diff
339
+X-Git-Multimail-Version: %(multimail_version)s
340
Auto-Submitted: auto-generated
341
"""
342
@@ -352,12 +407,14 @@ def read_git_output(args, input=None, keepends=False, **kw):
407
def read_output(cmd, input=None, keepends=False, **kw):
408
if input:
409
stdin = subprocess.PIPE
410
+ input = str_to_bytes(input)
411
else:
412
stdin = None
413
p = subprocess.Popen(
414
cmd, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
415
)
416
(out, err) = p.communicate(input)
417
+ out = bytes_to_str(out)
418
retcode = p.wait()
419
if retcode:
420
raise CommandError(cmd, retcode)
@@ -418,26 +475,37 @@ def git_log(spec, **kw):
475
def header_encode(text, header_name=None):
476
"""Encode and line-wrap the value of an email header field."""
477
421
- try:
422
- if isinstance(text, str):
423
- text = text.decode(ENCODING, 'replace')
424
- return Header(text, header_name=header_name).encode()
425
- except UnicodeEncodeError:
426
- return Header(text, header_name=header_name, charset=CHARSET,
427
- errors='replace').encode()
478
+ # Convert to unicode, if required.
479
+ if not isinstance(text, unicode):
480
+ text = unicode(text, 'utf-8')
481
+
482
+ if is_ascii(text):
483
+ charset = 'ascii'
484
+ else:
485
+ charset = 'utf-8'
486
+
487
+ return Header(text, header_name=header_name, charset=Charset(charset)).encode()
488
489
490
def addr_header_encode(text, header_name=None):
491
"""Encode and line-wrap the value of an email header field containing
492
email addresses."""
493
434
- return Header(
435
- ', '.join(
436
- formataddr((header_encode(name), emailaddr))
437
- for name, emailaddr in getaddresses([text])
438
- ),
439
- header_name=header_name
440
- ).encode()
494
+ # Convert to unicode, if required.
495
+ if not isinstance(text, unicode):
496
+ text = unicode(text, 'utf-8')
497
+
498
+ text = ', '.join(
499
+ formataddr((header_encode(name), emailaddr))
500
+ for name, emailaddr in getaddresses([text])
501
+ )
502
+
503
+ if is_ascii(text):
504
+ charset = 'ascii'
505
+ else:
506
+ charset = 'utf-8'
507
+
508
+ return Header(text, header_name=header_name, charset=Charset(charset)).encode()
509
510
511
class Config(object):
@@ -496,7 +564,8 @@ class Config(object):
564
['config', '--get-all', '--null', '%s.%s' % (self.section, name)],
565
env=self.env, keepends=True,
566
))
499
- except CommandError, e:
567
+ except CommandError:
568
+ t, e, traceback = sys.exc_info()
569
if e.retcode == 1:
570
# "the section or key is invalid"; i.e., there is no
571
# value for the specified key.
@@ -504,18 +573,6 @@ class Config(object):
573
else:
574
raise
575
507
- def get_recipients(self, name, default=None):
508
- """Read a recipients list from the configuration.
509
-
510
- Return the result as a comma-separated list of email
511
- addresses, or default if the option is unset. If the setting
512
- has multiple values, concatenate them with comma separators."""
513
-
514
- lines = self.get_all(name, default=None)
515
- if lines is None:
516
- return default
517
- return ', '.join(line.strip() for line in lines)
518
-
576
def set(self, name, value):
577
read_git_output(
578
['config', '%s.%s' % (self.section, name), value],
@@ -542,7 +599,8 @@ class Config(object):
599
['config', '--unset-all', '%s.%s' % (self.section, name)],
600
env=self.env,
601
)
545
- except CommandError, e:
602
+ except CommandError:
603
+ t, e, traceback = sys.exc_info()
604
if e.retcode == 5:
605
# The name doesn't exist, which is what we wanted anyway...
606
pass
@@ -636,7 +694,7 @@ class GitObject(object):
694
if not self.sha1:
695
raise ValueError('Empty commit has no summary')
696
639
- return iter(generate_summaries('--no-walk', self.sha1)).next()
697
+ return next(iter(generate_summaries('--no-walk', self.sha1)))
698
699
def __eq__(self, other):
700
return isinstance(other, GitObject) and self.sha1 == other.sha1
@@ -647,6 +705,10 @@ class GitObject(object):
705
def __nonzero__(self):
706
return bool(self.sha1)
707
708
+ def __bool__(self):
709
+ """Python 2 backward compatibility"""
710
+ return self.__nonzero__()
711
+
712
def __str__(self):
713
return self.sha1 or ZEROS
714
@@ -661,6 +723,12 @@ class Change(object):
723
def __init__(self, environment):
724
self.environment = environment
725
self._values = None
726
+ self._contains_html_diff = False
727
+
728
+ def _contains_diff(self):
729
+ # We do contain a diff, should it be rendered in HTML?
730
+ if self.environment.commit_email_format == "html":
731
+ self._contains_html_diff = True
732
733
def _compute_values(self):
734
"""Return a dictionary {keyword: expansion} for this Change.
@@ -670,7 +738,12 @@ class Change(object):
738
get_values(). The return value should always be a new
739
dictionary."""
740
673
- return self.environment.get_values()
741
+ values = self.environment.get_values()
742
+ fromaddr = self.environment.get_fromaddr(change=self)
743
+ if fromaddr is not None:
744
+ values['fromaddr'] = fromaddr
745
+ values['multimail_version'] = get_version()
746
+ return values
747
748
def get_values(self, **extra_values):
749
"""Return a dictionary {keyword: expansion} for this Change.
@@ -713,12 +786,18 @@ class Change(object):
786
skip lines that contain references to unknown variables."""
787
788
values = self.get_values(**extra_values)
789
+ if self._contains_html_diff:
790
+ values['contenttype'] = 'html'
791
+ else:
792
+ values['contenttype'] = 'plain'
793
+
794
for line in template.splitlines():
717
- (name, value) = line.split(':', 1)
795
+ (name, value) = line.split(': ', 1)
796
797
try:
798
value = value % values
721
- except KeyError, e:
799
+ except KeyError:
800
+ t, e, traceback = sys.exc_info()
801
if DEBUG:
802
self.environment.log_warning(
803
'Warning: unknown variable %r in the following line; line skipped:\n'
@@ -764,6 +843,24 @@ class Change(object):
843
844
raise NotImplementedError()
845
846
+ def _wrap_for_html(self, lines):
847
+ """Wrap the lines in HTML <pre> tag when using HTML format.
848
+
849
+ Escape special HTML characters and add <pre> and </pre> tags around
850
+ the given lines if we should be generating HTML as indicated by
851
+ self._contains_html_diff being set to true.
852
+ """
853
+ if self._contains_html_diff:
854
+ yield "<pre style='margin:0'>\n"
855
+
856
+ for line in lines:
857
+ yield cgi.escape(line)
858
+
859
+ yield '</pre>\n'
860
+ else:
861
+ for line in lines:
862
+ yield line
863
+
864
def generate_email(self, push, body_filter=None, extra_header_values={}):
865
"""Generate an email describing this change.
866
@@ -779,18 +876,76 @@ class Change(object):
876
for line in self.generate_email_header(**extra_header_values):
877
yield line
878
yield '\n'
782
- for line in self.generate_email_intro():
879
+ for line in self._wrap_for_html(self.generate_email_intro()):
880
yield line
881
882
body = self.generate_email_body(push)
883
if body_filter is not None:
884
body = body_filter(body)
885
+
886
+ diff_started = False
887
+ if self._contains_html_diff:
888
+ # "white-space: pre" is the default, but we need to
889
+ # specify it again in case the message is viewed in a
890
+ # webmail which wraps it in an element setting white-space
891
+ # to something else (Zimbra does this and sets
892
+ # white-space: pre-line).
893
+ yield '<pre style="white-space: pre; background: #F8F8F8">'
894
for line in body:
895
+ if self._contains_html_diff:
896
+ # This is very, very naive. It would be much better to really
897
+ # parse the diff, i.e. look at how many lines do we have in
898
+ # the hunk headers instead of blindly highlighting everything
899
+ # that looks like it might be part of a diff.
900
+ bgcolor = ''
901
+ fgcolor = ''
902
+ if line.startswith('--- a/'):
903
+ diff_started = True
904
+ bgcolor = 'e0e0ff'
905
+ elif line.startswith('diff ') or line.startswith('index '):
906
+ diff_started = True
907
+ fgcolor = '808080'
908
+ elif diff_started:
909
+ if line.startswith('+++ '):
910
+ bgcolor = 'e0e0ff'
911
+ elif line.startswith('@@'):
912
+ bgcolor = 'e0e0e0'
913
+ elif line.startswith('+'):
914
+ bgcolor = 'e0ffe0'
915
+ elif line.startswith('-'):
916
+ bgcolor = 'ffe0e0'
917
+ elif line.startswith('commit '):
918
+ fgcolor = '808000'
919
+ elif line.startswith(' '):
920
+ fgcolor = '404040'
921
+
922
+ # Chop the trailing LF, we don't want it inside <pre>.
923
+ line = cgi.escape(line[:-1])
924
+
925
+ if bgcolor or fgcolor:
926
+ style = 'display:block; white-space:pre;'
927
+ if bgcolor:
928
+ style += 'background:#' + bgcolor + ';'
929
+ if fgcolor:
930
+ style += 'color:#' + fgcolor + ';'
931
+ # Use a <span style='display:block> to color the
932
+ # whole line. The newline must be inside the span
933
+ # to display properly both in Firefox and in
934
+ # text-based browser.
935
+ line = "<span style='%s'>%s\n</span>" % (style, line)
936
+ else:
937
+ line = line + '\n'
938
+
939
yield line
940
+ if self._contains_html_diff:
941
+ yield '</pre>'
942
791
- for line in self.generate_email_footer():
943
+ for line in self._wrap_for_html(self.generate_email_footer()):
944
yield line
945
946
+ def get_alt_fromaddr(self):
947
+ return None
948
+
949
950
class Revision(Change):
951
"""A Change consisting of a single git commit."""
@@ -867,14 +1022,25 @@ class Revision(Change):
1022
def generate_email_body(self, push):
1023
"""Show this revision."""
1024
870
- return read_git_lines(
871
- ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],
872
- keepends=True,
873
- )
1025
+ for line in read_git_lines(
1026
+ ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],
1027
+ keepends=True,
1028
+ ):
1029
+ if line.startswith('Date: ') and self.environment.date_substitute:
1030
+ yield self.environment.date_substitute + line[len('Date: '):]
1031
+ else:
1032
+ yield line
1033
1034
def generate_email_footer(self):
1035
return self.expand_lines(REVISION_FOOTER_TEMPLATE)
1036
1037
+ def generate_email(self, push, body_filter=None, extra_header_values={}):
1038
+ self._contains_diff()
1039
+ return Change.generate_email(self, push, body_filter, extra_header_values)
1040
+
1041
+ def get_alt_fromaddr(self):
1042
+ return self.environment.from_commit
1043
+
1044
1045
class ReferenceChange(Change):
1046
"""A Change to a Git reference.
@@ -1096,10 +1262,10 @@ class ReferenceChange(Change):
1262
yield '\n'
1263
yield 'Detailed log of new commits:\n\n'
1264
for line in read_git_lines(
1099
- ['log', '--no-walk']
1100
- + self.logopts
1101
- + new_commits_list
1102
- + ['--'],
1265
+ ['log', '--no-walk'] +
1266
+ self.logopts +
1267
+ new_commits_list +
1268
+ ['--'],
1269
keepends=True,
1270
):
1271
yield line
@@ -1253,9 +1419,9 @@ class ReferenceChange(Change):
1419
yield '\n'
1420
yield 'Summary of changes:\n'
1421
for line in read_git_lines(
1256
- ['diff-tree']
1257
- + self.diffopts
1258
- + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],
1422
+ ['diff-tree'] +
1423
+ self.diffopts +
1424
+ ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],
1425
keepends=True,
1426
):
1427
yield line
@@ -1316,6 +1482,9 @@ class ReferenceChange(Change):
1482
)
1483
yield '\n'
1484
1485
+ def get_alt_fromaddr(self):
1486
+ return self.environment.from_refchange
1487
+
1488
1489
class BranchChange(ReferenceChange):
1490
refname_type = 'branch'
@@ -1397,9 +1566,9 @@ class BranchChange(ReferenceChange):
1566
# commit is a non-merge commit, though it may make sense to
1567
# combine if it is a merge as well.
1568
if not (
1400
- len(new_commits) == 1
1401
- and len(new_commits[0][1]) == 1
1402
- and new_commits[0][0] in known_added_sha1s
1569
+ len(new_commits) == 1 and
1570
+ len(new_commits[0][1]) == 1 and
1571
+ new_commits[0][0] in known_added_sha1s
1572
):
1573
return None
1574
@@ -1432,6 +1601,7 @@ class BranchChange(ReferenceChange):
1601
values['subject'] = self.expand(COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE, **values)
1602
1603
self._single_revision = revision
1604
+ self._contains_diff()
1605
self.header_template = COMBINED_HEADER_TEMPLATE
1606
self.intro_template = COMBINED_INTRO_TEMPLATE
1607
self.footer_template = COMBINED_FOOTER_TEMPLATE
@@ -1690,17 +1860,18 @@ class SendMailer(Mailer):
1860
def send(self, lines, to_addrs):
1861
try:
1862
p = subprocess.Popen(self.command, stdin=subprocess.PIPE)
1693
- except OSError, e:
1863
+ except OSError:
1864
sys.stderr.write(
1695
- '*** Cannot execute command: %s\n' % ' '.join(self.command)
1696
- + '*** %s\n' % str(e)
1697
- + '*** Try setting multimailhook.mailer to "smtp"\n'
1865
+ '*** Cannot execute command: %s\n' % ' '.join(self.command) +
1866
+ '*** %s\n' % sys.exc_info()[1] +
1867
+ '*** Try setting multimailhook.mailer to "smtp"\n' +
1868
'*** to send emails without using the sendmail command.\n'
1869
)
1870
sys.exit(1)
1871
try:
1872
+ lines = (str_to_bytes(line) for line in lines)
1873
p.stdin.writelines(lines)
1703
- except Exception, e:
1874
+ except Exception:
1875
sys.stderr.write(
1876
'*** Error while generating commit email\n'
1877
'*** - mail sending aborted.\n'
@@ -1710,7 +1881,7 @@ class SendMailer(Mailer):
1881
p.terminate()
1882
except AttributeError:
1883
pass
1713
- raise e
1884
+ raise
1885
else:
1886
p.stdin.close()
1887
retcode = p.wait()
@@ -1770,11 +1941,11 @@ class SMTPMailer(Mailer):
1941
"*** Setting debug on for SMTP server connection (%s) ***\n"
1942
% self.smtpserverdebuglevel)
1943
self.smtp.set_debuglevel(self.smtpserverdebuglevel)
1773
- except Exception, e:
1944
+ except Exception:
1945
sys.stderr.write(
1946
'*** Error establishing SMTP connection to %s ***\n'
1947
% self.smtpserver)
1777
- sys.stderr.write('*** %s\n' % str(e))
1948
+ sys.stderr.write('*** %s\n' % sys.exc_info()[1])
1949
sys.exit(1)
1950
1951
def __del__(self):
@@ -1784,16 +1955,15 @@ class SMTPMailer(Mailer):
1955
def send(self, lines, to_addrs):
1956
try:
1957
if self.username or self.password:
1787
- sys.stderr.write("*** Authenticating as %s ***\n" % self.username)
1958
self.smtp.login(self.username, self.password)
1959
msg = ''.join(lines)
1960
# turn comma-separated list into Python list if needed.
1961
if isinstance(to_addrs, basestring):
1962
to_addrs = [email for (name, email) in getaddresses([to_addrs])]
1963
self.smtp.sendmail(self.envelopesender, to_addrs, msg)
1794
- except Exception, e:
1964
+ except Exception:
1965
sys.stderr.write('*** Error sending email ***\n')
1796
- sys.stderr.write('*** %s\n' % str(e))
1966
+ sys.stderr.write('*** %s\n' % sys.exc_info()[1])
1967
self.smtp.quit()
1968
sys.exit(1)
1969
@@ -1809,9 +1979,10 @@ class OutputMailer(Mailer):
1979
self.f = f
1980
1981
def send(self, lines, to_addrs):
1812
- self.f.write(self.SEPARATOR)
1813
- self.f.writelines(lines)
1814
- self.f.write(self.SEPARATOR)
1982
+ write_str(self.f, self.SEPARATOR)
1983
+ for line in lines:
1984
+ write_str(self.f, line)
1985
+ write_str(self.f, self.SEPARATOR)
1986
1987
1988
def get_git_dir():
@@ -1877,11 +2048,13 @@ class Environment(object):
2048
Return the address to be used as the 'From' email address
2049
in the email envelope.
2050
1880
- get_fromaddr()
2051
+ get_fromaddr(change=None)
2052
2053
Return the 'From' email address used in the email 'From:'
1883
- headers. (May be a full RFC 2822 email address like 'Joe
1884
- User <user@example.com>'.)
2054
+ headers. If the change is known when this function is
2055
+ called, it is passed in as the 'change' parameter. (May
2056
+ be a full RFC 2822 email address like 'Joe User
2057
+ <user@example.com>'.)
2058
2059
get_administrator()
2060
@@ -1901,12 +2074,29 @@ class Environment(object):
2074
get_reply_to_commit() is used for individual commit
2075
emails.
2076
2077
+ get_ref_filter_regex()
2078
+
2079
+ Return a tuple -- a compiled regex, and a boolean indicating
2080
+ whether the regex picks refs to include (if False, the regex
2081
+ matches on refs to exclude).
2082
+
2083
+ get_default_ref_ignore_regex()
2084
+
2085
+ Return a regex that should be ignored for both what emails
2086
+ to send and when computing what commits are considered new
2087
+ to the repository. Default is "^refs/notes/".
2088
+
2089
They should also define the following attributes:
2090
2091
announce_show_shortlog (bool)
2092
2093
True iff announce emails should include a shortlog.
2094
2095
+ commit_email_format (string)
2096
+
2097
+ If "html", generate commit emails in HTML instead of plain text
2098
+ used by default.
2099
+
2100
refchange_showgraph (bool)
2101
2102
True iff refchanges emails should include a detailed graph.
@@ -1939,6 +2129,11 @@ class Environment(object):
2129
commit mail. The value should be a list of strings
2130
representing words to be passed to the command.
2131
2132
+ date_substitute (string)
2133
+
2134
+ String to be used in substitution for 'Date:' at start of
2135
+ line in the output of 'git log'.
2136
+
2137
quiet (bool)
2138
On success do not write to stderr
2139
@@ -1950,6 +2145,13 @@ class Environment(object):
2145
True if a combined email should be produced when a single
2146
new commit is pushed to a branch, False otherwise.
2147
2148
+ from_refchange, from_commit (strings)
2149
+
2150
+ Addresses to use for the From: field for refchange emails
2151
+ and commit emails respectively. Set from
2152
+ multimailhook.fromRefchange and multimailhook.fromCommit
2153
+ by ConfigEnvironmentMixin.
2154
+
2155
"""
2156
2157
REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$')
@@ -1957,6 +2159,7 @@ class Environment(object):
2159
def __init__(self, osenv=None):
2160
self.osenv = osenv or os.environ
2161
self.announce_show_shortlog = False
2162
+ self.commit_email_format = "text"
2163
self.maxcommitemails = 500
2164
self.diffopts = ['--stat', '--summary', '--find-copies-harder']
2165
self.graphopts = ['--oneline', '--decorate']
@@ -1964,6 +2167,7 @@ class Environment(object):
2167
self.refchange_showgraph = False
2168
self.refchange_showlog = False
2169
self.commitlogopts = ['-C', '--stat', '-p', '--cc']
2170
+ self.date_substitute = 'AuthorDate: '
2171
self.quiet = False
2172
self.stdout = False
2173
self.combine_when_single_commit = True
@@ -1972,7 +2176,6 @@ class Environment(object):
2176
'administrator',
2177
'charset',
2178
'emailprefix',
1975
- 'fromaddr',
2179
'pusher',
2180
'pusher_email',
2181
'repo_path',
@@ -1998,7 +2201,7 @@ class Environment(object):
2201
def get_pusher_email(self):
2202
return None
2203
2001
- def get_fromaddr(self):
2204
+ def get_fromaddr(self, change=None):
2205
config = Config('user')
2206
fromname = config.get('name', default='')
2207
fromemail = config.get('email', default='')
@@ -2080,6 +2283,15 @@ class Environment(object):
2283
def get_reply_to_commit(self, revision):
2284
return revision.author
2285
2286
+ def get_default_ref_ignore_regex(self):
2287
+ # The commit messages of git notes are essentially meaningless
2288
+ # and "filenames" in git notes commits are an implementational
2289
+ # detail that might surprise users at first. As such, we
2290
+ # would need a completely different method for handling emails
2291
+ # of git notes in order for them to be of benefit for users,
2292
+ # which we simply do not have right now.
2293
+ return "^refs/notes/"
2294
+
2295
def filter_body(self, lines):
2296
"""Filter the lines intended for an email body.
2297
@@ -2095,19 +2307,19 @@ class Environment(object):
2307
"""Write the string msg on a log file or on stderr.
2308
2309
Sends the text to stderr by default, override to change the behavior."""
2098
- sys.stderr.write(msg)
2310
+ write_str(sys.stderr, msg)
2311
2312
def log_warning(self, msg):
2313
"""Write the string msg on a log file or on stderr.
2314
2315
Sends the text to stderr by default, override to change the behavior."""
2104
- sys.stderr.write(msg)
2316
+ write_str(sys.stderr, msg)
2317
2318
def log_error(self, msg):
2319
"""Write the string msg on a log file or on stderr.
2320
2321
Sends the text to stderr by default, override to change the behavior."""
2110
- sys.stderr.write(msg)
2322
+ write_str(sys.stderr, msg)
2323
2324
2325
class ConfigEnvironmentMixin(Environment):
@@ -2128,6 +2340,14 @@ class ConfigEnvironmentMixin(Environment):
2340
class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2341
"""An Environment that reads most of its information from "git config"."""
2342
2343
+ @staticmethod
2344
+ def forbid_field_values(name, value, forbidden):
2345
+ for forbidden_val in forbidden:
2346
+ if value is not None and value.lower() == forbidden:
2347
+ raise ConfigurationException(
2348
+ '"%s" is not an allowed setting for %s' % (value, name)
2349
+ )
2350
+
2351
def __init__(self, config, **kw):
2352
super(ConfigOptionsEnvironmentMixin, self).__init__(
2353
config=config, **kw
@@ -2144,14 +2364,26 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2364
if val is not None:
2365
setattr(self, var, val)
2366
2367
+ commit_email_format = config.get('commitEmailFormat')
2368
+ if commit_email_format is not None:
2369
+ if commit_email_format != "html" and commit_email_format != "text":
2370
+ self.log_warning(
2371
+ '*** Unknown value for multimailhook.commitEmailFormat: %s\n' %
2372
+ commit_email_format +
2373
+ '*** Expected either "text" or "html". Ignoring.\n'
2374
+ )
2375
+ else:
2376
+ self.commit_email_format = commit_email_format
2377
+
2378
maxcommitemails = config.get('maxcommitemails')
2379
if maxcommitemails is not None:
2380
try:
2381
self.maxcommitemails = int(maxcommitemails)
2382
except ValueError:
2383
self.log_warning(
2153
- '*** Malformed value for multimailhook.maxCommitEmails: %s\n' % maxcommitemails
2154
- + '*** Expected a number. Ignoring.\n'
2384
+ '*** Malformed value for multimailhook.maxCommitEmails: %s\n'
2385
+ % maxcommitemails +
2386
+ '*** Expected a number. Ignoring.\n'
2387
)
2388
2389
diffopts = config.get('diffopts')
@@ -2170,32 +2402,44 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2402
if commitlogopts is not None:
2403
self.commitlogopts = shlex.split(commitlogopts)
2404
2405
+ date_substitute = config.get('dateSubstitute')
2406
+ if date_substitute == 'none':
2407
+ self.date_substitute = None
2408
+ elif date_substitute is not None:
2409
+ self.date_substitute = date_substitute
2410
+
2411
reply_to = config.get('replyTo')
2412
self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to)
2175
- if (
2176
- self.__reply_to_refchange is not None
2177
- and self.__reply_to_refchange.lower() == 'author'
2178
- ):
2179
- raise ConfigurationException(
2180
- '"author" is not an allowed setting for replyToRefchange'
2181
- )
2413
+ self.forbid_field_values('replyToRefchange',
2414
+ self.__reply_to_refchange,
2415
+ ['author'])
2416
self.__reply_to_commit = config.get('replyToCommit', default=reply_to)
2417
2418
+ from_addr = self.config.get('from')
2419
+ self.from_refchange = config.get('fromRefchange')
2420
+ self.forbid_field_values('fromRefchange',
2421
+ self.from_refchange,
2422
+ ['author', 'none'])
2423
+ self.from_commit = config.get('fromCommit')
2424
+ self.forbid_field_values('fromCommit',
2425
+ self.from_commit,
2426
+ ['none'])
2427
+
2428
combine = config.get_bool('combineWhenSingleCommit')
2429
if combine is not None:
2430
self.combine_when_single_commit = combine
2431
2432
def get_administrator(self):
2433
return (
2190
- self.config.get('administrator')
2191
- or self.get_sender()
2192
- or super(ConfigOptionsEnvironmentMixin, self).get_administrator()
2434
+ self.config.get('administrator') or
2435
+ self.get_sender() or
2436
+ super(ConfigOptionsEnvironmentMixin, self).get_administrator()
2437
)
2438
2439
def get_repo_shortname(self):
2440
return (
2197
- self.config.get('reponame')
2198
- or super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname()
2441
+ self.config.get('reponame') or
2442
+ super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname()
2443
)
2444
2445
def get_emailprefix(self):
@@ -2212,33 +2456,42 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
2456
def get_sender(self):
2457
return self.config.get('envelopesender')
2458
2215
- def get_fromaddr(self):
2459
+ def process_addr(self, addr, change):
2460
+ if addr.lower() == 'author':
2461
+ if hasattr(change, 'author'):
2462
+ return change.author
2463
+ else:
2464
+ return None
2465
+ elif addr.lower() == 'pusher':
2466
+ return self.get_pusher_email()
2467
+ elif addr.lower() == 'none':
2468
+ return None
2469
+ else:
2470
+ return addr
2471
+
2472
+ def get_fromaddr(self, change=None):
2473
fromaddr = self.config.get('from')
2474
+ if change:
2475
+ alt_fromaddr = change.get_alt_fromaddr()
2476
+ if alt_fromaddr:
2477
+ fromaddr = alt_fromaddr
2478
+ if fromaddr:
2479
+ fromaddr = self.process_addr(fromaddr, change)
2480
if fromaddr:
2481
return fromaddr
2219
- return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr()
2482
+ return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr(change)
2483
2484
def get_reply_to_refchange(self, refchange):
2485
if self.__reply_to_refchange is None:
2486
return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_refchange(refchange)
2224
- elif self.__reply_to_refchange.lower() == 'pusher':
2225
- return self.get_pusher_email()
2226
- elif self.__reply_to_refchange.lower() == 'none':
2227
- return None
2487
else:
2229
- return self.__reply_to_refchange
2488
+ return self.process_addr(self.__reply_to_refchange, refchange)
2489
2490
def get_reply_to_commit(self, revision):
2491
if self.__reply_to_commit is None:
2492
return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_commit(revision)
2234
- elif self.__reply_to_commit.lower() == 'author':
2235
- return revision.author
2236
- elif self.__reply_to_commit.lower() == 'pusher':
2237
- return self.get_pusher_email()
2238
- elif self.__reply_to_commit.lower() == 'none':
2239
- return None
2493
else:
2241
- return self.__reply_to_commit
2494
+ return self.process_addr(self.__reply_to_commit, revision)
2495
2496
def get_scancommitforcc(self):
2497
return self.config.get('scancommitforcc')
@@ -2270,12 +2523,14 @@ class FilterLinesEnvironmentMixin(Environment):
2523
def filter_body(self, lines):
2524
lines = super(FilterLinesEnvironmentMixin, self).filter_body(lines)
2525
if self.__strict_utf8:
2273
- lines = (line.decode(ENCODING, 'replace') for line in lines)
2526
+ if not PYTHON3:
2527
+ lines = (line.decode(ENCODING, 'replace') for line in lines)
2528
# Limit the line length in Unicode-space to avoid
2529
# splitting characters:
2530
if self.__emailmaxlinelength:
2531
lines = limit_linelength(lines, self.__emailmaxlinelength)
2278
- lines = (line.encode(ENCODING, 'replace') for line in lines)
2532
+ if not PYTHON3:
2533
+ lines = (line.encode(ENCODING, 'replace') for line in lines)
2534
elif self.__emailmaxlinelength:
2535
lines = limit_linelength(lines, self.__emailmaxlinelength)
2536
@@ -2404,10 +2659,10 @@ class StaticRecipientsEnvironmentMixin(Environment):
2659
# actual *contents* of the change being reported, we only
2660
# choose based on the *type* of the change. Therefore we can
2661
# compute them once and for all:
2407
- if not (refchange_recipients
2408
- or announce_recipients
2409
- or revision_recipients
2410
- or scancommitforcc):
2662
+ if not (refchange_recipients or
2663
+ announce_recipients or
2664
+ revision_recipients or
2665
+ scancommitforcc):
2666
raise ConfigurationException('No email recipients configured!')
2667
self.__refchange_recipients = refchange_recipients
2668
self.__announce_recipients = announce_recipients
@@ -2457,13 +2712,104 @@ class ConfigRecipientsEnvironmentMixin(
2712
found, raise a ConfigurationException."""
2713
2714
for name in names:
2460
- retval = config.get_recipients(name)
2461
- if retval is not None:
2462
- return retval
2715
+ lines = config.get_all(name)
2716
+ if lines is not None:
2717
+ lines = [line.strip() for line in lines]
2718
+ # Single "none" is a special value equivalen to empty string.
2719
+ if lines == ['none']:
2720
+ lines = ['']
2721
+ return ', '.join(lines)
2722
else:
2723
return ''
2724
2725
2726
+class StaticRefFilterEnvironmentMixin(Environment):
2727
+ """Set branch filter statically based on constructor parameters."""
2728
+
2729
+ def __init__(self, ref_filter_incl_regex, ref_filter_excl_regex,
2730
+ ref_filter_do_send_regex, ref_filter_dont_send_regex,
2731
+ **kw):
2732
+ super(StaticRefFilterEnvironmentMixin, self).__init__(**kw)
2733
+
2734
+ if ref_filter_incl_regex and ref_filter_excl_regex:
2735
+ raise ConfigurationException(
2736
+ "Cannot specify both a ref inclusion and exclusion regex.")
2737
+ self.__is_inclusion_filter = bool(ref_filter_incl_regex)
2738
+ default_exclude = self.get_default_ref_ignore_regex()
2739
+ if ref_filter_incl_regex:
2740
+ ref_filter_regex = ref_filter_incl_regex
2741
+ elif ref_filter_excl_regex:
2742
+ ref_filter_regex = ref_filter_excl_regex + '|' + default_exclude
2743
+ else:
2744
+ ref_filter_regex = default_exclude
2745
+ try:
2746
+ self.__compiled_regex = re.compile(ref_filter_regex)
2747
+ except Exception:
2748
+ raise ConfigurationException(
2749
+ 'Invalid Ref Filter Regex "%s": %s' % (ref_filter_regex, sys.exc_info()[1]))
2750
+
2751
+ if ref_filter_do_send_regex and ref_filter_dont_send_regex:
2752
+ raise ConfigurationException(
2753
+ "Cannot specify both a ref doSend and dontSend regex.")
2754
+ if ref_filter_do_send_regex or ref_filter_dont_send_regex:
2755
+ self.__is_do_send_filter = bool(ref_filter_do_send_regex)
2756
+ if ref_filter_incl_regex:
2757
+ ref_filter_send_regex = ref_filter_incl_regex
2758
+ elif ref_filter_excl_regex:
2759
+ ref_filter_send_regex = ref_filter_excl_regex
2760
+ else:
2761
+ ref_filter_send_regex = '.*'
2762
+ self.__is_do_send_filter = True
2763
+ try:
2764
+ self.__send_compiled_regex = re.compile(ref_filter_send_regex)
2765
+ except Exception:
2766
+ raise ConfigurationException(
2767
+ 'Invalid Ref Filter Regex "%s": %s' %
2768
+ (ref_filter_send_regex, sys.exc_info()[1]))
2769
+ else:
2770
+ self.__send_compiled_regex = self.__compiled_regex
2771
+ self.__is_do_send_filter = self.__is_inclusion_filter
2772
+
2773
+ def get_ref_filter_regex(self, send_filter=False):
2774
+ if send_filter:
2775
+ return self.__send_compiled_regex, self.__is_do_send_filter
2776
+ else:
2777
+ return self.__compiled_regex, self.__is_inclusion_filter
2778
+
2779
+
2780
+class ConfigRefFilterEnvironmentMixin(
2781
+ ConfigEnvironmentMixin,
2782
+ StaticRefFilterEnvironmentMixin
2783
+ ):
2784
+ """Determine branch filtering statically based on config."""
2785
+
2786
+ def _get_regex(self, config, key):
2787
+ """Get a list of whitespace-separated regex. The refFilter* config
2788
+ variables are multivalued (hence the use of get_all), and we
2789
+ allow each entry to be a whitespace-separated list (hence the
2790
+ split on each line). The whole thing is glued into a single regex."""
2791
+ values = config.get_all(key)
2792
+ if values is None:
2793
+ return values
2794
+ items = []
2795
+ for line in values:
2796
+ for i in line.split():
2797
+ items.append(i)
2798
+ if items == []:
2799
+ return None
2800
+ return '|'.join(items)
2801
+
2802
+ def __init__(self, config, **kw):
2803
+ super(ConfigRefFilterEnvironmentMixin, self).__init__(
2804
+ config=config,
2805
+ ref_filter_incl_regex=self._get_regex(config, 'refFilterInclusionRegex'),
2806
+ ref_filter_excl_regex=self._get_regex(config, 'refFilterExclusionRegex'),
2807
+ ref_filter_do_send_regex=self._get_regex(config, 'refFilterDoSendRegex'),
2808
+ ref_filter_dont_send_regex=self._get_regex(config, 'refFilterDontSendRegex'),
2809
+ **kw
2810
+ )
2811
+
2812
+
2813
class ProjectdescEnvironmentMixin(Environment):
2814
"""Make a "projectdesc" value available for templates.
2815
@@ -2499,6 +2845,7 @@ class GenericEnvironment(
2845
ComputeFQDNEnvironmentMixin,
2846
ConfigFilterLinesEnvironmentMixin,
2847
ConfigRecipientsEnvironmentMixin,
2848
+ ConfigRefFilterEnvironmentMixin,
2849
PusherDomainEnvironmentMixin,
2850
ConfigOptionsEnvironmentMixin,
2851
GenericEnvironmentMixin,
@@ -2513,14 +2860,14 @@ class GitoliteEnvironmentMixin(Environment):
2860
# repo_shortname (though it's probably not as good as a value
2861
# the user might have explicitly put in his config).
2862
return (
2516
- self.osenv.get('GL_REPO', None)
2517
- or super(GitoliteEnvironmentMixin, self).get_repo_shortname()
2863
+ self.osenv.get('GL_REPO', None) or
2864
+ super(GitoliteEnvironmentMixin, self).get_repo_shortname()
2865
)
2866
2867
def get_pusher(self):
2868
return self.osenv.get('GL_USER', 'unknown user')
2869
2523
- def get_fromaddr(self):
2870
+ def get_fromaddr(self, change=None):
2871
GL_USER = self.osenv.get('GL_USER')
2872
if GL_USER is not None:
2873
# Find the path to gitolite.conf. Note that gitolite v3
@@ -2536,9 +2883,9 @@ class GitoliteEnvironmentMixin(Environment):
2883
f = open(GL_CONF, 'rU')
2884
try:
2885
in_user_emails_section = False
2539
- re_template = r'^\s*#\s*{}\s*$'
2886
+ re_template = r'^\s*#\s*%s\s*$'
2887
re_begin, re_user, re_end = (
2541
- re.compile(re_template.format(x))
2888
+ re.compile(re_template % x)
2889
for x in (
2890
r'BEGIN\s+USER\s+EMAILS',
2891
re.escape(GL_USER) + r'\s+(.*)',
@@ -2557,7 +2904,7 @@ class GitoliteEnvironmentMixin(Environment):
2904
return m.group(1)
2905
finally:
2906
f.close()
2560
- return super(GitoliteEnvironmentMixin, self).get_fromaddr()
2907
+ return super(GitoliteEnvironmentMixin, self).get_fromaddr(change)
2908
2909
2910
class IncrementalDateTime(object):
@@ -2570,8 +2917,9 @@ class IncrementalDateTime(object):
2917
2918
def __init__(self):
2919
self.time = time.time()
2920
+ self.next = self.__next__ # Python 2 backward compatibility
2921
2574
- def next(self):
2922
+ def __next__(self):
2923
formatted = formatdate(self.time, True)
2924
self.time += 1
2925
return formatted
@@ -2583,6 +2931,7 @@ class GitoliteEnvironment(
2931
ComputeFQDNEnvironmentMixin,
2932
ConfigFilterLinesEnvironmentMixin,
2933
ConfigRecipientsEnvironmentMixin,
2934
+ ConfigRefFilterEnvironmentMixin,
2935
PusherDomainEnvironmentMixin,
2936
ConfigOptionsEnvironmentMixin,
2937
GitoliteEnvironmentMixin,
@@ -2591,6 +2940,117 @@ class GitoliteEnvironment(
2940
pass
2941
2942
2943
+class StashEnvironmentMixin(Environment):
2944
+ def __init__(self, user=None, repo=None, **kw):
2945
+ super(StashEnvironmentMixin, self).__init__(**kw)
2946
+ self.__user = user
2947
+ self.__repo = repo
2948
+
2949
+ def get_repo_shortname(self):
2950
+ return self.__repo
2951
+
2952
+ def get_pusher(self):
2953
+ return re.match('(.*?)\s*<', self.__user).group(1)
2954
+
2955
+ def get_pusher_email(self):
2956
+ return self.__user
2957
+
2958
+ def get_fromaddr(self, change=None):
2959
+ return self.__user
2960
+
2961
+
2962
+class StashEnvironment(
2963
+ StashEnvironmentMixin,
2964
+ ProjectdescEnvironmentMixin,
2965
+ ConfigMaxlinesEnvironmentMixin,
2966
+ ComputeFQDNEnvironmentMixin,
2967
+ ConfigFilterLinesEnvironmentMixin,
2968
+ ConfigRecipientsEnvironmentMixin,
2969
+ ConfigRefFilterEnvironmentMixin,
2970
+ PusherDomainEnvironmentMixin,
2971
+ ConfigOptionsEnvironmentMixin,
2972
+ Environment,
2973
+ ):
2974
+ pass
2975
+
2976
+
2977
+class GerritEnvironmentMixin(Environment):
2978
+ def __init__(self, project=None, submitter=None, update_method=None, **kw):
2979
+ super(GerritEnvironmentMixin, self).__init__(**kw)
2980
+ self.__project = project
2981
+ self.__submitter = submitter
2982
+ self.__update_method = update_method
2983
+ "Make an 'update_method' value available for templates."
2984
+ self.COMPUTED_KEYS += ['update_method']
2985
+
2986
+ def get_repo_shortname(self):
2987
+ return self.__project
2988
+
2989
+ def get_pusher(self):
2990
+ if self.__submitter:
2991
+ if self.__submitter.find('<') != -1:
2992
+ # Submitter has a configured email, we transformed
2993
+ # __submitter into an RFC 2822 string already.
2994
+ return re.match('(.*?)\s*<', self.__submitter).group(1)
2995
+ else:
2996
+ # Submitter has no configured email, it's just his name.
2997
+ return self.__submitter
2998
+ else:
2999
+ # If we arrive here, this means someone pushed "Submit" from
3000
+ # the gerrit web UI for the CR (or used one of the programmatic
3001
+ # APIs to do the same, such as gerrit review) and the
3002
+ # merge/push was done by the Gerrit user. It was technically
3003
+ # triggered by someone else, but sadly we have no way of
3004
+ # determining who that someone else is at this point.
3005
+ return 'Gerrit' # 'unknown user'?
3006
+
3007
+ def get_pusher_email(self):
3008
+ if self.__submitter:
3009
+ return self.__submitter
3010
+ else:
3011
+ return super(GerritEnvironmentMixin, self).get_pusher_email()
3012
+
3013
+ def get_fromaddr(self, change=None):
3014
+ if self.__submitter and self.__submitter.find('<') != -1:
3015
+ return self.__submitter
3016
+ else:
3017
+ return super(GerritEnvironmentMixin, self).get_fromaddr(change)
3018
+
3019
+ def get_default_ref_ignore_regex(self):
3020
+ default = super(GerritEnvironmentMixin, self).get_default_ref_ignore_regex()
3021
+ return default + '|^refs/changes/|^refs/cache-automerge/|^refs/meta/'
3022
+
3023
+ def get_revision_recipients(self, revision):
3024
+ # Merge commits created by Gerrit when users hit "Submit this patchset"
3025
+ # in the Web UI (or do equivalently with REST APIs or the gerrit review
3026
+ # command) are not something users want to see an individual email for.
3027
+ # Filter them out.
3028
+ committer = read_git_output(['log', '--no-walk', '--format=%cN',
3029
+ revision.rev.sha1])
3030
+ if committer == 'Gerrit Code Review':
3031
+ return []
3032
+ else:
3033
+ return super(GerritEnvironmentMixin, self).get_revision_recipients(revision)
3034
+
3035
+ def get_update_method(self):
3036
+ return self.__update_method
3037
+
3038
+
3039
+class GerritEnvironment(
3040
+ GerritEnvironmentMixin,
3041
+ ProjectdescEnvironmentMixin,
3042
+ ConfigMaxlinesEnvironmentMixin,
3043
+ ComputeFQDNEnvironmentMixin,
3044
+ ConfigFilterLinesEnvironmentMixin,
3045
+ ConfigRecipientsEnvironmentMixin,
3046
+ ConfigRefFilterEnvironmentMixin,
3047
+ PusherDomainEnvironmentMixin,
3048
+ ConfigOptionsEnvironmentMixin,
3049
+ Environment,
3050
+ ):
3051
+ pass
3052
+
3053
+
3054
class Push(object):
3055
"""Represent an entire push (i.e., a group of ReferenceChanges).
3056
@@ -2673,10 +3133,11 @@ class Push(object):
3133
])
3134
)
3135
2676
- def __init__(self, changes, ignore_other_refs=False):
3136
+ def __init__(self, environment, changes, ignore_other_refs=False):
3137
self.changes = sorted(changes, key=self._sort_key)
3138
self.__other_ref_sha1s = None
3139
self.__cached_commits_spec = {}
3140
+ self.environment = environment
3141
3142
if ignore_other_refs:
3143
self.__other_ref_sha1s = set()
@@ -2703,10 +3164,14 @@ class Push(object):
3164
'%(objectname) %(objecttype) %(refname)\n'
3165
'%(*objectname) %(*objecttype) %(refname)'
3166
)
3167
+ ref_filter_regex, is_inclusion_filter = \
3168
+ self.environment.get_ref_filter_regex()
3169
for line in read_git_lines(
3170
['for-each-ref', '--format=%s' % (fmt,)]):
3171
(sha1, type, name) = line.split(' ', 2)
2709
- if sha1 and type == 'commit' and name not in updated_refs:
3172
+ if (sha1 and type == 'commit' and
3173
+ name not in updated_refs and
3174
+ include_ref(name, ref_filter_regex, is_inclusion_filter)):
3175
sha1s.add(sha1)
3176
3177
self.__other_ref_sha1s = sha1s
@@ -2856,7 +3321,7 @@ class Push(object):
3321
if not change.environment.quiet:
3322
change.environment.log_msg(
3323
'Sending notification emails to: %s\n' % (change.recipients,))
2859
- extra_values = {'send_date': send_date.next()}
3324
+ extra_values = {'send_date': next(send_date)}
3325
3326
rev = change.send_single_combined_email(sha1s)
3327
if rev:
@@ -2876,9 +3341,9 @@ class Push(object):
3341
max_emails = change.environment.maxcommitemails
3342
if max_emails and len(sha1s) > max_emails:
3343
change.environment.log_warning(
2879
- '*** Too many new commits (%d), not sending commit emails.\n' % len(sha1s)
2880
- + '*** Try setting multimailhook.maxCommitEmails to a greater value\n'
2881
- + '*** Currently, multimailhook.maxCommitEmails=%d\n' % max_emails
3344
+ '*** Too many new commits (%d), not sending commit emails.\n' % len(sha1s) +
3345
+ '*** Try setting multimailhook.maxCommitEmails to a greater value\n' +
3346
+ '*** Currently, multimailhook.maxCommitEmails=%d\n' % max_emails
3347
)
3348
return
3349
@@ -2889,7 +3354,7 @@ class Push(object):
3354
rev.recipients = rev.cc_recipients
3355
rev.cc_recipients = None
3356
if rev.recipients:
2892
- extra_values = {'send_date': send_date.next()}
3357
+ extra_values = {'send_date': next(send_date)}
3358
mailer.send(
3359
rev.generate_email(self, body_filter, extra_values),
3360
rev.recipients,
@@ -2904,18 +3369,33 @@ class Push(object):
3369
)
3370
3371
3372
+def include_ref(refname, ref_filter_regex, is_inclusion_filter):
3373
+ does_match = bool(ref_filter_regex.search(refname))
3374
+ if is_inclusion_filter:
3375
+ return does_match
3376
+ else: # exclusion filter -- we include the ref if the regex doesn't match
3377
+ return not does_match
3378
+
3379
+
3380
def run_as_post_receive_hook(environment, mailer):
3381
+ ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
3382
changes = []
3383
for line in sys.stdin:
3384
(oldrev, newrev, refname) = line.strip().split(' ', 2)
3385
+ if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
3386
+ continue
3387
changes.append(
3388
ReferenceChange.create(environment, oldrev, newrev, refname)
3389
)
2914
- push = Push(changes)
2915
- push.send_emails(mailer, body_filter=environment.filter_body)
3390
+ if changes:
3391
+ push = Push(environment, changes)
3392
+ push.send_emails(mailer, body_filter=environment.filter_body)
3393
3394
3395
def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
3396
+ ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
3397
+ if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
3398
+ return
3399
changes = [
3400
ReferenceChange.create(
3401
environment,
@@ -2924,7 +3404,7 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=
3404
refname,
3405
),
3406
]
2927
- push = Push(changes, force_send)
3407
+ push = Push(environment, changes, force_send)
3408
push.send_emails(mailer, body_filter=environment.filter_body)
3409
3410
@@ -2953,8 +3433,8 @@ def choose_mailer(config, environment):
3433
mailer = SendMailer(command=command, envelopesender=environment.get_sender())
3434
else:
3435
environment.log_error(
2956
- 'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer
2957
- + 'please use one of "smtp" or "sendmail".\n'
3436
+ 'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer +
3437
+ 'please use one of "smtp" or "sendmail".\n'
3438
)
3439
sys.exit(1)
3440
return mailer
@@ -2963,14 +3443,18 @@ def choose_mailer(config, environment):
3443
KNOWN_ENVIRONMENTS = {
3444
'generic': GenericEnvironmentMixin,
3445
'gitolite': GitoliteEnvironmentMixin,
3446
+ 'stash': StashEnvironmentMixin,
3447
+ 'gerrit': GerritEnvironmentMixin,
3448
}
3449
3450
2969
-def choose_environment(config, osenv=None, env=None, recipients=None):
3451
+def choose_environment(config, osenv=None, env=None, recipients=None,
3452
+ hook_info=None):
3453
if not osenv:
3454
osenv = os.environ
3455
3456
environment_mixins = [
3457
+ ConfigRefFilterEnvironmentMixin,
3458
ProjectdescEnvironmentMixin,
3459
ConfigMaxlinesEnvironmentMixin,
3460
ComputeFQDNEnvironmentMixin,
@@ -2992,7 +3476,15 @@ def choose_environment(config, osenv=None, env=None, recipients=None):
3476
else:
3477
env = 'generic'
3478
2995
- environment_mixins.append(KNOWN_ENVIRONMENTS[env])
3479
+ environment_mixins.insert(0, KNOWN_ENVIRONMENTS[env])
3480
+
3481
+ if env == 'stash':
3482
+ environment_kw['user'] = hook_info['stash_user']
3483
+ environment_kw['repo'] = hook_info['stash_repo']
3484
+ elif env == 'gerrit':
3485
+ environment_kw['project'] = hook_info['project']
3486
+ environment_kw['submitter'] = hook_info['submitter']
3487
+ environment_kw['update_method'] = hook_info['update_method']
3488
3489
if recipients:
3490
environment_mixins.insert(0, StaticRecipientsEnvironmentMixin)
@@ -3011,6 +3503,116 @@ def choose_environment(config, osenv=None, env=None, recipients=None):
3503
return environment_klass(**environment_kw)
3504
3505
3506
+def get_version():
3507
+ oldcwd = os.getcwd()
3508
+ try:
3509
+ try:
3510
+ os.chdir(os.path.dirname(os.path.realpath(__file__)))
3511
+ git_version = read_git_output(['describe', '--tags', 'HEAD'])
3512
+ if git_version == __version__:
3513
+ return git_version
3514
+ else:
3515
+ return '%s (%s)' % (__version__, git_version)
3516
+ except:
3517
+ pass
3518
+ finally:
3519
+ os.chdir(oldcwd)
3520
+ return __version__
3521
+
3522
+
3523
+def compute_gerrit_options(options, args, required_gerrit_options):
3524
+ if None in required_gerrit_options:
3525
+ raise SystemExit("Error: Specify all of --oldrev, --newrev, --refname, "
3526
+ "and --project; or none of them.")
3527
+
3528
+ if options.environment not in (None, 'gerrit'):
3529
+ raise SystemExit("Non-gerrit environments incompatible with --oldrev, "
3530
+ "--newrev, --refname, and --project")
3531
+ options.environment = 'gerrit'
3532
+
3533
+ if args:
3534
+ raise SystemExit("Error: Positional parameters not allowed with "
3535
+ "--oldrev, --newrev, and --refname.")
3536
+
3537
+ # Gerrit oddly omits 'refs/heads/' in the refname when calling
3538
+ # ref-updated hook; put it back.
3539
+ git_dir = get_git_dir()
3540
+ if (not os.path.exists(os.path.join(git_dir, options.refname)) and
3541
+ os.path.exists(os.path.join(git_dir, 'refs', 'heads',
3542
+ options.refname))):
3543
+ options.refname = 'refs/heads/' + options.refname
3544
+
3545
+ # Convert each string option unicode for Python3.
3546
+ if PYTHON3:
3547
+ opts = ['environment', 'recipients', 'oldrev', 'newrev', 'refname',
3548
+ 'project', 'submitter', 'stash-user', 'stash-repo']
3549
+ for opt in opts:
3550
+ if not hasattr(options, opt):
3551
+ continue
3552
+ obj = getattr(options, opt)
3553
+ if obj:
3554
+ enc = obj.encode('utf-8', 'surrogateescape')
3555
+ dec = enc.decode('utf-8', 'replace')
3556
+ setattr(options, opt, dec)
3557
+
3558
+ # New revisions can appear in a gerrit repository either due to someone
3559
+ # pushing directly (in which case options.submitter will be set), or they
3560
+ # can press "Submit this patchset" in the web UI for some CR (in which
3561
+ # case options.submitter will not be set and gerrit will not have provided
3562
+ # us the information about who pressed the button).
3563
+ #
3564
+ # Note for the nit-picky: I'm lumping in REST API calls and the ssh
3565
+ # gerrit review command in with "Submit this patchset" button, since they
3566
+ # have the same effect.
3567
+ if options.submitter:
3568
+ update_method = 'pushed'
3569
+ # The submitter argument is almost an RFC 2822 email address; change it
3570
+ # from 'User Name (email@domain)' to 'User Name <email@domain>' so it is
3571
+ options.submitter = options.submitter.replace('(', '<').replace(')', '>')
3572
+ else:
3573
+ update_method = 'submitted'
3574
+ # Gerrit knew who submitted this patchset, but threw that information
3575
+ # away when it invoked this hook. However, *IF* Gerrit created a
3576
+ # merge to bring the patchset in (project 'Submit Type' is either
3577
+ # "Always Merge", or is "Merge if Necessary" and happens to be
3578
+ # necessary for this particular CR), then it will have the committer
3579
+ # of that merge be 'Gerrit Code Review' and the author will be the
3580
+ # person who requested the submission of the CR. Since this is fairly
3581
+ # likely for most gerrit installations (of a reasonable size), it's
3582
+ # worth the extra effort to try to determine the actual submitter.
3583
+ rev_info = read_git_lines(['log', '--no-walk', '--merges',
3584
+ '--format=%cN%n%aN <%aE>', options.newrev])
3585
+ if rev_info and rev_info[0] == 'Gerrit Code Review':
3586
+ options.submitter = rev_info[1]
3587
+
3588
+ # We pass back refname, oldrev, newrev as args because then the
3589
+ # gerrit ref-updated hook is much like the git update hook
3590
+ return (options,
3591
+ [options.refname, options.oldrev, options.newrev],
3592
+ {'project': options.project, 'submitter': options.submitter,
3593
+ 'update_method': update_method})
3594
+
3595
+
3596
+def check_hook_specific_args(options, args):
3597
+ # First check for stash arguments
3598
+ if (options.stash_user is None) != (options.stash_repo is None):
3599
+ raise SystemExit("Error: Specify both of --stash-user and "
3600
+ "--stash-repo or neither.")
3601
+ if options.stash_user:
3602
+ options.environment = 'stash'
3603
+ return options, args, {'stash_user': options.stash_user,
3604
+ 'stash_repo': options.stash_repo}
3605
+
3606
+ # Finally, check for gerrit specific arguments
3607
+ required_gerrit_options = (options.oldrev, options.newrev, options.refname,
3608
+ options.project)
3609
+ if required_gerrit_options != (None,) * 4:
3610
+ return compute_gerrit_options(options, args, required_gerrit_options)
3611
+
3612
+ # No special options in use, just return what we started with
3613
+ return options, args, {}
3614
+
3615
+
3616
def main(args):
3617
parser = optparse.OptionParser(
3618
description=__doc__,
@@ -3019,7 +3621,7 @@ def main(args):
3621
3622
parser.add_option(
3623
'--environment', '--env', action='store', type='choice',
3022
- choices=['generic', 'gitolite'], default=None,
3624
+ choices=list(KNOWN_ENVIRONMENTS.keys()), default=None,
3625
help=(
3626
'Choose type of environment is in use. Default is taken from '
3627
'multimailhook.environment if set; otherwise "generic".'
@@ -3048,8 +3650,58 @@ def main(args):
3650
'detection in this mode.'
3651
),
3652
)
3653
+ parser.add_option(
3654
+ '-c', metavar="<name>=<value>", action='append',
3655
+ help=(
3656
+ 'Pass a configuration parameter through to git. The value given '
3657
+ 'will override values from configuration files. See the -c option '
3658
+ 'of git(1) for more details. (Only works with git >= 1.7.3)'
3659
+ ),
3660
+ )
3661
+ parser.add_option(
3662
+ '--version', '-v', action='store_true', default=False,
3663
+ help=(
3664
+ "Display git-multimail's version"
3665
+ ),
3666
+ )
3667
+ # The following options permit this script to be run as a gerrit
3668
+ # ref-updated hook. See e.g.
3669
+ # code.google.com/p/gerrit/source/browse/Documentation/config-hooks.txt
3670
+ # We suppress help for these items, since these are specific to gerrit,
3671
+ # and we don't want users directly using them any way other than how the
3672
+ # gerrit ref-updated hook is called.
3673
+ parser.add_option('--oldrev', action='store', help=optparse.SUPPRESS_HELP)
3674
+ parser.add_option('--newrev', action='store', help=optparse.SUPPRESS_HELP)
3675
+ parser.add_option('--refname', action='store', help=optparse.SUPPRESS_HELP)
3676
+ parser.add_option('--project', action='store', help=optparse.SUPPRESS_HELP)
3677
+ parser.add_option('--submitter', action='store', help=optparse.SUPPRESS_HELP)
3678
+
3679
+ # The following allow this to be run as a stash asynchronous post-receive
3680
+ # hook (almost identical to a git post-receive hook but triggered also for
3681
+ # merges of pull requests from the UI). We suppress help for these items,
3682
+ # since these are specific to stash.
3683
+ parser.add_option('--stash-user', action='store', help=optparse.SUPPRESS_HELP)
3684
+ parser.add_option('--stash-repo', action='store', help=optparse.SUPPRESS_HELP)
3685
3686
(options, args) = parser.parse_args(args)
3687
+ (options, args, hook_info) = check_hook_specific_args(options, args)
3688
+
3689
+ if options.version:
3690
+ sys.stdout.write('git-multimail version ' + get_version() + '\n')
3691
+ return
3692
+
3693
+ if options.c:
3694
+ parameters = os.environ.get('GIT_CONFIG_PARAMETERS', '')
3695
+ if parameters:
3696
+ parameters += ' '
3697
+ # git expects GIT_CONFIG_PARAMETERS to be of the form
3698
+ # "'name1=value1' 'name2=value2' 'name3=value3'"
3699
+ # including everything inside the double quotes (but not the double
3700
+ # quotes themselves). Spacing is critical. Also, if a value contains
3701
+ # a literal single quote that quote must be represented using the
3702
+ # four character sequence: '\''
3703
+ parameters += ' '.join("'" + x.replace("'", "'\\''") + "'" for x in options.c)
3704
+ os.environ['GIT_CONFIG_PARAMETERS'] = parameters
3705
3706
config = Config('multimailhook')
3707
@@ -3058,6 +3710,7 @@ def main(args):
3710
config, osenv=os.environ,
3711
env=options.environment,
3712
recipients=options.recipients,
3713
+ hook_info=hook_info,
3714
)
3715
3716
if options.show_env:
@@ -3080,9 +3733,20 @@ def main(args):
3733
run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send)
3734
else:
3735
run_as_post_receive_hook(environment, mailer)
3083
- except ConfigurationException, e:
3084
- sys.exit(str(e))
3085
-
3736
+ except ConfigurationException:
3737
+ sys.exit(sys.exc_info()[1])
3738
+ except Exception:
3739
+ t, e, tb = sys.exc_info()
3740
+ import traceback
3741
+ sys.stdout.write('\n')
3742
+ sys.stdout.write('Exception \'' + t.__name__ +
3743
+ '\' raised. Please report this as a bug to\n')
3744
+ sys.stdout.write('https://github.com/git-multimail/git-multimail/issues\n')
3745
+ sys.stdout.write('with the information below:\n\n')
3746
+ sys.stdout.write('git-multimail version ' + get_version() + '\n')
3747
+ sys.stdout.write('Python version ' + sys.version + '\n')
3748
+ traceback.print_exc(file=sys.stdout)
3749
+ sys.exit(1)
3750
3751
if __name__ == '__main__':
3752
main(sys.argv[1:])
contrib/hooks/multimail/migrate-mailhook-config
+1
-1
@@ -1,4 +1,4 @@
1
-#! /usr/bin/env python2
1
+#! /usr/bin/env python
2
3
"""Migrate a post-receive-email configuration to be usable with git_multimail.py.
4
contrib/hooks/multimail/post-receive.example
+4
-4
@@ -1,4 +1,4 @@
1
-#! /usr/bin/env python2
1
+#! /usr/bin/env python
2
3
"""Example post-receive hook based on git-multimail.
4
@@ -42,7 +42,6 @@ import os
42
43
import git_multimail
44
45
-
45
# It is possible to modify the output templates here; e.g.:
46
47
#git_multimail.FOOTER_TEMPLATE = """\
@@ -61,8 +60,9 @@ config = git_multimail.Config('multimailhook')
60
try:
61
environment = git_multimail.GenericEnvironment(config=config)
62
#environment = git_multimail.GitoliteEnvironment(config=config)
64
-except git_multimail.ConfigurationException, e:
65
- sys.exit(str(e))
63
+except git_multimail.ConfigurationException:
64
+ sys.stderr.write('*** %s\n' % sys.exc_info()[1])
65
+ sys.exit(1)
66
67
68
# Choose the method of sending emails based on the git config: