{"thread":{"id":"40437","subject":"[PATCH] git-multimail version 1.2 RC1","startedAt":"2015-09-27T16:24:44Z","lastAt":"2015-09-27T16:24:44Z","messageCount":1,"participants":["Matthieu Moy"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"270796","messageId":"1443371084-3591-1-git-send-email-Matthieu.Moy@imag.fr","threadId":"40437","inReplyTo":null,"subject":"[PATCH] git-multimail version 1.2 RC1","fromName":"Matthieu Moy","fromEmail":"matthieu.moy@imag.fr","sentAt":"2015-09-27T16:24:44Z","receivedAt":"2015-09-27T16:24:44Z","isPatch":true,"sender":{"key":"git@matthieu-moy.fr","avatar":"https://avatars.githubusercontent.com/u/14709?v=4"},"body":"The changes are described in CHANGES.\n\nContributions-by: Job Snijders <job@instituut.net>\nContributions-by: Richard Hansen <rhansen@rhansen.org>\nContributions-by: Elijah Newren <newren@gmail.com>\nContributions-by: Michael Haggerty <mhagger@alum.mit.edu>\nContributions-by: Paul Sokolovsky <paul.sokolovsky@linaro.org>\nContributions-by: Vadim Zeitlin <vadim@zeitlins.org>\nContributions-by: Edward d'Auvergne <edward@nmr-relax.com>\nContributions-by: Elijah Newren <newren@palantir.com>\nSigned-off-by: Matthieu Moy <Matthieu.Moy@imag.fr>\n---\n\nThis should not go into Git 2.6, but I'll release git-multimail 1.2\nfinal rather soon, so this patch can be queued and merged after 2.6 is\nreleased.\n\n contrib/hooks/multimail/CHANGES                 |  49 ++\n contrib/hooks/multimail/CONTRIBUTING.rst        |  30 +\n contrib/hooks/multimail/README                  | 255 ++++---\n contrib/hooks/multimail/README.Git              |   4 +-\n contrib/hooks/multimail/doc/gerrit.rst          |  56 ++\n contrib/hooks/multimail/doc/gitolite.rst        | 109 +++\n contrib/hooks/multimail/git_multimail.py        | 932 ++++++++++++++++++++----\n contrib/hooks/multimail/migrate-mailhook-config |   2 +-\n contrib/hooks/multimail/post-receive.example    |   8 +-\n 9 files changed, 1221 insertions(+), 224 deletions(-)\n create mode 100644 contrib/hooks/multimail/CONTRIBUTING.rst\n create mode 100644 contrib/hooks/multimail/doc/gerrit.rst\n create mode 100644 contrib/hooks/multimail/doc/gitolite.rst\n\ndiff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES\nindex 6bb1230..1c41e04 100644\n--- a/contrib/hooks/multimail/CHANGES\n+++ b/contrib/hooks/multimail/CHANGES\n@@ -1,3 +1,52 @@\n+Release 1.2.0 RC1\n+=================\n+\n+This release has been well tested, but contains possibly controversial\n+changes visible to the user. It is an alpha release in the sense that\n+the user-interface may still change before the final release depending\n+on the user feedback.\n+\n+* It is now possible to exclude some refs (e.g. exclude some branches\n+  or tags). See refFilterDoSendRegex, refFilterDontSendRegex,\n+  refFilterInclusionRegex and refFilterExclusionRegex.\n+\n+* New commitEmailFormat option which can be set to \"html\" to generate\n+  simple colorized diffs using HTML for the commit emails.\n+\n+* git-multimail can now be ran as a Gerrit ref-updated hook, or from\n+  Atlassian Stash.\n+\n+* The From: field is now more customizeable. It can be set\n+  independently for refchange emails and commit emails (see\n+  fromCommit, fromRefChange). The special values pusher and author can\n+  be used in these configuration variable.\n+\n+* A new command-line option, --version, was added. The version is also\n+  available in the X-Git-Multimail-Version header of sent emails.\n+\n+* Set X-Git-NotificationType header to differentiate the various types\n+  of notifications. Current values are: diff, ref_changed_plus_diff,\n+  ref_changed.\n+\n+* Preliminary support for Python 3. The testsuite passes with Python 3,\n+  but it has not received as much testing as the Python 2 version yet.\n+\n+* Several encoding-related fixes. UTF-8 characters work in more\n+  situations (but non-ascii characters in email address are still not\n+  supported).\n+\n+* The testsuite and its documentation has been greatly improved.\n+\n+Plus all the bugfixes from version 1.1.1.\n+\n+This version has been tested with Python 2.4 and 2.6 to 3.5, and Git\n+v1.7.10-406-gdc801e7, git-1.8.2.3 and 2.6.0-rc0. Git versions prior to\n+v1.7.10-406-gdc801e7 probably work, but cannot run the testsuite\n+properly.\n+\n+Changes since 1.2 Alpha 1: testsuite improvements and fixes, better\n+HTML formatting.\n+\n Release 1.1.1 (bugfix-only release)\n ===================================\n \ndiff --git a/contrib/hooks/multimail/CONTRIBUTING.rst b/contrib/hooks/multimail/CONTRIBUTING.rst\nnew file mode 100644\nindex 0000000..09efdb0\n--- /dev/null\n+++ b/contrib/hooks/multimail/CONTRIBUTING.rst\n@@ -0,0 +1,30 @@\n+git-multimail is an open-source project, built by volunteers. We would\n+welcome your help!\n+\n+The current maintainers are Michael Haggerty <mhagger@alum.mit.edu>\n+and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.\n+\n+Please note that although a copy of git-multimail is distributed in\n+the \"contrib\" section of the main Git project, development takes place\n+in a separate git-multimail repository on GitHub:\n+\n+    https://github.com/git-multimail/git-multimail\n+\n+Whenever enough changes to git-multimail have accumulated, a new\n+code-drop of git-multimail will be submitted for inclusion in the Git\n+project.\n+\n+We use the GitHub issue tracker to keep track of bugs and feature\n+requests, and we use GitHub pull requests to exchange patches (though,\n+if you prefer, you can send patches via the Git mailing list with CC\n+to the maintainers). Please sign off your patches as per the `Git\n+project practice\n+<https://github.com/git/git/blob/master/Documentation/SubmittingPatches#L234>`__.\n+\n+General discussion of git-multimail can take place on the main Git\n+mailing list,\n+\n+    git@vger.kernel.org\n+\n+Please CC emails regarding git-multimail to the maintainers so that we\n+don't overlook them.\ndiff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README\nindex e552c90..fb8223b 100644\n--- a/contrib/hooks/multimail/README\n+++ b/contrib/hooks/multimail/README\n@@ -1,5 +1,5 @@\n-git-multimail Version 1.1.1\n-===========================\n+git-multimail (version 1.2 RC1)\n+===============================\n \n .. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master\n     :target: https://travis-ci.org/git-multimail/git-multimail\n@@ -53,11 +53,13 @@ By default, for each push received by the repository, git-multimail:\n      + [git] 07/08: Merge branch 'mm/api-credentials-doc'\n      + [git] 08/08: Git 1.7.11-rc2\n \n-   Each commit appears in exactly one commit email, the first time\n-   that it is pushed to the repository.  If a commit is later merged\n-   into another branch, then a one-line summary of the commit is\n-   included in the reference change email (as usual), but no\n-   additional commit email is generated.\n+   By default, each commit appears in exactly one commit email, the\n+   first time that it is pushed to the repository.  If a commit is later\n+   merged into another branch, then a one-line summary of the commit\n+   is included in the reference change email (as usual), but no\n+   additional commit email is generated. See\n+   `multimailhook.refFilter(Inclusion|Exclusion|DoSend|DontSend)Regex`\n+   below to configure which branches and tags are watched by the hook.\n \n    By default, reference change emails have their \"Reply-To\" field set\n    to the person who pushed the change, and commit emails have their\n@@ -73,21 +75,8 @@ Requirements\n ------------\n \n * Python 2.x, version 2.4 or later.  No non-standard Python modules\n-  are required.  git-multimail does *not* currently work with Python\n-  3.x.\n-\n-  The example scripts invoke Python using the following shebang line\n-  (following PEP 394 [1]_)::\n-\n-      #! /usr/bin/env python2\n-\n-  If your system's Python2 interpreter is not in your PATH or is not\n-  called ``python2``, you can change the lines accordingly.  Or you can\n-  invoke the Python interpreter explicitly, for example via a tiny\n-  shell script like::\n-\n-      #! /bin/sh\n-      /usr/local/bin/python /path/to/git_multimail.py \"$@\"\n+  are required.  git-multimail has preliminary support for Python 3\n+  (but it has been better tested with Python 2).\n \n * The ``git`` command must be in your PATH.  git-multimail is known to\n   work with Git versions back to 1.7.1.  (Earlier versions have not\n@@ -146,7 +135,9 @@ following ``git config`` settings:\n \n multimailhook.environment\n \n-    This describes the general environment of the repository.\n+    This describes the general environment of the repository. In most\n+    cases, you do not need to specify a value for this variable:\n+    `git-multimail` will autodetect which environment to use.\n     Currently supported values:\n \n     * generic\n@@ -161,18 +152,54 @@ multimailhook.environment\n       optionally read from gitolite.conf (see multimailhook.from).\n \n       For more information about gitolite and git-multimail, read\n-      doc/gitolite.rst\n+      `<doc/gitolite.rst>`__\n+\n+    * stash\n+\n+      **Warning:** this mode was provided by a third-party contributor\n+      and never tested by the git-multimail maintainers. It is\n+      provided as-is and may or may not work for you.\n \n-    If neither of these environments is suitable for your setup, then\n-    you can implement a Python class that inherits from Environment\n-    and instantiate it via a script that looks like the example\n+      This value is automatically assumed when the stash-specific\n+      flags (``--stash-user`` and ``--stash-repo``) are specified on\n+      the command line. When this environment is active, the username\n+      and repo come from these two command line flags, which must be\n+      specified.\n+\n+    * gerrit\n+\n+      Environment to use when ``git-multimail`` is ran as a\n+      ``ref-updated`` Gerrit hook.\n+\n+      This value is used when the gerrit-specific command line flags\n+      (``--oldrev``, ``--newrev``, ``--refname``, ``--project``) for\n+      gerrit's ref-updated hook are present. When this environment is\n+      active, the username of the pusher is taken from the\n+      ``--submitter`` argument if that command line option is passed,\n+      otherwise 'Gerrit' is used. The repository name is taken from\n+      the ``--project`` option on the command line, which must be passed.\n+\n+      For more information about gerrit and git-multimail, read\n+      `<doc/gerrit.rst>`__\n+\n+    If none of these environments is suitable for your setup, then you\n+    can implement a Python class that inherits from Environment and\n+    instantiate it via a script that looks like the example\n     post-receive script.\n \n     The environment value can be specified on the command line using\n-    the --environment option.  If it is not specified on the command\n-    line or by multimailhook.environment, then it defaults to\n-    ``gitolite`` if the environment contains variables $GL_USER and\n-    $GL_REPO; otherwise ``generic``.\n+    the ``--environment`` option. If it is not specified on the\n+    command line or by ``multimailhook.environment``, the value is\n+    guessed as follows:\n+\n+    * If stash-specific (respectively gerrit-specific) command flags\n+      are present on the command-line, then ``stash`` (respectively\n+      ``gerrit``) is used.\n+\n+    * If the environment variables $GL_USER and $GL_REPO are set, then\n+      ``gitolite`` is used.\n+\n+    * If none of the above apply, then ``generic`` is used.\n \n multimailhook.repoName\n \n@@ -196,8 +223,8 @@ multimailhook.refchangeList\n     reference changes should be sent, as RFC 2822 email addresses\n     separated by commas.  This configuration option can be\n     multivalued.  The default is the value in\n-    multimailhook.mailingList.  Set this value to the empty string to\n-    prevent reference change emails from being sent even if\n+    multimailhook.mailingList.  Set this value to \"none\" (or the empty\n+    string) to prevent reference change emails from being sent even if\n     multimailhook.mailingList is set.\n \n multimailhook.announceList\n@@ -206,9 +233,9 @@ multimailhook.announceList\n     tags should be sent, as RFC 2822 email addresses separated by\n     commas.  This configuration option can be multivalued.  The\n     default is the value in multimailhook.refchangeList or\n-    multimailhook.mailingList.  Set this value to the empty string to\n-    prevent annotated tag announcement emails from being sent even if\n-    one of the other values is set.\n+    multimailhook.mailingList.  Set this value to \"none\" (or the empty\n+    string) to prevent annotated tag announcement emails from being sent\n+    even if one of the other values is set.\n \n multimailhook.commitList\n \n@@ -216,7 +243,7 @@ multimailhook.commitList\n     commits should be sent, as RFC 2822 email addresses separated by\n     commas.  This configuration option can be multivalued.  The\n     default is the value in multimailhook.mailingList.  Set this value\n-    to the empty string to prevent notification emails about\n+    to \"none\" (or the empty string) to prevent notification emails about\n     individual commits from being sent even if\n     multimailhook.mailingList is set.\n \n@@ -230,6 +257,20 @@ multimailhook.announceShortlog\n     not so straightforward, then the shortlog might be confusing\n     rather than useful.  Default is false.\n \n+multimailhook.commitEmailFormat\n+\n+    The format of email messages for the individual commits, can be \"text\" or\n+    \"html\". In the latter case, the emails will include diffs using colorized\n+    HTML instead of plain text used by default. Note that this  currently the\n+    ref change emails are always sent in plain text.\n+\n+    Note that when using \"html\", the formatting is done by parsing the\n+    output of ``git log`` with ``-p``. When using\n+    ``multimailhook.commitLogOpts`` to specify a ``--format`` for\n+    ``git log``, one may get false positive (e.g. lines in the body of\n+    the message starting with ``+++`` or ``---`` colored in red or\n+    green).\n+\n multimailhook.refchangeShowGraph\n \n     If this option is set to true, then summary emails about reference\n@@ -305,7 +346,7 @@ multimailhook.mailer\n \n       * multimailhook.smtpEncryption\n \n-        Set the security type. Allowed values: none, ssl.\n+        Set the security type. Allowed values: none, ssl, tls.\n         Default=none.\n \n       * multimailhook.smtpServerDebugLevel\n@@ -313,9 +354,26 @@ multimailhook.mailer\n         Integer number. Set to greater than 0 to activate debugging.\n \n multimailhook.from\n+multimailhook.fromCommit\n+multimailhook.fromRefchange\n+\n+    If set, use this value in the From: field of generated emails.\n+    ``fromCommit`` is used for commit emails, ``fromRefchange`` is\n+    used for refchange emails, and ``from`` is used as fall-back in\n+    all cases.\n+\n+    The value for these variables can be either:\n+\n+    - An email address, which will be used directly.\n+\n+    - The value ``pusher``, in which case the pusher's address (if\n+      available) will be used.\n+\n+    - The value ``author`` (meaningful only for replyToCommit), in which\n+      case the commit author's address will be used.\n \n-    If set, use this value in the From: field of generated emails.  If\n-    unset, the value of the From: header is determined as follows:\n+    If config values are unset, the value of the From: header is\n+    determined as follows:\n \n     1. (gitolite environment only) Parse gitolite.conf, looking for a\n        block of comments that looks like this::\n@@ -425,6 +483,15 @@ multimailhook.commitLogOpts\n     --stat -p --cc``.  Shell quoting is allowed; see\n     multimailhook.logOpts for details.\n \n+multimailhook.dateSubstitute\n+\n+    String to use as a substitute for ``Date:`` in the output of ``git\n+    log`` while formatting commit messages. This is usefull to avoid\n+    emitting a line that can be interpreted by mailers as the start of\n+    a cited message (Zimbra webmail in particular). Defaults to\n+    ``CommitDate: ``. Set to an empty string or ``none`` to deactivate\n+    the behavior.\n+\n multimailhook.emailDomain\n \n     Domain name appended to the username of the person doing the push\n@@ -440,21 +507,13 @@ multimailhook.replyToRefchange\n     Addresses to use in the Reply-To: field for commit emails\n     (replyToCommit) and refchange emails (replyToRefchange).\n     multimailhook.replyTo is used as default when replyToCommit or\n-    replyToRefchange is not set.  The value for these variables can be\n-    either:\n-\n-    - An email address, which will be used directly.\n-\n-    - The value `pusher`, in which case the pusher's address (if\n-      available) will be used.  This is the default for refchange\n-      emails.\n-\n-    - The value `author` (meaningful only for replyToCommit), in which\n-      case the commit author's address will be used.  This is the\n-      default for commit emails.\n+    replyToRefchange is not set. The shortcuts ``pusher`` and\n+    ``author`` are allowed with the same semantics as for\n+    ``multimailhook.from``. In addition, the value ``none`` can be\n+    used to omit the ``Reply-To:`` field.\n \n-    - The value `none`, in which case the Reply-To: field will be\n-      omitted.\n+    The default is ``pusher`` for refchange emails, and ``author`` for\n+    commit emails.\n \n multimailhook.quiet\n \n@@ -478,6 +537,63 @@ multimailhook.combineWhenSingleCommit\n     single email.\n     Default: true\n \n+multimailhook.refFilterInclusionRegex\n+multimailhook.refFilterExclusionRegex\n+multimailhook.refFilterDoSendRegex\n+multimailhook.refFilterDontSendRegex\n+\n+    **Warning:** these options are experimental. They should work, but\n+    the user-interface is not stable yet (in particular, the option\n+    names may change). If you want to participate in stabilizing the\n+    feature, please contact the maintainers and/or send pull-requests.\n+\n+    Regular expressions that can be used to limit refs for which email\n+    updates will be sent.  It is an error to specify both an inclusion\n+    and an exclusion regex.  If a ``refFilterInclusionRegex`` is\n+    specified, emails will only be sent for refs which match this\n+    regex.  If a ``refFilterExclusionRegex`` regex is specified,\n+    emails will be sent for all refs except those that match this\n+    regex (or that match a predefined regex specific to the\n+    environment, such as \"^refs/notes\" for most environments and\n+    \"^refs/notes|^refs/changes\" for the gerrit environment).\n+\n+    The expressions are matched against the complete refname, and is\n+    considered to match if any substring matches. For example, to\n+    filter-out all tags, set ``refFilterExclusionRegex`` to\n+    ``^refs/tags/`` (note the leading ``^`` but no trailing ``$``). If\n+    you set ``refFilterExclusionRegex`` to ``master``, then any ref\n+    containing ``master`` will be excluded (the ``master`` branch, but\n+    also ``refs/tags/master`` or ``refs/heads/foo-master-bar``).\n+\n+    ``refFilterDoSendRegex`` and ``refFilterDontSendRegex`` are\n+    analogous to ``refFilterInclusionRegex`` and\n+    ``refFilterExclusionRegex`` with one difference: with\n+    ``refFilterDoSendRegex`` and ``refFilterDontSendRegex``, commits\n+    introduced by one excluded ref will not be considered as new when\n+    they reach an included ref. Typically, if you add a branch ``foo``\n+    to  ``refFilterDontSendRegex``, push commits to this branch, and\n+    later merge branch ``foo`` into ``master``, then the notification\n+    email for ``master`` will contain a commit email only for the\n+    merge commit. If you include ``foo`` in\n+    ``refFilterExclusionRegex``, then at the time of merge, you will\n+    receive one commit email per commit in the branch.\n+\n+    These variables can be multi-valued, like::\n+\n+      [multimailhook]\n+              refFilterExclusionRegex = ^refs/tags/\n+              refFilterExclusionRegex = ^refs/heads/master$\n+\n+    You can also provide a whitespace-separated list like::\n+\n+      [multimailhook]\n+              refFilterExclusionRegex = ^refs/tags/ ^refs/heads/master$\n+\n+    Both examples exclude tags and the master branch, and are\n+    equivalent to::\n+\n+      [multimailhook]\n+              refFilterExclusionRegex = ^refs/tags/|^refs/heads/master$\n \n Email filtering aids\n --------------------\n@@ -547,35 +663,8 @@ consider sharing them with the community!\n Getting involved\n ----------------\n \n-git-multimail is an open-source project, built by volunteers. We would\n-welcome your help!\n-\n-The current maintainers are Michael Haggerty <mhagger@alum.mit.edu>\n-and Matthieu Moy <matthieu.moy@grenoble-inp.fr>.\n-\n-Please note that although a copy of git-multimail is distributed in\n-the \"contrib\" section of the main Git project, development takes place\n-in a separate git-multimail repository on GitHub:\n-\n-    https://github.com/git-multimail/git-multimail\n-\n-Whenever enough changes to git-multimail have accumulated, a new\n-code-drop of git-multimail will be submitted for inclusion in the Git\n-project.\n-\n-We use the GitHub issue tracker to keep track of bugs and feature\n-requests, and we use GitHub pull requests to exchange patches (though,\n-if you prefer, you can send patches via the Git mailing list with CC\n-to the maintainers). Please sign off your patches as per the Git\n-project practice.\n-\n-General discussion of git-multimail can take place on the main Git\n-mailing list,\n-\n-    git@vger.kernel.org\n-\n-Please CC emails regarding git-multimail to the maintainers so that we\n-don't overlook them.\n+Please, read `<CONTRIBUTING.rst>`__ for instructions on how to\n+contribute to git-multimail.\n \n \n Footnotes\ndiff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git\nindex f5d59a8..7259850 100644\n--- a/contrib/hooks/multimail/README.Git\n+++ b/contrib/hooks/multimail/README.Git\n@@ -6,10 +6,10 @@ website:\n     https://github.com/git-multimail/git-multimail\n \n The version in this directory was obtained from the upstream project\n-on July 03 2015 and consists of the \"git-multimail\" subdirectory from\n+on September 27 2015 and consists of the \"git-multimail\" subdirectory from\n revision\n \n-    6d6c9eb62a054143322cfaecde3949189c065b46 refs/tags/1.1.1\n+    3ad58519d17aafeae421abf06e66c826fa142956 refs/tags/1.2.0-rc1\n \n Please see the README file in this directory for information about how\n to report bugs or contribute to git-multimail.\ndiff --git a/contrib/hooks/multimail/doc/gerrit.rst b/contrib/hooks/multimail/doc/gerrit.rst\nnew file mode 100644\nindex 0000000..8011d05\n--- /dev/null\n+++ b/contrib/hooks/multimail/doc/gerrit.rst\n@@ -0,0 +1,56 @@\n+Setting up git-multimail on Gerrit\n+==================================\n+\n+Gerrit has its own email-sending system, but you may prefer using\n+``git-multimail`` instead. It supports Gerrit natively as a Gerrit\n+``ref-updated`` hook (Warning: `Gerrit hooks\n+<https://gerrit-review.googlesource.com/Documentation/config-hooks.html>`__\n+are distinct from Git hooks). Setting up ``git-multimail`` on a Gerrit\n+installation can be done following the instructions below.\n+\n+The explanations show an easy way to set up ``git-multimail``,\n+but leave ``git-multimail`` installed and unconfigured for a while. If\n+you run Gerrit on a production server, it is advised that you\n+execute the step \"Set up the hook\" last to avoid confusing your users\n+in the meantime.\n+\n+Set up the hook\n+---------------\n+\n+Create a directory ``$site_path/hooks/`` if it does not exist (if you\n+don't know what ``$site_path`` is, run ``gerrit.sh status`` and look\n+for a ``GERRIT_SITE`` line). Either copy ``git_multimail.py`` to\n+``$site_path/hooks/ref-updated`` or create a wrapper script like\n+this::\n+\n+  #! /bin/sh\n+  exec /path/to/git_multimail.py \"$@\"\n+\n+In both cases, make sure the file is named exactly\n+``$site_path/hooks/ref-updated`` and is executable.\n+\n+(Alternatively, you may configure the ``[hooks]`` section of\n+gerrit.config)\n+\n+Configuration\n+-------------\n+\n+Log on the gerrit server and edit ``$site_path/git/$project/config``\n+to configure ``git-multimail``.\n+\n+Troubleshooting\n+---------------\n+\n+Warning: this will disable ``git-multimail`` during the debug, and\n+could confuse your users. Don't run on a production server.\n+\n+To debug configuration issues with ``git-multimail``, you can add the\n+``--stdout`` option when calling ``git_multimail.py`` like this::\n+\n+  #!/bin/sh\n+  exec /path/to/git-multimail/git-multimail/git_multimail.py \\\n+    --stdout \"$@\" >> /tmp/log.txt\n+\n+and try pushing from a test repository. You should see the source of\n+the email that would have been sent in the output of ``git push`` in\n+the file ``/tmp/log.txt``.\ndiff --git a/contrib/hooks/multimail/doc/gitolite.rst b/contrib/hooks/multimail/doc/gitolite.rst\nnew file mode 100644\nindex 0000000..00aedd9\n--- /dev/null\n+++ b/contrib/hooks/multimail/doc/gitolite.rst\n@@ -0,0 +1,109 @@\n+Setting up git-multimail on gitolite\n+====================================\n+\n+``git-multimail`` supports gitolite 3 natively.\n+The explanations below show an easy way to set up ``git-multimail``,\n+but leave ``git-multimail`` installed and unconfigured for a while. If\n+you run gitolite on a production server, it is advised that you\n+execute the step \"Set up the hook\" last to avoid confusing your users\n+in the meantime.\n+\n+Set up the hook\n+---------------\n+\n+Log in as your gitolite user.\n+\n+Create a file ``.gitolite/hooks/common/post-receive`` on your gitolite\n+account containing (adapt the path, obviously)::\n+\n+  #!/bin/sh\n+  exec /path/to/git-multimail/git-multimail/git_multimail.py \"$@\"\n+\n+Make sure it's executable (``chmod +x``). Record the hook in\n+gitolite::\n+\n+  gitolite setup\n+\n+Configuration\n+-------------\n+\n+First, you have to allow the admin to set Git configuration variables.\n+\n+As gitolite user, edit the line containing ``GIT_CONFIG_KEYS`` in file\n+``.gitolite.rc``, to make it look like::\n+\n+  GIT_CONFIG_KEYS                 =>  'multimailhook\\..*',\n+\n+You can now log out and return to your normal user.\n+\n+In the ``gitolite-admin`` clone, edit the file ``conf/gitolite.conf``\n+and add::\n+\n+  repo @all\n+      # Not strictly needed as git_multimail.py will chose gitolite if\n+      # $GL_USER is set.\n+      config multimailhook.environment = gitolite\n+      config multimailhook.mailingList = # Where emails should be sent\n+      config multimailhook.from = # From address to use\n+\n+Obviously, you can customize all parameters on a per-repository basis by\n+adding these ``config multimailhook.*`` lines in the section\n+corresponding to a repository or set of repositories.\n+\n+To activate ``git-multimail`` on a per-repository basis, do not set\n+``multimailhook.mailingList`` in the ``@all`` section and set it only\n+for repositories for which you want ``git-multimail``.\n+\n+Alternatively, you can set up the ``From:`` field on a per-user basis\n+by adding a ``BEGIN USER EMAILS``/``END USER EMAILS`` section (see\n+``../README``).\n+\n+Specificities of Gitolite for Configuration\n+-------------------------------------------\n+\n+Empty configuration variables\n+.............................\n+\n+With gitolite, the syntax ``config multimailhook.commitList = \"\"``\n+unsets the variable instead of setting it to an empty string (see\n+`here\n+<http://gitolite.com/gitolite/git-config.html#an-important-warning-about-deleting-a-config-line>`__).\n+As a result, there is no way to set a variable to the empty string.\n+In all most places where an empty value is required, git-multimail\n+now allows to specify special ``\"none\"`` value (case-sensitive) to\n+mean the same.\n+\n+Alternatively, one can use ``\" \"`` (a single space) instead of ``\"\"``.\n+In most cases (in particular ``multimailhook.*List`` variables), this\n+will be equivalent to an empty string.\n+\n+If you have a use-case where ``\"none\"`` is not an acceptable value and\n+you need ``\" \"`` or  ``\"\"`` instead, please report it as a bug to\n+git-multimail.\n+\n+Allowing Regular Expressions in Configuration\n+.............................................\n+\n+gitolite has a mechanism to prevent unsafe configuration variable\n+values, which prevent characters like ``|`` commonly used in regular\n+expressions. If you do not need the safety feature of gitolite and\n+need to use regular expressions in your configuration (e.g. for\n+``multimailhook.refFilter*`` variables), set\n+`UNSAFE_PATT\n+<http://gitolite.com/gitolite/git-config.html#unsafe-patt>`__ to a\n+less restrictive value.\n+\n+Troubleshooting\n+---------------\n+\n+Warning: this will disable ``git-multimail`` during the debug, and\n+could confuse your users. Don't run on a production server.\n+\n+To debug configuration issues with ``git-multimail``, you can add the\n+``--stdout`` option when calling ``git_multimail.py`` like this::\n+\n+  #!/bin/sh\n+  exec /path/to/git-multimail/git-multimail/git_multimail.py --stdout \"$@\"\n+\n+and try pushing from a test repository. You should see the source of\n+the email that would have been sent in the output of ``git push``.\ndiff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py\nindex c06ce7a..bf33f37 100755\n--- a/contrib/hooks/multimail/git_multimail.py\n+++ b/contrib/hooks/multimail/git_multimail.py\n@@ -1,4 +1,6 @@\n-#! /usr/bin/env python2\n+#! /usr/bin/env python\n+\n+__version__ = '1.2.rc1'\n \n # Copyright (c) 2015 Matthieu Moy and others\n # Copyright (c) 2012-2014 Michael Haggerty and others\n@@ -56,8 +58,54 @@ import shlex\n import optparse\n import smtplib\n import time\n+import cgi\n+\n+PYTHON3 = sys.version_info >= (3, 0)\n+\n+if sys.version_info <= (2, 5):\n+    def all(iterable):\n+        for element in iterable:\n+            if not element:\n+                return False\n+            return True\n+\n+\n+def is_ascii(s):\n+    return all(ord(c) < 128 and ord(c) > 0 for c in s)\n+\n+\n+if PYTHON3:\n+    def str_to_bytes(s):\n+        return s.encode(ENCODING)\n+\n+    def bytes_to_str(s):\n+        return s.decode(ENCODING)\n+\n+    unicode = str\n+\n+    def write_str(f, msg):\n+        # Try outputing with the default encoding. If it fails,\n+        # try UTF-8.\n+        try:\n+            f.buffer.write(msg.encode(sys.getdefaultencoding()))\n+        except UnicodeEncodeError:\n+            f.buffer.write(msg.encode(ENCODING))\n+else:\n+    def str_to_bytes(s):\n+        return s\n+\n+    def bytes_to_str(s):\n+        return s\n+\n+    def write_str(f, msg):\n+        f.write(msg)\n+\n+    def next(it):\n+        return it.next()\n+\n \n try:\n+    from email.charset import Charset\n     from email.utils import make_msgid\n     from email.utils import getaddresses\n     from email.utils import formataddr\n@@ -65,6 +113,7 @@ try:\n     from email.header import Header\n except ImportError:\n     # Prior to Python 2.5, the email module used different names:\n+    from email.Charset import Charset\n     from email.Utils import make_msgid\n     from email.Utils import getaddresses\n     from email.Utils import formataddr\n@@ -109,7 +158,7 @@ Date: %(send_date)s\n To: %(recipients)s\n Subject: %(subject)s\n MIME-Version: 1.0\n-Content-Type: text/plain; charset=%(charset)s\n+Content-Type: text/%(contenttype)s; charset=%(charset)s\n Content-Transfer-Encoding: 8bit\n Message-ID: %(msgid)s\n From: %(fromaddr)s\n@@ -120,6 +169,8 @@ X-Git-Refname: %(refname)s\n X-Git-Reftype: %(refname_type)s\n X-Git-Oldrev: %(oldrev)s\n X-Git-Newrev: %(newrev)s\n+X-Git-NotificationType: ref_changed\n+X-Git-Multimail-Version: %(multimail_version)s\n Auto-Submitted: auto-generated\n \"\"\"\n \n@@ -238,7 +289,7 @@ To: %(recipients)s\n Cc: %(cc_recipients)s\n Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s\n MIME-Version: 1.0\n-Content-Type: text/plain; charset=%(charset)s\n+Content-Type: text/%(contenttype)s; charset=%(charset)s\n Content-Transfer-Encoding: 8bit\n From: %(fromaddr)s\n Reply-To: %(reply_to)s\n@@ -249,6 +300,8 @@ X-Git-Repo: %(repo_shortname)s\n X-Git-Refname: %(refname)s\n X-Git-Reftype: %(refname_type)s\n X-Git-Rev: %(rev)s\n+X-Git-NotificationType: diff\n+X-Git-Multimail-Version: %(multimail_version)s\n Auto-Submitted: auto-generated\n \"\"\"\n \n@@ -270,7 +323,7 @@ Date: %(send_date)s\n To: %(recipients)s\n Subject: %(subject)s\n MIME-Version: 1.0\n-Content-Type: text/plain; charset=%(charset)s\n+Content-Type: text/%(contenttype)s; charset=%(charset)s\n Content-Transfer-Encoding: 8bit\n Message-ID: %(msgid)s\n From: %(fromaddr)s\n@@ -282,6 +335,8 @@ X-Git-Reftype: %(refname_type)s\n X-Git-Oldrev: %(oldrev)s\n X-Git-Newrev: %(newrev)s\n X-Git-Rev: %(rev)s\n+X-Git-NotificationType: ref_changed_plus_diff\n+X-Git-Multimail-Version: %(multimail_version)s\n Auto-Submitted: auto-generated\n \"\"\"\n \n@@ -352,12 +407,14 @@ def read_git_output(args, input=None, keepends=False, **kw):\n def read_output(cmd, input=None, keepends=False, **kw):\n     if input:\n         stdin = subprocess.PIPE\n+        input = str_to_bytes(input)\n     else:\n         stdin = None\n     p = subprocess.Popen(\n         cmd, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw\n         )\n     (out, err) = p.communicate(input)\n+    out = bytes_to_str(out)\n     retcode = p.wait()\n     if retcode:\n         raise CommandError(cmd, retcode)\n@@ -418,26 +475,37 @@ def git_log(spec, **kw):\n def header_encode(text, header_name=None):\n     \"\"\"Encode and line-wrap the value of an email header field.\"\"\"\n \n-    try:\n-        if isinstance(text, str):\n-            text = text.decode(ENCODING, 'replace')\n-        return Header(text, header_name=header_name).encode()\n-    except UnicodeEncodeError:\n-        return Header(text, header_name=header_name, charset=CHARSET,\n-                      errors='replace').encode()\n+    # Convert to unicode, if required.\n+    if not isinstance(text, unicode):\n+        text = unicode(text, 'utf-8')\n+\n+    if is_ascii(text):\n+        charset = 'ascii'\n+    else:\n+        charset = 'utf-8'\n+\n+    return Header(text, header_name=header_name, charset=Charset(charset)).encode()\n \n \n def addr_header_encode(text, header_name=None):\n     \"\"\"Encode and line-wrap the value of an email header field containing\n     email addresses.\"\"\"\n \n-    return Header(\n-        ', '.join(\n-            formataddr((header_encode(name), emailaddr))\n-            for name, emailaddr in getaddresses([text])\n-            ),\n-        header_name=header_name\n-        ).encode()\n+    # Convert to unicode, if required.\n+    if not isinstance(text, unicode):\n+        text = unicode(text, 'utf-8')\n+\n+    text = ', '.join(\n+        formataddr((header_encode(name), emailaddr))\n+        for name, emailaddr in getaddresses([text])\n+        )\n+\n+    if is_ascii(text):\n+        charset = 'ascii'\n+    else:\n+        charset = 'utf-8'\n+\n+    return Header(text, header_name=header_name, charset=Charset(charset)).encode()\n \n \n class Config(object):\n@@ -496,7 +564,8 @@ class Config(object):\n                 ['config', '--get-all', '--null', '%s.%s' % (self.section, name)],\n                 env=self.env, keepends=True,\n                 ))\n-        except CommandError, e:\n+        except CommandError:\n+            t, e, traceback = sys.exc_info()\n             if e.retcode == 1:\n                 # \"the section or key is invalid\"; i.e., there is no\n                 # value for the specified key.\n@@ -504,18 +573,6 @@ class Config(object):\n             else:\n                 raise\n \n-    def get_recipients(self, name, default=None):\n-        \"\"\"Read a recipients list from the configuration.\n-\n-        Return the result as a comma-separated list of email\n-        addresses, or default if the option is unset.  If the setting\n-        has multiple values, concatenate them with comma separators.\"\"\"\n-\n-        lines = self.get_all(name, default=None)\n-        if lines is None:\n-            return default\n-        return ', '.join(line.strip() for line in lines)\n-\n     def set(self, name, value):\n         read_git_output(\n             ['config', '%s.%s' % (self.section, name), value],\n@@ -542,7 +599,8 @@ class Config(object):\n                 ['config', '--unset-all', '%s.%s' % (self.section, name)],\n                 env=self.env,\n                 )\n-        except CommandError, e:\n+        except CommandError:\n+            t, e, traceback = sys.exc_info()\n             if e.retcode == 5:\n                 # The name doesn't exist, which is what we wanted anyway...\n                 pass\n@@ -636,7 +694,7 @@ class GitObject(object):\n         if not self.sha1:\n             raise ValueError('Empty commit has no summary')\n \n-        return iter(generate_summaries('--no-walk', self.sha1)).next()\n+        return next(iter(generate_summaries('--no-walk', self.sha1)))\n \n     def __eq__(self, other):\n         return isinstance(other, GitObject) and self.sha1 == other.sha1\n@@ -647,6 +705,10 @@ class GitObject(object):\n     def __nonzero__(self):\n         return bool(self.sha1)\n \n+    def __bool__(self):\n+        \"\"\"Python 2 backward compatibility\"\"\"\n+        return self.__nonzero__()\n+\n     def __str__(self):\n         return self.sha1 or ZEROS\n \n@@ -661,6 +723,12 @@ class Change(object):\n     def __init__(self, environment):\n         self.environment = environment\n         self._values = None\n+        self._contains_html_diff = False\n+\n+    def _contains_diff(self):\n+        # We do contain a diff, should it be rendered in HTML?\n+        if self.environment.commit_email_format == \"html\":\n+            self._contains_html_diff = True\n \n     def _compute_values(self):\n         \"\"\"Return a dictionary {keyword: expansion} for this Change.\n@@ -670,7 +738,12 @@ class Change(object):\n         get_values().  The return value should always be a new\n         dictionary.\"\"\"\n \n-        return self.environment.get_values()\n+        values = self.environment.get_values()\n+        fromaddr = self.environment.get_fromaddr(change=self)\n+        if fromaddr is not None:\n+            values['fromaddr'] = fromaddr\n+        values['multimail_version'] = get_version()\n+        return values\n \n     def get_values(self, **extra_values):\n         \"\"\"Return a dictionary {keyword: expansion} for this Change.\n@@ -713,12 +786,18 @@ class Change(object):\n         skip lines that contain references to unknown variables.\"\"\"\n \n         values = self.get_values(**extra_values)\n+        if self._contains_html_diff:\n+            values['contenttype'] = 'html'\n+        else:\n+            values['contenttype'] = 'plain'\n+\n         for line in template.splitlines():\n-            (name, value) = line.split(':', 1)\n+            (name, value) = line.split(': ', 1)\n \n             try:\n                 value = value % values\n-            except KeyError, e:\n+            except KeyError:\n+                t, e, traceback = sys.exc_info()\n                 if DEBUG:\n                     self.environment.log_warning(\n                         'Warning: unknown variable %r in the following line; line skipped:\\n'\n@@ -764,6 +843,24 @@ class Change(object):\n \n         raise NotImplementedError()\n \n+    def _wrap_for_html(self, lines):\n+        \"\"\"Wrap the lines in HTML <pre> tag when using HTML format.\n+\n+        Escape special HTML characters and add <pre> and </pre> tags around\n+        the given lines if we should be generating HTML as indicated by\n+        self._contains_html_diff being set to true.\n+        \"\"\"\n+        if self._contains_html_diff:\n+            yield \"<pre style='margin:0'>\\n\"\n+\n+            for line in lines:\n+                yield cgi.escape(line)\n+\n+            yield '</pre>\\n'\n+        else:\n+            for line in lines:\n+                yield line\n+\n     def generate_email(self, push, body_filter=None, extra_header_values={}):\n         \"\"\"Generate an email describing this change.\n \n@@ -779,18 +876,76 @@ class Change(object):\n         for line in self.generate_email_header(**extra_header_values):\n             yield line\n         yield '\\n'\n-        for line in self.generate_email_intro():\n+        for line in self._wrap_for_html(self.generate_email_intro()):\n             yield line\n \n         body = self.generate_email_body(push)\n         if body_filter is not None:\n             body = body_filter(body)\n+\n+        diff_started = False\n+        if self._contains_html_diff:\n+            # \"white-space: pre\" is the default, but we need to\n+            # specify it again in case the message is viewed in a\n+            # webmail which wraps it in an element setting white-space\n+            # to something else (Zimbra does this and sets\n+            # white-space: pre-line).\n+            yield '<pre style=\"white-space: pre; background: #F8F8F8\">'\n         for line in body:\n+            if self._contains_html_diff:\n+                # This is very, very naive. It would be much better to really\n+                # parse the diff, i.e. look at how many lines do we have in\n+                # the hunk headers instead of blindly highlighting everything\n+                # that looks like it might be part of a diff.\n+                bgcolor = ''\n+                fgcolor = ''\n+                if line.startswith('--- a/'):\n+                    diff_started = True\n+                    bgcolor = 'e0e0ff'\n+                elif line.startswith('diff ') or line.startswith('index '):\n+                    diff_started = True\n+                    fgcolor = '808080'\n+                elif diff_started:\n+                    if line.startswith('+++ '):\n+                        bgcolor = 'e0e0ff'\n+                    elif line.startswith('@@'):\n+                        bgcolor = 'e0e0e0'\n+                    elif line.startswith('+'):\n+                        bgcolor = 'e0ffe0'\n+                    elif line.startswith('-'):\n+                        bgcolor = 'ffe0e0'\n+                elif line.startswith('commit '):\n+                    fgcolor = '808000'\n+                elif line.startswith('    '):\n+                    fgcolor = '404040'\n+\n+                # Chop the trailing LF, we don't want it inside <pre>.\n+                line = cgi.escape(line[:-1])\n+\n+                if bgcolor or fgcolor:\n+                    style = 'display:block; white-space:pre;'\n+                    if bgcolor:\n+                        style += 'background:#' + bgcolor + ';'\n+                    if fgcolor:\n+                        style += 'color:#' + fgcolor + ';'\n+                    # Use a <span style='display:block> to color the\n+                    # whole line. The newline must be inside the span\n+                    # to display properly both in Firefox and in\n+                    # text-based browser.\n+                    line = \"<span style='%s'>%s\\n</span>\" % (style, line)\n+                else:\n+                    line = line + '\\n'\n+\n             yield line\n+        if self._contains_html_diff:\n+            yield '</pre>'\n \n-        for line in self.generate_email_footer():\n+        for line in self._wrap_for_html(self.generate_email_footer()):\n             yield line\n \n+    def get_alt_fromaddr(self):\n+        return None\n+\n \n class Revision(Change):\n     \"\"\"A Change consisting of a single git commit.\"\"\"\n@@ -867,14 +1022,25 @@ class Revision(Change):\n     def generate_email_body(self, push):\n         \"\"\"Show this revision.\"\"\"\n \n-        return read_git_lines(\n-            ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],\n-            keepends=True,\n-            )\n+        for line in read_git_lines(\n+                ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],\n+                keepends=True,\n+                ):\n+            if line.startswith('Date:   ') and self.environment.date_substitute:\n+                yield self.environment.date_substitute + line[len('Date:   '):]\n+            else:\n+                yield line\n \n     def generate_email_footer(self):\n         return self.expand_lines(REVISION_FOOTER_TEMPLATE)\n \n+    def generate_email(self, push, body_filter=None, extra_header_values={}):\n+        self._contains_diff()\n+        return Change.generate_email(self, push, body_filter, extra_header_values)\n+\n+    def get_alt_fromaddr(self):\n+        return self.environment.from_commit\n+\n \n class ReferenceChange(Change):\n     \"\"\"A Change to a Git reference.\n@@ -1096,10 +1262,10 @@ class ReferenceChange(Change):\n             yield '\\n'\n             yield 'Detailed log of new commits:\\n\\n'\n             for line in read_git_lines(\n-                    ['log', '--no-walk']\n-                    + self.logopts\n-                    + new_commits_list\n-                    + ['--'],\n+                    ['log', '--no-walk'] +\n+                    self.logopts +\n+                    new_commits_list +\n+                    ['--'],\n                     keepends=True,\n                     ):\n                 yield line\n@@ -1253,9 +1419,9 @@ class ReferenceChange(Change):\n             yield '\\n'\n             yield 'Summary of changes:\\n'\n             for line in read_git_lines(\n-                    ['diff-tree']\n-                    + self.diffopts\n-                    + ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],\n+                    ['diff-tree'] +\n+                    self.diffopts +\n+                    ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],\n                     keepends=True,\n                     ):\n                 yield line\n@@ -1316,6 +1482,9 @@ class ReferenceChange(Change):\n             )\n         yield '\\n'\n \n+    def get_alt_fromaddr(self):\n+        return self.environment.from_refchange\n+\n \n class BranchChange(ReferenceChange):\n     refname_type = 'branch'\n@@ -1397,9 +1566,9 @@ class BranchChange(ReferenceChange):\n             # commit is a non-merge commit, though it may make sense to\n             # combine if it is a merge as well.\n             if not (\n-                    len(new_commits) == 1\n-                    and len(new_commits[0][1]) == 1\n-                    and new_commits[0][0] in known_added_sha1s\n+                    len(new_commits) == 1 and\n+                    len(new_commits[0][1]) == 1 and\n+                    new_commits[0][0] in known_added_sha1s\n                     ):\n                 return None\n \n@@ -1432,6 +1601,7 @@ class BranchChange(ReferenceChange):\n             values['subject'] = self.expand(COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE, **values)\n \n         self._single_revision = revision\n+        self._contains_diff()\n         self.header_template = COMBINED_HEADER_TEMPLATE\n         self.intro_template = COMBINED_INTRO_TEMPLATE\n         self.footer_template = COMBINED_FOOTER_TEMPLATE\n@@ -1690,17 +1860,18 @@ class SendMailer(Mailer):\n     def send(self, lines, to_addrs):\n         try:\n             p = subprocess.Popen(self.command, stdin=subprocess.PIPE)\n-        except OSError, e:\n+        except OSError:\n             sys.stderr.write(\n-                '*** Cannot execute command: %s\\n' % ' '.join(self.command)\n-                + '*** %s\\n' % str(e)\n-                + '*** Try setting multimailhook.mailer to \"smtp\"\\n'\n+                '*** Cannot execute command: %s\\n' % ' '.join(self.command) +\n+                '*** %s\\n' % sys.exc_info()[1] +\n+                '*** Try setting multimailhook.mailer to \"smtp\"\\n' +\n                 '*** to send emails without using the sendmail command.\\n'\n                 )\n             sys.exit(1)\n         try:\n+            lines = (str_to_bytes(line) for line in lines)\n             p.stdin.writelines(lines)\n-        except Exception, e:\n+        except Exception:\n             sys.stderr.write(\n                 '*** Error while generating commit email\\n'\n                 '***  - mail sending aborted.\\n'\n@@ -1710,7 +1881,7 @@ class SendMailer(Mailer):\n                 p.terminate()\n             except AttributeError:\n                 pass\n-            raise e\n+            raise\n         else:\n             p.stdin.close()\n             retcode = p.wait()\n@@ -1770,11 +1941,11 @@ class SMTPMailer(Mailer):\n                     \"*** Setting debug on for SMTP server connection (%s) ***\\n\"\n                     % self.smtpserverdebuglevel)\n                 self.smtp.set_debuglevel(self.smtpserverdebuglevel)\n-        except Exception, e:\n+        except Exception:\n             sys.stderr.write(\n                 '*** Error establishing SMTP connection to %s ***\\n'\n                 % self.smtpserver)\n-            sys.stderr.write('*** %s\\n' % str(e))\n+            sys.stderr.write('*** %s\\n' % sys.exc_info()[1])\n             sys.exit(1)\n \n     def __del__(self):\n@@ -1784,16 +1955,15 @@ class SMTPMailer(Mailer):\n     def send(self, lines, to_addrs):\n         try:\n             if self.username or self.password:\n-                sys.stderr.write(\"*** Authenticating as %s ***\\n\" % self.username)\n                 self.smtp.login(self.username, self.password)\n             msg = ''.join(lines)\n             # turn comma-separated list into Python list if needed.\n             if isinstance(to_addrs, basestring):\n                 to_addrs = [email for (name, email) in getaddresses([to_addrs])]\n             self.smtp.sendmail(self.envelopesender, to_addrs, msg)\n-        except Exception, e:\n+        except Exception:\n             sys.stderr.write('*** Error sending email ***\\n')\n-            sys.stderr.write('*** %s\\n' % str(e))\n+            sys.stderr.write('*** %s\\n' % sys.exc_info()[1])\n             self.smtp.quit()\n             sys.exit(1)\n \n@@ -1809,9 +1979,10 @@ class OutputMailer(Mailer):\n         self.f = f\n \n     def send(self, lines, to_addrs):\n-        self.f.write(self.SEPARATOR)\n-        self.f.writelines(lines)\n-        self.f.write(self.SEPARATOR)\n+        write_str(self.f, self.SEPARATOR)\n+        for line in lines:\n+            write_str(self.f, line)\n+        write_str(self.f, self.SEPARATOR)\n \n \n def get_git_dir():\n@@ -1877,11 +2048,13 @@ class Environment(object):\n             Return the address to be used as the 'From' email address\n             in the email envelope.\n \n-        get_fromaddr()\n+        get_fromaddr(change=None)\n \n             Return the 'From' email address used in the email 'From:'\n-            headers.  (May be a full RFC 2822 email address like 'Joe\n-            User <user@example.com>'.)\n+            headers.  If the change is known when this function is\n+            called, it is passed in as the 'change' parameter.  (May\n+            be a full RFC 2822 email address like 'Joe User\n+            <user@example.com>'.)\n \n         get_administrator()\n \n@@ -1901,12 +2074,29 @@ class Environment(object):\n             get_reply_to_commit() is used for individual commit\n             emails.\n \n+        get_ref_filter_regex()\n+\n+            Return a tuple -- a compiled regex, and a boolean indicating\n+            whether the regex picks refs to include (if False, the regex\n+            matches on refs to exclude).\n+\n+        get_default_ref_ignore_regex()\n+\n+            Return a regex that should be ignored for both what emails\n+            to send and when computing what commits are considered new\n+            to the repository.  Default is \"^refs/notes/\".\n+\n     They should also define the following attributes:\n \n         announce_show_shortlog (bool)\n \n             True iff announce emails should include a shortlog.\n \n+        commit_email_format (string)\n+\n+            If \"html\", generate commit emails in HTML instead of plain text\n+            used by default.\n+\n         refchange_showgraph (bool)\n \n             True iff refchanges emails should include a detailed graph.\n@@ -1939,6 +2129,11 @@ class Environment(object):\n             commit mail.  The value should be a list of strings\n             representing words to be passed to the command.\n \n+        date_substitute (string)\n+\n+            String to be used in substitution for 'Date:' at start of\n+            line in the output of 'git log'.\n+\n         quiet (bool)\n             On success do not write to stderr\n \n@@ -1950,6 +2145,13 @@ class Environment(object):\n             True if a combined email should be produced when a single\n             new commit is pushed to a branch, False otherwise.\n \n+        from_refchange, from_commit (strings)\n+\n+            Addresses to use for the From: field for refchange emails\n+            and commit emails respectively.  Set from\n+            multimailhook.fromRefchange and multimailhook.fromCommit\n+            by ConfigEnvironmentMixin.\n+\n     \"\"\"\n \n     REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\\.git)$')\n@@ -1957,6 +2159,7 @@ class Environment(object):\n     def __init__(self, osenv=None):\n         self.osenv = osenv or os.environ\n         self.announce_show_shortlog = False\n+        self.commit_email_format = \"text\"\n         self.maxcommitemails = 500\n         self.diffopts = ['--stat', '--summary', '--find-copies-harder']\n         self.graphopts = ['--oneline', '--decorate']\n@@ -1964,6 +2167,7 @@ class Environment(object):\n         self.refchange_showgraph = False\n         self.refchange_showlog = False\n         self.commitlogopts = ['-C', '--stat', '-p', '--cc']\n+        self.date_substitute = 'AuthorDate: '\n         self.quiet = False\n         self.stdout = False\n         self.combine_when_single_commit = True\n@@ -1972,7 +2176,6 @@ class Environment(object):\n             'administrator',\n             'charset',\n             'emailprefix',\n-            'fromaddr',\n             'pusher',\n             'pusher_email',\n             'repo_path',\n@@ -1998,7 +2201,7 @@ class Environment(object):\n     def get_pusher_email(self):\n         return None\n \n-    def get_fromaddr(self):\n+    def get_fromaddr(self, change=None):\n         config = Config('user')\n         fromname = config.get('name', default='')\n         fromemail = config.get('email', default='')\n@@ -2080,6 +2283,15 @@ class Environment(object):\n     def get_reply_to_commit(self, revision):\n         return revision.author\n \n+    def get_default_ref_ignore_regex(self):\n+        # The commit messages of git notes are essentially meaningless\n+        # and \"filenames\" in git notes commits are an implementational\n+        # detail that might surprise users at first.  As such, we\n+        # would need a completely different method for handling emails\n+        # of git notes in order for them to be of benefit for users,\n+        # which we simply do not have right now.\n+        return \"^refs/notes/\"\n+\n     def filter_body(self, lines):\n         \"\"\"Filter the lines intended for an email body.\n \n@@ -2095,19 +2307,19 @@ class Environment(object):\n         \"\"\"Write the string msg on a log file or on stderr.\n \n         Sends the text to stderr by default, override to change the behavior.\"\"\"\n-        sys.stderr.write(msg)\n+        write_str(sys.stderr, msg)\n \n     def log_warning(self, msg):\n         \"\"\"Write the string msg on a log file or on stderr.\n \n         Sends the text to stderr by default, override to change the behavior.\"\"\"\n-        sys.stderr.write(msg)\n+        write_str(sys.stderr, msg)\n \n     def log_error(self, msg):\n         \"\"\"Write the string msg on a log file or on stderr.\n \n         Sends the text to stderr by default, override to change the behavior.\"\"\"\n-        sys.stderr.write(msg)\n+        write_str(sys.stderr, msg)\n \n \n class ConfigEnvironmentMixin(Environment):\n@@ -2128,6 +2340,14 @@ class ConfigEnvironmentMixin(Environment):\n class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):\n     \"\"\"An Environment that reads most of its information from \"git config\".\"\"\"\n \n+    @staticmethod\n+    def forbid_field_values(name, value, forbidden):\n+        for forbidden_val in forbidden:\n+            if value is not None and value.lower() == forbidden:\n+                raise ConfigurationException(\n+                    '\"%s\" is not an allowed setting for %s' % (value, name)\n+                    )\n+\n     def __init__(self, config, **kw):\n         super(ConfigOptionsEnvironmentMixin, self).__init__(\n             config=config, **kw\n@@ -2144,14 +2364,26 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):\n             if val is not None:\n                 setattr(self, var, val)\n \n+        commit_email_format = config.get('commitEmailFormat')\n+        if commit_email_format is not None:\n+            if commit_email_format != \"html\" and commit_email_format != \"text\":\n+                self.log_warning(\n+                    '*** Unknown value for multimailhook.commitEmailFormat: %s\\n' %\n+                    commit_email_format +\n+                    '*** Expected either \"text\" or \"html\".  Ignoring.\\n'\n+                    )\n+            else:\n+                self.commit_email_format = commit_email_format\n+\n         maxcommitemails = config.get('maxcommitemails')\n         if maxcommitemails is not None:\n             try:\n                 self.maxcommitemails = int(maxcommitemails)\n             except ValueError:\n                 self.log_warning(\n-                    '*** Malformed value for multimailhook.maxCommitEmails: %s\\n' % maxcommitemails\n-                    + '*** Expected a number.  Ignoring.\\n'\n+                    '*** Malformed value for multimailhook.maxCommitEmails: %s\\n'\n+                    % maxcommitemails +\n+                    '*** Expected a number.  Ignoring.\\n'\n                     )\n \n         diffopts = config.get('diffopts')\n@@ -2170,32 +2402,44 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):\n         if commitlogopts is not None:\n             self.commitlogopts = shlex.split(commitlogopts)\n \n+        date_substitute = config.get('dateSubstitute')\n+        if date_substitute == 'none':\n+            self.date_substitute = None\n+        elif date_substitute is not None:\n+            self.date_substitute = date_substitute\n+\n         reply_to = config.get('replyTo')\n         self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to)\n-        if (\n-                self.__reply_to_refchange is not None\n-                and self.__reply_to_refchange.lower() == 'author'\n-                ):\n-            raise ConfigurationException(\n-                '\"author\" is not an allowed setting for replyToRefchange'\n-                )\n+        self.forbid_field_values('replyToRefchange',\n+                                 self.__reply_to_refchange,\n+                                 ['author'])\n         self.__reply_to_commit = config.get('replyToCommit', default=reply_to)\n \n+        from_addr = self.config.get('from')\n+        self.from_refchange = config.get('fromRefchange')\n+        self.forbid_field_values('fromRefchange',\n+                                 self.from_refchange,\n+                                 ['author', 'none'])\n+        self.from_commit = config.get('fromCommit')\n+        self.forbid_field_values('fromCommit',\n+                                 self.from_commit,\n+                                 ['none'])\n+\n         combine = config.get_bool('combineWhenSingleCommit')\n         if combine is not None:\n             self.combine_when_single_commit = combine\n \n     def get_administrator(self):\n         return (\n-            self.config.get('administrator')\n-            or self.get_sender()\n-            or super(ConfigOptionsEnvironmentMixin, self).get_administrator()\n+            self.config.get('administrator') or\n+            self.get_sender() or\n+            super(ConfigOptionsEnvironmentMixin, self).get_administrator()\n             )\n \n     def get_repo_shortname(self):\n         return (\n-            self.config.get('reponame')\n-            or super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname()\n+            self.config.get('reponame') or\n+            super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname()\n             )\n \n     def get_emailprefix(self):\n@@ -2212,33 +2456,42 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):\n     def get_sender(self):\n         return self.config.get('envelopesender')\n \n-    def get_fromaddr(self):\n+    def process_addr(self, addr, change):\n+        if addr.lower() == 'author':\n+            if hasattr(change, 'author'):\n+                return change.author\n+            else:\n+                return None\n+        elif addr.lower() == 'pusher':\n+            return self.get_pusher_email()\n+        elif addr.lower() == 'none':\n+            return None\n+        else:\n+            return addr\n+\n+    def get_fromaddr(self, change=None):\n         fromaddr = self.config.get('from')\n+        if change:\n+            alt_fromaddr = change.get_alt_fromaddr()\n+            if alt_fromaddr:\n+                fromaddr = alt_fromaddr\n+        if fromaddr:\n+            fromaddr = self.process_addr(fromaddr, change)\n         if fromaddr:\n             return fromaddr\n-        return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr()\n+        return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr(change)\n \n     def get_reply_to_refchange(self, refchange):\n         if self.__reply_to_refchange is None:\n             return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_refchange(refchange)\n-        elif self.__reply_to_refchange.lower() == 'pusher':\n-            return self.get_pusher_email()\n-        elif self.__reply_to_refchange.lower() == 'none':\n-            return None\n         else:\n-            return self.__reply_to_refchange\n+            return self.process_addr(self.__reply_to_refchange, refchange)\n \n     def get_reply_to_commit(self, revision):\n         if self.__reply_to_commit is None:\n             return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_commit(revision)\n-        elif self.__reply_to_commit.lower() == 'author':\n-            return revision.author\n-        elif self.__reply_to_commit.lower() == 'pusher':\n-            return self.get_pusher_email()\n-        elif self.__reply_to_commit.lower() == 'none':\n-            return None\n         else:\n-            return self.__reply_to_commit\n+            return self.process_addr(self.__reply_to_commit, revision)\n \n     def get_scancommitforcc(self):\n         return self.config.get('scancommitforcc')\n@@ -2270,12 +2523,14 @@ class FilterLinesEnvironmentMixin(Environment):\n     def filter_body(self, lines):\n         lines = super(FilterLinesEnvironmentMixin, self).filter_body(lines)\n         if self.__strict_utf8:\n-            lines = (line.decode(ENCODING, 'replace') for line in lines)\n+            if not PYTHON3:\n+                lines = (line.decode(ENCODING, 'replace') for line in lines)\n             # Limit the line length in Unicode-space to avoid\n             # splitting characters:\n             if self.__emailmaxlinelength:\n                 lines = limit_linelength(lines, self.__emailmaxlinelength)\n-            lines = (line.encode(ENCODING, 'replace') for line in lines)\n+            if not PYTHON3:\n+                lines = (line.encode(ENCODING, 'replace') for line in lines)\n         elif self.__emailmaxlinelength:\n             lines = limit_linelength(lines, self.__emailmaxlinelength)\n \n@@ -2404,10 +2659,10 @@ class StaticRecipientsEnvironmentMixin(Environment):\n         # actual *contents* of the change being reported, we only\n         # choose based on the *type* of the change.  Therefore we can\n         # compute them once and for all:\n-        if not (refchange_recipients\n-                or announce_recipients\n-                or revision_recipients\n-                or scancommitforcc):\n+        if not (refchange_recipients or\n+                announce_recipients or\n+                revision_recipients or\n+                scancommitforcc):\n             raise ConfigurationException('No email recipients configured!')\n         self.__refchange_recipients = refchange_recipients\n         self.__announce_recipients = announce_recipients\n@@ -2457,13 +2712,104 @@ class ConfigRecipientsEnvironmentMixin(\n         found, raise a ConfigurationException.\"\"\"\n \n         for name in names:\n-            retval = config.get_recipients(name)\n-            if retval is not None:\n-                return retval\n+            lines = config.get_all(name)\n+            if lines is not None:\n+                lines = [line.strip() for line in lines]\n+                # Single \"none\" is a special value equivalen to empty string.\n+                if lines == ['none']:\n+                    lines = ['']\n+                return ', '.join(lines)\n         else:\n             return ''\n \n \n+class StaticRefFilterEnvironmentMixin(Environment):\n+    \"\"\"Set branch filter statically based on constructor parameters.\"\"\"\n+\n+    def __init__(self, ref_filter_incl_regex, ref_filter_excl_regex,\n+                 ref_filter_do_send_regex, ref_filter_dont_send_regex,\n+                 **kw):\n+        super(StaticRefFilterEnvironmentMixin, self).__init__(**kw)\n+\n+        if ref_filter_incl_regex and ref_filter_excl_regex:\n+            raise ConfigurationException(\n+                \"Cannot specify both a ref inclusion and exclusion regex.\")\n+        self.__is_inclusion_filter = bool(ref_filter_incl_regex)\n+        default_exclude = self.get_default_ref_ignore_regex()\n+        if ref_filter_incl_regex:\n+            ref_filter_regex = ref_filter_incl_regex\n+        elif ref_filter_excl_regex:\n+            ref_filter_regex = ref_filter_excl_regex + '|' + default_exclude\n+        else:\n+            ref_filter_regex = default_exclude\n+        try:\n+            self.__compiled_regex = re.compile(ref_filter_regex)\n+        except Exception:\n+            raise ConfigurationException(\n+                'Invalid Ref Filter Regex \"%s\": %s' % (ref_filter_regex, sys.exc_info()[1]))\n+\n+        if ref_filter_do_send_regex and ref_filter_dont_send_regex:\n+            raise ConfigurationException(\n+                \"Cannot specify both a ref doSend and dontSend regex.\")\n+        if ref_filter_do_send_regex or ref_filter_dont_send_regex:\n+            self.__is_do_send_filter = bool(ref_filter_do_send_regex)\n+            if ref_filter_incl_regex:\n+                ref_filter_send_regex = ref_filter_incl_regex\n+            elif ref_filter_excl_regex:\n+                ref_filter_send_regex = ref_filter_excl_regex\n+            else:\n+                ref_filter_send_regex = '.*'\n+                self.__is_do_send_filter = True\n+            try:\n+                self.__send_compiled_regex = re.compile(ref_filter_send_regex)\n+            except Exception:\n+                raise ConfigurationException(\n+                    'Invalid Ref Filter Regex \"%s\": %s' %\n+                    (ref_filter_send_regex, sys.exc_info()[1]))\n+        else:\n+            self.__send_compiled_regex = self.__compiled_regex\n+            self.__is_do_send_filter = self.__is_inclusion_filter\n+\n+    def get_ref_filter_regex(self, send_filter=False):\n+        if send_filter:\n+            return self.__send_compiled_regex, self.__is_do_send_filter\n+        else:\n+            return self.__compiled_regex, self.__is_inclusion_filter\n+\n+\n+class ConfigRefFilterEnvironmentMixin(\n+        ConfigEnvironmentMixin,\n+        StaticRefFilterEnvironmentMixin\n+        ):\n+    \"\"\"Determine branch filtering statically based on config.\"\"\"\n+\n+    def _get_regex(self, config, key):\n+        \"\"\"Get a list of whitespace-separated regex. The refFilter* config\n+        variables are multivalued (hence the use of get_all), and we\n+        allow each entry to be a whitespace-separated list (hence the\n+        split on each line). The whole thing is glued into a single regex.\"\"\"\n+        values = config.get_all(key)\n+        if values is None:\n+            return values\n+        items = []\n+        for line in values:\n+            for i in line.split():\n+                items.append(i)\n+        if items == []:\n+            return None\n+        return '|'.join(items)\n+\n+    def __init__(self, config, **kw):\n+        super(ConfigRefFilterEnvironmentMixin, self).__init__(\n+            config=config,\n+            ref_filter_incl_regex=self._get_regex(config, 'refFilterInclusionRegex'),\n+            ref_filter_excl_regex=self._get_regex(config, 'refFilterExclusionRegex'),\n+            ref_filter_do_send_regex=self._get_regex(config, 'refFilterDoSendRegex'),\n+            ref_filter_dont_send_regex=self._get_regex(config, 'refFilterDontSendRegex'),\n+            **kw\n+            )\n+\n+\n class ProjectdescEnvironmentMixin(Environment):\n     \"\"\"Make a \"projectdesc\" value available for templates.\n \n@@ -2499,6 +2845,7 @@ class GenericEnvironment(\n         ComputeFQDNEnvironmentMixin,\n         ConfigFilterLinesEnvironmentMixin,\n         ConfigRecipientsEnvironmentMixin,\n+        ConfigRefFilterEnvironmentMixin,\n         PusherDomainEnvironmentMixin,\n         ConfigOptionsEnvironmentMixin,\n         GenericEnvironmentMixin,\n@@ -2513,14 +2860,14 @@ class GitoliteEnvironmentMixin(Environment):\n         # repo_shortname (though it's probably not as good as a value\n         # the user might have explicitly put in his config).\n         return (\n-            self.osenv.get('GL_REPO', None)\n-            or super(GitoliteEnvironmentMixin, self).get_repo_shortname()\n+            self.osenv.get('GL_REPO', None) or\n+            super(GitoliteEnvironmentMixin, self).get_repo_shortname()\n             )\n \n     def get_pusher(self):\n         return self.osenv.get('GL_USER', 'unknown user')\n \n-    def get_fromaddr(self):\n+    def get_fromaddr(self, change=None):\n         GL_USER = self.osenv.get('GL_USER')\n         if GL_USER is not None:\n             # Find the path to gitolite.conf.  Note that gitolite v3\n@@ -2557,7 +2904,7 @@ class GitoliteEnvironmentMixin(Environment):\n                             return m.group(1)\n                 finally:\n                     f.close()\n-        return super(GitoliteEnvironmentMixin, self).get_fromaddr()\n+        return super(GitoliteEnvironmentMixin, self).get_fromaddr(change)\n \n \n class IncrementalDateTime(object):\n@@ -2570,8 +2917,9 @@ class IncrementalDateTime(object):\n \n     def __init__(self):\n         self.time = time.time()\n+        self.next = self.__next__  # Python 2 backward compatibility\n \n-    def next(self):\n+    def __next__(self):\n         formatted = formatdate(self.time, True)\n         self.time += 1\n         return formatted\n@@ -2583,6 +2931,7 @@ class GitoliteEnvironment(\n         ComputeFQDNEnvironmentMixin,\n         ConfigFilterLinesEnvironmentMixin,\n         ConfigRecipientsEnvironmentMixin,\n+        ConfigRefFilterEnvironmentMixin,\n         PusherDomainEnvironmentMixin,\n         ConfigOptionsEnvironmentMixin,\n         GitoliteEnvironmentMixin,\n@@ -2591,6 +2940,117 @@ class GitoliteEnvironment(\n     pass\n \n \n+class StashEnvironmentMixin(Environment):\n+    def __init__(self, user=None, repo=None, **kw):\n+        super(StashEnvironmentMixin, self).__init__(**kw)\n+        self.__user = user\n+        self.__repo = repo\n+\n+    def get_repo_shortname(self):\n+        return self.__repo\n+\n+    def get_pusher(self):\n+        return re.match('(.*?)\\s*<', self.__user).group(1)\n+\n+    def get_pusher_email(self):\n+        return self.__user\n+\n+    def get_fromaddr(self, change=None):\n+        return self.__user\n+\n+\n+class StashEnvironment(\n+        StashEnvironmentMixin,\n+        ProjectdescEnvironmentMixin,\n+        ConfigMaxlinesEnvironmentMixin,\n+        ComputeFQDNEnvironmentMixin,\n+        ConfigFilterLinesEnvironmentMixin,\n+        ConfigRecipientsEnvironmentMixin,\n+        ConfigRefFilterEnvironmentMixin,\n+        PusherDomainEnvironmentMixin,\n+        ConfigOptionsEnvironmentMixin,\n+        Environment,\n+        ):\n+    pass\n+\n+\n+class GerritEnvironmentMixin(Environment):\n+    def __init__(self, project=None, submitter=None, update_method=None, **kw):\n+        super(GerritEnvironmentMixin, self).__init__(**kw)\n+        self.__project = project\n+        self.__submitter = submitter\n+        self.__update_method = update_method\n+        \"Make an 'update_method' value available for templates.\"\n+        self.COMPUTED_KEYS += ['update_method']\n+\n+    def get_repo_shortname(self):\n+        return self.__project\n+\n+    def get_pusher(self):\n+        if self.__submitter:\n+            if self.__submitter.find('<') != -1:\n+                # Submitter has a configured email, we transformed\n+                # __submitter into an RFC 2822 string already.\n+                return re.match('(.*?)\\s*<', self.__submitter).group(1)\n+            else:\n+                # Submitter has no configured email, it's just his name.\n+                return self.__submitter\n+        else:\n+            # If we arrive here, this means someone pushed \"Submit\" from\n+            # the gerrit web UI for the CR (or used one of the programmatic\n+            # APIs to do the same, such as gerrit review) and the\n+            # merge/push was done by the Gerrit user.  It was technically\n+            # triggered by someone else, but sadly we have no way of\n+            # determining who that someone else is at this point.\n+            return 'Gerrit'  # 'unknown user'?\n+\n+    def get_pusher_email(self):\n+        if self.__submitter:\n+            return self.__submitter\n+        else:\n+            return super(GerritEnvironmentMixin, self).get_pusher_email()\n+\n+    def get_fromaddr(self, change=None):\n+        if self.__submitter and self.__submitter.find('<') != -1:\n+            return self.__submitter\n+        else:\n+            return super(GerritEnvironmentMixin, self).get_fromaddr(change)\n+\n+    def get_default_ref_ignore_regex(self):\n+        default = super(GerritEnvironmentMixin, self).get_default_ref_ignore_regex()\n+        return default + '|^refs/changes/|^refs/cache-automerge/|^refs/meta/'\n+\n+    def get_revision_recipients(self, revision):\n+        # Merge commits created by Gerrit when users hit \"Submit this patchset\"\n+        # in the Web UI (or do equivalently with REST APIs or the gerrit review\n+        # command) are not something users want to see an individual email for.\n+        # Filter them out.\n+        committer = read_git_output(['log', '--no-walk', '--format=%cN',\n+                                     revision.rev.sha1])\n+        if committer == 'Gerrit Code Review':\n+            return []\n+        else:\n+            return super(GerritEnvironmentMixin, self).get_revision_recipients(revision)\n+\n+    def get_update_method(self):\n+        return self.__update_method\n+\n+\n+class GerritEnvironment(\n+        GerritEnvironmentMixin,\n+        ProjectdescEnvironmentMixin,\n+        ConfigMaxlinesEnvironmentMixin,\n+        ComputeFQDNEnvironmentMixin,\n+        ConfigFilterLinesEnvironmentMixin,\n+        ConfigRecipientsEnvironmentMixin,\n+        ConfigRefFilterEnvironmentMixin,\n+        PusherDomainEnvironmentMixin,\n+        ConfigOptionsEnvironmentMixin,\n+        Environment,\n+        ):\n+    pass\n+\n+\n class Push(object):\n     \"\"\"Represent an entire push (i.e., a group of ReferenceChanges).\n \n@@ -2673,10 +3133,11 @@ class Push(object):\n             ])\n         )\n \n-    def __init__(self, changes, ignore_other_refs=False):\n+    def __init__(self, environment, changes, ignore_other_refs=False):\n         self.changes = sorted(changes, key=self._sort_key)\n         self.__other_ref_sha1s = None\n         self.__cached_commits_spec = {}\n+        self.environment = environment\n \n         if ignore_other_refs:\n             self.__other_ref_sha1s = set()\n@@ -2703,10 +3164,14 @@ class Push(object):\n                 '%(objectname) %(objecttype) %(refname)\\n'\n                 '%(*objectname) %(*objecttype) %(refname)'\n                 )\n+            ref_filter_regex, is_inclusion_filter = \\\n+                self.environment.get_ref_filter_regex()\n             for line in read_git_lines(\n                     ['for-each-ref', '--format=%s' % (fmt,)]):\n                 (sha1, type, name) = line.split(' ', 2)\n-                if sha1 and type == 'commit' and name not in updated_refs:\n+                if (sha1 and type == 'commit' and\n+                        name not in updated_refs and\n+                        include_ref(name, ref_filter_regex, is_inclusion_filter)):\n                     sha1s.add(sha1)\n \n             self.__other_ref_sha1s = sha1s\n@@ -2856,7 +3321,7 @@ class Push(object):\n                 if not change.environment.quiet:\n                     change.environment.log_msg(\n                         'Sending notification emails to: %s\\n' % (change.recipients,))\n-                extra_values = {'send_date': send_date.next()}\n+                extra_values = {'send_date': next(send_date)}\n \n                 rev = change.send_single_combined_email(sha1s)\n                 if rev:\n@@ -2876,9 +3341,9 @@ class Push(object):\n             max_emails = change.environment.maxcommitemails\n             if max_emails and len(sha1s) > max_emails:\n                 change.environment.log_warning(\n-                    '*** Too many new commits (%d), not sending commit emails.\\n' % len(sha1s)\n-                    + '*** Try setting multimailhook.maxCommitEmails to a greater value\\n'\n-                    + '*** Currently, multimailhook.maxCommitEmails=%d\\n' % max_emails\n+                    '*** Too many new commits (%d), not sending commit emails.\\n' % len(sha1s) +\n+                    '*** Try setting multimailhook.maxCommitEmails to a greater value\\n' +\n+                    '*** Currently, multimailhook.maxCommitEmails=%d\\n' % max_emails\n                     )\n                 return\n \n@@ -2889,7 +3354,7 @@ class Push(object):\n                     rev.recipients = rev.cc_recipients\n                     rev.cc_recipients = None\n                 if rev.recipients:\n-                    extra_values = {'send_date': send_date.next()}\n+                    extra_values = {'send_date': next(send_date)}\n                     mailer.send(\n                         rev.generate_email(self, body_filter, extra_values),\n                         rev.recipients,\n@@ -2904,18 +3369,33 @@ class Push(object):\n                 )\n \n \n+def include_ref(refname, ref_filter_regex, is_inclusion_filter):\n+    does_match = bool(ref_filter_regex.search(refname))\n+    if is_inclusion_filter:\n+        return does_match\n+    else:  # exclusion filter -- we include the ref if the regex doesn't match\n+        return not does_match\n+\n+\n def run_as_post_receive_hook(environment, mailer):\n+    ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)\n     changes = []\n     for line in sys.stdin:\n         (oldrev, newrev, refname) = line.strip().split(' ', 2)\n+        if not include_ref(refname, ref_filter_regex, is_inclusion_filter):\n+            continue\n         changes.append(\n             ReferenceChange.create(environment, oldrev, newrev, refname)\n             )\n-    push = Push(changes)\n-    push.send_emails(mailer, body_filter=environment.filter_body)\n+    if changes:\n+        push = Push(environment, changes)\n+        push.send_emails(mailer, body_filter=environment.filter_body)\n \n \n def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):\n+    ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)\n+    if not include_ref(refname, ref_filter_regex, is_inclusion_filter):\n+        return\n     changes = [\n         ReferenceChange.create(\n             environment,\n@@ -2924,7 +3404,7 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=\n             refname,\n             ),\n         ]\n-    push = Push(changes, force_send)\n+    push = Push(environment, changes, force_send)\n     push.send_emails(mailer, body_filter=environment.filter_body)\n \n \n@@ -2953,8 +3433,8 @@ def choose_mailer(config, environment):\n         mailer = SendMailer(command=command, envelopesender=environment.get_sender())\n     else:\n         environment.log_error(\n-            'fatal: multimailhook.mailer is set to an incorrect value: \"%s\"\\n' % mailer\n-            + 'please use one of \"smtp\" or \"sendmail\".\\n'\n+            'fatal: multimailhook.mailer is set to an incorrect value: \"%s\"\\n' % mailer +\n+            'please use one of \"smtp\" or \"sendmail\".\\n'\n             )\n         sys.exit(1)\n     return mailer\n@@ -2963,14 +3443,18 @@ def choose_mailer(config, environment):\n KNOWN_ENVIRONMENTS = {\n     'generic': GenericEnvironmentMixin,\n     'gitolite': GitoliteEnvironmentMixin,\n+    'stash': StashEnvironmentMixin,\n+    'gerrit': GerritEnvironmentMixin,\n     }\n \n \n-def choose_environment(config, osenv=None, env=None, recipients=None):\n+def choose_environment(config, osenv=None, env=None, recipients=None,\n+                       hook_info=None):\n     if not osenv:\n         osenv = os.environ\n \n     environment_mixins = [\n+        ConfigRefFilterEnvironmentMixin,\n         ProjectdescEnvironmentMixin,\n         ConfigMaxlinesEnvironmentMixin,\n         ComputeFQDNEnvironmentMixin,\n@@ -2992,7 +3476,15 @@ def choose_environment(config, osenv=None, env=None, recipients=None):\n         else:\n             env = 'generic'\n \n-    environment_mixins.append(KNOWN_ENVIRONMENTS[env])\n+    environment_mixins.insert(0, KNOWN_ENVIRONMENTS[env])\n+\n+    if env == 'stash':\n+        environment_kw['user'] = hook_info['stash_user']\n+        environment_kw['repo'] = hook_info['stash_repo']\n+    elif env == 'gerrit':\n+        environment_kw['project'] = hook_info['project']\n+        environment_kw['submitter'] = hook_info['submitter']\n+        environment_kw['update_method'] = hook_info['update_method']\n \n     if recipients:\n         environment_mixins.insert(0, StaticRecipientsEnvironmentMixin)\n@@ -3011,6 +3503,116 @@ def choose_environment(config, osenv=None, env=None, recipients=None):\n     return environment_klass(**environment_kw)\n \n \n+def get_version():\n+    oldcwd = os.getcwd()\n+    try:\n+        try:\n+            os.chdir(os.path.dirname(os.path.realpath(__file__)))\n+            git_version = read_git_output(['describe', '--tags', 'HEAD'])\n+            if git_version == __version__:\n+                return git_version\n+            else:\n+                return '%s (%s)' % (__version__, git_version)\n+        except:\n+            pass\n+    finally:\n+        os.chdir(oldcwd)\n+    return __version__\n+\n+\n+def compute_gerrit_options(options, args, required_gerrit_options):\n+    if None in required_gerrit_options:\n+        raise SystemExit(\"Error: Specify all of --oldrev, --newrev, --refname, \"\n+                         \"and --project; or none of them.\")\n+\n+    if options.environment not in (None, 'gerrit'):\n+        raise SystemExit(\"Non-gerrit environments incompatible with --oldrev, \"\n+                         \"--newrev, --refname, and --project\")\n+    options.environment = 'gerrit'\n+\n+    if args:\n+        raise SystemExit(\"Error: Positional parameters not allowed with \"\n+                         \"--oldrev, --newrev, and --refname.\")\n+\n+    # Gerrit oddly omits 'refs/heads/' in the refname when calling\n+    # ref-updated hook; put it back.\n+    git_dir = get_git_dir()\n+    if (not os.path.exists(os.path.join(git_dir, options.refname)) and\n+        os.path.exists(os.path.join(git_dir, 'refs', 'heads',\n+                                    options.refname))):\n+        options.refname = 'refs/heads/' + options.refname\n+\n+    # Convert each string option unicode for Python3.\n+    if PYTHON3:\n+        opts = ['environment', 'recipients', 'oldrev', 'newrev', 'refname',\n+                'project', 'submitter', 'stash-user', 'stash-repo']\n+        for opt in opts:\n+            if not hasattr(options, opt):\n+                continue\n+            obj = getattr(options, opt)\n+            if obj:\n+                enc = obj.encode('utf-8', 'surrogateescape')\n+                dec = enc.decode('utf-8', 'replace')\n+                setattr(options, opt, dec)\n+\n+    # New revisions can appear in a gerrit repository either due to someone\n+    # pushing directly (in which case options.submitter will be set), or they\n+    # can press \"Submit this patchset\" in the web UI for some CR (in which\n+    # case options.submitter will not be set and gerrit will not have provided\n+    # us the information about who pressed the button).\n+    #\n+    # Note for the nit-picky: I'm lumping in REST API calls and the ssh\n+    # gerrit review command in with \"Submit this patchset\" button, since they\n+    # have the same effect.\n+    if options.submitter:\n+        update_method = 'pushed'\n+        # The submitter argument is almost an RFC 2822 email address; change it\n+        # from 'User Name (email@domain)' to 'User Name <email@domain>' so it is\n+        options.submitter = options.submitter.replace('(', '<').replace(')', '>')\n+    else:\n+        update_method = 'submitted'\n+        # Gerrit knew who submitted this patchset, but threw that information\n+        # away when it invoked this hook.  However, *IF* Gerrit created a\n+        # merge to bring the patchset in (project 'Submit Type' is either\n+        # \"Always Merge\", or is \"Merge if Necessary\" and happens to be\n+        # necessary for this particular CR), then it will have the committer\n+        # of that merge be 'Gerrit Code Review' and the author will be the\n+        # person who requested the submission of the CR.  Since this is fairly\n+        # likely for most gerrit installations (of a reasonable size), it's\n+        # worth the extra effort to try to determine the actual submitter.\n+        rev_info = read_git_lines(['log', '--no-walk', '--merges',\n+                                   '--format=%cN%n%aN <%aE>', options.newrev])\n+        if rev_info and rev_info[0] == 'Gerrit Code Review':\n+            options.submitter = rev_info[1]\n+\n+    # We pass back refname, oldrev, newrev as args because then the\n+    # gerrit ref-updated hook is much like the git update hook\n+    return (options,\n+            [options.refname, options.oldrev, options.newrev],\n+            {'project': options.project, 'submitter': options.submitter,\n+             'update_method': update_method})\n+\n+\n+def check_hook_specific_args(options, args):\n+    # First check for stash arguments\n+    if (options.stash_user is None) != (options.stash_repo is None):\n+        raise SystemExit(\"Error: Specify both of --stash-user and \"\n+                         \"--stash-repo or neither.\")\n+    if options.stash_user:\n+        options.environment = 'stash'\n+        return options, args, {'stash_user': options.stash_user,\n+                               'stash_repo': options.stash_repo}\n+\n+    # Finally, check for gerrit specific arguments\n+    required_gerrit_options = (options.oldrev, options.newrev, options.refname,\n+                               options.project)\n+    if required_gerrit_options != (None,) * 4:\n+        return compute_gerrit_options(options, args, required_gerrit_options)\n+\n+    # No special options in use, just return what we started with\n+    return options, args, {}\n+\n+\n def main(args):\n     parser = optparse.OptionParser(\n         description=__doc__,\n@@ -3019,7 +3621,7 @@ def main(args):\n \n     parser.add_option(\n         '--environment', '--env', action='store', type='choice',\n-        choices=['generic', 'gitolite'], default=None,\n+        choices=list(KNOWN_ENVIRONMENTS.keys()), default=None,\n         help=(\n             'Choose type of environment is in use.  Default is taken from '\n             'multimailhook.environment if set; otherwise \"generic\".'\n@@ -3048,8 +3650,58 @@ def main(args):\n             'detection in this mode.'\n             ),\n         )\n+    parser.add_option(\n+        '-c', metavar=\"<name>=<value>\", action='append',\n+        help=(\n+            'Pass a configuration parameter through to git.  The value given '\n+            'will override values from configuration files.  See the -c option '\n+            'of git(1) for more details.  (Only works with git >= 1.7.3)'\n+            ),\n+        )\n+    parser.add_option(\n+        '--version', '-v', action='store_true', default=False,\n+        help=(\n+            \"Display git-multimail's version\"\n+            ),\n+        )\n+    # The following options permit this script to be run as a gerrit\n+    # ref-updated hook.  See e.g.\n+    # code.google.com/p/gerrit/source/browse/Documentation/config-hooks.txt\n+    # We suppress help for these items, since these are specific to gerrit,\n+    # and we don't want users directly using them any way other than how the\n+    # gerrit ref-updated hook is called.\n+    parser.add_option('--oldrev', action='store', help=optparse.SUPPRESS_HELP)\n+    parser.add_option('--newrev', action='store', help=optparse.SUPPRESS_HELP)\n+    parser.add_option('--refname', action='store', help=optparse.SUPPRESS_HELP)\n+    parser.add_option('--project', action='store', help=optparse.SUPPRESS_HELP)\n+    parser.add_option('--submitter', action='store', help=optparse.SUPPRESS_HELP)\n+\n+    # The following allow this to be run as a stash asynchronous post-receive\n+    # hook (almost identical to a git post-receive hook but triggered also for\n+    # merges of pull requests from the UI).  We suppress help for these items,\n+    # since these are specific to stash.\n+    parser.add_option('--stash-user', action='store', help=optparse.SUPPRESS_HELP)\n+    parser.add_option('--stash-repo', action='store', help=optparse.SUPPRESS_HELP)\n \n     (options, args) = parser.parse_args(args)\n+    (options, args, hook_info) = check_hook_specific_args(options, args)\n+\n+    if options.version:\n+        sys.stdout.write('git-multimail version ' + get_version() + '\\n')\n+        return\n+\n+    if options.c:\n+        parameters = os.environ.get('GIT_CONFIG_PARAMETERS', '')\n+        if parameters:\n+            parameters += ' '\n+        # git expects GIT_CONFIG_PARAMETERS to be of the form\n+        #    \"'name1=value1' 'name2=value2' 'name3=value3'\"\n+        # including everything inside the double quotes (but not the double\n+        # quotes themselves).  Spacing is critical.  Also, if a value contains\n+        # a literal single quote that quote must be represented using the\n+        # four character sequence: '\\''\n+        parameters += ' '.join(\"'\" + x.replace(\"'\", \"'\\\\''\") + \"'\" for x in options.c)\n+        os.environ['GIT_CONFIG_PARAMETERS'] = parameters\n \n     config = Config('multimailhook')\n \n@@ -3058,6 +3710,7 @@ def main(args):\n             config, osenv=os.environ,\n             env=options.environment,\n             recipients=options.recipients,\n+            hook_info=hook_info,\n             )\n \n         if options.show_env:\n@@ -3080,9 +3733,20 @@ def main(args):\n             run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send)\n         else:\n             run_as_post_receive_hook(environment, mailer)\n-    except ConfigurationException, e:\n-        sys.exit(str(e))\n-\n+    except ConfigurationException:\n+        sys.exit(sys.exc_info()[1])\n+    except Exception:\n+        t, e, tb = sys.exc_info()\n+        import traceback\n+        sys.stdout.write('\\n')\n+        sys.stdout.write('Exception \\'' + t.__name__ +\n+                         '\\' raised. Please report this as a bug to\\n')\n+        sys.stdout.write('https://github.com/git-multimail/git-multimail/issues\\n')\n+        sys.stdout.write('with the information below:\\n\\n')\n+        sys.stdout.write('git-multimail version ' + get_version() + '\\n')\n+        sys.stdout.write('Python version ' + sys.version + '\\n')\n+        traceback.print_exc(file=sys.stdout)\n+        sys.exit(1)\n \n if __name__ == '__main__':\n     main(sys.argv[1:])\ndiff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config\nindex d0e9b39..992657b 100755\n--- a/contrib/hooks/multimail/migrate-mailhook-config\n+++ b/contrib/hooks/multimail/migrate-mailhook-config\n@@ -1,4 +1,4 @@\n-#! /usr/bin/env python2\n+#! /usr/bin/env python\n \n \"\"\"Migrate a post-receive-email configuration to be usable with git_multimail.py.\n \ndiff --git a/contrib/hooks/multimail/post-receive.example b/contrib/hooks/multimail/post-receive.example\nindex 43f7b6b..9975df7 100755\n--- a/contrib/hooks/multimail/post-receive.example\n+++ b/contrib/hooks/multimail/post-receive.example\n@@ -1,4 +1,4 @@\n-#! /usr/bin/env python2\n+#! /usr/bin/env python\n \n \"\"\"Example post-receive hook based on git-multimail.\n \n@@ -42,7 +42,6 @@ import os\n \n import git_multimail\n \n-\n # It is possible to modify the output templates here; e.g.:\n \n #git_multimail.FOOTER_TEMPLATE = \"\"\"\\\n@@ -61,8 +60,9 @@ config = git_multimail.Config('multimailhook')\n try:\n     environment = git_multimail.GenericEnvironment(config=config)\n     #environment = git_multimail.GitoliteEnvironment(config=config)\n-except git_multimail.ConfigurationException, e:\n-    sys.exit(str(e))\n+except git_multimail.ConfigurationException:\n+    sys.stderr.write('*** %s\\n' % sys.exc_info()[1])\n+    sys.exit(1)\n \n \n # Choose the method of sending emails based on the git config:\n-- \n2.5.0.402.g8854c44\n"}]}