git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH] multimail: stop shipping a copy
@ 2021-06-10  8:29 Johannes Schindelin via GitGitGadget
  2021-06-10 10:04 ` Ævar Arnfjörð Bjarmason
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: Johannes Schindelin via GitGitGadget @ 2021-06-10  8:29 UTC (permalink / raw)
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin

From: Johannes Schindelin <johannes.schindelin@gmx.de>

The multimail project is developed independently and has its own project
page. Traditionally, we shipped a copy in contrib/.

However, such a copy is prone to become stale, and users are much better
served to be directed to the actual project instead.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
    Stop shipping a (potentially stale) copy of multimail
    
    The project is maintained elsewhere.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-977%2Fdscho%2Fstub-out-contrib-multimail-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-977/dscho/stub-out-contrib-multimail-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/977

 contrib/hooks/multimail/CHANGES               |  285 --
 contrib/hooks/multimail/CONTRIBUTING.rst      |   60 -
 contrib/hooks/multimail/README.Git            |   12 +-
 .../README.migrate-from-post-receive-email    |  145 -
 contrib/hooks/multimail/README.rst            |  774 ---
 .../multimail/doc/customizing-emails.rst      |   56 -
 contrib/hooks/multimail/doc/gerrit.rst        |   56 -
 contrib/hooks/multimail/doc/gitolite.rst      |  118 -
 .../hooks/multimail/doc/troubleshooting.rst   |   78 -
 contrib/hooks/multimail/git_multimail.py      | 4346 -----------------
 .../hooks/multimail/migrate-mailhook-config   |  274 --
 contrib/hooks/multimail/post-receive.example  |  101 -
 12 files changed, 2 insertions(+), 6303 deletions(-)
 delete mode 100644 contrib/hooks/multimail/CHANGES
 delete mode 100644 contrib/hooks/multimail/CONTRIBUTING.rst
 delete mode 100644 contrib/hooks/multimail/README.migrate-from-post-receive-email
 delete mode 100644 contrib/hooks/multimail/README.rst
 delete mode 100644 contrib/hooks/multimail/doc/customizing-emails.rst
 delete mode 100644 contrib/hooks/multimail/doc/gerrit.rst
 delete mode 100644 contrib/hooks/multimail/doc/gitolite.rst
 delete mode 100644 contrib/hooks/multimail/doc/troubleshooting.rst
 delete mode 100755 contrib/hooks/multimail/git_multimail.py
 delete mode 100755 contrib/hooks/multimail/migrate-mailhook-config
 delete mode 100755 contrib/hooks/multimail/post-receive.example

diff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES
deleted file mode 100644
index 35791fd02c21..000000000000
--- a/contrib/hooks/multimail/CHANGES
+++ /dev/null
@@ -1,285 +0,0 @@
-Release 1.5.0
-=============
-
-Backward-incompatible change
-----------------------------
-
-The name of classes for environment was misnamed as `*Environement`.
-It is now `*Environment`.
-
-New features
-------------
-
-* A Thread-Index header is now added to each email sent (except for
-  combined emails where it would not make sense), so that MS Outlook
-  properly groups messages by threads even though they have a
-  different subject line. Unfortunately, even adding this header the
-  threading still seems to be unreliable, but it is unclear whether
-  this is an issue on our side or on MS Outlook's side (see discussion
-  here: https://github.com/git-multimail/git-multimail/pull/194).
-
-* A new variable multimailhook.ExcludeMergeRevisions was added to send
-  notification emails only for non-merge commits.
-
-* For gitolite environment, it is now possible to specify the mail map
-  in a separate file in addition to gitolite.conf, using the variable
-  multimailhook.MailaddressMap.
-
-Internal changes
-----------------
-
-* The testsuite now uses GIT_PRINT_SHA1_ELLIPSIS where needed for
-  compatibility with recent Git versions. Only tests are affected.
-
-* We don't try to install pyflakes in the continuous integration job
-  for old Python versions where it's no longer available.
-
-* Stop using the deprecated cgi.escape in Python 3.
-
-* New flake8 warnings have been fixed.
-
-* Python 3.6 is now tested against on Travis-CI.
-
-* A bunch of lgtm.com warnings have been fixed.
-
-Bug fixes
----------
-
-* SMTPMailer logs in only once now. It used to re-login for each email
-  sent which triggered errors for some SMTP servers.
-
-* migrate-mailhook-config was broken by internal refactoring, it
-  should now work again.
-
-This version was tested with Python 2.6 to 3.7. It was tested with Git
-1.7.10.406.gdc801, 2.15.1 and 2.20.1.98.gecbdaf0.
-
-Release 1.4.0
-=============
-
-New features to troubleshoot a git-multimail installation
----------------------------------------------------------
-
-* One can now perform a basic check of git-multimail's setup by
-  running the hook with the environment variable
-  GIT_MULTIMAIL_CHECK_SETUP set to a non-empty string. See
-  doc/troubleshooting.rst for details.
-
-* A new log files system was added. See the multimailhook.logFile,
-  multimailhook.errorLogFile and multimailhook.debugLogFile variables.
-
-* git_multimail.py can now be made more verbose using
-  multimailhook.verbose.
-
-* A new option --check-ref-filter is now available to help debugging
-  the refFilter* options.
-
-Formatting emails
------------------
-
-* Formatting of emails was made slightly more compact, to reduce the
-  odds of having long subject lines truncated or wrapped in short list
-  of commits.
-
-* multimailhook.emailPrefix may now use the '%(repo_shortname)s'
-  placeholder for the repository's short name.
-
-* A new option multimailhook.subjectMaxLength is available to truncate
-  overly long subject lines.
-
-Bug fixes and minor changes
----------------------------
-
-* Options refFilterDoSendRegex and refFilterDontSendRegex were
-  essentially broken. They should work now.
-
-* The behavior when both refFilter{Do,Dont}SendRegex and
-  refFilter{Exclusion,Inclusion}Regex are set have been slightly
-  changed. Exclusion/Inclusion is now strictly stronger than
-  DoSend/DontSend.
-
-* The management of precedence when a setting can be computed in
-  multiple ways has been considerably refactored and modified.
-  multimailhook.from and multimailhook.reponame now have precedence
-  over the environment-specific settings ($GL_REPO/$GL_USER for
-  gitolite, --stash-user/repo for Stash, --submitter/--project for
-  Gerrit).
-
-* The coverage of the testsuite has been considerably improved. All
-  configuration variables now appear at least once in the testsuite.
-
-This version was tested with Python 2.6 to 3.5. It also mostly works
-with Python 2.4, but there is one known breakage in the testsuite
-related to non-ascii characters. It was tested with Git
-1.7.10.406.gdc801, 1.8.5.6, 2.1.4, and 2.10.0.rc0.1.g07c9292.
-
-Release 1.3.1 (bugfix-only release)
-===================================
-
-* Generate links to commits in combined emails (it was done only for
-  commit emails in 1.3.0).
-
-* Fix broken links on PyPi.
-
-Release 1.3.0
-=============
-
-* New options multimailhook.htmlInIntro and multimailhook.htmlInFooter
-  now allow using HTML in the introduction and footer of emails (e.g.
-  for a more pleasant formatting or to insert a link to the commit on
-  a web interface).
-
-* A new option multimailhook.commitBrowseURL gives a simpler (and less
-  flexible) way to add a link to a web interface for commit emails
-  than multimailhook.htmlInIntro and multimailhook.htmlInFooter.
-
-* A new public function config.add_config_parameters was added to
-  allow custom hooks to set specific Git configuration variables
-  without modifying the configuration files. See an example in
-  post-receive.example.
-
-* Error handling for SMTP has been improved (we used to print Python
-  backtraces for legitimate errors).
-
-* The SMTP mailer can now check TLS certificates when the newly added
-  configuration variable multimailhook.smtpCACerts.
-
-* Python 3 portability has been improved.
-
-* The documentation's formatting has been improved.
-
-* The testsuite has been improved (we now use pyflakes to check for
-  errors in the code).
-
-This version has been tested with Python 2.4 and 2.6 to 3.5, and Git
-v1.7.10-406-gdc801e7, 2.1.4 and 2.8.1.339.g3ad15fd.
-
-No change since 1.3 RC1.
-
-Release 1.2.0
-=============
-
-* It is now possible to exclude some refs (e.g. exclude some branches
-  or tags). See refFilterDoSendRegex, refFilterDontSendRegex,
-  refFilterInclusionRegex and refFilterExclusionRegex.
-
-* New commitEmailFormat option which can be set to "html" to generate
-  simple colorized diffs using HTML for the commit emails.
-
-* git-multimail can now be ran as a Gerrit ref-updated hook, or from
-  Atlassian BitBucket Server (formerly known as Atlassian Stash).
-
-* The From: field is now more customizeable. It can be set
-  independently for refchange emails and commit emails (see
-  fromCommit, fromRefChange). The special values pusher and author can
-  be used in these configuration variable.
-
-* A new command-line option, --version, was added. The version is also
-  available in the X-Git-Multimail-Version header of sent emails.
-
-* Set X-Git-NotificationType header to differentiate the various types
-  of notifications. Current values are: diff, ref_changed_plus_diff,
-  ref_changed.
-
-* Preliminary support for Python 3. The testsuite passes with Python 3,
-  but it has not received as much testing as the Python 2 version yet.
-
-* Several encoding-related fixes. UTF-8 characters work in more
-  situations (but non-ascii characters in email address are still not
-  supported).
-
-* The testsuite and its documentation has been greatly improved.
-
-Plus all the bugfixes from version 1.1.1.
-
-This version has been tested with Python 2.4 and 2.6 to 3.5, and Git
-v1.7.10-406-gdc801e7, git-1.8.2.3 and 2.6.0. Git versions prior to
-v1.7.10-406-gdc801e7 probably work, but cannot run the testsuite
-properly.
-
-Release 1.1.1 (bugfix-only release)
-===================================
-
-* The SMTP mailer was not working with Python 2.4.
-
-Release 1.1.0
-=============
-
-* When a single commit is pushed, omit the reference changed email.
-  Set multimailhook.combineWhenSingleCommit to false to disable this
-  new feature.
-
-* In gitolite environments, the pusher's email address can be used as
-  the From address by creating a specially formatted comment block in
-  gitolite.conf (see multimailhook.from in README).
-
-* Support for SMTP authentication and SSL/TLS encryption was added,
-  see smtpUser, smtpPass, smtpEncryption in README.
-
-* A new option scanCommitForCc was added to allow git-multimail to
-  search the commit message for 'Cc: ...' lines, and add the
-  corresponding emails in Cc.
-
-* If $USER is not set, use the variable $USERNAME. This is needed on
-  Windows platform to recognize the pusher.
-
-* The emailPrefix variable can now be set to an empty string to remove
-  the prefix.
-
-* A short tutorial was added in doc/gitolite.rst to set up
-  git-multimail with gitolite.
-
-* The post-receive file was renamed to post-receive.example. It has
-  always been an example (the standard way to call git-multimail is to
-  call git_multimail.py), but it was unclear to many users.
-
-* A new refchangeShowGraph option was added to make it possible to
-  include both a graph and a log in the summary emails.  The options
-  to control the graph formatting can be set via the new graphOpts
-  option.
-
-* New option --force-send was added to disable new commit detection
-  for update hook. One use-case is to run git_multimail.py after
-  running "git fetch" to send emails about commits that have just been
-  fetched (the detection of new commits was unreliable in this mode).
-
-* The testing infrastructure was considerably improved (continuous
-  integration with travis-ci, automatic check of PEP8 and RST syntax,
-  many improvements to the test scripts).
-
-This version has been tested with Python 2.4 to 2.7, and Git 1.7.1 to
-2.4.
-
-Release 1.0.0
-=============
-
-* Fix encoding of non-ASCII email addresses in email headers.
-
-* Fix backwards-compatibility bugs for older Python 2.x versions.
-
-* Fix a backwards-compatibility bug for Git 1.7.1.
-
-* Add an option commitDiffOpts to customize logs for revisions.
-
-* Pass "-oi" to sendmail by default to prevent premature termination
-  on a line containing only ".".
-
-* Stagger email "Date:" values in an attempt to help mail clients
-  thread the emails in the right order.
-
-* If a mailing list setting is missing, just skip sending the
-  corresponding email (with a warning) instead of failing.
-
-* Add a X-Git-Host header that can be used for email filtering.
-
-* Allow the sender's fully-qualified domain name to be configured.
-
-* Minor documentation improvements.
-
-* Add this CHANGES file.
-
-
-Release 0.9.0
-=============
-
-* Initial release.
diff --git a/contrib/hooks/multimail/CONTRIBUTING.rst b/contrib/hooks/multimail/CONTRIBUTING.rst
deleted file mode 100644
index de20a5428730..000000000000
--- a/contrib/hooks/multimail/CONTRIBUTING.rst
+++ /dev/null
@@ -1,60 +0,0 @@
-Contributing
-============
-
-git-multimail is an open-source project, built by volunteers. We would
-welcome your help!
-
-The current maintainers are `Matthieu Moy <http://matthieu-moy.fr>`__ and
-`Michael Haggerty <https://github.com/mhagger>`__.
-
-Please note that although a copy of git-multimail is distributed in
-the "contrib" section of the main Git project, development takes place
-in a separate `git-multimail repository on GitHub`_.
-
-Whenever enough changes to git-multimail have accumulated, a new
-code-drop of git-multimail will be submitted for inclusion in the Git
-project.
-
-We use the GitHub issue tracker to keep track of bugs and feature
-requests, and we use GitHub pull requests to exchange patches (though,
-if you prefer, you can send patches via the Git mailing list with CC
-to the maintainers). Please sign off your patches as per the `Git
-project practice
-<https://github.com/git/git/blob/master/Documentation/SubmittingPatches#L234>`__.
-
-Please vote for issues you would like to be addressed in priority
-(click "add your reaction" and then the "+1" thumbs-up button on the
-GitHub issue).
-
-General discussion of git-multimail can take place on the main `Git
-mailing list`_.
-
-Please CC emails regarding git-multimail to the maintainers so that we
-don't overlook them.
-
-Help needed: testers/maintainer for specific environments/OS
-------------------------------------------------------------
-
-The current maintainer uses and tests git-multimail on Linux with the
-Generic environment. More testers, or better contributors are needed
-to test git-multimail on other real-life setups:
-
-* Mac OS X, Windows: git-multimail is currently not supported on these
-  platforms. But since we have no external dependencies and try to
-  write code as portable as possible, it is possible that
-  git-multimail already runs there and if not, it is likely that it
-  could be ported easily.
-
-  Patches to improve support for Windows and OS X are welcome.
-  Ideally, there would be a sub-maintainer for each OS who would test
-  at least once before each release (around twice a year).
-
-* Gerrit, Stash, Gitolite environments: although the testsuite
-  contains tests for these environments, a tester/maintainer for each
-  environment would be welcome to test and report failure (or success)
-  on real-life environments periodically (here also, feedback before
-  each release would be highly appreciated).
-
-
-.. _`git-multimail repository on GitHub`: https://github.com/git-multimail/git-multimail
-.. _`Git mailing list`: git@vger.kernel.org
diff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git
index 044444245d09..c427efc7bd8e 100644
--- a/contrib/hooks/multimail/README.Git
+++ b/contrib/hooks/multimail/README.Git
@@ -1,15 +1,7 @@
-This copy of git-multimail is distributed as part of the "contrib"
-section of the Git project as a convenience to Git users.
 git-multimail is developed as an independent project at the following
 website:
 
     https://github.com/git-multimail/git-multimail
 
-The version in this directory was obtained from the upstream project
-on January 07 2019 and consists of the "git-multimail" subdirectory from
-revision
-
-    04e80e6c40be465cc62b6c246f0fcb8fd2cfd454 refs/tags/1.5.0
-
-Please see the README file in this directory for information about how
-to report bugs or contribute to git-multimail.
+Please refer to that project page for information about how to report
+bugs or contribute to git-multimail.
diff --git a/contrib/hooks/multimail/README.migrate-from-post-receive-email b/contrib/hooks/multimail/README.migrate-from-post-receive-email
deleted file mode 100644
index 1e6a976699aa..000000000000
--- a/contrib/hooks/multimail/README.migrate-from-post-receive-email
+++ /dev/null
@@ -1,145 +0,0 @@
-git-multimail is close to, but not exactly, a plug-in replacement for
-the old Git project script contrib/hooks/post-receive-email.  This
-document describes the differences and explains how to configure
-git-multimail to get behavior closest to that of post-receive-email.
-
-If you are in a hurry
-=====================
-
-A script called migrate-mailhook-config is included with
-git-multimail.  If you run this script within a Git repository that is
-configured to use post-receive-email, it will convert the
-configuration settings into the approximate equivalent settings for
-git-multimail.  For more information, run
-
-    migrate-mailhook-config --help
-
-
-Configuration differences
-=========================
-
-* The names of the config options for git-multimail are in namespace
-  "multimailhook.*" instead of "hooks.*".  (Editorial comment:
-  post-receive-email should never have used such a generic top-level
-  namespace.)
-
-* In emails about new annotated tags, post-receive-email includes a
-  shortlog of all changes since the previous annotated tag.  To get
-  this behavior with git-multimail, you need to set
-  multimailhook.announceshortlog to true:
-
-      git config multimailhook.announceshortlog true
-
-* multimailhook.commitlist -- This is a new configuration variable.
-  Recipients listed here will receive a separate email for each new
-  commit.  However, if this variable is *not* set, it defaults to the
-  value of multimailhook.mailinglist.  Therefore, if you *don't* want
-  the members of multimailhook.mailinglist to receive one email per
-  commit, then set this value to the empty string:
-
-      git config multimailhook.commitlist ''
-
-* multimailhook.emailprefix -- If this value is not set, then the
-  subjects of generated emails are prefixed with the short name of the
-  repository enclosed in square brackets; e.g., "[myrepo]".
-  post-receive-email defaults to prefix "[SCM]" if this option is not
-  set.  So if you were using the old default and want to retain it
-  (for example, to avoid having to change your email filters), set
-  this variable explicitly to the old value:
-
-      git config multimailhook.emailprefix "[SCM]"
-
-* The "multimailhook.showrev" configuration option is not supported.
-  Its main use is obsoleted by the one-email-per-commit feature of
-  git-multimail.
-
-
-Other differences
-=================
-
-This section describes other differences in the behavior of
-git-multimail vs. post-receive-email.  For full details, please refer
-to the main README file:
-
-* One email per commit.  For each reference change, the script first
-  outputs one email summarizing the reference change (including
-  one-line summaries of the new commits), then it outputs a separate
-  email for each new commit that was introduced, including patches.
-  These one-email-per-commit emails go to the addresses listed in
-  multimailhook.commitlist.  post-receive-email sends only one email
-  for each *reference* that is changed, no matter how many commits
-  were added to the reference.
-
-* Better algorithm for detecting new commits.  post-receive-email
-  processes one reference change at a time, which causes it to fail to
-  describe new commits that were included in multiple branches.  For
-  example, if a single push adds the "*" commits in the diagram below,
-  then post-receive-email would never include the details of the two
-  commits that are common to "master" and "branch" in its
-  notifications.
-
-      o---o---o---*---*---*    <-- master
-                       \
-                        *---*  <-- branch
-
-  git-multimail analyzes all reference modifications to determine
-  which commits were not present before the change, therefore avoiding
-  that error.
-
-* In reference change emails, git-multimail tells which commits have
-  been added to the reference vs. are entirely new to the repository,
-  and which commits that have been omitted from the reference
-  vs. entirely discarded from the repository.
-
-* The environment in which Git is running can be configured via an
-  "Environment" abstraction.
-
-* Built-in support for Gitolite-managed repositories.
-
-* Instead of using full SHA1 object names in emails, git-multimail
-  mostly uses abbreviated SHA1s, plus one-line log message summaries
-  where appropriate.
-
-* In the schematic diagrams that explain non-fast-forward commits,
-  git-multimail shows the names of the branches involved.
-
-* The emails generated by git-multimail include the name of the Git
-  repository that was modified; this is convenient for recipients who
-  are monitoring multiple repositories.
-
-* git-multimail allows the email "From" addresses to be configured.
-
-* The recipients lists (multimailhook.mailinglist,
-  multimailhook.refchangelist, multimailhook.announcelist, and
-  multimailhook.commitlist) can be comma-separated values and/or
-  multivalued settings in the config file; e.g.,
-
-      [multimailhook]
-              mailinglist = mr.brown@example.com, mr.black@example.com
-              announcelist = Him <him@example.com>
-              announcelist = Jim <jim@example.com>
-              announcelist = pop@example.com
-
-  This might make it easier to maintain short recipients lists without
-  requiring full-fledged mailing list software.
-
-* By default, git-multimail sets email "Reply-To" headers to reply to
-  the pusher (for reference updates) and to the author (for commit
-  notifications).  By default, the pusher's email address is
-  constructed by appending "multimailhook.emaildomain" to the pusher's
-  username.
-
-* The generated emails contain a configurable footer.  By default, it
-  lists the name of the administrator who should be contacted to
-  unsubscribe from notification emails.
-
-* New option multimailhook.emailmaxlinelength to limit the length of
-  lines in the main part of the email body.  The default limit is 500
-  characters.
-
-* New option multimailhook.emailstrictutf8 to ensure that the main
-  part of the email body is valid UTF-8.  Invalid characters are
-  turned into the Unicode replacement character, U+FFFD.  By default
-  this option is turned on.
-
-* Written in Python.  Easier to add new features.
diff --git a/contrib/hooks/multimail/README.rst b/contrib/hooks/multimail/README.rst
deleted file mode 100644
index 7c0fc4a6ef00..000000000000
--- a/contrib/hooks/multimail/README.rst
+++ /dev/null
@@ -1,774 +0,0 @@
-git-multimail version 1.5.0
-===========================
-
-.. image:: https://travis-ci.org/git-multimail/git-multimail.svg?branch=master
-    :target: https://travis-ci.org/git-multimail/git-multimail
-
-git-multimail is a tool for sending notification emails on pushes to a
-Git repository.  It includes a Python module called ``git_multimail.py``,
-which can either be used as a hook script directly or can be imported
-as a Python module into another script.
-
-git-multimail is derived from the Git project's old
-contrib/hooks/post-receive-email, and is mostly compatible with that
-script.  See README.migrate-from-post-receive-email for details about
-the differences and for how to migrate from post-receive-email to
-git-multimail.
-
-git-multimail, like the rest of the Git project, is licensed under
-GPLv2 (see the COPYING file for details).
-
-Please note: although, as a convenience, git-multimail may be
-distributed along with the main Git project, development of
-git-multimail takes place in its own, separate project.  Please, read
-`<CONTRIBUTING.rst>`__ for more information.
-
-
-By default, for each push received by the repository, git-multimail:
-
-1. Outputs one email summarizing each reference that was changed.
-   These "reference change" (called "refchange" below) emails describe
-   the nature of the change (e.g., was the reference created, deleted,
-   fast-forwarded, etc.) and include a one-line summary of each commit
-   that was added to the reference.
-
-2. Outputs one email for each new commit that was introduced by the
-   reference change.  These "commit" emails include a list of the
-   files changed by the commit, followed by the diffs of files
-   modified by the commit.  The commit emails are threaded to the
-   corresponding reference change email via "In-Reply-To".  This style
-   (similar to the "git format-patch" style used on the Git mailing
-   list) makes it easy to scan through the emails, jump to patches
-   that need further attention, and write comments about specific
-   commits.  Commits are handled in reverse topological order (i.e.,
-   parents shown before children).  For example::
-
-     [git] branch master updated
-     + [git] 01/08: doc: fix xref link from api docs to manual pages
-     + [git] 02/08: api-credentials.txt: show the big picture first
-     + [git] 03/08: api-credentials.txt: mention credential.helper explicitly
-     + [git] 04/08: api-credentials.txt: add "see also" section
-     + [git] 05/08: t3510 (cherry-pick-sequence): add missing '&&'
-     + [git] 06/08: Merge branch 'rr/maint-t3510-cascade-fix'
-     + [git] 07/08: Merge branch 'mm/api-credentials-doc'
-     + [git] 08/08: Git 1.7.11-rc2
-
-   By default, each commit appears in exactly one commit email, the
-   first time that it is pushed to the repository.  If a commit is later
-   merged into another branch, then a one-line summary of the commit
-   is included in the reference change email (as usual), but no
-   additional commit email is generated. See
-   `multimailhook.refFilter(Inclusion|Exclusion|DoSend|DontSend)Regex`
-   below to configure which branches and tags are watched by the hook.
-
-   By default, reference change emails have their "Reply-To" field set
-   to the person who pushed the change, and commit emails have their
-   "Reply-To" field set to the author of the commit.
-
-3. Output one "announce" mail for each new annotated tag, including
-   information about the tag and optionally a shortlog describing the
-   changes since the previous tag.  Such emails might be useful if you
-   use annotated tags to mark releases of your project.
-
-
-Requirements
-------------
-
-* Python 2.x, version 2.4 or later.  No non-standard Python modules
-  are required.  git-multimail has preliminary support for Python 3
-  (but it has been better tested with Python 2).
-
-* The ``git`` command must be in your PATH.  git-multimail is known to
-  work with Git versions back to 1.7.1.  (Earlier versions have not
-  been tested; if you do so, please report your results.)
-
-* To send emails using the default configuration, a standard sendmail
-  program must be located at '/usr/sbin/sendmail' or
-  '/usr/lib/sendmail' and must be configured correctly to send emails.
-  If this is not the case, set multimailhook.sendmailCommand, or see
-  the multimailhook.mailer configuration variable below for how to
-  configure git-multimail to send emails via an SMTP server.
-
-* git-multimail is currently tested only on Linux. It may or may not
-  work on other platforms such as Windows and Mac OS. See
-  `<CONTRIBUTING.rst>`__ to improve the situation.
-
-
-Invocation
-----------
-
-``git_multimail.py`` is designed to be used as a ``post-receive`` hook in a
-Git repository (see githooks(5)).  Link or copy it to
-$GIT_DIR/hooks/post-receive within the repository for which email
-notifications are desired.  Usually it should be installed on the
-central repository for a project, to which all commits are eventually
-pushed.
-
-For use on pre-v1.5.1 Git servers, ``git_multimail.py`` can also work as
-an ``update`` hook, taking its arguments on the command line.  To use
-this script in this manner, link or copy it to $GIT_DIR/hooks/update.
-Please note that the script is not completely reliable in this mode
-[1]_.
-
-Alternatively, ``git_multimail.py`` can be imported as a Python module
-into your own Python post-receive script.  This method is a bit more
-work, but allows the behavior of the hook to be customized using
-arbitrary Python code.  For example, you can use a custom environment
-(perhaps inheriting from GenericEnvironment or GitoliteEnvironment) to
-
-* change how the user who did the push is determined
-
-* read users' email addresses from an LDAP server or from a database
-
-* decide which users should be notified about which commits based on
-  the contents of the commits (e.g., for users who want to be notified
-  only about changes affecting particular files or subdirectories)
-
-Or you can change how emails are sent by writing your own Mailer
-class.  The ``post-receive`` script in this directory demonstrates how
-to use ``git_multimail.py`` as a Python module.  (If you make interesting
-changes of this type, please consider sharing them with the
-community.)
-
-
-Troubleshooting/FAQ
--------------------
-
-Please read `<doc/troubleshooting.rst>`__ for frequently asked
-questions and common issues with git-multimail.
-
-
-Configuration
--------------
-
-By default, git-multimail mostly takes its configuration from the
-following ``git config`` settings:
-
-multimailhook.environment
-    This describes the general environment of the repository. In most
-    cases, you do not need to specify a value for this variable:
-    `git-multimail` will autodetect which environment to use.
-    Currently supported values:
-
-    generic
-      the username of the pusher is read from $USER or $USERNAME and
-      the repository name is derived from the repository's path.
-
-    gitolite
-      Environment to use when ``git-multimail`` is ran as a gitolite_
-      hook.
-
-      The username of the pusher is read from $GL_USER, the repository
-      name is read from $GL_REPO, and the From: header value is
-      optionally read from gitolite.conf (see multimailhook.from).
-
-      For more information about gitolite and git-multimail, read
-      `<doc/gitolite.rst>`__
-
-    stash
-      Environment to use when ``git-multimail`` is ran as an Atlassian
-      BitBucket Server (formerly known as Atlassian Stash) hook.
-
-      **Warning:** this mode was provided by a third-party contributor
-      and never tested by the git-multimail maintainers. It is
-      provided as-is and may or may not work for you.
-
-      This value is automatically assumed when the stash-specific
-      flags (``--stash-user`` and ``--stash-repo``) are specified on
-      the command line. When this environment is active, the username
-      and repo come from these two command line flags, which must be
-      specified.
-
-    gerrit
-      Environment to use when ``git-multimail`` is ran as a
-      ``ref-updated`` Gerrit hook.
-
-      This value is used when the gerrit-specific command line flags
-      (``--oldrev``, ``--newrev``, ``--refname``, ``--project``) for
-      gerrit's ref-updated hook are present. When this environment is
-      active, the username of the pusher is taken from the
-      ``--submitter`` argument if that command line option is passed,
-      otherwise 'Gerrit' is used. The repository name is taken from
-      the ``--project`` option on the command line, which must be passed.
-
-      For more information about gerrit and git-multimail, read
-      `<doc/gerrit.rst>`__
-
-    If none of these environments is suitable for your setup, then you
-    can implement a Python class that inherits from Environment and
-    instantiate it via a script that looks like the example
-    post-receive script.
-
-    The environment value can be specified on the command line using
-    the ``--environment`` option. If it is not specified on the
-    command line or by ``multimailhook.environment``, the value is
-    guessed as follows:
-
-    * If stash-specific (respectively gerrit-specific) command flags
-      are present on the command-line, then ``stash`` (respectively
-      ``gerrit``) is used.
-
-    * If the environment variables $GL_USER and $GL_REPO are set, then
-      ``gitolite`` is used.
-
-    * If none of the above apply, then ``generic`` is used.
-
-multimailhook.repoName
-    A short name of this Git repository, to be used in various places
-    in the notification email text.  The default is to use $GL_REPO
-    for gitolite repositories, or otherwise to derive this value from
-    the repository path name.
-
-multimailhook.mailingList
-    The list of email addresses to which notification emails should be
-    sent, as RFC 2822 email addresses separated by commas.  This
-    configuration option can be multivalued.  Leave it unset or set it
-    to the empty string to not send emails by default.  The next few
-    settings can be used to configure specific address lists for
-    specific types of notification email.
-
-multimailhook.refchangeList
-    The list of email addresses to which summary emails about
-    reference changes should be sent, as RFC 2822 email addresses
-    separated by commas.  This configuration option can be
-    multivalued.  The default is the value in
-    multimailhook.mailingList.  Set this value to "none" (or the empty
-    string) to prevent reference change emails from being sent even if
-    multimailhook.mailingList is set.
-
-multimailhook.announceList
-    The list of email addresses to which emails about new annotated
-    tags should be sent, as RFC 2822 email addresses separated by
-    commas.  This configuration option can be multivalued.  The
-    default is the value in multimailhook.refchangeList or
-    multimailhook.mailingList.  Set this value to "none" (or the empty
-    string) to prevent annotated tag announcement emails from being sent
-    even if one of the other values is set.
-
-multimailhook.commitList
-    The list of email addresses to which emails about individual new
-    commits should be sent, as RFC 2822 email addresses separated by
-    commas.  This configuration option can be multivalued.  The
-    default is the value in multimailhook.mailingList.  Set this value
-    to "none" (or the empty string) to prevent notification emails about
-    individual commits from being sent even if
-    multimailhook.mailingList is set.
-
-multimailhook.announceShortlog
-    If this option is set to true, then emails about changes to
-    annotated tags include a shortlog of changes since the previous
-    tag.  This can be useful if the annotated tags represent releases;
-    then the shortlog will be a kind of rough summary of what has
-    happened since the last release.  But if your tagging policy is
-    not so straightforward, then the shortlog might be confusing
-    rather than useful.  Default is false.
-
-multimailhook.commitEmailFormat
-    The format of email messages for the individual commits, can be "text" or
-    "html". In the latter case, the emails will include diffs using colorized
-    HTML instead of plain text used by default. Note that this  currently the
-    ref change emails are always sent in plain text.
-
-    Note that when using "html", the formatting is done by parsing the
-    output of ``git log`` with ``-p``. When using
-    ``multimailhook.commitLogOpts`` to specify a ``--format`` for
-    ``git log``, one may get false positive (e.g. lines in the body of
-    the message starting with ``+++`` or ``---`` colored in red or
-    green).
-
-    By default, all the message is HTML-escaped. See
-    ``multimailhook.htmlInIntro`` to change this behavior.
-
-multimailhook.commitBrowseURL
-    Used to generate a link to an online repository browser in commit
-    emails. This variable must be a string. Format directives like
-    ``%(<variable>)s`` will be expanded the same way as template
-    strings. In particular, ``%(id)s`` will be replaced by the full
-    Git commit identifier (40-chars hexadecimal).
-
-    If the string does not contain any format directive, then
-    ``%(id)s`` will be automatically added to the string. If you don't
-    want ``%(id)s`` to be automatically added, use the empty format
-    directive ``%()s`` anywhere in the string.
-
-    For example, a suitable value for the git-multimail project itself
-    would be
-    ``https://github.com/git-multimail/git-multimail/commit/%(id)s``.
-
-multimailhook.htmlInIntro, multimailhook.htmlInFooter
-    When generating an HTML message, git-multimail escapes any HTML
-    sequence by default. This means that if a template contains HTML
-    like ``<a href="foo">link</a>``, the reader will see the HTML
-    source code and not a proper link.
-
-    Set ``multimailhook.htmlInIntro`` to true to allow writing HTML
-    formatting in introduction templates. Similarly, set
-    ``multimailhook.htmlInFooter`` for HTML in the footer.
-
-    Variables expanded in the template are still escaped. For example,
-    if a repository's path contains a ``<``, it will be rendered as
-    such in the message.
-
-    Read `<doc/customizing-emails.rst>`__ for more details and
-    examples.
-
-multimailhook.refchangeShowGraph
-    If this option is set to true, then summary emails about reference
-    changes will additionally include:
-
-    * a graph of the added commits (if any)
-
-    * a graph of the discarded commits (if any)
-
-    The log is generated by running ``git log --graph`` with the options
-    specified in graphOpts.  The default is false.
-
-multimailhook.refchangeShowLog
-    If this option is set to true, then summary emails about reference
-    changes will include a detailed log of the added commits in
-    addition to the one line summary.  The log is generated by running
-    ``git log`` with the options specified in multimailhook.logOpts.
-    Default is false.
-
-multimailhook.mailer
-    This option changes the way emails are sent.  Accepted values are:
-
-    * **sendmail (the default)**: use the command ``/usr/sbin/sendmail`` or
-      ``/usr/lib/sendmail`` (or sendmailCommand, if configured).  This
-      mode can be further customized via the following options:
-
-      multimailhook.sendmailCommand
-          The command used by mailer ``sendmail`` to send emails.  Shell
-          quoting is allowed in the value of this setting, but remember that
-          Git requires double-quotes to be escaped; e.g.::
-
-              git config multimailhook.sendmailcommand '/usr/sbin/sendmail -oi -t -F \"Git Repo\"'
-
-          Default is '/usr/sbin/sendmail -oi -t' or
-          '/usr/lib/sendmail -oi -t' (depending on which file is
-          present and executable).
-
-      multimailhook.envelopeSender
-          If set then pass this value to sendmail via the -f option to set
-          the envelope sender address.
-
-    * **smtp**: use Python's smtplib.  This is useful when the sendmail
-      command is not available on the system.  This mode can be
-      further customized via the following options:
-
-      multimailhook.smtpServer
-          The name of the SMTP server to connect to.  The value can
-          also include a colon and a port number; e.g.,
-          ``mail.example.com:25``.  Default is 'localhost' using port 25.
-
-      multimailhook.smtpUser, multimailhook.smtpPass
-          Server username and password. Required if smtpEncryption is 'ssl'.
-          Note that the username and password currently need to be
-          set cleartext in the configuration file, which is not
-          recommended. If you need to use this option, be sure your
-          configuration file is read-only.
-
-      multimailhook.envelopeSender
-        The sender address to be passed to the SMTP server.  If
-        unset, then the value of multimailhook.from is used.
-
-      multimailhook.smtpServerTimeout
-        Timeout in seconds. Default is 10.
-
-      multimailhook.smtpEncryption
-        Set the security type. Allowed values: ``none``, ``ssl``, ``tls`` (starttls).
-        Default is ``none``.
-
-      multimailhook.smtpCACerts
-        Set the path to a list of trusted CA certificate to verify the
-        server certificate, only supported when ``smtpEncryption`` is
-        ``tls``. If unset or empty, the server certificate is not
-        verified. If it targets a file containing a list of trusted CA
-        certificates (PEM format) these CAs will be used to verify the
-        server certificate. For debian, you can set
-        ``/etc/ssl/certs/ca-certificates.crt`` for using the system
-        trusted CAs. For self-signed server, you can add your server
-        certificate to the system store::
-
-            cd /usr/local/share/ca-certificates/
-            openssl s_client -starttls smtp \
-                   -connect mail.example.net:587 -showcerts \
-                   </dev/null 2>/dev/null \
-                 | openssl x509 -outform PEM >mail.example.net.crt
-            update-ca-certificates
-
-        and used the updated ``/etc/ssl/certs/ca-certificates.crt``. Or
-        directly use your ``/path/to/mail.example.net.crt``. Default is
-        unset.
-
-      multimailhook.smtpServerDebugLevel
-        Integer number. Set to greater than 0 to activate debugging.
-
-multimailhook.from, multimailhook.fromCommit, multimailhook.fromRefchange
-    If set, use this value in the From: field of generated emails.
-    ``fromCommit`` is used for commit emails, ``fromRefchange`` is
-    used for refchange emails, and ``from`` is used as fall-back in
-    all cases.
-
-    The value for these variables can be either:
-
-    - An email address, which will be used directly.
-
-    - The value ``pusher``, in which case the pusher's address (if
-      available) will be used.
-
-    - The value ``author`` (meaningful only for ``fromCommit``), in which
-      case the commit author's address will be used.
-
-    If config values are unset, the value of the From: header is
-    determined as follows:
-
-    1. (gitolite environment only)
-       1.a) If ``multimailhook.MailaddressMap`` is set, and is a path
-       to an existing file (if relative, it is considered relative to
-       the place where ``gitolite.conf`` is located), then this file
-       should contain lines like::
-
-           username Firstname Lastname <email@example.com>
-
-       git-multimail will then look for a line where ``$GL_USER``
-       matches the ``username`` part, and use the rest of the line for
-       the ``From:`` header.
-
-       1.b) Parse gitolite.conf, looking for a block of comments that
-       looks like this::
-
-           # BEGIN USER EMAILS
-           # username Firstname Lastname <email@example.com>
-           # END USER EMAILS
-
-       If that block exists, and there is a line between the BEGIN
-       USER EMAILS and END USER EMAILS lines where the first field
-       matches the gitolite username ($GL_USER), use the rest of the
-       line for the From: header.
-
-    2. If the user.email configuration setting is set, use its value
-       (and the value of user.name, if set).
-
-    3. Use the value of multimailhook.envelopeSender.
-
-multimailhook.MailaddressMap
-    (gitolite environment only)
-    File to look for a ``From:`` address based on the user doing the
-    push. Defaults to unset. See ``multimailhook.from`` for details.
-
-multimailhook.administrator
-    The name and/or email address of the administrator of the Git
-    repository; used in FOOTER_TEMPLATE.  Default is
-    multimailhook.envelopesender if it is set; otherwise a generic
-    string is used.
-
-multimailhook.emailPrefix
-    All emails have this string prepended to their subjects, to aid
-    email filtering (though filtering based on the X-Git-* email
-    headers is probably more robust).  Default is the short name of
-    the repository in square brackets; e.g., ``[myrepo]``.  Set this
-    value to the empty string to suppress the email prefix. You may
-    use the placeholder ``%(repo_shortname)s`` for the short name of
-    the repository.
-
-multimailhook.emailMaxLines
-    The maximum number of lines that should be included in the body of
-    a generated email.  If not specified, there is no limit.  Lines
-    beyond the limit are suppressed and counted, and a final line is
-    added indicating the number of suppressed lines.
-
-multimailhook.emailMaxLineLength
-    The maximum length of a line in the email body.  Lines longer than
-    this limit are truncated to this length with a trailing ``[...]``
-    added to indicate the missing text.  The default is 500, because
-    (a) diffs with longer lines are probably from binary files, for
-    which a diff is useless, and (b) even if a text file has such long
-    lines, the diffs are probably unreadable anyway.  To disable line
-    truncation, set this option to 0.
-
-multimailhook.subjectMaxLength
-    The maximum length of the subject line (i.e. the ``oneline`` field
-    in templates, not including the prefix). Lines longer than this
-    limit are truncated to this length with a trailing ``[...]`` added
-    to indicate the missing text. This option The default is to use
-    ``multimailhook.emailMaxLineLength``. This option avoids sending
-    emails with overly long subject lines, but should not be needed if
-    the commit messages follow the Git convention (one short subject
-    line, then a blank line, then the message body). To disable line
-    truncation, set this option to 0.
-
-multimailhook.maxCommitEmails
-    The maximum number of commit emails to send for a given change.
-    When the number of patches is larger that this value, only the
-    summary refchange email is sent.  This can avoid accidental
-    mailbombing, for example on an initial push.  To disable commit
-    emails limit, set this option to 0.  The default is 500.
-
-multimailhook.excludeMergeRevisions
-    When sending out revision emails, do not consider merge commits (the
-    functional equivalent of `rev-list --no-merges`).
-    The default is `false` (send merge commit emails).
-
-multimailhook.emailStrictUTF8
-    If this boolean option is set to `true`, then the main part of the
-    email body is forced to be valid UTF-8.  Any characters that are
-    not valid UTF-8 are converted to the Unicode replacement
-    character, U+FFFD.  The default is `true`.
-
-    This option is ineffective with Python 3, where non-UTF-8
-    characters are unconditionally replaced.
-
-multimailhook.diffOpts
-    Options passed to ``git diff-tree`` when generating the summary
-    information for ReferenceChange emails.  Default is ``--stat
-    --summary --find-copies-harder``.  Add -p to those options to
-    include a unified diff of changes in addition to the usual summary
-    output.  Shell quoting is allowed; see ``multimailhook.logOpts`` for
-    details.
-
-multimailhook.graphOpts
-    Options passed to ``git log --graph`` when generating graphs for the
-    reference change summary emails (used only if refchangeShowGraph
-    is true).  The default is '--oneline --decorate'.
-
-    Shell quoting is allowed; see logOpts for details.
-
-multimailhook.logOpts
-    Options passed to ``git log`` to generate additional info for
-    reference change emails (used only if refchangeShowLog is set).
-    For example, adding -p will show each commit's complete diff.  The
-    default is empty.
-
-    Shell quoting is allowed; for example, a log format that contains
-    spaces can be specified using something like::
-
-      git config multimailhook.logopts '--pretty=format:"%h %aN <%aE>%n%s%n%n%b%n"'
-
-    If you want to set this by editing your configuration file
-    directly, remember that Git requires double-quotes to be escaped
-    (see git-config(1) for more information)::
-
-      [multimailhook]
-              logopts = --pretty=format:\"%h %aN <%aE>%n%s%n%n%b%n\"
-
-multimailhook.commitLogOpts
-    Options passed to ``git log`` to generate additional info for
-    revision change emails.  For example, adding --ignore-all-spaces
-    will suppress whitespace changes.  The default options are ``-C
-    --stat -p --cc``.  Shell quoting is allowed; see
-    multimailhook.logOpts for details.
-
-multimailhook.dateSubstitute
-    String to use as a substitute for ``Date:`` in the output of ``git
-    log`` while formatting commit messages. This is useful to avoid
-    emitting a line that can be interpreted by mailers as the start of
-    a cited message (Zimbra webmail in particular). Defaults to
-    ``CommitDate:``. Set to an empty string or ``none`` to deactivate
-    the behavior.
-
-multimailhook.emailDomain
-    Domain name appended to the username of the person doing the push
-    to convert it into an email address
-    (via ``"%s@%s" % (username, emaildomain)``). More complicated
-    schemes can be implemented by overriding Environment and
-    overriding its get_pusher_email() method.
-
-multimailhook.replyTo, multimailhook.replyToCommit, multimailhook.replyToRefchange
-    Addresses to use in the Reply-To: field for commit emails
-    (replyToCommit) and refchange emails (replyToRefchange).
-    multimailhook.replyTo is used as default when replyToCommit or
-    replyToRefchange is not set. The shortcuts ``pusher`` and
-    ``author`` are allowed with the same semantics as for
-    ``multimailhook.from``. In addition, the value ``none`` can be
-    used to omit the ``Reply-To:`` field.
-
-    The default is ``pusher`` for refchange emails, and ``author`` for
-    commit emails.
-
-multimailhook.quiet
-    Do not output the list of email recipients from the hook
-
-multimailhook.stdout
-    For debugging, send emails to stdout rather than to the
-    mailer.  Equivalent to the --stdout command line option
-
-multimailhook.scanCommitForCc
-    If this option is set to true, than recipients from lines in commit body
-    that starts with ``CC:`` will be added to CC list.
-    Default: false
-
-multimailhook.combineWhenSingleCommit
-    If this option is set to true and a single new commit is pushed to
-    a branch, combine the summary and commit email messages into a
-    single email.
-    Default: true
-
-multimailhook.refFilterInclusionRegex, multimailhook.refFilterExclusionRegex, multimailhook.refFilterDoSendRegex, multimailhook.refFilterDontSendRegex
-    **Warning:** these options are experimental. They should work, but
-    the user-interface is not stable yet (in particular, the option
-    names may change). If you want to participate in stabilizing the
-    feature, please contact the maintainers and/or send pull-requests.
-    If you are happy with the current shape of the feature, please
-    report it too.
-
-    Regular expressions that can be used to limit refs for which email
-    updates will be sent.  It is an error to specify both an inclusion
-    and an exclusion regex.  If a ``refFilterInclusionRegex`` is
-    specified, emails will only be sent for refs which match this
-    regex.  If a ``refFilterExclusionRegex`` regex is specified,
-    emails will be sent for all refs except those that match this
-    regex (or that match a predefined regex specific to the
-    environment, such as "^refs/notes" for most environments and
-    "^refs/notes|^refs/changes" for the gerrit environment).
-
-    The expressions are matched against the complete refname, and is
-    considered to match if any substring matches. For example, to
-    filter-out all tags, set ``refFilterExclusionRegex`` to
-    ``^refs/tags/`` (note the leading ``^`` but no trailing ``$``). If
-    you set ``refFilterExclusionRegex`` to ``master``, then any ref
-    containing ``master`` will be excluded (the ``master`` branch, but
-    also ``refs/tags/master`` or ``refs/heads/foo-master-bar``).
-
-    ``refFilterDoSendRegex`` and ``refFilterDontSendRegex`` are
-    analogous to ``refFilterInclusionRegex`` and
-    ``refFilterExclusionRegex`` with one difference: with
-    ``refFilterDoSendRegex`` and ``refFilterDontSendRegex``, commits
-    introduced by one excluded ref will not be considered as new when
-    they reach an included ref. Typically, if you add a branch ``foo``
-    to  ``refFilterDontSendRegex``, push commits to this branch, and
-    later merge branch ``foo`` into ``master``, then the notification
-    email for ``master`` will contain a commit email only for the
-    merge commit. If you include ``foo`` in
-    ``refFilterExclusionRegex``, then at the time of merge, you will
-    receive one commit email per commit in the branch.
-
-    These variables can be multi-valued, like::
-
-      [multimailhook]
-              refFilterExclusionRegex = ^refs/tags/
-              refFilterExclusionRegex = ^refs/heads/master$
-
-    You can also provide a whitespace-separated list like::
-
-      [multimailhook]
-              refFilterExclusionRegex = ^refs/tags/ ^refs/heads/master$
-
-    Both examples exclude tags and the master branch, and are
-    equivalent to::
-
-      [multimailhook]
-              refFilterExclusionRegex = ^refs/tags/|^refs/heads/master$
-
-    ``refFilterInclusionRegex`` and ``refFilterExclusionRegex`` are
-    strictly stronger than ``refFilterDoSendRegex`` and
-    ``refFilterDontSendRegex``. In other words, adding a ref to a
-    DoSend/DontSend regex has no effect if it is already excluded by a
-    Exclusion/Inclusion regex.
-
-multimailhook.logFile, multimailhook.errorLogFile, multimailhook.debugLogFile
-
-    When set, these variable designate path to files where
-    git-multimail will log some messages. Normal messages and error
-    messages are sent to ``logFile``, and error messages are also sent
-    to ``errorLogFile``. Debug messages and all other messages are
-    sent to ``debugLogFile``. The recommended way is to set only one
-    of these variables, but it is also possible to set several of them
-    (part of the information is then duplicated in several log files,
-    for example errors are duplicated to all log files).
-
-    Relative path are relative to the Git repository where the push is
-    done.
-
-multimailhook.verbose
-
-    Verbosity level of git-multimail on its standard output. By
-    default, show only error and info messages. If set to true, show
-    also debug messages.
-
-Email filtering aids
---------------------
-
-All emails include extra headers to enable fine tuned filtering and
-give information for debugging.  All emails include the headers
-``X-Git-Host``, ``X-Git-Repo``, ``X-Git-Refname``, and ``X-Git-Reftype``.
-ReferenceChange emails also include headers ``X-Git-Oldrev`` and ``X-Git-Newrev``;
-Revision emails also include header ``X-Git-Rev``.
-
-
-Customizing email contents
---------------------------
-
-git-multimail mostly generates emails by expanding templates.  The
-templates can be customized.  To avoid the need to edit
-``git_multimail.py`` directly, the preferred way to change the templates
-is to write a separate Python script that imports ``git_multimail.py`` as
-a module, then replaces the templates in place.  See the provided
-post-receive script for an example of how this is done.
-
-
-Customizing git-multimail for your environment
-----------------------------------------------
-
-git-multimail is mostly customized via an "environment" that describes
-the local environment in which Git is running.  Two types of
-environment are built in:
-
-GenericEnvironment
-    a stand-alone Git repository.
-
-GitoliteEnvironment
-    a Git repository that is managed by gitolite_.  For such
-    repositories, the identity of the pusher is read from
-    environment variable $GL_USER, the name of the repository is read
-    from $GL_REPO (if it is not overridden by multimailhook.reponame),
-    and the From: header value is optionally read from gitolite.conf
-    (see multimailhook.from).
-
-By default, git-multimail assumes GitoliteEnvironment if $GL_USER and
-$GL_REPO are set, and otherwise assumes GenericEnvironment.
-Alternatively, you can choose one of these two environments explicitly
-by setting a ``multimailhook.environment`` config setting (which can
-have the value `generic` or `gitolite`) or by passing an --environment
-option to the script.
-
-If you need to customize the script in ways that are not supported by
-the existing environments, you can define your own environment class
-class using arbitrary Python code.  To do so, you need to import
-``git_multimail.py`` as a Python module, as demonstrated by the example
-post-receive script.  Then implement your environment class; it should
-usually inherit from one of the existing Environment classes and
-possibly one or more of the EnvironmentMixin classes.  Then set the
-``environment`` variable to an instance of your own environment class
-and pass it to ``run_as_post_receive_hook()``.
-
-The standard environment classes, GenericEnvironment and
-GitoliteEnvironment, are in fact themselves put together out of a
-number of mixin classes, each of which handles one aspect of the
-customization.  For the finest control over your configuration, you
-can specify exactly which mixin classes your own environment class
-should inherit from, and override individual methods (or even add your
-own mixin classes) to implement entirely new behaviors.  If you
-implement any mixins that might be useful to other people, please
-consider sharing them with the community!
-
-
-Getting involved
-----------------
-
-Please, read `<CONTRIBUTING.rst>`__ for instructions on how to
-contribute to git-multimail.
-
-
-Footnotes
----------
-
-.. [1] Because of the way information is passed to update hooks, the
-       script's method of determining whether a commit has already
-       been seen does not work when it is used as an ``update`` script.
-       In particular, no notification email will be generated for a
-       new commit that is added to multiple references in the same
-       push. A workaround is to use --force-send to force sending the
-       emails.
-
-.. _gitolite: https://github.com/sitaramc/gitolite
diff --git a/contrib/hooks/multimail/doc/customizing-emails.rst b/contrib/hooks/multimail/doc/customizing-emails.rst
deleted file mode 100644
index 3f5b67f768db..000000000000
--- a/contrib/hooks/multimail/doc/customizing-emails.rst
+++ /dev/null
@@ -1,56 +0,0 @@
-Customizing the content and formatting of emails
-================================================
-
-Overloading template strings
-----------------------------
-
-The content of emails is generated based on template strings defined
-in ``git_multimail.py``. You can customize these template strings
-without changing the script itself, by defining a Python wrapper
-around it. The python wrapper should ``import git_multimail`` and then
-override the ``git_multimail.*`` strings like this::
-
-  import sys  # needed for sys.argv
-
-  # Import and customize git_multimail:
-  import git_multimail
-  git_multimail.REVISION_INTRO_TEMPLATE = """..."""
-  git_multimail.COMBINED_INTRO_TEMPLATE = git_multimail.REVISION_INTRO_TEMPLATE
-
-  # start git_multimail itself:
-  git_multimail.main(sys.argv[1:])
-
-The template strings can use any value already used in the existing
-templates (read the source code).
-
-Using HTML in template strings
-------------------------------
-
-If ``multimailhook.commitEmailFormat`` is set to HTML, then
-git-multimail will generate HTML emails for commit notifications. The
-log and diff will be formatted automatically by git-multimail. By
-default, any HTML special character in the templates will be escaped.
-
-To use HTML formatting in the introduction of the email, set
-``multimailhook.htmlInIntro`` to ``true``. Then, the template can
-contain any HTML tags, that will be sent as-is in the email. For
-example, to add some formatting and a link to the online commit, use
-a format like::
-
-  git_multimail.REVISION_INTRO_TEMPLATE = """\
-  <span style="color:#808080">This is an automated email from the git hooks/post-receive script.</span><br /><br />
-
-  <strong>%(pusher)s</strong> pushed a commit to %(refname_type)s %(short_refname)s
-  in repository %(repo_shortname)s.<br />
-
-  <a href="https://github.com/git-multimail/git-multimail/commit/%(newrev)s">View on GitHub</a>.
-  """
-
-Note that the values expanded from ``%(variable)s`` in the format
-strings will still be escaped.
-
-For a less flexible but easier to set up way to add a link to commit
-emails, see ``multimailhook.commitBrowseURL``.
-
-Similarly, one can set ``multimailhook.htmlInFooter`` and override any
-of the ``*_FOOTER*`` template strings.
diff --git a/contrib/hooks/multimail/doc/gerrit.rst b/contrib/hooks/multimail/doc/gerrit.rst
deleted file mode 100644
index 8011d05dec03..000000000000
--- a/contrib/hooks/multimail/doc/gerrit.rst
+++ /dev/null
@@ -1,56 +0,0 @@
-Setting up git-multimail on Gerrit
-==================================
-
-Gerrit has its own email-sending system, but you may prefer using
-``git-multimail`` instead. It supports Gerrit natively as a Gerrit
-``ref-updated`` hook (Warning: `Gerrit hooks
-<https://gerrit-review.googlesource.com/Documentation/config-hooks.html>`__
-are distinct from Git hooks). Setting up ``git-multimail`` on a Gerrit
-installation can be done following the instructions below.
-
-The explanations show an easy way to set up ``git-multimail``,
-but leave ``git-multimail`` installed and unconfigured for a while. If
-you run Gerrit on a production server, it is advised that you
-execute the step "Set up the hook" last to avoid confusing your users
-in the meantime.
-
-Set up the hook
----------------
-
-Create a directory ``$site_path/hooks/`` if it does not exist (if you
-don't know what ``$site_path`` is, run ``gerrit.sh status`` and look
-for a ``GERRIT_SITE`` line). Either copy ``git_multimail.py`` to
-``$site_path/hooks/ref-updated`` or create a wrapper script like
-this::
-
-  #! /bin/sh
-  exec /path/to/git_multimail.py "$@"
-
-In both cases, make sure the file is named exactly
-``$site_path/hooks/ref-updated`` and is executable.
-
-(Alternatively, you may configure the ``[hooks]`` section of
-gerrit.config)
-
-Configuration
--------------
-
-Log on the gerrit server and edit ``$site_path/git/$project/config``
-to configure ``git-multimail``.
-
-Troubleshooting
----------------
-
-Warning: this will disable ``git-multimail`` during the debug, and
-could confuse your users. Don't run on a production server.
-
-To debug configuration issues with ``git-multimail``, you can add the
-``--stdout`` option when calling ``git_multimail.py`` like this::
-
-  #!/bin/sh
-  exec /path/to/git-multimail/git-multimail/git_multimail.py \
-    --stdout "$@" >> /tmp/log.txt
-
-and try pushing from a test repository. You should see the source of
-the email that would have been sent in the output of ``git push`` in
-the file ``/tmp/log.txt``.
diff --git a/contrib/hooks/multimail/doc/gitolite.rst b/contrib/hooks/multimail/doc/gitolite.rst
deleted file mode 100644
index 505483310552..000000000000
--- a/contrib/hooks/multimail/doc/gitolite.rst
+++ /dev/null
@@ -1,118 +0,0 @@
-Setting up git-multimail on gitolite
-====================================
-
-``git-multimail`` supports gitolite 3 natively.
-The explanations below show an easy way to set up ``git-multimail``,
-but leave ``git-multimail`` installed and unconfigured for a while. If
-you run gitolite on a production server, it is advised that you
-execute the step "Set up the hook" last to avoid confusing your users
-in the meantime.
-
-Set up the hook
----------------
-
-Log in as your gitolite user.
-
-Create a file ``.gitolite/hooks/common/post-receive`` on your gitolite
-account containing (adapt the path, obviously)::
-
-  #!/bin/sh
-  exec /path/to/git-multimail/git-multimail/git_multimail.py "$@"
-
-Make sure it's executable (``chmod +x``). Record the hook in
-gitolite::
-
-  gitolite setup
-
-Configuration
--------------
-
-First, you have to allow the admin to set Git configuration variables.
-
-As gitolite user, edit the line containing ``GIT_CONFIG_KEYS`` in file
-``.gitolite.rc``, to make it look like::
-
-  GIT_CONFIG_KEYS                 =>  'multimailhook\..*',
-
-You can now log out and return to your normal user.
-
-In the ``gitolite-admin`` clone, edit the file ``conf/gitolite.conf``
-and add::
-
-  repo @all
-      # Not strictly needed as git_multimail.py will chose gitolite if
-      # $GL_USER is set.
-      config multimailhook.environment = gitolite
-      config multimailhook.mailingList = # Where emails should be sent
-      config multimailhook.from = # From address to use
-
-Note that by default, gitolite forbids ``<`` and ``>`` in variable
-values (for security/paranoia reasons, see
-`compensating for UNSAFE_PATT
-<http://gitolite.com/gitolite/git-config/index.html#compensating-for-unsafe95patt>`__
-in gitolite's documentation for explanations and a way to disable
-this). As a consequence, you will not be able to use ``First Last
-<First.Last@example.com>`` as recipient email, but specifying
-``First.Last@example.com`` alone works.
-
-Obviously, you can customize all parameters on a per-repository basis by
-adding these ``config multimailhook.*`` lines in the section
-corresponding to a repository or set of repositories.
-
-To activate ``git-multimail`` on a per-repository basis, do not set
-``multimailhook.mailingList`` in the ``@all`` section and set it only
-for repositories for which you want ``git-multimail``.
-
-Alternatively, you can set up the ``From:`` field on a per-user basis
-by adding a ``BEGIN USER EMAILS``/``END USER EMAILS`` section (see
-``../README``).
-
-Specificities of Gitolite for Configuration
--------------------------------------------
-
-Empty configuration variables
-.............................
-
-With gitolite, the syntax ``config multimailhook.commitList = ""``
-unsets the variable instead of setting it to an empty string (see
-`here
-<http://gitolite.com/gitolite/git-config.html#an-important-warning-about-deleting-a-config-line>`__).
-As a result, there is no way to set a variable to the empty string.
-In all most places where an empty value is required, git-multimail
-now allows to specify special ``"none"`` value (case-sensitive) to
-mean the same.
-
-Alternatively, one can use ``" "`` (a single space) instead of ``""``.
-In most cases (in particular ``multimailhook.*List`` variables), this
-will be equivalent to an empty string.
-
-If you have a use-case where ``"none"`` is not an acceptable value and
-you need ``" "`` or  ``""`` instead, please report it as a bug to
-git-multimail.
-
-Allowing Regular Expressions in Configuration
-.............................................
-
-gitolite has a mechanism to prevent unsafe configuration variable
-values, which prevent characters like ``|`` commonly used in regular
-expressions. If you do not need the safety feature of gitolite and
-need to use regular expressions in your configuration (e.g. for
-``multimailhook.refFilter*`` variables), set
-`UNSAFE_PATT
-<http://gitolite.com/gitolite/git-config.html#unsafe-patt>`__ to a
-less restrictive value.
-
-Troubleshooting
----------------
-
-Warning: this will disable ``git-multimail`` during the debug, and
-could confuse your users. Don't run on a production server.
-
-To debug configuration issues with ``git-multimail``, you can add the
-``--stdout`` option when calling ``git_multimail.py`` like this::
-
-  #!/bin/sh
-  exec /path/to/git-multimail/git-multimail/git_multimail.py --stdout "$@"
-
-and try pushing from a test repository. You should see the source of
-the email that would have been sent in the output of ``git push``.
diff --git a/contrib/hooks/multimail/doc/troubleshooting.rst b/contrib/hooks/multimail/doc/troubleshooting.rst
deleted file mode 100644
index 651b509ee66c..000000000000
--- a/contrib/hooks/multimail/doc/troubleshooting.rst
+++ /dev/null
@@ -1,78 +0,0 @@
-Troubleshooting issues with git-multimail: a FAQ
-================================================
-
-How to check that git-multimail is properly set up?
----------------------------------------------------
-
-Since version 1.4.0, git-multimail allows a simple self-checking of
-its configuration: run it with the environment variable
-``GIT_MULTIMAIL_CHECK_SETUP`` set to a non-empty string. You should
-get something like this::
-
-  $ GIT_MULTIMAIL_CHECK_SETUP=true /home/moy/dev/git-multimail/git-multimail/git_multimail.py
-  Environment values:
-      administrator : 'the administrator of this repository'
-      charset : 'utf-8'
-      emailprefix : '[git-multimail] '
-      fqdn : 'anie'
-      projectdesc : 'UNNAMED PROJECT'
-      pusher : 'moy'
-      repo_path : '/home/moy/dev/git-multimail'
-      repo_shortname : 'git-multimail'
-
-  Now, checking that git-multimail's standard input is properly set ...
-  Please type some text and then press Return
-  foo
-  You have just entered:
-  foo
-  git-multimail seems properly set up.
-
-If you forgot to set an important variable, you may get instead::
-
-  $ GIT_MULTIMAIL_CHECK_SETUP=true /home/moy/dev/git-multimail/git-multimail/git_multimail.py
-  No email recipients configured!
-
-Do not set ``$GIT_MULTIMAIL_CHECK_SETUP`` other than for testing your
-configuration: it would disable the hook completely.
-
-Git is not using the right address in the From/To/Reply-To field
-----------------------------------------------------------------
-
-First, make sure that git-multimail actually uses what you think it is
-using. A lot happens to your email (especially when posting to a
-mailing-list) between the time `git_multimail.py` sends it and the
-time it reaches your inbox.
-
-A simple test (to do on a test repository, do not use in production as
-it would disable email sending): change your post-receive hook to call
-`git_multimail.py` with the `--stdout` option, and try to push to the
-repository. You should see something like::
-
-  Counting objects: 3, done.
-  Writing objects: 100% (3/3), 263 bytes | 0 bytes/s, done.
-  Total 3 (delta 0), reused 0 (delta 0)
-  remote: Sending notification emails to: foo.bar@example.com
-  remote: ===========================================================================
-  remote: Date: Mon, 25 Apr 2016 18:39:59 +0200
-  remote: To: foo.bar@example.com
-  remote: Subject: [git] branch master updated: foo
-  remote: MIME-Version: 1.0
-  remote: Content-Type: text/plain; charset=utf-8
-  remote: Content-Transfer-Encoding: 8bit
-  remote: Message-ID: <20160425163959.2311.20498@anie>
-  remote: From: Auth Or <Foo.Bar@example.com>
-  remote: Reply-To: Auth Or <Foo.Bar@example.com>
-  remote: X-Git-Host: example
-  ...
-  remote: --
-  remote: To stop receiving notification emails like this one, please contact
-  remote: the administrator of this repository.
-  remote: ===========================================================================
-  To /path/to/repo
-     6278f04..e173f20  master -> master
-
-Note: this does not include the sender (Return-Path: header), as it is
-not part of the message content but passed to the mailer. Some mailer
-show the ``Sender:`` field instead of the ``From:`` field (for
-example, Zimbra Webmail shows ``From: <sender-field> on behalf of
-<from-field>``).
diff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py
deleted file mode 100755
index f563be82fc7e..000000000000
--- a/contrib/hooks/multimail/git_multimail.py
+++ /dev/null
@@ -1,4346 +0,0 @@
-#! /usr/bin/env python
-
-__version__ = '1.5.0'
-
-# Copyright (c) 2015-2016 Matthieu Moy and others
-# Copyright (c) 2012-2014 Michael Haggerty and others
-# Derived from contrib/hooks/post-receive-email, which is
-# Copyright (c) 2007 Andy Parkins
-# and also includes contributions by other authors.
-#
-# This file is part of git-multimail.
-#
-# git-multimail is free software: you can redistribute it and/or
-# modify it under the terms of the GNU General Public License version
-# 2 as published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see
-# <http://www.gnu.org/licenses/>.
-
-"""Generate notification emails for pushes to a git repository.
-
-This hook sends emails describing changes introduced by pushes to a
-git repository.  For each reference that was changed, it emits one
-ReferenceChange email summarizing how the reference was changed,
-followed by one Revision email for each new commit that was introduced
-by the reference change.
-
-Each commit is announced in exactly one Revision email.  If the same
-commit is merged into another branch in the same or a later push, then
-the ReferenceChange email will list the commit's SHA1 and its one-line
-summary, but no new Revision email will be generated.
-
-This script is designed to be used as a "post-receive" hook in a git
-repository (see githooks(5)).  It can also be used as an "update"
-script, but this usage is not completely reliable and is deprecated.
-
-To help with debugging, this script accepts a --stdout option, which
-causes the emails to be written to standard output rather than sent
-using sendmail.
-
-See the accompanying README file for the complete documentation.
-
-"""
-
-import sys
-import os
-import re
-import bisect
-import socket
-import subprocess
-import shlex
-import optparse
-import logging
-import smtplib
-try:
-    import ssl
-except ImportError:
-    # Python < 2.6 do not have ssl, but that's OK if we don't use it.
-    pass
-import time
-
-import uuid
-import base64
-
-PYTHON3 = sys.version_info >= (3, 0)
-
-if sys.version_info <= (2, 5):
-    def all(iterable):
-        for element in iterable:
-            if not element:
-                return False
-        return True
-
-
-def is_ascii(s):
-    return all(ord(c) < 128 and ord(c) > 0 for c in s)
-
-
-if PYTHON3:
-    def is_string(s):
-        return isinstance(s, str)
-
-    def str_to_bytes(s):
-        return s.encode(ENCODING)
-
-    def bytes_to_str(s, errors='strict'):
-        return s.decode(ENCODING, errors)
-
-    unicode = str
-
-    def write_str(f, msg):
-        # Try outputting with the default encoding. If it fails,
-        # try UTF-8.
-        try:
-            f.buffer.write(msg.encode(sys.getdefaultencoding()))
-        except UnicodeEncodeError:
-            f.buffer.write(msg.encode(ENCODING))
-
-    def read_line(f):
-        # Try reading with the default encoding. If it fails,
-        # try UTF-8.
-        out = f.buffer.readline()
-        try:
-            return out.decode(sys.getdefaultencoding())
-        except UnicodeEncodeError:
-            return out.decode(ENCODING)
-
-    import html
-
-    def html_escape(s):
-        return html.escape(s)
-
-else:
-    def is_string(s):
-        try:
-            return isinstance(s, basestring)
-        except NameError:  # Silence Pyflakes warning
-            raise
-
-    def str_to_bytes(s):
-        return s
-
-    def bytes_to_str(s, errors='strict'):
-        return s
-
-    def write_str(f, msg):
-        f.write(msg)
-
-    def read_line(f):
-        return f.readline()
-
-    def next(it):
-        return it.next()
-
-    import cgi
-
-    def html_escape(s):
-        return cgi.escape(s, True)
-
-try:
-    from email.charset import Charset
-    from email.utils import make_msgid
-    from email.utils import getaddresses
-    from email.utils import formataddr
-    from email.utils import formatdate
-    from email.header import Header
-except ImportError:
-    # Prior to Python 2.5, the email module used different names:
-    from email.Charset import Charset
-    from email.Utils import make_msgid
-    from email.Utils import getaddresses
-    from email.Utils import formataddr
-    from email.Utils import formatdate
-    from email.Header import Header
-
-
-DEBUG = False
-
-ZEROS = '0' * 40
-LOGBEGIN = '- Log -----------------------------------------------------------------\n'
-LOGEND = '-----------------------------------------------------------------------\n'
-
-ADDR_HEADERS = set(['from', 'to', 'cc', 'bcc', 'reply-to', 'sender'])
-
-# It is assumed in many places that the encoding is uniformly UTF-8,
-# so changing these constants is unsupported.  But define them here
-# anyway, to make it easier to find (at least most of) the places
-# where the encoding is important.
-(ENCODING, CHARSET) = ('UTF-8', 'utf-8')
-
-
-REF_CREATED_SUBJECT_TEMPLATE = (
-    '%(emailprefix)s%(refname_type)s %(short_refname)s created'
-    ' (now %(newrev_short)s)'
-    )
-REF_UPDATED_SUBJECT_TEMPLATE = (
-    '%(emailprefix)s%(refname_type)s %(short_refname)s updated'
-    ' (%(oldrev_short)s -> %(newrev_short)s)'
-    )
-REF_DELETED_SUBJECT_TEMPLATE = (
-    '%(emailprefix)s%(refname_type)s %(short_refname)s deleted'
-    ' (was %(oldrev_short)s)'
-    )
-
-COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE = (
-    '%(emailprefix)s%(refname_type)s %(short_refname)s updated: %(oneline)s'
-    )
-
-REFCHANGE_HEADER_TEMPLATE = """\
-Date: %(send_date)s
-To: %(recipients)s
-Subject: %(subject)s
-MIME-Version: 1.0
-Content-Type: text/%(contenttype)s; charset=%(charset)s
-Content-Transfer-Encoding: 8bit
-Message-ID: %(msgid)s
-From: %(fromaddr)s
-Reply-To: %(reply_to)s
-Thread-Index: %(thread_index)s
-X-Git-Host: %(fqdn)s
-X-Git-Repo: %(repo_shortname)s
-X-Git-Refname: %(refname)s
-X-Git-Reftype: %(refname_type)s
-X-Git-Oldrev: %(oldrev)s
-X-Git-Newrev: %(newrev)s
-X-Git-NotificationType: ref_changed
-X-Git-Multimail-Version: %(multimail_version)s
-Auto-Submitted: auto-generated
-"""
-
-REFCHANGE_INTRO_TEMPLATE = """\
-This is an automated email from the git hooks/post-receive script.
-
-%(pusher)s pushed a change to %(refname_type)s %(short_refname)s
-in repository %(repo_shortname)s.
-
-"""
-
-
-FOOTER_TEMPLATE = """\
-
--- \n\
-To stop receiving notification emails like this one, please contact
-%(administrator)s.
-"""
-
-
-REWIND_ONLY_TEMPLATE = """\
-This update removed existing revisions from the reference, leaving the
-reference pointing at a previous point in the repository history.
-
- * -- * -- N   %(refname)s (%(newrev_short)s)
-            \\
-             O -- O -- O   (%(oldrev_short)s)
-
-Any revisions marked "omit" are not gone; other references still
-refer to them.  Any revisions marked "discard" are gone forever.
-"""
-
-
-NON_FF_TEMPLATE = """\
-This update added new revisions after undoing existing revisions.
-That is to say, some revisions that were in the old version of the
-%(refname_type)s are not in the new version.  This situation occurs
-when a user --force pushes a change and generates a repository
-containing something like this:
-
- * -- * -- B -- O -- O -- O   (%(oldrev_short)s)
-            \\
-             N -- N -- N   %(refname)s (%(newrev_short)s)
-
-You should already have received notification emails for all of the O
-revisions, and so the following emails describe only the N revisions
-from the common base, B.
-
-Any revisions marked "omit" are not gone; other references still
-refer to them.  Any revisions marked "discard" are gone forever.
-"""
-
-
-NO_NEW_REVISIONS_TEMPLATE = """\
-No new revisions were added by this update.
-"""
-
-
-DISCARDED_REVISIONS_TEMPLATE = """\
-This change permanently discards the following revisions:
-"""
-
-
-NO_DISCARDED_REVISIONS_TEMPLATE = """\
-The revisions that were on this %(refname_type)s are still contained in
-other references; therefore, this change does not discard any commits
-from the repository.
-"""
-
-
-NEW_REVISIONS_TEMPLATE = """\
-The %(tot)s revisions listed above as "new" are entirely new to this
-repository and will be described in separate emails.  The revisions
-listed as "add" were already present in the repository and have only
-been added to this reference.
-
-"""
-
-
-TAG_CREATED_TEMPLATE = """\
-      at %(newrev_short)-8s (%(newrev_type)s)
-"""
-
-
-TAG_UPDATED_TEMPLATE = """\
-*** WARNING: tag %(short_refname)s was modified! ***
-
-    from %(oldrev_short)-8s (%(oldrev_type)s)
-      to %(newrev_short)-8s (%(newrev_type)s)
-"""
-
-
-TAG_DELETED_TEMPLATE = """\
-*** WARNING: tag %(short_refname)s was deleted! ***
-
-"""
-
-
-# The template used in summary tables.  It looks best if this uses the
-# same alignment as TAG_CREATED_TEMPLATE and TAG_UPDATED_TEMPLATE.
-BRIEF_SUMMARY_TEMPLATE = """\
-%(action)8s %(rev_short)-8s %(text)s
-"""
-
-
-NON_COMMIT_UPDATE_TEMPLATE = """\
-This is an unusual reference change because the reference did not
-refer to a commit either before or after the change.  We do not know
-how to provide full information about this reference change.
-"""
-
-
-REVISION_HEADER_TEMPLATE = """\
-Date: %(send_date)s
-To: %(recipients)s
-Cc: %(cc_recipients)s
-Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s
-MIME-Version: 1.0
-Content-Type: text/%(contenttype)s; charset=%(charset)s
-Content-Transfer-Encoding: 8bit
-From: %(fromaddr)s
-Reply-To: %(reply_to)s
-In-Reply-To: %(reply_to_msgid)s
-References: %(reply_to_msgid)s
-Thread-Index: %(thread_index)s
-X-Git-Host: %(fqdn)s
-X-Git-Repo: %(repo_shortname)s
-X-Git-Refname: %(refname)s
-X-Git-Reftype: %(refname_type)s
-X-Git-Rev: %(rev)s
-X-Git-NotificationType: diff
-X-Git-Multimail-Version: %(multimail_version)s
-Auto-Submitted: auto-generated
-"""
-
-REVISION_INTRO_TEMPLATE = """\
-This is an automated email from the git hooks/post-receive script.
-
-%(pusher)s pushed a commit to %(refname_type)s %(short_refname)s
-in repository %(repo_shortname)s.
-
-"""
-
-LINK_TEXT_TEMPLATE = """\
-View the commit online:
-%(browse_url)s
-
-"""
-
-LINK_HTML_TEMPLATE = """\
-<p><a href="%(browse_url)s">View the commit online</a>.</p>
-"""
-
-
-REVISION_FOOTER_TEMPLATE = FOOTER_TEMPLATE
-
-
-# Combined, meaning refchange+revision email (for single-commit additions)
-COMBINED_HEADER_TEMPLATE = """\
-Date: %(send_date)s
-To: %(recipients)s
-Subject: %(subject)s
-MIME-Version: 1.0
-Content-Type: text/%(contenttype)s; charset=%(charset)s
-Content-Transfer-Encoding: 8bit
-Message-ID: %(msgid)s
-From: %(fromaddr)s
-Reply-To: %(reply_to)s
-X-Git-Host: %(fqdn)s
-X-Git-Repo: %(repo_shortname)s
-X-Git-Refname: %(refname)s
-X-Git-Reftype: %(refname_type)s
-X-Git-Oldrev: %(oldrev)s
-X-Git-Newrev: %(newrev)s
-X-Git-Rev: %(rev)s
-X-Git-NotificationType: ref_changed_plus_diff
-X-Git-Multimail-Version: %(multimail_version)s
-Auto-Submitted: auto-generated
-"""
-
-COMBINED_INTRO_TEMPLATE = """\
-This is an automated email from the git hooks/post-receive script.
-
-%(pusher)s pushed a commit to %(refname_type)s %(short_refname)s
-in repository %(repo_shortname)s.
-
-"""
-
-COMBINED_FOOTER_TEMPLATE = FOOTER_TEMPLATE
-
-
-class CommandError(Exception):
-    def __init__(self, cmd, retcode):
-        self.cmd = cmd
-        self.retcode = retcode
-        Exception.__init__(
-            self,
-            'Command "%s" failed with retcode %s' % (' '.join(cmd), retcode,)
-            )
-
-
-class ConfigurationException(Exception):
-    pass
-
-
-# The "git" program (this could be changed to include a full path):
-GIT_EXECUTABLE = 'git'
-
-
-# How "git" should be invoked (including global arguments), as a list
-# of words.  This variable is usually initialized automatically by
-# read_git_output() via choose_git_command(), but if a value is set
-# here then it will be used unconditionally.
-GIT_CMD = None
-
-
-def choose_git_command():
-    """Decide how to invoke git, and record the choice in GIT_CMD."""
-
-    global GIT_CMD
-
-    if GIT_CMD is None:
-        try:
-            # Check to see whether the "-c" option is accepted (it was
-            # only added in Git 1.7.2).  We don't actually use the
-            # output of "git --version", though if we needed more
-            # specific version information this would be the place to
-            # do it.
-            cmd = [GIT_EXECUTABLE, '-c', 'foo.bar=baz', '--version']
-            read_output(cmd)
-            GIT_CMD = [GIT_EXECUTABLE, '-c', 'i18n.logoutputencoding=%s' % (ENCODING,)]
-        except CommandError:
-            GIT_CMD = [GIT_EXECUTABLE]
-
-
-def read_git_output(args, input=None, keepends=False, **kw):
-    """Read the output of a Git command."""
-
-    if GIT_CMD is None:
-        choose_git_command()
-
-    return read_output(GIT_CMD + args, input=input, keepends=keepends, **kw)
-
-
-def read_output(cmd, input=None, keepends=False, **kw):
-    if input:
-        stdin = subprocess.PIPE
-        input = str_to_bytes(input)
-    else:
-        stdin = None
-    errors = 'strict'
-    if 'errors' in kw:
-        errors = kw['errors']
-        del kw['errors']
-    p = subprocess.Popen(
-        tuple(str_to_bytes(w) for w in cmd),
-        stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
-        )
-    (out, err) = p.communicate(input)
-    out = bytes_to_str(out, errors=errors)
-    retcode = p.wait()
-    if retcode:
-        raise CommandError(cmd, retcode)
-    if not keepends:
-        out = out.rstrip('\n\r')
-    return out
-
-
-def read_git_lines(args, keepends=False, **kw):
-    """Return the lines output by Git command.
-
-    Return as single lines, with newlines stripped off."""
-
-    return read_git_output(args, keepends=True, **kw).splitlines(keepends)
-
-
-def git_rev_list_ish(cmd, spec, args=None, **kw):
-    """Common functionality for invoking a 'git rev-list'-like command.
-
-    Parameters:
-      * cmd is the Git command to run, e.g., 'rev-list' or 'log'.
-      * spec is a list of revision arguments to pass to the named
-        command.  If None, this function returns an empty list.
-      * args is a list of extra arguments passed to the named command.
-      * All other keyword arguments (if any) are passed to the
-        underlying read_git_lines() function.
-
-    Return the output of the Git command in the form of a list, one
-    entry per output line.
-    """
-    if spec is None:
-        return []
-    if args is None:
-        args = []
-    args = [cmd, '--stdin'] + args
-    spec_stdin = ''.join(s + '\n' for s in spec)
-    return read_git_lines(args, input=spec_stdin, **kw)
-
-
-def git_rev_list(spec, **kw):
-    """Run 'git rev-list' with the given list of revision arguments.
-
-    See git_rev_list_ish() for parameter and return value
-    documentation.
-    """
-    return git_rev_list_ish('rev-list', spec, **kw)
-
-
-def git_log(spec, **kw):
-    """Run 'git log' with the given list of revision arguments.
-
-    See git_rev_list_ish() for parameter and return value
-    documentation.
-    """
-    return git_rev_list_ish('log', spec, **kw)
-
-
-def header_encode(text, header_name=None):
-    """Encode and line-wrap the value of an email header field."""
-
-    # Convert to unicode, if required.
-    if not isinstance(text, unicode):
-        text = unicode(text, 'utf-8')
-
-    if is_ascii(text):
-        charset = 'ascii'
-    else:
-        charset = 'utf-8'
-
-    return Header(text, header_name=header_name, charset=Charset(charset)).encode()
-
-
-def addr_header_encode(text, header_name=None):
-    """Encode and line-wrap the value of an email header field containing
-    email addresses."""
-
-    # Convert to unicode, if required.
-    if not isinstance(text, unicode):
-        text = unicode(text, 'utf-8')
-
-    text = ', '.join(
-        formataddr((header_encode(name), emailaddr))
-        for name, emailaddr in getaddresses([text])
-        )
-
-    if is_ascii(text):
-        charset = 'ascii'
-    else:
-        charset = 'utf-8'
-
-    return Header(text, header_name=header_name, charset=Charset(charset)).encode()
-
-
-class Config(object):
-    def __init__(self, section, git_config=None):
-        """Represent a section of the git configuration.
-
-        If git_config is specified, it is passed to "git config" in
-        the GIT_CONFIG environment variable, meaning that "git config"
-        will read the specified path rather than the Git default
-        config paths."""
-
-        self.section = section
-        if git_config:
-            self.env = os.environ.copy()
-            self.env['GIT_CONFIG'] = git_config
-        else:
-            self.env = None
-
-    @staticmethod
-    def _split(s):
-        """Split NUL-terminated values."""
-
-        words = s.split('\0')
-        assert words[-1] == ''
-        return words[:-1]
-
-    @staticmethod
-    def add_config_parameters(c):
-        """Add configuration parameters to Git.
-
-        c is either an str or a list of str, each element being of the
-        form 'var=val' or 'var', with the same syntax and meaning as
-        the argument of 'git -c var=val'.
-        """
-        if isinstance(c, str):
-            c = (c,)
-        parameters = os.environ.get('GIT_CONFIG_PARAMETERS', '')
-        if parameters:
-            parameters += ' '
-        # git expects GIT_CONFIG_PARAMETERS to be of the form
-        #    "'name1=value1' 'name2=value2' 'name3=value3'"
-        # including everything inside the double quotes (but not the double
-        # quotes themselves).  Spacing is critical.  Also, if a value contains
-        # a literal single quote that quote must be represented using the
-        # four character sequence: '\''
-        parameters += ' '.join("'" + x.replace("'", "'\\''") + "'" for x in c)
-        os.environ['GIT_CONFIG_PARAMETERS'] = parameters
-
-    def get(self, name, default=None):
-        try:
-            values = self._split(read_git_output(
-                ['config', '--get', '--null', '%s.%s' % (self.section, name)],
-                env=self.env, keepends=True,
-                ))
-            assert len(values) == 1
-            return values[0]
-        except CommandError:
-            return default
-
-    def get_bool(self, name, default=None):
-        try:
-            value = read_git_output(
-                ['config', '--get', '--bool', '%s.%s' % (self.section, name)],
-                env=self.env,
-                )
-        except CommandError:
-            return default
-        return value == 'true'
-
-    def get_all(self, name, default=None):
-        """Read a (possibly multivalued) setting from the configuration.
-
-        Return the result as a list of values, or default if the name
-        is unset."""
-
-        try:
-            return self._split(read_git_output(
-                ['config', '--get-all', '--null', '%s.%s' % (self.section, name)],
-                env=self.env, keepends=True,
-                ))
-        except CommandError:
-            t, e, traceback = sys.exc_info()
-            if e.retcode == 1:
-                # "the section or key is invalid"; i.e., there is no
-                # value for the specified key.
-                return default
-            else:
-                raise
-
-    def set(self, name, value):
-        read_git_output(
-            ['config', '%s.%s' % (self.section, name), value],
-            env=self.env,
-            )
-
-    def add(self, name, value):
-        read_git_output(
-            ['config', '--add', '%s.%s' % (self.section, name), value],
-            env=self.env,
-            )
-
-    def __contains__(self, name):
-        return self.get_all(name, default=None) is not None
-
-    # We don't use this method anymore internally, but keep it here in
-    # case somebody is calling it from their own code:
-    def has_key(self, name):
-        return name in self
-
-    def unset_all(self, name):
-        try:
-            read_git_output(
-                ['config', '--unset-all', '%s.%s' % (self.section, name)],
-                env=self.env,
-                )
-        except CommandError:
-            t, e, traceback = sys.exc_info()
-            if e.retcode == 5:
-                # The name doesn't exist, which is what we wanted anyway...
-                pass
-            else:
-                raise
-
-    def set_recipients(self, name, value):
-        self.unset_all(name)
-        for pair in getaddresses([value]):
-            self.add(name, formataddr(pair))
-
-
-def generate_summaries(*log_args):
-    """Generate a brief summary for each revision requested.
-
-    log_args are strings that will be passed directly to "git log" as
-    revision selectors.  Iterate over (sha1_short, subject) for each
-    commit specified by log_args (subject is the first line of the
-    commit message as a string without EOLs)."""
-
-    cmd = [
-        'log', '--abbrev', '--format=%h %s',
-        ] + list(log_args) + ['--']
-    for line in read_git_lines(cmd):
-        yield tuple(line.split(' ', 1))
-
-
-def limit_lines(lines, max_lines):
-    for (index, line) in enumerate(lines):
-        if index < max_lines:
-            yield line
-
-    if index >= max_lines:
-        yield '... %d lines suppressed ...\n' % (index + 1 - max_lines,)
-
-
-def limit_linelength(lines, max_linelength):
-    for line in lines:
-        # Don't forget that lines always include a trailing newline.
-        if len(line) > max_linelength + 1:
-            line = line[:max_linelength - 7] + ' [...]\n'
-        yield line
-
-
-class CommitSet(object):
-    """A (constant) set of object names.
-
-    The set should be initialized with full SHA1 object names.  The
-    __contains__() method returns True iff its argument is an
-    abbreviation of any the names in the set."""
-
-    def __init__(self, names):
-        self._names = sorted(names)
-
-    def __len__(self):
-        return len(self._names)
-
-    def __contains__(self, sha1_abbrev):
-        """Return True iff this set contains sha1_abbrev (which might be abbreviated)."""
-
-        i = bisect.bisect_left(self._names, sha1_abbrev)
-        return i < len(self) and self._names[i].startswith(sha1_abbrev)
-
-
-class GitObject(object):
-    def __init__(self, sha1, type=None):
-        if sha1 == ZEROS:
-            self.sha1 = self.type = self.commit_sha1 = None
-        else:
-            self.sha1 = sha1
-            self.type = type or read_git_output(['cat-file', '-t', self.sha1])
-
-            if self.type == 'commit':
-                self.commit_sha1 = self.sha1
-            elif self.type == 'tag':
-                try:
-                    self.commit_sha1 = read_git_output(
-                        ['rev-parse', '--verify', '%s^0' % (self.sha1,)]
-                        )
-                except CommandError:
-                    # Cannot deref tag to determine commit_sha1
-                    self.commit_sha1 = None
-            else:
-                self.commit_sha1 = None
-
-        self.short = read_git_output(['rev-parse', '--short', sha1])
-
-    def get_summary(self):
-        """Return (sha1_short, subject) for this commit."""
-
-        if not self.sha1:
-            raise ValueError('Empty commit has no summary')
-
-        return next(iter(generate_summaries('--no-walk', self.sha1)))
-
-    def __eq__(self, other):
-        return isinstance(other, GitObject) and self.sha1 == other.sha1
-
-    def __ne__(self, other):
-        return not self == other
-
-    def __hash__(self):
-        return hash(self.sha1)
-
-    def __nonzero__(self):
-        return bool(self.sha1)
-
-    def __bool__(self):
-        """Python 2 backward compatibility"""
-        return self.__nonzero__()
-
-    def __str__(self):
-        return self.sha1 or ZEROS
-
-
-class Change(object):
-    """A Change that has been made to the Git repository.
-
-    Abstract class from which both Revisions and ReferenceChanges are
-    derived.  A Change knows how to generate a notification email
-    describing itself."""
-
-    def __init__(self, environment):
-        self.environment = environment
-        self._values = None
-        self._contains_html_diff = False
-
-    def _contains_diff(self):
-        # We do contain a diff, should it be rendered in HTML?
-        if self.environment.commit_email_format == "html":
-            self._contains_html_diff = True
-
-    def _compute_values(self):
-        """Return a dictionary {keyword: expansion} for this Change.
-
-        Derived classes overload this method to add more entries to
-        the return value.  This method is used internally by
-        get_values().  The return value should always be a new
-        dictionary."""
-
-        values = self.environment.get_values()
-        fromaddr = self.environment.get_fromaddr(change=self)
-        if fromaddr is not None:
-            values['fromaddr'] = fromaddr
-        values['multimail_version'] = get_version()
-        return values
-
-    # Aliases usable in template strings. Tuple of pairs (destination,
-    # source).
-    VALUES_ALIAS = (
-        ("id", "newrev"),
-        )
-
-    def get_values(self, **extra_values):
-        """Return a dictionary {keyword: expansion} for this Change.
-
-        Return a dictionary mapping keywords to the values that they
-        should be expanded to for this Change (used when interpolating
-        template strings).  If any keyword arguments are supplied, add
-        those to the return value as well.  The return value is always
-        a new dictionary."""
-
-        if self._values is None:
-            self._values = self._compute_values()
-
-        values = self._values.copy()
-        if extra_values:
-            values.update(extra_values)
-
-        for alias, val in self.VALUES_ALIAS:
-            values[alias] = values[val]
-        return values
-
-    def expand(self, template, **extra_values):
-        """Expand template.
-
-        Expand the template (which should be a string) using string
-        interpolation of the values for this Change.  If any keyword
-        arguments are provided, also include those in the keywords
-        available for interpolation."""
-
-        return template % self.get_values(**extra_values)
-
-    def expand_lines(self, template, html_escape_val=False, **extra_values):
-        """Break template into lines and expand each line."""
-
-        values = self.get_values(**extra_values)
-        if html_escape_val:
-            for k in values:
-                if is_string(values[k]):
-                    values[k] = html_escape(values[k])
-        for line in template.splitlines(True):
-            yield line % values
-
-    def expand_header_lines(self, template, **extra_values):
-        """Break template into lines and expand each line as an RFC 2822 header.
-
-        Encode values and split up lines that are too long.  Silently
-        skip lines that contain references to unknown variables."""
-
-        values = self.get_values(**extra_values)
-        if self._contains_html_diff:
-            self._content_type = 'html'
-        else:
-            self._content_type = 'plain'
-        values['contenttype'] = self._content_type
-
-        for line in template.splitlines():
-            (name, value) = line.split(': ', 1)
-
-            try:
-                value = value % values
-            except KeyError:
-                t, e, traceback = sys.exc_info()
-                if DEBUG:
-                    self.environment.log_warning(
-                        'Warning: unknown variable %r in the following line; line skipped:\n'
-                        '    %s\n'
-                        % (e.args[0], line,)
-                        )
-            else:
-                if name.lower() in ADDR_HEADERS:
-                    value = addr_header_encode(value, name)
-                else:
-                    value = header_encode(value, name)
-                for splitline in ('%s: %s\n' % (name, value)).splitlines(True):
-                    yield splitline
-
-    def generate_email_header(self):
-        """Generate the RFC 2822 email headers for this Change, a line at a time.
-
-        The output should not include the trailing blank line."""
-
-        raise NotImplementedError()
-
-    def generate_browse_link(self, base_url):
-        """Generate a link to an online repository browser."""
-        return iter(())
-
-    def generate_email_intro(self, html_escape_val=False):
-        """Generate the email intro for this Change, a line at a time.
-
-        The output will be used as the standard boilerplate at the top
-        of the email body."""
-
-        raise NotImplementedError()
-
-    def generate_email_body(self, push):
-        """Generate the main part of the email body, a line at a time.
-
-        The text in the body might be truncated after a specified
-        number of lines (see multimailhook.emailmaxlines)."""
-
-        raise NotImplementedError()
-
-    def generate_email_footer(self, html_escape_val):
-        """Generate the footer of the email, a line at a time.
-
-        The footer is always included, irrespective of
-        multimailhook.emailmaxlines."""
-
-        raise NotImplementedError()
-
-    def _wrap_for_html(self, lines):
-        """Wrap the lines in HTML <pre> tag when using HTML format.
-
-        Escape special HTML characters and add <pre> and </pre> tags around
-        the given lines if we should be generating HTML as indicated by
-        self._contains_html_diff being set to true.
-        """
-        if self._contains_html_diff:
-            yield "<pre style='margin:0'>\n"
-
-            for line in lines:
-                yield html_escape(line)
-
-            yield '</pre>\n'
-        else:
-            for line in lines:
-                yield line
-
-    def generate_email(self, push, body_filter=None, extra_header_values={}):
-        """Generate an email describing this change.
-
-        Iterate over the lines (including the header lines) of an
-        email describing this change.  If body_filter is not None,
-        then use it to filter the lines that are intended for the
-        email body.
-
-        The extra_header_values field is received as a dict and not as
-        **kwargs, to allow passing other keyword arguments in the
-        future (e.g. passing extra values to generate_email_intro()"""
-
-        for line in self.generate_email_header(**extra_header_values):
-            yield line
-        yield '\n'
-        html_escape_val = (self.environment.html_in_intro and
-                           self._contains_html_diff)
-        intro = self.generate_email_intro(html_escape_val)
-        if not self.environment.html_in_intro:
-            intro = self._wrap_for_html(intro)
-        for line in intro:
-            yield line
-
-        if self.environment.commitBrowseURL:
-            for line in self.generate_browse_link(self.environment.commitBrowseURL):
-                yield line
-
-        body = self.generate_email_body(push)
-        if body_filter is not None:
-            body = body_filter(body)
-
-        diff_started = False
-        if self._contains_html_diff:
-            # "white-space: pre" is the default, but we need to
-            # specify it again in case the message is viewed in a
-            # webmail which wraps it in an element setting white-space
-            # to something else (Zimbra does this and sets
-            # white-space: pre-line).
-            yield '<pre style="white-space: pre; background: #F8F8F8">'
-        for line in body:
-            if self._contains_html_diff:
-                # This is very, very naive. It would be much better to really
-                # parse the diff, i.e. look at how many lines do we have in
-                # the hunk headers instead of blindly highlighting everything
-                # that looks like it might be part of a diff.
-                bgcolor = ''
-                fgcolor = ''
-                if line.startswith('--- a/'):
-                    diff_started = True
-                    bgcolor = 'e0e0ff'
-                elif line.startswith('diff ') or line.startswith('index '):
-                    diff_started = True
-                    fgcolor = '808080'
-                elif diff_started:
-                    if line.startswith('+++ '):
-                        bgcolor = 'e0e0ff'
-                    elif line.startswith('@@'):
-                        bgcolor = 'e0e0e0'
-                    elif line.startswith('+'):
-                        bgcolor = 'e0ffe0'
-                    elif line.startswith('-'):
-                        bgcolor = 'ffe0e0'
-                elif line.startswith('commit '):
-                    fgcolor = '808000'
-                elif line.startswith('    '):
-                    fgcolor = '404040'
-
-                # Chop the trailing LF, we don't want it inside <pre>.
-                line = html_escape(line[:-1])
-
-                if bgcolor or fgcolor:
-                    style = 'display:block; white-space:pre;'
-                    if bgcolor:
-                        style += 'background:#' + bgcolor + ';'
-                    if fgcolor:
-                        style += 'color:#' + fgcolor + ';'
-                    # Use a <span style='display:block> to color the
-                    # whole line. The newline must be inside the span
-                    # to display properly both in Firefox and in
-                    # text-based browser.
-                    line = "<span style='%s'>%s\n</span>" % (style, line)
-                else:
-                    line = line + '\n'
-
-            yield line
-        if self._contains_html_diff:
-            yield '</pre>'
-        html_escape_val = (self.environment.html_in_footer and
-                           self._contains_html_diff)
-        footer = self.generate_email_footer(html_escape_val)
-        if not self.environment.html_in_footer:
-            footer = self._wrap_for_html(footer)
-        for line in footer:
-            yield line
-
-    def get_specific_fromaddr(self):
-        """For kinds of Changes which specify it, return the kind-specific
-        From address to use."""
-        return None
-
-
-class Revision(Change):
-    """A Change consisting of a single git commit."""
-
-    CC_RE = re.compile(r'^\s*C[Cc]:\s*(?P<to>[^#]+@[^\s#]*)\s*(#.*)?$')
-
-    def __init__(self, reference_change, rev, num, tot):
-        Change.__init__(self, reference_change.environment)
-        self.reference_change = reference_change
-        self.rev = rev
-        self.change_type = self.reference_change.change_type
-        self.refname = self.reference_change.refname
-        self.num = num
-        self.tot = tot
-        self.author = read_git_output(['log', '--no-walk', '--format=%aN <%aE>', self.rev.sha1])
-        self.recipients = self.environment.get_revision_recipients(self)
-
-        # -s is short for --no-patch, but -s works on older git's (e.g. 1.7)
-        self.parents = read_git_lines(['show', '-s', '--format=%P',
-                                      self.rev.sha1])[0].split()
-
-        self.cc_recipients = ''
-        if self.environment.get_scancommitforcc():
-            self.cc_recipients = ', '.join(to.strip() for to in self._cc_recipients())
-            if self.cc_recipients:
-                self.environment.log_msg(
-                    'Add %s to CC for %s' % (self.cc_recipients, self.rev.sha1))
-
-    def _cc_recipients(self):
-        cc_recipients = []
-        message = read_git_output(['log', '--no-walk', '--format=%b', self.rev.sha1])
-        lines = message.strip().split('\n')
-        for line in lines:
-            m = re.match(self.CC_RE, line)
-            if m:
-                cc_recipients.append(m.group('to'))
-
-        return cc_recipients
-
-    def _compute_values(self):
-        values = Change._compute_values(self)
-
-        oneline = read_git_output(
-            ['log', '--format=%s', '--no-walk', self.rev.sha1]
-            )
-
-        max_subject_length = self.environment.get_max_subject_length()
-        if max_subject_length > 0 and len(oneline) > max_subject_length:
-            oneline = oneline[:max_subject_length - 6] + ' [...]'
-
-        values['rev'] = self.rev.sha1
-        values['parents'] = ' '.join(self.parents)
-        values['rev_short'] = self.rev.short
-        values['change_type'] = self.change_type
-        values['refname'] = self.refname
-        values['newrev'] = self.rev.sha1
-        values['short_refname'] = self.reference_change.short_refname
-        values['refname_type'] = self.reference_change.refname_type
-        values['reply_to_msgid'] = self.reference_change.msgid
-        values['thread_index'] = self.reference_change.thread_index
-        values['num'] = self.num
-        values['tot'] = self.tot
-        values['recipients'] = self.recipients
-        if self.cc_recipients:
-            values['cc_recipients'] = self.cc_recipients
-        values['oneline'] = oneline
-        values['author'] = self.author
-
-        reply_to = self.environment.get_reply_to_commit(self)
-        if reply_to:
-            values['reply_to'] = reply_to
-
-        return values
-
-    def generate_email_header(self, **extra_values):
-        for line in self.expand_header_lines(
-                REVISION_HEADER_TEMPLATE, **extra_values
-                ):
-            yield line
-
-    def generate_browse_link(self, base_url):
-        if '%(' not in base_url:
-            base_url += '%(id)s'
-        url = "".join(self.expand_lines(base_url))
-        if self._content_type == 'html':
-            for line in self.expand_lines(LINK_HTML_TEMPLATE,
-                                          html_escape_val=True,
-                                          browse_url=url):
-                yield line
-        elif self._content_type == 'plain':
-            for line in self.expand_lines(LINK_TEXT_TEMPLATE,
-                                          html_escape_val=False,
-                                          browse_url=url):
-                yield line
-        else:
-            raise NotImplementedError("Content-type %s unsupported. Please report it as a bug.")
-
-    def generate_email_intro(self, html_escape_val=False):
-        for line in self.expand_lines(REVISION_INTRO_TEMPLATE,
-                                      html_escape_val=html_escape_val):
-            yield line
-
-    def generate_email_body(self, push):
-        """Show this revision."""
-
-        for line in read_git_lines(
-                ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],
-                keepends=True,
-                errors='replace'):
-            if line.startswith('Date:   ') and self.environment.date_substitute:
-                yield self.environment.date_substitute + line[len('Date:   '):]
-            else:
-                yield line
-
-    def generate_email_footer(self, html_escape_val):
-        return self.expand_lines(REVISION_FOOTER_TEMPLATE,
-                                 html_escape_val=html_escape_val)
-
-    def generate_email(self, push, body_filter=None, extra_header_values={}):
-        self._contains_diff()
-        return Change.generate_email(self, push, body_filter, extra_header_values)
-
-    def get_specific_fromaddr(self):
-        return self.environment.from_commit
-
-
-class ReferenceChange(Change):
-    """A Change to a Git reference.
-
-    An abstract class representing a create, update, or delete of a
-    Git reference.  Derived classes handle specific types of reference
-    (e.g., tags vs. branches).  These classes generate the main
-    reference change email summarizing the reference change and
-    whether it caused any any commits to be added or removed.
-
-    ReferenceChange objects are usually created using the static
-    create() method, which has the logic to decide which derived class
-    to instantiate."""
-
-    REF_RE = re.compile(r'^refs\/(?P<area>[^\/]+)\/(?P<shortname>.*)$')
-
-    @staticmethod
-    def create(environment, oldrev, newrev, refname):
-        """Return a ReferenceChange object representing the change.
-
-        Return an object that represents the type of change that is being
-        made. oldrev and newrev should be SHA1s or ZEROS."""
-
-        old = GitObject(oldrev)
-        new = GitObject(newrev)
-        rev = new or old
-
-        # The revision type tells us what type the commit is, combined with
-        # the location of the ref we can decide between
-        #  - working branch
-        #  - tracking branch
-        #  - unannotated tag
-        #  - annotated tag
-        m = ReferenceChange.REF_RE.match(refname)
-        if m:
-            area = m.group('area')
-            short_refname = m.group('shortname')
-        else:
-            area = ''
-            short_refname = refname
-
-        if rev.type == 'tag':
-            # Annotated tag:
-            klass = AnnotatedTagChange
-        elif rev.type == 'commit':
-            if area == 'tags':
-                # Non-annotated tag:
-                klass = NonAnnotatedTagChange
-            elif area == 'heads':
-                # Branch:
-                klass = BranchChange
-            elif area == 'remotes':
-                # Tracking branch:
-                environment.log_warning(
-                    '*** Push-update of tracking branch %r\n'
-                    '***  - incomplete email generated.'
-                    % (refname,)
-                    )
-                klass = OtherReferenceChange
-            else:
-                # Some other reference namespace:
-                environment.log_warning(
-                    '*** Push-update of strange reference %r\n'
-                    '***  - incomplete email generated.'
-                    % (refname,)
-                    )
-                klass = OtherReferenceChange
-        else:
-            # Anything else (is there anything else?)
-            environment.log_warning(
-                '*** Unknown type of update to %r (%s)\n'
-                '***  - incomplete email generated.'
-                % (refname, rev.type,)
-                )
-            klass = OtherReferenceChange
-
-        return klass(
-            environment,
-            refname=refname, short_refname=short_refname,
-            old=old, new=new, rev=rev,
-            )
-
-    @staticmethod
-    def make_thread_index():
-        """Return a string appropriate for the Thread-Index header,
-        needed by MS Outlook to get threading right.
-
-        The format is (base64-encoded):
-        - 1 byte must be 1
-        - 5 bytes encode a date (hardcoded here)
-        - 16 bytes for a globally unique identifier
-
-        FIXME: Unfortunately, even with the Thread-Index field, MS
-        Outlook doesn't seem to do the threading reliably (see
-        https://github.com/git-multimail/git-multimail/pull/194).
-        """
-        thread_index = b'\x01\x00\x00\x12\x34\x56' + uuid.uuid4().bytes
-        return base64.standard_b64encode(thread_index).decode('ascii')
-
-    def __init__(self, environment, refname, short_refname, old, new, rev):
-        Change.__init__(self, environment)
-        self.change_type = {
-            (False, True): 'create',
-            (True, True): 'update',
-            (True, False): 'delete',
-            }[bool(old), bool(new)]
-        self.refname = refname
-        self.short_refname = short_refname
-        self.old = old
-        self.new = new
-        self.rev = rev
-        self.msgid = make_msgid()
-        self.thread_index = self.make_thread_index()
-        self.diffopts = environment.diffopts
-        self.graphopts = environment.graphopts
-        self.logopts = environment.logopts
-        self.commitlogopts = environment.commitlogopts
-        self.showgraph = environment.refchange_showgraph
-        self.showlog = environment.refchange_showlog
-
-        self.header_template = REFCHANGE_HEADER_TEMPLATE
-        self.intro_template = REFCHANGE_INTRO_TEMPLATE
-        self.footer_template = FOOTER_TEMPLATE
-
-    def _compute_values(self):
-        values = Change._compute_values(self)
-
-        values['change_type'] = self.change_type
-        values['refname_type'] = self.refname_type
-        values['refname'] = self.refname
-        values['short_refname'] = self.short_refname
-        values['msgid'] = self.msgid
-        values['thread_index'] = self.thread_index
-        values['recipients'] = self.recipients
-        values['oldrev'] = str(self.old)
-        values['oldrev_short'] = self.old.short
-        values['newrev'] = str(self.new)
-        values['newrev_short'] = self.new.short
-
-        if self.old:
-            values['oldrev_type'] = self.old.type
-        if self.new:
-            values['newrev_type'] = self.new.type
-
-        reply_to = self.environment.get_reply_to_refchange(self)
-        if reply_to:
-            values['reply_to'] = reply_to
-
-        return values
-
-    def send_single_combined_email(self, known_added_sha1s):
-        """Determine if a combined refchange/revision email should be sent
-
-        If there is only a single new (non-merge) commit added by a
-        change, it is useful to combine the ReferenceChange and
-        Revision emails into one.  In such a case, return the single
-        revision; otherwise, return None.
-
-        This method is overridden in BranchChange."""
-
-        return None
-
-    def generate_combined_email(self, push, revision, body_filter=None, extra_header_values={}):
-        """Generate an email describing this change AND specified revision.
-
-        Iterate over the lines (including the header lines) of an
-        email describing this change.  If body_filter is not None,
-        then use it to filter the lines that are intended for the
-        email body.
-
-        The extra_header_values field is received as a dict and not as
-        **kwargs, to allow passing other keyword arguments in the
-        future (e.g. passing extra values to generate_email_intro()
-
-        This method is overridden in BranchChange."""
-
-        raise NotImplementedError
-
-    def get_subject(self):
-        template = {
-            'create': REF_CREATED_SUBJECT_TEMPLATE,
-            'update': REF_UPDATED_SUBJECT_TEMPLATE,
-            'delete': REF_DELETED_SUBJECT_TEMPLATE,
-            }[self.change_type]
-        return self.expand(template)
-
-    def generate_email_header(self, **extra_values):
-        if 'subject' not in extra_values:
-            extra_values['subject'] = self.get_subject()
-
-        for line in self.expand_header_lines(
-                self.header_template, **extra_values
-                ):
-            yield line
-
-    def generate_email_intro(self, html_escape_val=False):
-        for line in self.expand_lines(self.intro_template,
-                                      html_escape_val=html_escape_val):
-            yield line
-
-    def generate_email_body(self, push):
-        """Call the appropriate body-generation routine.
-
-        Call one of generate_create_summary() /
-        generate_update_summary() / generate_delete_summary()."""
-
-        change_summary = {
-            'create': self.generate_create_summary,
-            'delete': self.generate_delete_summary,
-            'update': self.generate_update_summary,
-            }[self.change_type](push)
-        for line in change_summary:
-            yield line
-
-        for line in self.generate_revision_change_summary(push):
-            yield line
-
-    def generate_email_footer(self, html_escape_val):
-        return self.expand_lines(self.footer_template,
-                                 html_escape_val=html_escape_val)
-
-    def generate_revision_change_graph(self, push):
-        if self.showgraph:
-            args = ['--graph'] + self.graphopts
-            for newold in ('new', 'old'):
-                has_newold = False
-                spec = push.get_commits_spec(newold, self)
-                for line in git_log(spec, args=args, keepends=True):
-                    if not has_newold:
-                        has_newold = True
-                        yield '\n'
-                        yield 'Graph of %s commits:\n\n' % (
-                            {'new': 'new', 'old': 'discarded'}[newold],)
-                    yield '  ' + line
-                if has_newold:
-                    yield '\n'
-
-    def generate_revision_change_log(self, new_commits_list):
-        if self.showlog:
-            yield '\n'
-            yield 'Detailed log of new commits:\n\n'
-            for line in read_git_lines(
-                    ['log', '--no-walk'] +
-                    self.logopts +
-                    new_commits_list +
-                    ['--'],
-                    keepends=True,
-                    ):
-                yield line
-
-    def generate_new_revision_summary(self, tot, new_commits_list, push):
-        for line in self.expand_lines(NEW_REVISIONS_TEMPLATE, tot=tot):
-            yield line
-        for line in self.generate_revision_change_graph(push):
-            yield line
-        for line in self.generate_revision_change_log(new_commits_list):
-            yield line
-
-    def generate_revision_change_summary(self, push):
-        """Generate a summary of the revisions added/removed by this change."""
-
-        if self.new.commit_sha1 and not self.old.commit_sha1:
-            # A new reference was created.  List the new revisions
-            # brought by the new reference (i.e., those revisions that
-            # were not in the repository before this reference
-            # change).
-            sha1s = list(push.get_new_commits(self))
-            sha1s.reverse()
-            tot = len(sha1s)
-            new_revisions = [
-                Revision(self, GitObject(sha1), num=i + 1, tot=tot)
-                for (i, sha1) in enumerate(sha1s)
-                ]
-
-            if new_revisions:
-                yield self.expand('This %(refname_type)s includes the following new commits:\n')
-                yield '\n'
-                for r in new_revisions:
-                    (sha1, subject) = r.rev.get_summary()
-                    yield r.expand(
-                        BRIEF_SUMMARY_TEMPLATE, action='new', text=subject,
-                        )
-                yield '\n'
-                for line in self.generate_new_revision_summary(
-                        tot, [r.rev.sha1 for r in new_revisions], push):
-                    yield line
-            else:
-                for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE):
-                    yield line
-
-        elif self.new.commit_sha1 and self.old.commit_sha1:
-            # A reference was changed to point at a different commit.
-            # List the revisions that were removed and/or added *from
-            # that reference* by this reference change, along with a
-            # diff between the trees for its old and new values.
-
-            # List of the revisions that were added to the branch by
-            # this update.  Note this list can include revisions that
-            # have already had notification emails; we want such
-            # revisions in the summary even though we will not send
-            # new notification emails for them.
-            adds = list(generate_summaries(
-                '--topo-order', '--reverse', '%s..%s'
-                % (self.old.commit_sha1, self.new.commit_sha1,)
-                ))
-
-            # List of the revisions that were removed from the branch
-            # by this update.  This will be empty except for
-            # non-fast-forward updates.
-            discards = list(generate_summaries(
-                '%s..%s' % (self.new.commit_sha1, self.old.commit_sha1,)
-                ))
-
-            if adds:
-                new_commits_list = push.get_new_commits(self)
-            else:
-                new_commits_list = []
-            new_commits = CommitSet(new_commits_list)
-
-            if discards:
-                discarded_commits = CommitSet(push.get_discarded_commits(self))
-            else:
-                discarded_commits = CommitSet([])
-
-            if discards and adds:
-                for (sha1, subject) in discards:
-                    if sha1 in discarded_commits:
-                        action = 'discard'
-                    else:
-                        action = 'omit'
-                    yield self.expand(
-                        BRIEF_SUMMARY_TEMPLATE, action=action,
-                        rev_short=sha1, text=subject,
-                        )
-                for (sha1, subject) in adds:
-                    if sha1 in new_commits:
-                        action = 'new'
-                    else:
-                        action = 'add'
-                    yield self.expand(
-                        BRIEF_SUMMARY_TEMPLATE, action=action,
-                        rev_short=sha1, text=subject,
-                        )
-                yield '\n'
-                for line in self.expand_lines(NON_FF_TEMPLATE):
-                    yield line
-
-            elif discards:
-                for (sha1, subject) in discards:
-                    if sha1 in discarded_commits:
-                        action = 'discard'
-                    else:
-                        action = 'omit'
-                    yield self.expand(
-                        BRIEF_SUMMARY_TEMPLATE, action=action,
-                        rev_short=sha1, text=subject,
-                        )
-                yield '\n'
-                for line in self.expand_lines(REWIND_ONLY_TEMPLATE):
-                    yield line
-
-            elif adds:
-                (sha1, subject) = self.old.get_summary()
-                yield self.expand(
-                    BRIEF_SUMMARY_TEMPLATE, action='from',
-                    rev_short=sha1, text=subject,
-                    )
-                for (sha1, subject) in adds:
-                    if sha1 in new_commits:
-                        action = 'new'
-                    else:
-                        action = 'add'
-                    yield self.expand(
-                        BRIEF_SUMMARY_TEMPLATE, action=action,
-                        rev_short=sha1, text=subject,
-                        )
-
-            yield '\n'
-
-            if new_commits:
-                for line in self.generate_new_revision_summary(
-                        len(new_commits), new_commits_list, push):
-                    yield line
-            else:
-                for line in self.expand_lines(NO_NEW_REVISIONS_TEMPLATE):
-                    yield line
-                for line in self.generate_revision_change_graph(push):
-                    yield line
-
-            # The diffstat is shown from the old revision to the new
-            # revision.  This is to show the truth of what happened in
-            # this change.  There's no point showing the stat from the
-            # base to the new revision because the base is effectively a
-            # random revision at this point - the user will be interested
-            # in what this revision changed - including the undoing of
-            # previous revisions in the case of non-fast-forward updates.
-            yield '\n'
-            yield 'Summary of changes:\n'
-            for line in read_git_lines(
-                    ['diff-tree'] +
-                    self.diffopts +
-                    ['%s..%s' % (self.old.commit_sha1, self.new.commit_sha1,)],
-                    keepends=True,
-                    ):
-                yield line
-
-        elif self.old.commit_sha1 and not self.new.commit_sha1:
-            # A reference was deleted.  List the revisions that were
-            # removed from the repository by this reference change.
-
-            sha1s = list(push.get_discarded_commits(self))
-            tot = len(sha1s)
-            discarded_revisions = [
-                Revision(self, GitObject(sha1), num=i + 1, tot=tot)
-                for (i, sha1) in enumerate(sha1s)
-                ]
-
-            if discarded_revisions:
-                for line in self.expand_lines(DISCARDED_REVISIONS_TEMPLATE):
-                    yield line
-                yield '\n'
-                for r in discarded_revisions:
-                    (sha1, subject) = r.rev.get_summary()
-                    yield r.expand(
-                        BRIEF_SUMMARY_TEMPLATE, action='discard', text=subject,
-                        )
-                for line in self.generate_revision_change_graph(push):
-                    yield line
-            else:
-                for line in self.expand_lines(NO_DISCARDED_REVISIONS_TEMPLATE):
-                    yield line
-
-        elif not self.old.commit_sha1 and not self.new.commit_sha1:
-            for line in self.expand_lines(NON_COMMIT_UPDATE_TEMPLATE):
-                yield line
-
-    def generate_create_summary(self, push):
-        """Called for the creation of a reference."""
-
-        # This is a new reference and so oldrev is not valid
-        (sha1, subject) = self.new.get_summary()
-        yield self.expand(
-            BRIEF_SUMMARY_TEMPLATE, action='at',
-            rev_short=sha1, text=subject,
-            )
-        yield '\n'
-
-    def generate_update_summary(self, push):
-        """Called for the change of a pre-existing branch."""
-
-        return iter([])
-
-    def generate_delete_summary(self, push):
-        """Called for the deletion of any type of reference."""
-
-        (sha1, subject) = self.old.get_summary()
-        yield self.expand(
-            BRIEF_SUMMARY_TEMPLATE, action='was',
-            rev_short=sha1, text=subject,
-            )
-        yield '\n'
-
-    def get_specific_fromaddr(self):
-        return self.environment.from_refchange
-
-
-class BranchChange(ReferenceChange):
-    refname_type = 'branch'
-
-    def __init__(self, environment, refname, short_refname, old, new, rev):
-        ReferenceChange.__init__(
-            self, environment,
-            refname=refname, short_refname=short_refname,
-            old=old, new=new, rev=rev,
-            )
-        self.recipients = environment.get_refchange_recipients(self)
-        self._single_revision = None
-
-    def send_single_combined_email(self, known_added_sha1s):
-        if not self.environment.combine_when_single_commit:
-            return None
-
-        # In the sadly-all-too-frequent usecase of people pushing only
-        # one of their commits at a time to a repository, users feel
-        # the reference change summary emails are noise rather than
-        # important signal.  This is because, in this particular
-        # usecase, there is a reference change summary email for each
-        # new commit, and all these summaries do is point out that
-        # there is one new commit (which can readily be inferred by
-        # the existence of the individual revision email that is also
-        # sent).  In such cases, our users prefer there to be a combined
-        # reference change summary/new revision email.
-        #
-        # So, if the change is an update and it doesn't discard any
-        # commits, and it adds exactly one non-merge commit (gerrit
-        # forces a workflow where every commit is individually merged
-        # and the git-multimail hook fired off for just this one
-        # change), then we send a combined refchange/revision email.
-        try:
-            # If this change is a reference update that doesn't discard
-            # any commits...
-            if self.change_type != 'update':
-                return None
-
-            if read_git_lines(
-                    ['merge-base', self.old.sha1, self.new.sha1]
-                    ) != [self.old.sha1]:
-                return None
-
-            # Check if this update introduced exactly one non-merge
-            # commit:
-
-            def split_line(line):
-                """Split line into (sha1, [parent,...])."""
-
-                words = line.split()
-                return (words[0], words[1:])
-
-            # Get the new commits introduced by the push as a list of
-            # (sha1, [parent,...])
-            new_commits = [
-                split_line(line)
-                for line in read_git_lines(
-                    [
-                        'log', '-3', '--format=%H %P',
-                        '%s..%s' % (self.old.sha1, self.new.sha1),
-                        ]
-                    )
-                ]
-
-            if not new_commits:
-                return None
-
-            # If the newest commit is a merge, save it for a later check
-            # but otherwise ignore it
-            merge = None
-            tot = len(new_commits)
-            if len(new_commits[0][1]) > 1:
-                merge = new_commits[0][0]
-                del new_commits[0]
-
-            # Our primary check: we can't combine if more than one commit
-            # is introduced.  We also currently only combine if the new
-            # commit is a non-merge commit, though it may make sense to
-            # combine if it is a merge as well.
-            if not (
-                    len(new_commits) == 1 and
-                    len(new_commits[0][1]) == 1 and
-                    new_commits[0][0] in known_added_sha1s
-                    ):
-                return None
-
-            # We do not want to combine revision and refchange emails if
-            # those go to separate locations.
-            rev = Revision(self, GitObject(new_commits[0][0]), 1, tot)
-            if rev.recipients != self.recipients:
-                return None
-
-            # We ignored the newest commit if it was just a merge of the one
-            # commit being introduced.  But we don't want to ignore that
-            # merge commit it it involved conflict resolutions.  Check that.
-            if merge and merge != read_git_output(['diff-tree', '--cc', merge]):
-                return None
-
-            # We can combine the refchange and one new revision emails
-            # into one.  Return the Revision that a combined email should
-            # be sent about.
-            return rev
-        except CommandError:
-            # Cannot determine number of commits in old..new or new..old;
-            # don't combine reference/revision emails:
-            return None
-
-    def generate_combined_email(self, push, revision, body_filter=None, extra_header_values={}):
-        values = revision.get_values()
-        if extra_header_values:
-            values.update(extra_header_values)
-        if 'subject' not in extra_header_values:
-            values['subject'] = self.expand(COMBINED_REFCHANGE_REVISION_SUBJECT_TEMPLATE, **values)
-
-        self._single_revision = revision
-        self._contains_diff()
-        self.header_template = COMBINED_HEADER_TEMPLATE
-        self.intro_template = COMBINED_INTRO_TEMPLATE
-        self.footer_template = COMBINED_FOOTER_TEMPLATE
-
-        def revision_gen_link(base_url):
-            # revision is used only to generate the body, and
-            # _content_type is set while generating headers. Get it
-            # from the BranchChange object.
-            revision._content_type = self._content_type
-            return revision.generate_browse_link(base_url)
-        self.generate_browse_link = revision_gen_link
-        for line in self.generate_email(push, body_filter, values):
-            yield line
-
-    def generate_email_body(self, push):
-        '''Call the appropriate body generation routine.
-
-        If this is a combined refchange/revision email, the special logic
-        for handling this combined email comes from this function.  For
-        other cases, we just use the normal handling.'''
-
-        # If self._single_revision isn't set; don't override
-        if not self._single_revision:
-            for line in super(BranchChange, self).generate_email_body(push):
-                yield line
-            return
-
-        # This is a combined refchange/revision email; we first provide
-        # some info from the refchange portion, and then call the revision
-        # generate_email_body function to handle the revision portion.
-        adds = list(generate_summaries(
-            '--topo-order', '--reverse', '%s..%s'
-            % (self.old.commit_sha1, self.new.commit_sha1,)
-            ))
-
-        yield self.expand("The following commit(s) were added to %(refname)s by this push:\n")
-        for (sha1, subject) in adds:
-            yield self.expand(
-                BRIEF_SUMMARY_TEMPLATE, action='new',
-                rev_short=sha1, text=subject,
-                )
-
-        yield self._single_revision.rev.short + " is described below\n"
-        yield '\n'
-
-        for line in self._single_revision.generate_email_body(push):
-            yield line
-
-
-class AnnotatedTagChange(ReferenceChange):
-    refname_type = 'annotated tag'
-
-    def __init__(self, environment, refname, short_refname, old, new, rev):
-        ReferenceChange.__init__(
-            self, environment,
-            refname=refname, short_refname=short_refname,
-            old=old, new=new, rev=rev,
-            )
-        self.recipients = environment.get_announce_recipients(self)
-        self.show_shortlog = environment.announce_show_shortlog
-
-    ANNOTATED_TAG_FORMAT = (
-        '%(*objectname)\n'
-        '%(*objecttype)\n'
-        '%(taggername)\n'
-        '%(taggerdate)'
-        )
-
-    def describe_tag(self, push):
-        """Describe the new value of an annotated tag."""
-
-        # Use git for-each-ref to pull out the individual fields from
-        # the tag
-        [tagobject, tagtype, tagger, tagged] = read_git_lines(
-            ['for-each-ref', '--format=%s' % (self.ANNOTATED_TAG_FORMAT,), self.refname],
-            )
-
-        yield self.expand(
-            BRIEF_SUMMARY_TEMPLATE, action='tagging',
-            rev_short=tagobject, text='(%s)' % (tagtype,),
-            )
-        if tagtype == 'commit':
-            # If the tagged object is a commit, then we assume this is a
-            # release, and so we calculate which tag this tag is
-            # replacing
-            try:
-                prevtag = read_git_output(['describe', '--abbrev=0', '%s^' % (self.new,)])
-            except CommandError:
-                prevtag = None
-            if prevtag:
-                yield ' replaces %s\n' % (prevtag,)
-        else:
-            prevtag = None
-            yield '  length %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),)
-
-        yield '      by %s\n' % (tagger,)
-        yield '      on %s\n' % (tagged,)
-        yield '\n'
-
-        # Show the content of the tag message; this might contain a
-        # change log or release notes so is worth displaying.
-        yield LOGBEGIN
-        contents = list(read_git_lines(['cat-file', 'tag', self.new.sha1], keepends=True))
-        contents = contents[contents.index('\n') + 1:]
-        if contents and contents[-1][-1:] != '\n':
-            contents.append('\n')
-        for line in contents:
-            yield line
-
-        if self.show_shortlog and tagtype == 'commit':
-            # Only commit tags make sense to have rev-list operations
-            # performed on them
-            yield '\n'
-            if prevtag:
-                # Show changes since the previous release
-                revlist = read_git_output(
-                    ['rev-list', '--pretty=short', '%s..%s' % (prevtag, self.new,)],
-                    keepends=True,
-                    )
-            else:
-                # No previous tag, show all the changes since time
-                # began
-                revlist = read_git_output(
-                    ['rev-list', '--pretty=short', '%s' % (self.new,)],
-                    keepends=True,
-                    )
-            for line in read_git_lines(['shortlog'], input=revlist, keepends=True):
-                yield line
-
-        yield LOGEND
-        yield '\n'
-
-    def generate_create_summary(self, push):
-        """Called for the creation of an annotated tag."""
-
-        for line in self.expand_lines(TAG_CREATED_TEMPLATE):
-            yield line
-
-        for line in self.describe_tag(push):
-            yield line
-
-    def generate_update_summary(self, push):
-        """Called for the update of an annotated tag.
-
-        This is probably a rare event and may not even be allowed."""
-
-        for line in self.expand_lines(TAG_UPDATED_TEMPLATE):
-            yield line
-
-        for line in self.describe_tag(push):
-            yield line
-
-    def generate_delete_summary(self, push):
-        """Called when a non-annotated reference is updated."""
-
-        for line in self.expand_lines(TAG_DELETED_TEMPLATE):
-            yield line
-
-        yield self.expand('   tag was  %(oldrev_short)s\n')
-        yield '\n'
-
-
-class NonAnnotatedTagChange(ReferenceChange):
-    refname_type = 'tag'
-
-    def __init__(self, environment, refname, short_refname, old, new, rev):
-        ReferenceChange.__init__(
-            self, environment,
-            refname=refname, short_refname=short_refname,
-            old=old, new=new, rev=rev,
-            )
-        self.recipients = environment.get_refchange_recipients(self)
-
-    def generate_create_summary(self, push):
-        """Called for the creation of an annotated tag."""
-
-        for line in self.expand_lines(TAG_CREATED_TEMPLATE):
-            yield line
-
-    def generate_update_summary(self, push):
-        """Called when a non-annotated reference is updated."""
-
-        for line in self.expand_lines(TAG_UPDATED_TEMPLATE):
-            yield line
-
-    def generate_delete_summary(self, push):
-        """Called when a non-annotated reference is updated."""
-
-        for line in self.expand_lines(TAG_DELETED_TEMPLATE):
-            yield line
-
-        for line in ReferenceChange.generate_delete_summary(self, push):
-            yield line
-
-
-class OtherReferenceChange(ReferenceChange):
-    refname_type = 'reference'
-
-    def __init__(self, environment, refname, short_refname, old, new, rev):
-        # We use the full refname as short_refname, because otherwise
-        # the full name of the reference would not be obvious from the
-        # text of the email.
-        ReferenceChange.__init__(
-            self, environment,
-            refname=refname, short_refname=refname,
-            old=old, new=new, rev=rev,
-            )
-        self.recipients = environment.get_refchange_recipients(self)
-
-
-class Mailer(object):
-    """An object that can send emails."""
-
-    def __init__(self, environment):
-        self.environment = environment
-
-    def close(self):
-        pass
-
-    def send(self, lines, to_addrs):
-        """Send an email consisting of lines.
-
-        lines must be an iterable over the lines constituting the
-        header and body of the email.  to_addrs is a list of recipient
-        addresses (can be needed even if lines already contains a
-        "To:" field).  It can be either a string (comma-separated list
-        of email addresses) or a Python list of individual email
-        addresses.
-
-        """
-
-        raise NotImplementedError()
-
-
-class SendMailer(Mailer):
-    """Send emails using 'sendmail -oi -t'."""
-
-    SENDMAIL_CANDIDATES = [
-        '/usr/sbin/sendmail',
-        '/usr/lib/sendmail',
-        ]
-
-    @staticmethod
-    def find_sendmail():
-        for path in SendMailer.SENDMAIL_CANDIDATES:
-            if os.access(path, os.X_OK):
-                return path
-        else:
-            raise ConfigurationException(
-                'No sendmail executable found.  '
-                'Try setting multimailhook.sendmailCommand.'
-                )
-
-    def __init__(self, environment, command=None, envelopesender=None):
-        """Construct a SendMailer instance.
-
-        command should be the command and arguments used to invoke
-        sendmail, as a list of strings.  If an envelopesender is
-        provided, it will also be passed to the command, via '-f
-        envelopesender'."""
-        super(SendMailer, self).__init__(environment)
-        if command:
-            self.command = command[:]
-        else:
-            self.command = [self.find_sendmail(), '-oi', '-t']
-
-        if envelopesender:
-            self.command.extend(['-f', envelopesender])
-
-    def send(self, lines, to_addrs):
-        try:
-            p = subprocess.Popen(self.command, stdin=subprocess.PIPE)
-        except OSError:
-            self.environment.get_logger().error(
-                '*** Cannot execute command: %s\n' % ' '.join(self.command) +
-                '*** %s\n' % sys.exc_info()[1] +
-                '*** Try setting multimailhook.mailer to "smtp"\n' +
-                '*** to send emails without using the sendmail command.\n'
-                )
-            sys.exit(1)
-        try:
-            lines = (str_to_bytes(line) for line in lines)
-            p.stdin.writelines(lines)
-        except Exception:
-            self.environment.get_logger().error(
-                '*** Error while generating commit email\n'
-                '***  - mail sending aborted.\n'
-                )
-            if hasattr(p, 'terminate'):
-                # subprocess.terminate() is not available in Python 2.4
-                p.terminate()
-            else:
-                import signal
-                os.kill(p.pid, signal.SIGTERM)
-            raise
-        else:
-            p.stdin.close()
-            retcode = p.wait()
-            if retcode:
-                raise CommandError(self.command, retcode)
-
-
-class SMTPMailer(Mailer):
-    """Send emails using Python's smtplib."""
-
-    def __init__(self, environment,
-                 envelopesender, smtpserver,
-                 smtpservertimeout=10.0, smtpserverdebuglevel=0,
-                 smtpencryption='none',
-                 smtpuser='', smtppass='',
-                 smtpcacerts=''
-                 ):
-        super(SMTPMailer, self).__init__(environment)
-        if not envelopesender:
-            self.environment.get_logger().error(
-                'fatal: git_multimail: cannot use SMTPMailer without a sender address.\n'
-                'please set either multimailhook.envelopeSender or user.email\n'
-                )
-            sys.exit(1)
-        if smtpencryption == 'ssl' and not (smtpuser and smtppass):
-            raise ConfigurationException(
-                'Cannot use SMTPMailer with security option ssl '
-                'without options username and password.'
-                )
-        self.envelopesender = envelopesender
-        self.smtpserver = smtpserver
-        self.smtpservertimeout = smtpservertimeout
-        self.smtpserverdebuglevel = smtpserverdebuglevel
-        self.security = smtpencryption
-        self.username = smtpuser
-        self.password = smtppass
-        self.smtpcacerts = smtpcacerts
-        self.loggedin = False
-        try:
-            def call(klass, server, timeout):
-                try:
-                    return klass(server, timeout=timeout)
-                except TypeError:
-                    # Old Python versions do not have timeout= argument.
-                    return klass(server)
-            if self.security == 'none':
-                self.smtp = call(smtplib.SMTP, self.smtpserver, timeout=self.smtpservertimeout)
-            elif self.security == 'ssl':
-                if self.smtpcacerts:
-                    raise smtplib.SMTPException(
-                        "Checking certificate is not supported for ssl, prefer starttls"
-                        )
-                self.smtp = call(smtplib.SMTP_SSL, self.smtpserver, timeout=self.smtpservertimeout)
-            elif self.security == 'tls':
-                if 'ssl' not in sys.modules:
-                    self.environment.get_logger().error(
-                        '*** Your Python version does not have the ssl library installed\n'
-                        '*** smtpEncryption=tls is not available.\n'
-                        '*** Either upgrade Python to 2.6 or later\n'
-                        '    or use git_multimail.py version 1.2.\n')
-                if ':' not in self.smtpserver:
-                    self.smtpserver += ':587'  # default port for TLS
-                self.smtp = call(smtplib.SMTP, self.smtpserver, timeout=self.smtpservertimeout)
-                # start: ehlo + starttls
-                # equivalent to
-                #     self.smtp.ehlo()
-                #     self.smtp.starttls()
-                # with access to the ssl layer
-                self.smtp.ehlo()
-                if not self.smtp.has_extn("starttls"):
-                    raise smtplib.SMTPException("STARTTLS extension not supported by server")
-                resp, reply = self.smtp.docmd("STARTTLS")
-                if resp != 220:
-                    raise smtplib.SMTPException("Wrong answer to the STARTTLS command")
-                if self.smtpcacerts:
-                    self.smtp.sock = ssl.wrap_socket(
-                        self.smtp.sock,
-                        ca_certs=self.smtpcacerts,
-                        cert_reqs=ssl.CERT_REQUIRED
-                        )
-                else:
-                    self.smtp.sock = ssl.wrap_socket(
-                        self.smtp.sock,
-                        cert_reqs=ssl.CERT_NONE
-                        )
-                    self.environment.get_logger().error(
-                        '*** Warning, the server certificate is not verified (smtp) ***\n'
-                        '***          set the option smtpCACerts                   ***\n'
-                        )
-                if not hasattr(self.smtp.sock, "read"):
-                    # using httplib.FakeSocket with Python 2.5.x or earlier
-                    self.smtp.sock.read = self.smtp.sock.recv
-                self.smtp.file = smtplib.SSLFakeFile(self.smtp.sock)
-                self.smtp.helo_resp = None
-                self.smtp.ehlo_resp = None
-                self.smtp.esmtp_features = {}
-                self.smtp.does_esmtp = 0
-                # end:   ehlo + starttls
-                self.smtp.ehlo()
-            else:
-                sys.stdout.write('*** Error: Control reached an invalid option. ***')
-                sys.exit(1)
-            if self.smtpserverdebuglevel > 0:
-                sys.stdout.write(
-                    "*** Setting debug on for SMTP server connection (%s) ***\n"
-                    % self.smtpserverdebuglevel)
-                self.smtp.set_debuglevel(self.smtpserverdebuglevel)
-        except Exception:
-            self.environment.get_logger().error(
-                '*** Error establishing SMTP connection to %s ***\n'
-                '*** %s\n'
-                % (self.smtpserver, sys.exc_info()[1]))
-            sys.exit(1)
-
-    def close(self):
-        if hasattr(self, 'smtp'):
-            self.smtp.quit()
-            del self.smtp
-
-    def __del__(self):
-        self.close()
-
-    def send(self, lines, to_addrs):
-        try:
-            if self.username or self.password:
-                if not self.loggedin:
-                    self.smtp.login(self.username, self.password)
-                    self.loggedin = True
-            msg = ''.join(lines)
-            # turn comma-separated list into Python list if needed.
-            if is_string(to_addrs):
-                to_addrs = [email for (name, email) in getaddresses([to_addrs])]
-            self.smtp.sendmail(self.envelopesender, to_addrs, msg)
-        except socket.timeout:
-            self.environment.get_logger().error(
-                '*** Error sending email ***\n'
-                '*** SMTP server timed out (timeout is %s)\n'
-                % self.smtpservertimeout)
-        except smtplib.SMTPResponseException:
-            err = sys.exc_info()[1]
-            self.environment.get_logger().error(
-                '*** Error sending email ***\n'
-                '*** Error %d: %s\n'
-                % (err.smtp_code, bytes_to_str(err.smtp_error)))
-            try:
-                smtp = self.smtp
-                # delete the field before quit() so that in case of
-                # error, self.smtp is deleted anyway.
-                del self.smtp
-                smtp.quit()
-            except:
-                self.environment.get_logger().error(
-                    '*** Error closing the SMTP connection ***\n'
-                    '*** Exiting anyway ... ***\n'
-                    '*** %s\n' % sys.exc_info()[1])
-            sys.exit(1)
-
-
-class OutputMailer(Mailer):
-    """Write emails to an output stream, bracketed by lines of '=' characters.
-
-    This is intended for debugging purposes."""
-
-    SEPARATOR = '=' * 75 + '\n'
-
-    def __init__(self, f, environment=None):
-        super(OutputMailer, self).__init__(environment=environment)
-        self.f = f
-
-    def send(self, lines, to_addrs):
-        write_str(self.f, self.SEPARATOR)
-        for line in lines:
-            write_str(self.f, line)
-        write_str(self.f, self.SEPARATOR)
-
-
-def get_git_dir():
-    """Determine GIT_DIR.
-
-    Determine GIT_DIR either from the GIT_DIR environment variable or
-    from the working directory, using Git's usual rules."""
-
-    try:
-        return read_git_output(['rev-parse', '--git-dir'])
-    except CommandError:
-        sys.stderr.write('fatal: git_multimail: not in a git directory\n')
-        sys.exit(1)
-
-
-class Environment(object):
-    """Describes the environment in which the push is occurring.
-
-    An Environment object encapsulates information about the local
-    environment.  For example, it knows how to determine:
-
-    * the name of the repository to which the push occurred
-
-    * what user did the push
-
-    * what users want to be informed about various types of changes.
-
-    An Environment object is expected to have the following methods:
-
-        get_repo_shortname()
-
-            Return a short name for the repository, for display
-            purposes.
-
-        get_repo_path()
-
-            Return the absolute path to the Git repository.
-
-        get_emailprefix()
-
-            Return a string that will be prefixed to every email's
-            subject.
-
-        get_pusher()
-
-            Return the username of the person who pushed the changes.
-            This value is used in the email body to indicate who
-            pushed the change.
-
-        get_pusher_email() (may return None)
-
-            Return the email address of the person who pushed the
-            changes.  The value should be a single RFC 2822 email
-            address as a string; e.g., "Joe User <user@example.com>"
-            if available, otherwise "user@example.com".  If set, the
-            value is used as the Reply-To address for refchange
-            emails.  If it is impossible to determine the pusher's
-            email, this attribute should be set to None (in which case
-            no Reply-To header will be output).
-
-        get_sender()
-
-            Return the address to be used as the 'From' email address
-            in the email envelope.
-
-        get_fromaddr(change=None)
-
-            Return the 'From' email address used in the email 'From:'
-            headers.  If the change is known when this function is
-            called, it is passed in as the 'change' parameter.  (May
-            be a full RFC 2822 email address like 'Joe User
-            <user@example.com>'.)
-
-        get_administrator()
-
-            Return the name and/or email of the repository
-            administrator.  This value is used in the footer as the
-            person to whom requests to be removed from the
-            notification list should be sent.  Ideally, it should
-            include a valid email address.
-
-        get_reply_to_refchange()
-        get_reply_to_commit()
-
-            Return the address to use in the email "Reply-To" header,
-            as a string.  These can be an RFC 2822 email address, or
-            None to omit the "Reply-To" header.
-            get_reply_to_refchange() is used for refchange emails;
-            get_reply_to_commit() is used for individual commit
-            emails.
-
-        get_ref_filter_regex()
-
-            Return a tuple -- a compiled regex, and a boolean indicating
-            whether the regex picks refs to include (if False, the regex
-            matches on refs to exclude).
-
-        get_default_ref_ignore_regex()
-
-            Return a regex that should be ignored for both what emails
-            to send and when computing what commits are considered new
-            to the repository.  Default is "^refs/notes/".
-
-        get_max_subject_length()
-
-            Return an int giving the maximal length for the subject
-            (git log --oneline).
-
-    They should also define the following attributes:
-
-        announce_show_shortlog (bool)
-
-            True iff announce emails should include a shortlog.
-
-        commit_email_format (string)
-
-            If "html", generate commit emails in HTML instead of plain text
-            used by default.
-
-        html_in_intro (bool)
-        html_in_footer (bool)
-
-            When generating HTML emails, the introduction (respectively,
-            the footer) will be HTML-escaped iff html_in_intro (respectively,
-            the footer) is true. When false, only the values used to expand
-            the template are escaped.
-
-        refchange_showgraph (bool)
-
-            True iff refchanges emails should include a detailed graph.
-
-        refchange_showlog (bool)
-
-            True iff refchanges emails should include a detailed log.
-
-        diffopts (list of strings)
-
-            The options that should be passed to 'git diff' for the
-            summary email.  The value should be a list of strings
-            representing words to be passed to the command.
-
-        graphopts (list of strings)
-
-            Analogous to diffopts, but contains options passed to
-            'git log --graph' when generating the detailed graph for
-            a set of commits (see refchange_showgraph)
-
-        logopts (list of strings)
-
-            Analogous to diffopts, but contains options passed to
-            'git log' when generating the detailed log for a set of
-            commits (see refchange_showlog)
-
-        commitlogopts (list of strings)
-
-            The options that should be passed to 'git log' for each
-            commit mail.  The value should be a list of strings
-            representing words to be passed to the command.
-
-        date_substitute (string)
-
-            String to be used in substitution for 'Date:' at start of
-            line in the output of 'git log'.
-
-        quiet (bool)
-            On success do not write to stderr
-
-        stdout (bool)
-            Write email to stdout rather than emailing. Useful for debugging
-
-        combine_when_single_commit (bool)
-
-            True if a combined email should be produced when a single
-            new commit is pushed to a branch, False otherwise.
-
-        from_refchange, from_commit (strings)
-
-            Addresses to use for the From: field for refchange emails
-            and commit emails respectively.  Set from
-            multimailhook.fromRefchange and multimailhook.fromCommit
-            by ConfigEnvironmentMixin.
-
-        log_file, error_log_file, debug_log_file (string)
-
-            Name of a file to which logs should be sent.
-
-        verbose (int)
-
-            How verbose the system should be.
-            - 0 (default): show info, errors, ...
-            - 1 : show basic debug info
-    """
-
-    REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\.git)$')
-
-    def __init__(self, osenv=None):
-        self.osenv = osenv or os.environ
-        self.announce_show_shortlog = False
-        self.commit_email_format = "text"
-        self.html_in_intro = False
-        self.html_in_footer = False
-        self.commitBrowseURL = None
-        self.maxcommitemails = 500
-        self.excludemergerevisions = False
-        self.diffopts = ['--stat', '--summary', '--find-copies-harder']
-        self.graphopts = ['--oneline', '--decorate']
-        self.logopts = []
-        self.refchange_showgraph = False
-        self.refchange_showlog = False
-        self.commitlogopts = ['-C', '--stat', '-p', '--cc']
-        self.date_substitute = 'AuthorDate: '
-        self.quiet = False
-        self.stdout = False
-        self.combine_when_single_commit = True
-        self.logger = None
-
-        self.COMPUTED_KEYS = [
-            'administrator',
-            'charset',
-            'emailprefix',
-            'pusher',
-            'pusher_email',
-            'repo_path',
-            'repo_shortname',
-            'sender',
-            ]
-
-        self._values = None
-
-    def get_logger(self):
-        """Get (possibly creates) the logger associated to this environment."""
-        if self.logger is None:
-            self.logger = Logger(self)
-        return self.logger
-
-    def get_repo_shortname(self):
-        """Use the last part of the repo path, with ".git" stripped off if present."""
-
-        basename = os.path.basename(os.path.abspath(self.get_repo_path()))
-        m = self.REPO_NAME_RE.match(basename)
-        if m:
-            return m.group('name')
-        else:
-            return basename
-
-    def get_pusher(self):
-        raise NotImplementedError()
-
-    def get_pusher_email(self):
-        return None
-
-    def get_fromaddr(self, change=None):
-        config = Config('user')
-        fromname = config.get('name', default='')
-        fromemail = config.get('email', default='')
-        if fromemail:
-            return formataddr([fromname, fromemail])
-        return self.get_sender()
-
-    def get_administrator(self):
-        return 'the administrator of this repository'
-
-    def get_emailprefix(self):
-        return ''
-
-    def get_repo_path(self):
-        if read_git_output(['rev-parse', '--is-bare-repository']) == 'true':
-            path = get_git_dir()
-        else:
-            path = read_git_output(['rev-parse', '--show-toplevel'])
-        return os.path.abspath(path)
-
-    def get_charset(self):
-        return CHARSET
-
-    def get_values(self):
-        """Return a dictionary {keyword: expansion} for this Environment.
-
-        This method is called by Change._compute_values().  The keys
-        in the returned dictionary are available to be used in any of
-        the templates.  The dictionary is created by calling
-        self.get_NAME() for each of the attributes named in
-        COMPUTED_KEYS and recording those that do not return None.
-        The return value is always a new dictionary."""
-
-        if self._values is None:
-            values = {'': ''}  # %()s expands to the empty string.
-
-            for key in self.COMPUTED_KEYS:
-                value = getattr(self, 'get_%s' % (key,))()
-                if value is not None:
-                    values[key] = value
-
-            self._values = values
-
-        return self._values.copy()
-
-    def get_refchange_recipients(self, refchange):
-        """Return the recipients for notifications about refchange.
-
-        Return the list of email addresses to which notifications
-        about the specified ReferenceChange should be sent."""
-
-        raise NotImplementedError()
-
-    def get_announce_recipients(self, annotated_tag_change):
-        """Return the recipients for notifications about annotated_tag_change.
-
-        Return the list of email addresses to which notifications
-        about the specified AnnotatedTagChange should be sent."""
-
-        raise NotImplementedError()
-
-    def get_reply_to_refchange(self, refchange):
-        return self.get_pusher_email()
-
-    def get_revision_recipients(self, revision):
-        """Return the recipients for messages about revision.
-
-        Return the list of email addresses to which notifications
-        about the specified Revision should be sent.  This method
-        could be overridden, for example, to take into account the
-        contents of the revision when deciding whom to notify about
-        it.  For example, there could be a scheme for users to express
-        interest in particular files or subdirectories, and only
-        receive notification emails for revisions that affecting those
-        files."""
-
-        raise NotImplementedError()
-
-    def get_reply_to_commit(self, revision):
-        return revision.author
-
-    def get_default_ref_ignore_regex(self):
-        # The commit messages of git notes are essentially meaningless
-        # and "filenames" in git notes commits are an implementational
-        # detail that might surprise users at first.  As such, we
-        # would need a completely different method for handling emails
-        # of git notes in order for them to be of benefit for users,
-        # which we simply do not have right now.
-        return "^refs/notes/"
-
-    def get_max_subject_length(self):
-        """Return the maximal subject line (git log --oneline) length.
-        Longer subject lines will be truncated."""
-        raise NotImplementedError()
-
-    def filter_body(self, lines):
-        """Filter the lines intended for an email body.
-
-        lines is an iterable over the lines that would go into the
-        email body.  Filter it (e.g., limit the number of lines, the
-        line length, character set, etc.), returning another iterable.
-        See FilterLinesEnvironmentMixin and MaxlinesEnvironmentMixin
-        for classes implementing this functionality."""
-
-        return lines
-
-    def log_msg(self, msg):
-        """Write the string msg on a log file or on stderr.
-
-        Sends the text to stderr by default, override to change the behavior."""
-        self.get_logger().info(msg)
-
-    def log_warning(self, msg):
-        """Write the string msg on a log file or on stderr.
-
-        Sends the text to stderr by default, override to change the behavior."""
-        self.get_logger().warning(msg)
-
-    def log_error(self, msg):
-        """Write the string msg on a log file or on stderr.
-
-        Sends the text to stderr by default, override to change the behavior."""
-        self.get_logger().error(msg)
-
-    def check(self):
-        pass
-
-
-class ConfigEnvironmentMixin(Environment):
-    """A mixin that sets self.config to its constructor's config argument.
-
-    This class's constructor consumes the "config" argument.
-
-    Mixins that need to inspect the config should inherit from this
-    class (1) to make sure that "config" is still in the constructor
-    arguments with its own constructor runs and/or (2) to be sure that
-    self.config is set after construction."""
-
-    def __init__(self, config, **kw):
-        super(ConfigEnvironmentMixin, self).__init__(**kw)
-        self.config = config
-
-
-class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):
-    """An Environment that reads most of its information from "git config"."""
-
-    @staticmethod
-    def forbid_field_values(name, value, forbidden):
-        for forbidden_val in forbidden:
-            if value is not None and value.lower() == forbidden:
-                raise ConfigurationException(
-                    '"%s" is not an allowed setting for %s' % (value, name)
-                    )
-
-    def __init__(self, config, **kw):
-        super(ConfigOptionsEnvironmentMixin, self).__init__(
-            config=config, **kw
-            )
-
-        for var, cfg in (
-                ('announce_show_shortlog', 'announceshortlog'),
-                ('refchange_showgraph', 'refchangeShowGraph'),
-                ('refchange_showlog', 'refchangeshowlog'),
-                ('quiet', 'quiet'),
-                ('stdout', 'stdout'),
-                ):
-            val = config.get_bool(cfg)
-            if val is not None:
-                setattr(self, var, val)
-
-        commit_email_format = config.get('commitEmailFormat')
-        if commit_email_format is not None:
-            if commit_email_format != "html" and commit_email_format != "text":
-                self.log_warning(
-                    '*** Unknown value for multimailhook.commitEmailFormat: %s\n' %
-                    commit_email_format +
-                    '*** Expected either "text" or "html".  Ignoring.\n'
-                    )
-            else:
-                self.commit_email_format = commit_email_format
-
-        html_in_intro = config.get_bool('htmlInIntro')
-        if html_in_intro is not None:
-            self.html_in_intro = html_in_intro
-
-        html_in_footer = config.get_bool('htmlInFooter')
-        if html_in_footer is not None:
-            self.html_in_footer = html_in_footer
-
-        self.commitBrowseURL = config.get('commitBrowseURL')
-
-        self.excludemergerevisions = config.get('excludeMergeRevisions')
-
-        maxcommitemails = config.get('maxcommitemails')
-        if maxcommitemails is not None:
-            try:
-                self.maxcommitemails = int(maxcommitemails)
-            except ValueError:
-                self.log_warning(
-                    '*** Malformed value for multimailhook.maxCommitEmails: %s\n'
-                    % maxcommitemails +
-                    '*** Expected a number.  Ignoring.\n'
-                    )
-
-        diffopts = config.get('diffopts')
-        if diffopts is not None:
-            self.diffopts = shlex.split(diffopts)
-
-        graphopts = config.get('graphOpts')
-        if graphopts is not None:
-            self.graphopts = shlex.split(graphopts)
-
-        logopts = config.get('logopts')
-        if logopts is not None:
-            self.logopts = shlex.split(logopts)
-
-        commitlogopts = config.get('commitlogopts')
-        if commitlogopts is not None:
-            self.commitlogopts = shlex.split(commitlogopts)
-
-        date_substitute = config.get('dateSubstitute')
-        if date_substitute == 'none':
-            self.date_substitute = None
-        elif date_substitute is not None:
-            self.date_substitute = date_substitute
-
-        reply_to = config.get('replyTo')
-        self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to)
-        self.forbid_field_values('replyToRefchange',
-                                 self.__reply_to_refchange,
-                                 ['author'])
-        self.__reply_to_commit = config.get('replyToCommit', default=reply_to)
-
-        self.from_refchange = config.get('fromRefchange')
-        self.forbid_field_values('fromRefchange',
-                                 self.from_refchange,
-                                 ['author', 'none'])
-        self.from_commit = config.get('fromCommit')
-        self.forbid_field_values('fromCommit',
-                                 self.from_commit,
-                                 ['none'])
-
-        combine = config.get_bool('combineWhenSingleCommit')
-        if combine is not None:
-            self.combine_when_single_commit = combine
-
-        self.log_file = config.get('logFile', default=None)
-        self.error_log_file = config.get('errorLogFile', default=None)
-        self.debug_log_file = config.get('debugLogFile', default=None)
-        if config.get_bool('Verbose', default=False):
-            self.verbose = 1
-        else:
-            self.verbose = 0
-
-    def get_administrator(self):
-        return (
-            self.config.get('administrator') or
-            self.get_sender() or
-            super(ConfigOptionsEnvironmentMixin, self).get_administrator()
-            )
-
-    def get_repo_shortname(self):
-        return (
-            self.config.get('reponame') or
-            super(ConfigOptionsEnvironmentMixin, self).get_repo_shortname()
-            )
-
-    def get_emailprefix(self):
-        emailprefix = self.config.get('emailprefix')
-        if emailprefix is not None:
-            emailprefix = emailprefix.strip()
-            if emailprefix:
-                emailprefix += ' '
-        else:
-            emailprefix = '[%(repo_shortname)s] '
-        short_name = self.get_repo_shortname()
-        try:
-            return emailprefix % {'repo_shortname': short_name}
-        except:
-            self.get_logger().error(
-                '*** Invalid multimailhook.emailPrefix: %s\n' % emailprefix +
-                '*** %s\n' % sys.exc_info()[1] +
-                "*** Only the '%(repo_shortname)s' placeholder is allowed\n"
-                )
-            raise ConfigurationException(
-                '"%s" is not an allowed setting for emailPrefix' % emailprefix
-                )
-
-    def get_sender(self):
-        return self.config.get('envelopesender')
-
-    def process_addr(self, addr, change):
-        if addr.lower() == 'author':
-            if hasattr(change, 'author'):
-                return change.author
-            else:
-                return None
-        elif addr.lower() == 'pusher':
-            return self.get_pusher_email()
-        elif addr.lower() == 'none':
-            return None
-        else:
-            return addr
-
-    def get_fromaddr(self, change=None):
-        fromaddr = self.config.get('from')
-        if change:
-            specific_fromaddr = change.get_specific_fromaddr()
-            if specific_fromaddr:
-                fromaddr = specific_fromaddr
-        if fromaddr:
-            fromaddr = self.process_addr(fromaddr, change)
-        if fromaddr:
-            return fromaddr
-        return super(ConfigOptionsEnvironmentMixin, self).get_fromaddr(change)
-
-    def get_reply_to_refchange(self, refchange):
-        if self.__reply_to_refchange is None:
-            return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_refchange(refchange)
-        else:
-            return self.process_addr(self.__reply_to_refchange, refchange)
-
-    def get_reply_to_commit(self, revision):
-        if self.__reply_to_commit is None:
-            return super(ConfigOptionsEnvironmentMixin, self).get_reply_to_commit(revision)
-        else:
-            return self.process_addr(self.__reply_to_commit, revision)
-
-    def get_scancommitforcc(self):
-        return self.config.get('scancommitforcc')
-
-
-class FilterLinesEnvironmentMixin(Environment):
-    """Handle encoding and maximum line length of body lines.
-
-        email_max_line_length (int or None)
-
-            The maximum length of any single line in the email body.
-            Longer lines are truncated at that length with ' [...]'
-            appended.
-
-        strict_utf8 (bool)
-
-            If this field is set to True, then the email body text is
-            expected to be UTF-8.  Any invalid characters are
-            converted to U+FFFD, the Unicode replacement character
-            (encoded as UTF-8, of course).
-
-    """
-
-    def __init__(self, strict_utf8=True,
-                 email_max_line_length=500, max_subject_length=500,
-                 **kw):
-        super(FilterLinesEnvironmentMixin, self).__init__(**kw)
-        self.__strict_utf8 = strict_utf8
-        self.__email_max_line_length = email_max_line_length
-        self.__max_subject_length = max_subject_length
-
-    def filter_body(self, lines):
-        lines = super(FilterLinesEnvironmentMixin, self).filter_body(lines)
-        if self.__strict_utf8:
-            if not PYTHON3:
-                lines = (line.decode(ENCODING, 'replace') for line in lines)
-            # Limit the line length in Unicode-space to avoid
-            # splitting characters:
-            if self.__email_max_line_length > 0:
-                lines = limit_linelength(lines, self.__email_max_line_length)
-            if not PYTHON3:
-                lines = (line.encode(ENCODING, 'replace') for line in lines)
-        elif self.__email_max_line_length:
-            lines = limit_linelength(lines, self.__email_max_line_length)
-
-        return lines
-
-    def get_max_subject_length(self):
-        return self.__max_subject_length
-
-
-class ConfigFilterLinesEnvironmentMixin(
-        ConfigEnvironmentMixin,
-        FilterLinesEnvironmentMixin,
-        ):
-    """Handle encoding and maximum line length based on config."""
-
-    def __init__(self, config, **kw):
-        strict_utf8 = config.get_bool('emailstrictutf8', default=None)
-        if strict_utf8 is not None:
-            kw['strict_utf8'] = strict_utf8
-
-        email_max_line_length = config.get('emailmaxlinelength')
-        if email_max_line_length is not None:
-            kw['email_max_line_length'] = int(email_max_line_length)
-
-        max_subject_length = config.get('subjectMaxLength', default=email_max_line_length)
-        if max_subject_length is not None:
-            kw['max_subject_length'] = int(max_subject_length)
-
-        super(ConfigFilterLinesEnvironmentMixin, self).__init__(
-            config=config, **kw
-            )
-
-
-class MaxlinesEnvironmentMixin(Environment):
-    """Limit the email body to a specified number of lines."""
-
-    def __init__(self, emailmaxlines, **kw):
-        super(MaxlinesEnvironmentMixin, self).__init__(**kw)
-        self.__emailmaxlines = emailmaxlines
-
-    def filter_body(self, lines):
-        lines = super(MaxlinesEnvironmentMixin, self).filter_body(lines)
-        if self.__emailmaxlines > 0:
-            lines = limit_lines(lines, self.__emailmaxlines)
-        return lines
-
-
-class ConfigMaxlinesEnvironmentMixin(
-        ConfigEnvironmentMixin,
-        MaxlinesEnvironmentMixin,
-        ):
-    """Limit the email body to the number of lines specified in config."""
-
-    def __init__(self, config, **kw):
-        emailmaxlines = int(config.get('emailmaxlines', default='0'))
-        super(ConfigMaxlinesEnvironmentMixin, self).__init__(
-            config=config,
-            emailmaxlines=emailmaxlines,
-            **kw
-            )
-
-
-class FQDNEnvironmentMixin(Environment):
-    """A mixin that sets the host's FQDN to its constructor argument."""
-
-    def __init__(self, fqdn, **kw):
-        super(FQDNEnvironmentMixin, self).__init__(**kw)
-        self.COMPUTED_KEYS += ['fqdn']
-        self.__fqdn = fqdn
-
-    def get_fqdn(self):
-        """Return the fully-qualified domain name for this host.
-
-        Return None if it is unavailable or unwanted."""
-
-        return self.__fqdn
-
-
-class ConfigFQDNEnvironmentMixin(
-        ConfigEnvironmentMixin,
-        FQDNEnvironmentMixin,
-        ):
-    """Read the FQDN from the config."""
-
-    def __init__(self, config, **kw):
-        fqdn = config.get('fqdn')
-        super(ConfigFQDNEnvironmentMixin, self).__init__(
-            config=config,
-            fqdn=fqdn,
-            **kw
-            )
-
-
-class ComputeFQDNEnvironmentMixin(FQDNEnvironmentMixin):
-    """Get the FQDN by calling socket.getfqdn()."""
-
-    def __init__(self, **kw):
-        super(ComputeFQDNEnvironmentMixin, self).__init__(
-            fqdn=socket.getfqdn(),
-            **kw
-            )
-
-
-class PusherDomainEnvironmentMixin(ConfigEnvironmentMixin):
-    """Deduce pusher_email from pusher by appending an emaildomain."""
-
-    def __init__(self, **kw):
-        super(PusherDomainEnvironmentMixin, self).__init__(**kw)
-        self.__emaildomain = self.config.get('emaildomain')
-
-    def get_pusher_email(self):
-        if self.__emaildomain:
-            # Derive the pusher's full email address in the default way:
-            return '%s@%s' % (self.get_pusher(), self.__emaildomain)
-        else:
-            return super(PusherDomainEnvironmentMixin, self).get_pusher_email()
-
-
-class StaticRecipientsEnvironmentMixin(Environment):
-    """Set recipients statically based on constructor parameters."""
-
-    def __init__(
-            self,
-            refchange_recipients, announce_recipients, revision_recipients, scancommitforcc,
-            **kw
-            ):
-        super(StaticRecipientsEnvironmentMixin, self).__init__(**kw)
-
-        # The recipients for various types of notification emails, as
-        # RFC 2822 email addresses separated by commas (or the empty
-        # string if no recipients are configured).  Although there is
-        # a mechanism to choose the recipient lists based on on the
-        # actual *contents* of the change being reported, we only
-        # choose based on the *type* of the change.  Therefore we can
-        # compute them once and for all:
-        self.__refchange_recipients = refchange_recipients
-        self.__announce_recipients = announce_recipients
-        self.__revision_recipients = revision_recipients
-
-    def check(self):
-        if not (self.get_refchange_recipients(None) or
-                self.get_announce_recipients(None) or
-                self.get_revision_recipients(None) or
-                self.get_scancommitforcc()):
-            raise ConfigurationException('No email recipients configured!')
-        super(StaticRecipientsEnvironmentMixin, self).check()
-
-    def get_refchange_recipients(self, refchange):
-        if self.__refchange_recipients is None:
-            return super(StaticRecipientsEnvironmentMixin,
-                         self).get_refchange_recipients(refchange)
-        return self.__refchange_recipients
-
-    def get_announce_recipients(self, annotated_tag_change):
-        if self.__announce_recipients is None:
-            return super(StaticRecipientsEnvironmentMixin,
-                         self).get_refchange_recipients(annotated_tag_change)
-        return self.__announce_recipients
-
-    def get_revision_recipients(self, revision):
-        if self.__revision_recipients is None:
-            return super(StaticRecipientsEnvironmentMixin,
-                         self).get_refchange_recipients(revision)
-        return self.__revision_recipients
-
-
-class CLIRecipientsEnvironmentMixin(Environment):
-    """Mixin storing recipients information coming from the
-    command-line."""
-
-    def __init__(self, cli_recipients=None, **kw):
-        super(CLIRecipientsEnvironmentMixin, self).__init__(**kw)
-        self.__cli_recipients = cli_recipients
-
-    def get_refchange_recipients(self, refchange):
-        if self.__cli_recipients is None:
-            return super(CLIRecipientsEnvironmentMixin,
-                         self).get_refchange_recipients(refchange)
-        return self.__cli_recipients
-
-    def get_announce_recipients(self, annotated_tag_change):
-        if self.__cli_recipients is None:
-            return super(CLIRecipientsEnvironmentMixin,
-                         self).get_announce_recipients(annotated_tag_change)
-        return self.__cli_recipients
-
-    def get_revision_recipients(self, revision):
-        if self.__cli_recipients is None:
-            return super(CLIRecipientsEnvironmentMixin,
-                         self).get_revision_recipients(revision)
-        return self.__cli_recipients
-
-
-class ConfigRecipientsEnvironmentMixin(
-        ConfigEnvironmentMixin,
-        StaticRecipientsEnvironmentMixin
-        ):
-    """Determine recipients statically based on config."""
-
-    def __init__(self, config, **kw):
-        super(ConfigRecipientsEnvironmentMixin, self).__init__(
-            config=config,
-            refchange_recipients=self._get_recipients(
-                config, 'refchangelist', 'mailinglist',
-                ),
-            announce_recipients=self._get_recipients(
-                config, 'announcelist', 'refchangelist', 'mailinglist',
-                ),
-            revision_recipients=self._get_recipients(
-                config, 'commitlist', 'mailinglist',
-                ),
-            scancommitforcc=config.get('scancommitforcc'),
-            **kw
-            )
-
-    def _get_recipients(self, config, *names):
-        """Return the recipients for a particular type of message.
-
-        Return the list of email addresses to which a particular type
-        of notification email should be sent, by looking at the config
-        value for "multimailhook.$name" for each of names.  Use the
-        value from the first name that is configured.  The return
-        value is a (possibly empty) string containing RFC 2822 email
-        addresses separated by commas.  If no configuration could be
-        found, raise a ConfigurationException."""
-
-        for name in names:
-            lines = config.get_all(name)
-            if lines is not None:
-                lines = [line.strip() for line in lines]
-                # Single "none" is a special value equivalen to empty string.
-                if lines == ['none']:
-                    lines = ['']
-                return ', '.join(lines)
-        else:
-            return ''
-
-
-class StaticRefFilterEnvironmentMixin(Environment):
-    """Set branch filter statically based on constructor parameters."""
-
-    def __init__(self, ref_filter_incl_regex, ref_filter_excl_regex,
-                 ref_filter_do_send_regex, ref_filter_dont_send_regex,
-                 **kw):
-        super(StaticRefFilterEnvironmentMixin, self).__init__(**kw)
-
-        if ref_filter_incl_regex and ref_filter_excl_regex:
-            raise ConfigurationException(
-                "Cannot specify both a ref inclusion and exclusion regex.")
-        self.__is_inclusion_filter = bool(ref_filter_incl_regex)
-        default_exclude = self.get_default_ref_ignore_regex()
-        if ref_filter_incl_regex:
-            ref_filter_regex = ref_filter_incl_regex
-        elif ref_filter_excl_regex:
-            ref_filter_regex = ref_filter_excl_regex + '|' + default_exclude
-        else:
-            ref_filter_regex = default_exclude
-        try:
-            self.__compiled_regex = re.compile(ref_filter_regex)
-        except Exception:
-            raise ConfigurationException(
-                'Invalid Ref Filter Regex "%s": %s' % (ref_filter_regex, sys.exc_info()[1]))
-
-        if ref_filter_do_send_regex and ref_filter_dont_send_regex:
-            raise ConfigurationException(
-                "Cannot specify both a ref doSend and dontSend regex.")
-        self.__is_do_send_filter = bool(ref_filter_do_send_regex)
-        if ref_filter_do_send_regex:
-            ref_filter_send_regex = ref_filter_do_send_regex
-        elif ref_filter_dont_send_regex:
-            ref_filter_send_regex = ref_filter_dont_send_regex
-        else:
-            ref_filter_send_regex = '.*'
-            self.__is_do_send_filter = True
-        try:
-            self.__send_compiled_regex = re.compile(ref_filter_send_regex)
-        except Exception:
-            raise ConfigurationException(
-                'Invalid Ref Filter Regex "%s": %s' %
-                (ref_filter_send_regex, sys.exc_info()[1]))
-
-    def get_ref_filter_regex(self, send_filter=False):
-        if send_filter:
-            return self.__send_compiled_regex, self.__is_do_send_filter
-        else:
-            return self.__compiled_regex, self.__is_inclusion_filter
-
-
-class ConfigRefFilterEnvironmentMixin(
-        ConfigEnvironmentMixin,
-        StaticRefFilterEnvironmentMixin
-        ):
-    """Determine branch filtering statically based on config."""
-
-    def _get_regex(self, config, key):
-        """Get a list of whitespace-separated regex. The refFilter* config
-        variables are multivalued (hence the use of get_all), and we
-        allow each entry to be a whitespace-separated list (hence the
-        split on each line). The whole thing is glued into a single regex."""
-        values = config.get_all(key)
-        if values is None:
-            return values
-        items = []
-        for line in values:
-            for i in line.split():
-                items.append(i)
-        if items == []:
-            return None
-        return '|'.join(items)
-
-    def __init__(self, config, **kw):
-        super(ConfigRefFilterEnvironmentMixin, self).__init__(
-            config=config,
-            ref_filter_incl_regex=self._get_regex(config, 'refFilterInclusionRegex'),
-            ref_filter_excl_regex=self._get_regex(config, 'refFilterExclusionRegex'),
-            ref_filter_do_send_regex=self._get_regex(config, 'refFilterDoSendRegex'),
-            ref_filter_dont_send_regex=self._get_regex(config, 'refFilterDontSendRegex'),
-            **kw
-            )
-
-
-class ProjectdescEnvironmentMixin(Environment):
-    """Make a "projectdesc" value available for templates.
-
-    By default, it is set to the first line of $GIT_DIR/description
-    (if that file is present and appears to be set meaningfully)."""
-
-    def __init__(self, **kw):
-        super(ProjectdescEnvironmentMixin, self).__init__(**kw)
-        self.COMPUTED_KEYS += ['projectdesc']
-
-    def get_projectdesc(self):
-        """Return a one-line description of the project."""
-
-        git_dir = get_git_dir()
-        try:
-            projectdesc = open(os.path.join(git_dir, 'description')).readline().strip()
-            if projectdesc and not projectdesc.startswith('Unnamed repository'):
-                return projectdesc
-        except IOError:
-            pass
-
-        return 'UNNAMED PROJECT'
-
-
-class GenericEnvironmentMixin(Environment):
-    def get_pusher(self):
-        return self.osenv.get('USER', self.osenv.get('USERNAME', 'unknown user'))
-
-
-class GitoliteEnvironmentHighPrecMixin(Environment):
-    def get_pusher(self):
-        return self.osenv.get('GL_USER', 'unknown user')
-
-
-class GitoliteEnvironmentLowPrecMixin(
-        ConfigEnvironmentMixin,
-        Environment):
-
-    def get_repo_shortname(self):
-        # The gitolite environment variable $GL_REPO is a pretty good
-        # repo_shortname (though it's probably not as good as a value
-        # the user might have explicitly put in his config).
-        return (
-            self.osenv.get('GL_REPO', None) or
-            super(GitoliteEnvironmentLowPrecMixin, self).get_repo_shortname()
-            )
-
-    @staticmethod
-    def _compile_regex(re_template):
-        return (
-            re.compile(re_template % x)
-            for x in (
-                r'BEGIN\s+USER\s+EMAILS',
-                r'([^\s]+)\s+(.*)',
-                r'END\s+USER\s+EMAILS',
-                ))
-
-    def get_fromaddr(self, change=None):
-        GL_USER = self.osenv.get('GL_USER')
-        if GL_USER is not None:
-            # Find the path to gitolite.conf.  Note that gitolite v3
-            # did away with the GL_ADMINDIR and GL_CONF environment
-            # variables (they are now hard-coded).
-            GL_ADMINDIR = self.osenv.get(
-                'GL_ADMINDIR',
-                os.path.expanduser(os.path.join('~', '.gitolite')))
-            GL_CONF = self.osenv.get(
-                'GL_CONF',
-                os.path.join(GL_ADMINDIR, 'conf', 'gitolite.conf'))
-
-            mailaddress_map = self.config.get('MailaddressMap')
-            # If relative, consider relative to GL_CONF:
-            if mailaddress_map:
-                mailaddress_map = os.path.join(os.path.dirname(GL_CONF),
-                                               mailaddress_map)
-                if os.path.isfile(mailaddress_map):
-                    f = open(mailaddress_map, 'rU')
-                    try:
-                        # Leading '#' is optional
-                        re_begin, re_user, re_end = self._compile_regex(
-                            r'^(?:\s*#)?\s*%s\s*$')
-                        for l in f:
-                            l = l.rstrip('\n')
-                            if re_begin.match(l) or re_end.match(l):
-                                continue  # Ignore these lines
-                            m = re_user.match(l)
-                            if m:
-                                if m.group(1) == GL_USER:
-                                    return m.group(2)
-                                else:
-                                    continue  # Not this user, but not an error
-                            raise ConfigurationException(
-                                "Syntax error in mail address map.\n"
-                                "Check file {}.\n"
-                                "Line: {}".format(mailaddress_map, l))
-
-                    finally:
-                        f.close()
-
-            if os.path.isfile(GL_CONF):
-                f = open(GL_CONF, 'rU')
-                try:
-                    in_user_emails_section = False
-                    re_begin, re_user, re_end = self._compile_regex(
-                        r'^\s*#\s*%s\s*$')
-                    for l in f:
-                        l = l.rstrip('\n')
-                        if not in_user_emails_section:
-                            if re_begin.match(l):
-                                in_user_emails_section = True
-                            continue
-                        if re_end.match(l):
-                            break
-                        m = re_user.match(l)
-                        if m and m.group(1) == GL_USER:
-                            return m.group(2)
-                finally:
-                    f.close()
-        return super(GitoliteEnvironmentLowPrecMixin, self).get_fromaddr(change)
-
-
-class IncrementalDateTime(object):
-    """Simple wrapper to give incremental date/times.
-
-    Each call will result in a date/time a second later than the
-    previous call.  This can be used to falsify email headers, to
-    increase the likelihood that email clients sort the emails
-    correctly."""
-
-    def __init__(self):
-        self.time = time.time()
-        self.next = self.__next__  # Python 2 backward compatibility
-
-    def __next__(self):
-        formatted = formatdate(self.time, True)
-        self.time += 1
-        return formatted
-
-
-class StashEnvironmentHighPrecMixin(Environment):
-    def __init__(self, user=None, repo=None, **kw):
-        super(StashEnvironmentHighPrecMixin,
-              self).__init__(user=user, repo=repo, **kw)
-        self.__user = user
-        self.__repo = repo
-
-    def get_pusher(self):
-        return re.match(r'(.*?)\s*<', self.__user).group(1)
-
-    def get_pusher_email(self):
-        return self.__user
-
-
-class StashEnvironmentLowPrecMixin(Environment):
-    def __init__(self, user=None, repo=None, **kw):
-        super(StashEnvironmentLowPrecMixin, self).__init__(**kw)
-        self.__repo = repo
-        self.__user = user
-
-    def get_repo_shortname(self):
-        return self.__repo
-
-    def get_fromaddr(self, change=None):
-        return self.__user
-
-
-class GerritEnvironmentHighPrecMixin(Environment):
-    def __init__(self, project=None, submitter=None, update_method=None, **kw):
-        super(GerritEnvironmentHighPrecMixin,
-              self).__init__(submitter=submitter, project=project, **kw)
-        self.__project = project
-        self.__submitter = submitter
-        self.__update_method = update_method
-        "Make an 'update_method' value available for templates."
-        self.COMPUTED_KEYS += ['update_method']
-
-    def get_pusher(self):
-        if self.__submitter:
-            if self.__submitter.find('<') != -1:
-                # Submitter has a configured email, we transformed
-                # __submitter into an RFC 2822 string already.
-                return re.match(r'(.*?)\s*<', self.__submitter).group(1)
-            else:
-                # Submitter has no configured email, it's just his name.
-                return self.__submitter
-        else:
-            # If we arrive here, this means someone pushed "Submit" from
-            # the gerrit web UI for the CR (or used one of the programmatic
-            # APIs to do the same, such as gerrit review) and the
-            # merge/push was done by the Gerrit user.  It was technically
-            # triggered by someone else, but sadly we have no way of
-            # determining who that someone else is at this point.
-            return 'Gerrit'  # 'unknown user'?
-
-    def get_pusher_email(self):
-        if self.__submitter:
-            return self.__submitter
-        else:
-            return super(GerritEnvironmentHighPrecMixin, self).get_pusher_email()
-
-    def get_default_ref_ignore_regex(self):
-        default = super(GerritEnvironmentHighPrecMixin, self).get_default_ref_ignore_regex()
-        return default + '|^refs/changes/|^refs/cache-automerge/|^refs/meta/'
-
-    def get_revision_recipients(self, revision):
-        # Merge commits created by Gerrit when users hit "Submit this patchset"
-        # in the Web UI (or do equivalently with REST APIs or the gerrit review
-        # command) are not something users want to see an individual email for.
-        # Filter them out.
-        committer = read_git_output(['log', '--no-walk', '--format=%cN',
-                                     revision.rev.sha1])
-        if committer == 'Gerrit Code Review':
-            return []
-        else:
-            return super(GerritEnvironmentHighPrecMixin, self).get_revision_recipients(revision)
-
-    def get_update_method(self):
-        return self.__update_method
-
-
-class GerritEnvironmentLowPrecMixin(Environment):
-    def __init__(self, project=None, submitter=None, **kw):
-        super(GerritEnvironmentLowPrecMixin, self).__init__(**kw)
-        self.__project = project
-        self.__submitter = submitter
-
-    def get_repo_shortname(self):
-        return self.__project
-
-    def get_fromaddr(self, change=None):
-        if self.__submitter and self.__submitter.find('<') != -1:
-            return self.__submitter
-        else:
-            return super(GerritEnvironmentLowPrecMixin, self).get_fromaddr(change)
-
-
-class Push(object):
-    """Represent an entire push (i.e., a group of ReferenceChanges).
-
-    It is easy to figure out what commits were added to a *branch* by
-    a Reference change:
-
-        git rev-list change.old..change.new
-
-    or removed from a *branch*:
-
-        git rev-list change.new..change.old
-
-    But it is not quite so trivial to determine which entirely new
-    commits were added to the *repository* by a push and which old
-    commits were discarded by a push.  A big part of the job of this
-    class is to figure out these things, and to make sure that new
-    commits are only detailed once even if they were added to multiple
-    references.
-
-    The first step is to determine the "other" references--those
-    unaffected by the current push.  They are computed by listing all
-    references then removing any affected by this push.  The results
-    are stored in Push._other_ref_sha1s.
-
-    The commits contained in the repository before this push were
-
-        git rev-list other1 other2 other3 ... change1.old change2.old ...
-
-    Where "changeN.old" is the old value of one of the references
-    affected by this push.
-
-    The commits contained in the repository after this push are
-
-        git rev-list other1 other2 other3 ... change1.new change2.new ...
-
-    The commits added by this push are the difference between these
-    two sets, which can be written
-
-        git rev-list \
-            ^other1 ^other2 ... \
-            ^change1.old ^change2.old ... \
-            change1.new change2.new ...
-
-    The commits removed by this push can be computed by
-
-        git rev-list \
-            ^other1 ^other2 ... \
-            ^change1.new ^change2.new ... \
-            change1.old change2.old ...
-
-    The last point is that it is possible that other pushes are
-    occurring simultaneously to this one, so reference values can
-    change at any time.  It is impossible to eliminate all race
-    conditions, but we reduce the window of time during which problems
-    can occur by translating reference names to SHA1s as soon as
-    possible and working with SHA1s thereafter (because SHA1s are
-    immutable)."""
-
-    # A map {(changeclass, changetype): integer} specifying the order
-    # that reference changes will be processed if multiple reference
-    # changes are included in a single push.  The order is significant
-    # mostly because new commit notifications are threaded together
-    # with the first reference change that includes the commit.  The
-    # following order thus causes commits to be grouped with branch
-    # changes (as opposed to tag changes) if possible.
-    SORT_ORDER = dict(
-        (value, i) for (i, value) in enumerate([
-            (BranchChange, 'update'),
-            (BranchChange, 'create'),
-            (AnnotatedTagChange, 'update'),
-            (AnnotatedTagChange, 'create'),
-            (NonAnnotatedTagChange, 'update'),
-            (NonAnnotatedTagChange, 'create'),
-            (BranchChange, 'delete'),
-            (AnnotatedTagChange, 'delete'),
-            (NonAnnotatedTagChange, 'delete'),
-            (OtherReferenceChange, 'update'),
-            (OtherReferenceChange, 'create'),
-            (OtherReferenceChange, 'delete'),
-            ])
-        )
-
-    def __init__(self, environment, changes, ignore_other_refs=False):
-        self.changes = sorted(changes, key=self._sort_key)
-        self.__other_ref_sha1s = None
-        self.__cached_commits_spec = {}
-        self.environment = environment
-
-        if ignore_other_refs:
-            self.__other_ref_sha1s = set()
-
-    @classmethod
-    def _sort_key(klass, change):
-        return (klass.SORT_ORDER[change.__class__, change.change_type], change.refname,)
-
-    @property
-    def _other_ref_sha1s(self):
-        """The GitObjects referred to by references unaffected by this push.
-        """
-        if self.__other_ref_sha1s is None:
-            # The refnames being changed by this push:
-            updated_refs = set(
-                change.refname
-                for change in self.changes
-                )
-
-            # The SHA-1s of commits referred to by all references in this
-            # repository *except* updated_refs:
-            sha1s = set()
-            fmt = (
-                '%(objectname) %(objecttype) %(refname)\n'
-                '%(*objectname) %(*objecttype) %(refname)'
-                )
-            ref_filter_regex, is_inclusion_filter = \
-                self.environment.get_ref_filter_regex()
-            for line in read_git_lines(
-                    ['for-each-ref', '--format=%s' % (fmt,)]):
-                (sha1, type, name) = line.split(' ', 2)
-                if (sha1 and type == 'commit' and
-                        name not in updated_refs and
-                        include_ref(name, ref_filter_regex, is_inclusion_filter)):
-                    sha1s.add(sha1)
-
-            self.__other_ref_sha1s = sha1s
-
-        return self.__other_ref_sha1s
-
-    def _get_commits_spec_incl(self, new_or_old, reference_change=None):
-        """Get new or old SHA-1 from one or each of the changed refs.
-
-        Return a list of SHA-1 commit identifier strings suitable as
-        arguments to 'git rev-list' (or 'git log' or ...).  The
-        returned identifiers are either the old or new values from one
-        or all of the changed references, depending on the values of
-        new_or_old and reference_change.
-
-        new_or_old is either the string 'new' or the string 'old'.  If
-        'new', the returned SHA-1 identifiers are the new values from
-        each changed reference.  If 'old', the SHA-1 identifiers are
-        the old values from each changed reference.
-
-        If reference_change is specified and not None, only the new or
-        old reference from the specified reference is included in the
-        return value.
-
-        This function returns None if there are no matching revisions
-        (e.g., because a branch was deleted and new_or_old is 'new').
-        """
-
-        if not reference_change:
-            incl_spec = sorted(
-                getattr(change, new_or_old).sha1
-                for change in self.changes
-                if getattr(change, new_or_old)
-                )
-            if not incl_spec:
-                incl_spec = None
-        elif not getattr(reference_change, new_or_old).commit_sha1:
-            incl_spec = None
-        else:
-            incl_spec = [getattr(reference_change, new_or_old).commit_sha1]
-        return incl_spec
-
-    def _get_commits_spec_excl(self, new_or_old):
-        """Get exclusion revisions for determining new or discarded commits.
-
-        Return a list of strings suitable as arguments to 'git
-        rev-list' (or 'git log' or ...) that will exclude all
-        commits that, depending on the value of new_or_old, were
-        either previously in the repository (useful for determining
-        which commits are new to the repository) or currently in the
-        repository (useful for determining which commits were
-        discarded from the repository).
-
-        new_or_old is either the string 'new' or the string 'old'.  If
-        'new', the commits to be excluded are those that were in the
-        repository before the push.  If 'old', the commits to be
-        excluded are those that are currently in the repository.  """
-
-        old_or_new = {'old': 'new', 'new': 'old'}[new_or_old]
-        excl_revs = self._other_ref_sha1s.union(
-            getattr(change, old_or_new).sha1
-            for change in self.changes
-            if getattr(change, old_or_new).type in ['commit', 'tag']
-            )
-        return ['^' + sha1 for sha1 in sorted(excl_revs)]
-
-    def get_commits_spec(self, new_or_old, reference_change=None):
-        """Get rev-list arguments for added or discarded commits.
-
-        Return a list of strings suitable as arguments to 'git
-        rev-list' (or 'git log' or ...) that select those commits
-        that, depending on the value of new_or_old, are either new to
-        the repository or were discarded from the repository.
-
-        new_or_old is either the string 'new' or the string 'old'.  If
-        'new', the returned list is used to select commits that are
-        new to the repository.  If 'old', the returned value is used
-        to select the commits that have been discarded from the
-        repository.
-
-        If reference_change is specified and not None, the new or
-        discarded commits are limited to those that are reachable from
-        the new or old value of the specified reference.
-
-        This function returns None if there are no added (or discarded)
-        revisions.
-        """
-        key = (new_or_old, reference_change)
-        if key not in self.__cached_commits_spec:
-            ret = self._get_commits_spec_incl(new_or_old, reference_change)
-            if ret is not None:
-                ret.extend(self._get_commits_spec_excl(new_or_old))
-            self.__cached_commits_spec[key] = ret
-        return self.__cached_commits_spec[key]
-
-    def get_new_commits(self, reference_change=None):
-        """Return a list of commits added by this push.
-
-        Return a list of the object names of commits that were added
-        by the part of this push represented by reference_change.  If
-        reference_change is None, then return a list of *all* commits
-        added by this push."""
-
-        spec = self.get_commits_spec('new', reference_change)
-        return git_rev_list(spec)
-
-    def get_discarded_commits(self, reference_change):
-        """Return a list of commits discarded by this push.
-
-        Return a list of the object names of commits that were
-        entirely discarded from the repository by the part of this
-        push represented by reference_change."""
-
-        spec = self.get_commits_spec('old', reference_change)
-        return git_rev_list(spec)
-
-    def send_emails(self, mailer, body_filter=None):
-        """Use send all of the notification emails needed for this push.
-
-        Use send all of the notification emails (including reference
-        change emails and commit emails) needed for this push.  Send
-        the emails using mailer.  If body_filter is not None, then use
-        it to filter the lines that are intended for the email
-        body."""
-
-        # The sha1s of commits that were introduced by this push.
-        # They will be removed from this set as they are processed, to
-        # guarantee that one (and only one) email is generated for
-        # each new commit.
-        unhandled_sha1s = set(self.get_new_commits())
-        send_date = IncrementalDateTime()
-        for change in self.changes:
-            sha1s = []
-            for sha1 in reversed(list(self.get_new_commits(change))):
-                if sha1 in unhandled_sha1s:
-                    sha1s.append(sha1)
-                    unhandled_sha1s.remove(sha1)
-
-            # Check if we've got anyone to send to
-            if not change.recipients:
-                change.environment.log_warning(
-                    '*** no recipients configured so no email will be sent\n'
-                    '*** for %r update %s->%s'
-                    % (change.refname, change.old.sha1, change.new.sha1,)
-                    )
-            else:
-                if not change.environment.quiet:
-                    change.environment.log_msg(
-                        'Sending notification emails to: %s' % (change.recipients,))
-                extra_values = {'send_date': next(send_date)}
-
-                rev = change.send_single_combined_email(sha1s)
-                if rev:
-                    mailer.send(
-                        change.generate_combined_email(self, rev, body_filter, extra_values),
-                        rev.recipients,
-                        )
-                    # This change is now fully handled; no need to handle
-                    # individual revisions any further.
-                    continue
-                else:
-                    mailer.send(
-                        change.generate_email(self, body_filter, extra_values),
-                        change.recipients,
-                        )
-
-            max_emails = change.environment.maxcommitemails
-            if max_emails and len(sha1s) > max_emails:
-                change.environment.log_warning(
-                    '*** Too many new commits (%d), not sending commit emails.\n' % len(sha1s) +
-                    '*** Try setting multimailhook.maxCommitEmails to a greater value\n' +
-                    '*** Currently, multimailhook.maxCommitEmails=%d' % max_emails
-                    )
-                return
-
-            for (num, sha1) in enumerate(sha1s):
-                rev = Revision(change, GitObject(sha1), num=num + 1, tot=len(sha1s))
-                if len(rev.parents) > 1 and change.environment.excludemergerevisions:
-                    # skipping a merge commit
-                    continue
-                if not rev.recipients and rev.cc_recipients:
-                    change.environment.log_msg('*** Replacing Cc: with To:')
-                    rev.recipients = rev.cc_recipients
-                    rev.cc_recipients = None
-                if rev.recipients:
-                    extra_values = {'send_date': next(send_date)}
-                    mailer.send(
-                        rev.generate_email(self, body_filter, extra_values),
-                        rev.recipients,
-                        )
-
-        # Consistency check:
-        if unhandled_sha1s:
-            change.environment.log_error(
-                'ERROR: No emails were sent for the following new commits:\n'
-                '    %s'
-                % ('\n    '.join(sorted(unhandled_sha1s)),)
-                )
-
-
-def include_ref(refname, ref_filter_regex, is_inclusion_filter):
-    does_match = bool(ref_filter_regex.search(refname))
-    if is_inclusion_filter:
-        return does_match
-    else:  # exclusion filter -- we include the ref if the regex doesn't match
-        return not does_match
-
-
-def run_as_post_receive_hook(environment, mailer):
-    environment.check()
-    send_filter_regex, send_is_inclusion_filter = environment.get_ref_filter_regex(True)
-    ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(False)
-    changes = []
-    while True:
-        line = read_line(sys.stdin)
-        if line == '':
-            break
-        (oldrev, newrev, refname) = line.strip().split(' ', 2)
-        environment.get_logger().debug(
-            "run_as_post_receive_hook: oldrev=%s, newrev=%s, refname=%s" %
-            (oldrev, newrev, refname))
-
-        if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
-            continue
-        if not include_ref(refname, send_filter_regex, send_is_inclusion_filter):
-            continue
-        changes.append(
-            ReferenceChange.create(environment, oldrev, newrev, refname)
-            )
-    if not changes:
-        mailer.close()
-        return
-    push = Push(environment, changes)
-    try:
-        push.send_emails(mailer, body_filter=environment.filter_body)
-    finally:
-        mailer.close()
-
-
-def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
-    environment.check()
-    send_filter_regex, send_is_inclusion_filter = environment.get_ref_filter_regex(True)
-    ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(False)
-    if not include_ref(refname, ref_filter_regex, is_inclusion_filter):
-        return
-    if not include_ref(refname, send_filter_regex, send_is_inclusion_filter):
-        return
-    changes = [
-        ReferenceChange.create(
-            environment,
-            read_git_output(['rev-parse', '--verify', oldrev]),
-            read_git_output(['rev-parse', '--verify', newrev]),
-            refname,
-            ),
-        ]
-    if not changes:
-        mailer.close()
-        return
-    push = Push(environment, changes, force_send)
-    try:
-        push.send_emails(mailer, body_filter=environment.filter_body)
-    finally:
-        mailer.close()
-
-
-def check_ref_filter(environment):
-    send_filter_regex, send_is_inclusion = environment.get_ref_filter_regex(True)
-    ref_filter_regex, ref_is_inclusion = environment.get_ref_filter_regex(False)
-
-    def inc_exc_lusion(b):
-        if b:
-            return 'inclusion'
-        else:
-            return 'exclusion'
-
-    if send_filter_regex:
-        sys.stdout.write("DoSend/DontSend filter regex (" +
-                         (inc_exc_lusion(send_is_inclusion)) +
-                         '): ' + send_filter_regex.pattern +
-                         '\n')
-    if send_filter_regex:
-        sys.stdout.write("Include/Exclude filter regex (" +
-                         (inc_exc_lusion(ref_is_inclusion)) +
-                         '): ' + ref_filter_regex.pattern +
-                         '\n')
-    sys.stdout.write(os.linesep)
-
-    sys.stdout.write(
-        "Refs marked as EXCLUDE are excluded by either refFilterInclusionRegex\n"
-        "or refFilterExclusionRegex. No emails will be sent for commits included\n"
-        "in these refs.\n"
-        "Refs marked as DONT-SEND are excluded by either refFilterDoSendRegex or\n"
-        "refFilterDontSendRegex, but not by either refFilterInclusionRegex or\n"
-        "refFilterExclusionRegex. Emails will be sent for commits included in these\n"
-        "refs only when the commit reaches a ref which isn't excluded.\n"
-        "Refs marked as DO-SEND are not excluded by any filter. Emails will\n"
-        "be sent normally for commits included in these refs.\n")
-
-    sys.stdout.write(os.linesep)
-
-    for refname in read_git_lines(['for-each-ref', '--format', '%(refname)']):
-        sys.stdout.write(refname)
-        if not include_ref(refname, ref_filter_regex, ref_is_inclusion):
-            sys.stdout.write(' EXCLUDE')
-        elif not include_ref(refname, send_filter_regex, send_is_inclusion):
-            sys.stdout.write(' DONT-SEND')
-        else:
-            sys.stdout.write(' DO-SEND')
-
-        sys.stdout.write(os.linesep)
-
-
-def show_env(environment, out):
-    out.write('Environment values:\n')
-    for (k, v) in sorted(environment.get_values().items()):
-        if k:  # Don't show the {'' : ''} pair.
-            out.write('    %s : %r\n' % (k, v))
-    out.write('\n')
-    # Flush to avoid interleaving with further log output
-    out.flush()
-
-
-def check_setup(environment):
-    environment.check()
-    show_env(environment, sys.stdout)
-    sys.stdout.write("Now, checking that git-multimail's standard input "
-                     "is properly set ..." + os.linesep)
-    sys.stdout.write("Please type some text and then press Return" + os.linesep)
-    stdin = sys.stdin.readline()
-    sys.stdout.write("You have just entered:" + os.linesep)
-    sys.stdout.write(stdin)
-    sys.stdout.write("git-multimail seems properly set up." + os.linesep)
-
-
-def choose_mailer(config, environment):
-    mailer = config.get('mailer', default='sendmail')
-
-    if mailer == 'smtp':
-        smtpserver = config.get('smtpserver', default='localhost')
-        smtpservertimeout = float(config.get('smtpservertimeout', default=10.0))
-        smtpserverdebuglevel = int(config.get('smtpserverdebuglevel', default=0))
-        smtpencryption = config.get('smtpencryption', default='none')
-        smtpuser = config.get('smtpuser', default='')
-        smtppass = config.get('smtppass', default='')
-        smtpcacerts = config.get('smtpcacerts', default='')
-        mailer = SMTPMailer(
-            environment,
-            envelopesender=(environment.get_sender() or environment.get_fromaddr()),
-            smtpserver=smtpserver, smtpservertimeout=smtpservertimeout,
-            smtpserverdebuglevel=smtpserverdebuglevel,
-            smtpencryption=smtpencryption,
-            smtpuser=smtpuser,
-            smtppass=smtppass,
-            smtpcacerts=smtpcacerts
-            )
-    elif mailer == 'sendmail':
-        command = config.get('sendmailcommand')
-        if command:
-            command = shlex.split(command)
-        mailer = SendMailer(environment,
-                            command=command, envelopesender=environment.get_sender())
-    else:
-        environment.log_error(
-            'fatal: multimailhook.mailer is set to an incorrect value: "%s"\n' % mailer +
-            'please use one of "smtp" or "sendmail".'
-            )
-        sys.exit(1)
-    return mailer
-
-
-KNOWN_ENVIRONMENTS = {
-    'generic': {'highprec': GenericEnvironmentMixin},
-    'gitolite': {'highprec': GitoliteEnvironmentHighPrecMixin,
-                 'lowprec': GitoliteEnvironmentLowPrecMixin},
-    'stash': {'highprec': StashEnvironmentHighPrecMixin,
-              'lowprec': StashEnvironmentLowPrecMixin},
-    'gerrit': {'highprec': GerritEnvironmentHighPrecMixin,
-               'lowprec': GerritEnvironmentLowPrecMixin},
-    }
-
-
-def choose_environment(config, osenv=None, env=None, recipients=None,
-                       hook_info=None):
-    env_name = choose_environment_name(config, env, osenv)
-    environment_klass = build_environment_klass(env_name)
-    env = build_environment(environment_klass, env_name, config,
-                            osenv, recipients, hook_info)
-    return env
-
-
-def choose_environment_name(config, env, osenv):
-    if not osenv:
-        osenv = os.environ
-
-    if not env:
-        env = config.get('environment')
-
-    if not env:
-        if 'GL_USER' in osenv and 'GL_REPO' in osenv:
-            env = 'gitolite'
-        else:
-            env = 'generic'
-    return env
-
-
-COMMON_ENVIRONMENT_MIXINS = [
-    ConfigRecipientsEnvironmentMixin,
-    CLIRecipientsEnvironmentMixin,
-    ConfigRefFilterEnvironmentMixin,
-    ProjectdescEnvironmentMixin,
-    ConfigMaxlinesEnvironmentMixin,
-    ComputeFQDNEnvironmentMixin,
-    ConfigFilterLinesEnvironmentMixin,
-    PusherDomainEnvironmentMixin,
-    ConfigOptionsEnvironmentMixin,
-    ]
-
-
-def build_environment_klass(env_name):
-    if 'class' in KNOWN_ENVIRONMENTS[env_name]:
-        return KNOWN_ENVIRONMENTS[env_name]['class']
-
-    environment_mixins = []
-    known_env = KNOWN_ENVIRONMENTS[env_name]
-    if 'highprec' in known_env:
-        high_prec_mixin = known_env['highprec']
-        environment_mixins.append(high_prec_mixin)
-    environment_mixins = environment_mixins + COMMON_ENVIRONMENT_MIXINS
-    if 'lowprec' in known_env:
-        low_prec_mixin = known_env['lowprec']
-        environment_mixins.append(low_prec_mixin)
-    environment_mixins.append(Environment)
-    klass_name = env_name.capitalize() + 'Environment'
-    environment_klass = type(
-        klass_name,
-        tuple(environment_mixins),
-        {},
-        )
-    KNOWN_ENVIRONMENTS[env_name]['class'] = environment_klass
-    return environment_klass
-
-
-GerritEnvironment = build_environment_klass('gerrit')
-StashEnvironment = build_environment_klass('stash')
-GitoliteEnvironment = build_environment_klass('gitolite')
-GenericEnvironment = build_environment_klass('generic')
-
-
-def build_environment(environment_klass, env, config,
-                      osenv, recipients, hook_info):
-    environment_kw = {
-        'osenv': osenv,
-        'config': config,
-        }
-
-    if env == 'stash':
-        environment_kw['user'] = hook_info['stash_user']
-        environment_kw['repo'] = hook_info['stash_repo']
-    elif env == 'gerrit':
-        environment_kw['project'] = hook_info['project']
-        environment_kw['submitter'] = hook_info['submitter']
-        environment_kw['update_method'] = hook_info['update_method']
-
-    environment_kw['cli_recipients'] = recipients
-
-    return environment_klass(**environment_kw)
-
-
-def get_version():
-    oldcwd = os.getcwd()
-    try:
-        try:
-            os.chdir(os.path.dirname(os.path.realpath(__file__)))
-            git_version = read_git_output(['describe', '--tags', 'HEAD'])
-            if git_version == __version__:
-                return git_version
-            else:
-                return '%s (%s)' % (__version__, git_version)
-        except:
-            pass
-    finally:
-        os.chdir(oldcwd)
-    return __version__
-
-
-def compute_gerrit_options(options, args, required_gerrit_options,
-                           raw_refname):
-    if None in required_gerrit_options:
-        raise SystemExit("Error: Specify all of --oldrev, --newrev, --refname, "
-                         "and --project; or none of them.")
-
-    if options.environment not in (None, 'gerrit'):
-        raise SystemExit("Non-gerrit environments incompatible with --oldrev, "
-                         "--newrev, --refname, and --project")
-    options.environment = 'gerrit'
-
-    if args:
-        raise SystemExit("Error: Positional parameters not allowed with "
-                         "--oldrev, --newrev, and --refname.")
-
-    # Gerrit oddly omits 'refs/heads/' in the refname when calling
-    # ref-updated hook; put it back.
-    git_dir = get_git_dir()
-    if (not os.path.exists(os.path.join(git_dir, raw_refname)) and
-        os.path.exists(os.path.join(git_dir, 'refs', 'heads',
-                                    raw_refname))):
-        options.refname = 'refs/heads/' + options.refname
-
-    # New revisions can appear in a gerrit repository either due to someone
-    # pushing directly (in which case options.submitter will be set), or they
-    # can press "Submit this patchset" in the web UI for some CR (in which
-    # case options.submitter will not be set and gerrit will not have provided
-    # us the information about who pressed the button).
-    #
-    # Note for the nit-picky: I'm lumping in REST API calls and the ssh
-    # gerrit review command in with "Submit this patchset" button, since they
-    # have the same effect.
-    if options.submitter:
-        update_method = 'pushed'
-        # The submitter argument is almost an RFC 2822 email address; change it
-        # from 'User Name (email@domain)' to 'User Name <email@domain>' so it is
-        options.submitter = options.submitter.replace('(', '<').replace(')', '>')
-    else:
-        update_method = 'submitted'
-        # Gerrit knew who submitted this patchset, but threw that information
-        # away when it invoked this hook.  However, *IF* Gerrit created a
-        # merge to bring the patchset in (project 'Submit Type' is either
-        # "Always Merge", or is "Merge if Necessary" and happens to be
-        # necessary for this particular CR), then it will have the committer
-        # of that merge be 'Gerrit Code Review' and the author will be the
-        # person who requested the submission of the CR.  Since this is fairly
-        # likely for most gerrit installations (of a reasonable size), it's
-        # worth the extra effort to try to determine the actual submitter.
-        rev_info = read_git_lines(['log', '--no-walk', '--merges',
-                                   '--format=%cN%n%aN <%aE>', options.newrev])
-        if rev_info and rev_info[0] == 'Gerrit Code Review':
-            options.submitter = rev_info[1]
-
-    # We pass back refname, oldrev, newrev as args because then the
-    # gerrit ref-updated hook is much like the git update hook
-    return (options,
-            [options.refname, options.oldrev, options.newrev],
-            {'project': options.project, 'submitter': options.submitter,
-             'update_method': update_method})
-
-
-def check_hook_specific_args(options, args):
-    raw_refname = options.refname
-    # Convert each string option unicode for Python3.
-    if PYTHON3:
-        opts = ['environment', 'recipients', 'oldrev', 'newrev', 'refname',
-                'project', 'submitter', 'stash_user', 'stash_repo']
-        for opt in opts:
-            if not hasattr(options, opt):
-                continue
-            obj = getattr(options, opt)
-            if obj:
-                enc = obj.encode('utf-8', 'surrogateescape')
-                dec = enc.decode('utf-8', 'replace')
-                setattr(options, opt, dec)
-
-    # First check for stash arguments
-    if (options.stash_user is None) != (options.stash_repo is None):
-        raise SystemExit("Error: Specify both of --stash-user and "
-                         "--stash-repo or neither.")
-    if options.stash_user:
-        options.environment = 'stash'
-        return options, args, {'stash_user': options.stash_user,
-                               'stash_repo': options.stash_repo}
-
-    # Finally, check for gerrit specific arguments
-    required_gerrit_options = (options.oldrev, options.newrev, options.refname,
-                               options.project)
-    if required_gerrit_options != (None,) * 4:
-        return compute_gerrit_options(options, args, required_gerrit_options,
-                                      raw_refname)
-
-    # No special options in use, just return what we started with
-    return options, args, {}
-
-
-class Logger(object):
-    def parse_verbose(self, verbose):
-        if verbose > 0:
-            return logging.DEBUG
-        else:
-            return logging.INFO
-
-    def create_log_file(self, environment, name, path, verbosity):
-        log_file = logging.getLogger(name)
-        file_handler = logging.FileHandler(path)
-        log_fmt = logging.Formatter("%(asctime)s [%(levelname)-5.5s]  %(message)s")
-        file_handler.setFormatter(log_fmt)
-        log_file.addHandler(file_handler)
-        log_file.setLevel(verbosity)
-        return log_file
-
-    def __init__(self, environment):
-        self.environment = environment
-        self.loggers = []
-        stderr_log = logging.getLogger('git_multimail.stderr')
-
-        class EncodedStderr(object):
-            def write(self, x):
-                write_str(sys.stderr, x)
-
-            def flush(self):
-                sys.stderr.flush()
-
-        stderr_handler = logging.StreamHandler(EncodedStderr())
-        stderr_log.addHandler(stderr_handler)
-        stderr_log.setLevel(self.parse_verbose(environment.verbose))
-        self.loggers.append(stderr_log)
-
-        if environment.debug_log_file is not None:
-            debug_log_file = self.create_log_file(
-                environment, 'git_multimail.debug', environment.debug_log_file, logging.DEBUG)
-            self.loggers.append(debug_log_file)
-
-        if environment.log_file is not None:
-            log_file = self.create_log_file(
-                environment, 'git_multimail.file', environment.log_file, logging.INFO)
-            self.loggers.append(log_file)
-
-        if environment.error_log_file is not None:
-            error_log_file = self.create_log_file(
-                environment, 'git_multimail.error', environment.error_log_file, logging.ERROR)
-            self.loggers.append(error_log_file)
-
-    def info(self, msg, *args, **kwargs):
-        for l in self.loggers:
-            l.info(msg, *args, **kwargs)
-
-    def debug(self, msg, *args, **kwargs):
-        for l in self.loggers:
-            l.debug(msg, *args, **kwargs)
-
-    def warning(self, msg, *args, **kwargs):
-        for l in self.loggers:
-            l.warning(msg, *args, **kwargs)
-
-    def error(self, msg, *args, **kwargs):
-        for l in self.loggers:
-            l.error(msg, *args, **kwargs)
-
-
-def main(args):
-    parser = optparse.OptionParser(
-        description=__doc__,
-        usage='%prog [OPTIONS]\n   or: %prog [OPTIONS] REFNAME OLDREV NEWREV',
-        )
-
-    parser.add_option(
-        '--environment', '--env', action='store', type='choice',
-        choices=list(KNOWN_ENVIRONMENTS.keys()), default=None,
-        help=(
-            'Choose type of environment is in use.  Default is taken from '
-            'multimailhook.environment if set; otherwise "generic".'
-            ),
-        )
-    parser.add_option(
-        '--stdout', action='store_true', default=False,
-        help='Output emails to stdout rather than sending them.',
-        )
-    parser.add_option(
-        '--recipients', action='store', default=None,
-        help='Set list of email recipients for all types of emails.',
-        )
-    parser.add_option(
-        '--show-env', action='store_true', default=False,
-        help=(
-            'Write to stderr the values determined for the environment '
-            '(intended for debugging purposes), then proceed normally.'
-            ),
-        )
-    parser.add_option(
-        '--force-send', action='store_true', default=False,
-        help=(
-            'Force sending refchange email when using as an update hook. '
-            'This is useful to work around the unreliable new commits '
-            'detection in this mode.'
-            ),
-        )
-    parser.add_option(
-        '-c', metavar="<name>=<value>", action='append',
-        help=(
-            'Pass a configuration parameter through to git.  The value given '
-            'will override values from configuration files.  See the -c option '
-            'of git(1) for more details.  (Only works with git >= 1.7.3)'
-            ),
-        )
-    parser.add_option(
-        '--version', '-v', action='store_true', default=False,
-        help=(
-            "Display git-multimail's version"
-            ),
-        )
-
-    parser.add_option(
-        '--python-version', action='store_true', default=False,
-        help=(
-            "Display the version of Python used by git-multimail"
-            ),
-        )
-
-    parser.add_option(
-        '--check-ref-filter', action='store_true', default=False,
-        help=(
-            'List refs and show information on how git-multimail '
-            'will process them.'
-            )
-        )
-
-    # The following options permit this script to be run as a gerrit
-    # ref-updated hook.  See e.g.
-    # code.google.com/p/gerrit/source/browse/Documentation/config-hooks.txt
-    # We suppress help for these items, since these are specific to gerrit,
-    # and we don't want users directly using them any way other than how the
-    # gerrit ref-updated hook is called.
-    parser.add_option('--oldrev', action='store', help=optparse.SUPPRESS_HELP)
-    parser.add_option('--newrev', action='store', help=optparse.SUPPRESS_HELP)
-    parser.add_option('--refname', action='store', help=optparse.SUPPRESS_HELP)
-    parser.add_option('--project', action='store', help=optparse.SUPPRESS_HELP)
-    parser.add_option('--submitter', action='store', help=optparse.SUPPRESS_HELP)
-
-    # The following allow this to be run as a stash asynchronous post-receive
-    # hook (almost identical to a git post-receive hook but triggered also for
-    # merges of pull requests from the UI).  We suppress help for these items,
-    # since these are specific to stash.
-    parser.add_option('--stash-user', action='store', help=optparse.SUPPRESS_HELP)
-    parser.add_option('--stash-repo', action='store', help=optparse.SUPPRESS_HELP)
-
-    (options, args) = parser.parse_args(args)
-    (options, args, hook_info) = check_hook_specific_args(options, args)
-
-    if options.version:
-        sys.stdout.write('git-multimail version ' + get_version() + '\n')
-        return
-
-    if options.python_version:
-        sys.stdout.write('Python version ' + sys.version + '\n')
-        return
-
-    if options.c:
-        Config.add_config_parameters(options.c)
-
-    config = Config('multimailhook')
-
-    environment = None
-    try:
-        environment = choose_environment(
-            config, osenv=os.environ,
-            env=options.environment,
-            recipients=options.recipients,
-            hook_info=hook_info,
-            )
-
-        if options.show_env:
-            show_env(environment, sys.stderr)
-
-        if options.stdout or environment.stdout:
-            mailer = OutputMailer(sys.stdout, environment)
-        else:
-            mailer = choose_mailer(config, environment)
-
-        must_check_setup = os.environ.get('GIT_MULTIMAIL_CHECK_SETUP')
-        if must_check_setup == '':
-            must_check_setup = False
-        if options.check_ref_filter:
-            check_ref_filter(environment)
-        elif must_check_setup:
-            check_setup(environment)
-        # Dual mode: if arguments were specified on the command line, run
-        # like an update hook; otherwise, run as a post-receive hook.
-        elif args:
-            if len(args) != 3:
-                parser.error('Need zero or three non-option arguments')
-            (refname, oldrev, newrev) = args
-            environment.get_logger().debug(
-                "run_as_update_hook: refname=%s, oldrev=%s, newrev=%s, force_send=%s" %
-                (refname, oldrev, newrev, options.force_send))
-            run_as_update_hook(environment, mailer, refname, oldrev, newrev, options.force_send)
-        else:
-            run_as_post_receive_hook(environment, mailer)
-    except ConfigurationException:
-        sys.exit(sys.exc_info()[1])
-    except SystemExit:
-        raise
-    except Exception:
-        t, e, tb = sys.exc_info()
-        import traceback
-        sys.stderr.write('\n')  # Avoid mixing message with previous output
-        msg = (
-            'Exception \'' + t.__name__ +
-            '\' raised. Please report this as a bug to\n'
-            'https://github.com/git-multimail/git-multimail/issues\n'
-            'with the information below:\n\n'
-            'git-multimail version ' + get_version() + '\n'
-            'Python version ' + sys.version + '\n' +
-            traceback.format_exc())
-        try:
-            environment.get_logger().error(msg)
-        except:
-            sys.stderr.write(msg)
-        sys.exit(1)
-
-
-if __name__ == '__main__':
-    main(sys.argv[1:])
diff --git a/contrib/hooks/multimail/migrate-mailhook-config b/contrib/hooks/multimail/migrate-mailhook-config
deleted file mode 100755
index 241ba22fa3c8..000000000000
--- a/contrib/hooks/multimail/migrate-mailhook-config
+++ /dev/null
@@ -1,274 +0,0 @@
-#! /usr/bin/env python
-
-"""Migrate a post-receive-email configuration to be usable with git_multimail.py.
-
-See README.migrate-from-post-receive-email for more information.
-
-"""
-
-import sys
-import optparse
-
-from git_multimail import CommandError
-from git_multimail import Config
-from git_multimail import read_output
-
-
-OLD_NAMES = [
-    'mailinglist',
-    'announcelist',
-    'envelopesender',
-    'emailprefix',
-    'showrev',
-    'emailmaxlines',
-    'diffopts',
-    'scancommitforcc',
-    ]
-
-NEW_NAMES = [
-    'environment',
-    'reponame',
-    'mailinglist',
-    'refchangelist',
-    'commitlist',
-    'announcelist',
-    'announceshortlog',
-    'envelopesender',
-    'administrator',
-    'emailprefix',
-    'emailmaxlines',
-    'diffopts',
-    'emaildomain',
-    'scancommitforcc',
-    ]
-
-
-INFO = """\
-
-SUCCESS!
-
-Your post-receive-email configuration has been converted to
-git-multimail format.  Please see README and
-README.migrate-from-post-receive-email to learn about other
-git-multimail configuration possibilities.
-
-For example, git-multimail has the following new options with no
-equivalent in post-receive-email.  You might want to read about them
-to see if they would be useful in your situation:
-
-"""
-
-
-def _check_old_config_exists(old):
-    """Check that at least one old configuration value is set."""
-
-    for name in OLD_NAMES:
-        if name in old:
-            return True
-
-    return False
-
-
-def _check_new_config_clear(new):
-    """Check that none of the new configuration names are set."""
-
-    retval = True
-    for name in NEW_NAMES:
-        if name in new:
-            if retval:
-                sys.stderr.write('INFO: The following configuration values already exist:\n\n')
-            sys.stderr.write('    "%s.%s"\n' % (new.section, name))
-            retval = False
-
-    return retval
-
-
-def erase_values(config, names):
-    for name in names:
-        if name in config:
-            try:
-                sys.stderr.write('...unsetting "%s.%s"\n' % (config.section, name))
-                config.unset_all(name)
-            except CommandError:
-                sys.stderr.write(
-                    '\nWARNING: could not unset "%s.%s".  '
-                    'Perhaps it is not set at the --local level?\n\n'
-                    % (config.section, name)
-                    )
-
-
-def is_section_empty(section, local):
-    """Return True iff the specified configuration section is empty.
-
-    Iff local is True, use the --local option when invoking 'git
-    config'."""
-
-    if local:
-        local_option = ['--local']
-    else:
-        local_option = []
-
-    try:
-        read_output(
-            ['git', 'config'] +
-            local_option +
-            ['--get-regexp', '^%s\.' % (section,)]
-            )
-    except CommandError:
-        t, e, traceback = sys.exc_info()
-        if e.retcode == 1:
-            # This means that no settings were found.
-            return True
-        else:
-            raise
-    else:
-        return False
-
-
-def remove_section_if_empty(section):
-    """If the specified configuration section is empty, delete it."""
-
-    try:
-        empty = is_section_empty(section, local=True)
-    except CommandError:
-        # Older versions of git do not support the --local option, so
-        # if the first attempt fails, try without --local.
-        try:
-            empty = is_section_empty(section, local=False)
-        except CommandError:
-            sys.stderr.write(
-                '\nINFO: If configuration section "%s.*" is empty, you might want '
-                'to delete it.\n\n'
-                % (section,)
-                )
-            return
-
-    if empty:
-        sys.stderr.write('...removing section "%s.*"\n' % (section,))
-        read_output(['git', 'config', '--remove-section', section])
-    else:
-        sys.stderr.write(
-            '\nINFO: Configuration section "%s.*" still has contents.  '
-            'It will not be deleted.\n\n'
-            % (section,)
-            )
-
-
-def migrate_config(strict=False, retain=False, overwrite=False):
-    old = Config('hooks')
-    new = Config('multimailhook')
-    if not _check_old_config_exists(old):
-        sys.exit(
-            'Your repository has no post-receive-email configuration.  '
-            'Nothing to do.'
-            )
-    if not _check_new_config_clear(new):
-        if overwrite:
-            sys.stderr.write('\nWARNING: Erasing the above values...\n\n')
-            erase_values(new, NEW_NAMES)
-        else:
-            sys.exit(
-                '\nERROR: Refusing to overwrite existing values.  Use the --overwrite\n'
-                'option to continue anyway.'
-                )
-
-    name = 'showrev'
-    if name in old:
-        msg = 'git-multimail does not support "%s.%s"' % (old.section, name,)
-        if strict:
-            sys.exit(
-                'ERROR: %s.\n'
-                'Please unset that value then try again, or run without --strict.'
-                % (msg,)
-                )
-        else:
-            sys.stderr.write('\nWARNING: %s (ignoring).\n\n' % (msg,))
-
-    for name in ['mailinglist', 'announcelist']:
-        if name in old:
-            sys.stderr.write(
-                '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
-                )
-            old_recipients = old.get_all(name, default=None)
-            old_recipients = ', '.join(o.strip() for o in old_recipients)
-            new.set_recipients(name, old_recipients)
-
-    if strict:
-        sys.stderr.write(
-            '...setting "%s.commitlist" to the empty string\n' % (new.section,)
-            )
-        new.set_recipients('commitlist', '')
-        sys.stderr.write(
-            '...setting "%s.announceshortlog" to "true"\n' % (new.section,)
-            )
-        new.set('announceshortlog', 'true')
-
-    for name in ['envelopesender', 'emailmaxlines', 'diffopts', 'scancommitforcc']:
-        if name in old:
-            sys.stderr.write(
-                '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
-                )
-            new.set(name, old.get(name))
-
-    name = 'emailprefix'
-    if name in old:
-        sys.stderr.write(
-            '...copying "%s.%s" to "%s.%s"\n' % (old.section, name, new.section, name)
-            )
-        new.set(name, old.get(name))
-    elif strict:
-        sys.stderr.write(
-            '...setting "%s.%s" to "[SCM]" to preserve old subject lines\n'
-            % (new.section, name)
-            )
-        new.set(name, '[SCM]')
-
-    if not retain:
-        erase_values(old, OLD_NAMES)
-        remove_section_if_empty(old.section)
-
-    sys.stderr.write(INFO)
-    for name in NEW_NAMES:
-        if name not in OLD_NAMES:
-            sys.stderr.write('    "%s.%s"\n' % (new.section, name,))
-    sys.stderr.write('\n')
-
-
-def main(args):
-    parser = optparse.OptionParser(
-        description=__doc__,
-        usage='%prog [OPTIONS]',
-        )
-
-    parser.add_option(
-        '--strict', action='store_true', default=False,
-        help=(
-            'Slavishly configure git-multimail as closely as possible to '
-            'the post-receive-email configuration.  Default is to turn '
-            'on some new features that have no equivalent in post-receive-email.'
-            ),
-        )
-    parser.add_option(
-        '--retain', action='store_true', default=False,
-        help=(
-            'Retain the post-receive-email configuration values.  '
-            'Default is to delete them after the new values are set.'
-            ),
-        )
-    parser.add_option(
-        '--overwrite', action='store_true', default=False,
-        help=(
-            'Overwrite any existing git-multimail configuration settings.  '
-            'Default is to abort if such settings already exist.'
-            ),
-        )
-
-    (options, args) = parser.parse_args(args)
-
-    if args:
-        parser.error('Unexpected arguments: %s' % (' '.join(args),))
-
-    migrate_config(strict=options.strict, retain=options.retain, overwrite=options.overwrite)
-
-
-main(sys.argv[1:])
diff --git a/contrib/hooks/multimail/post-receive.example b/contrib/hooks/multimail/post-receive.example
deleted file mode 100755
index 0f98c5a23db1..000000000000
--- a/contrib/hooks/multimail/post-receive.example
+++ /dev/null
@@ -1,101 +0,0 @@
-#! /usr/bin/env python
-
-"""Example post-receive hook based on git-multimail.
-
-The simplest way to use git-multimail is to use the script
-git_multimail.py directly as a post-receive hook, and to configure it
-using Git's configuration files and command-line parameters.  You can
-also write your own Python wrapper for more advanced configurability,
-using git_multimail.py as a Python module.
-
-This script is a simple example of such a post-receive hook.  It is
-intended to be customized before use; see the comments in the script
-to help you get started.
-
-Using git-multimail as a Python module as done here provides more
-flexibility.  It has the following advantages:
-
-* The tool's behavior can be customized using arbitrary Python code,
-  without having to edit git_multimail.py.
-
-* Configuration settings can be read from other sources; for example,
-  user names and email addresses could be read from LDAP or from a
-  database.  Or the settings can even be hardcoded in the importing
-  Python script, if this is preferred.
-
-This script is a very basic example of how to use git_multimail.py as
-a module.  The comments below explain some of the points at which the
-script's behavior could be changed or customized.
-
-"""
-
-import sys
-
-# If necessary, add the path to the directory containing
-# git_multimail.py to the Python path as follows.  (This is not
-# necessary if git_multimail.py is in the same directory as this
-# script):
-
-#LIBDIR = 'path/to/directory/containing/module'
-#sys.path.insert(0, LIBDIR)
-
-import git_multimail
-
-# It is possible to modify the output templates here; e.g.:
-
-#git_multimail.FOOTER_TEMPLATE = """\
-#
-#-- \n\
-#This email was generated by the wonderful git-multimail tool.
-#"""
-
-
-# Specify which "git config" section contains the configuration for
-# git-multimail:
-config = git_multimail.Config('multimailhook')
-
-# Set some Git configuration variables. Equivalent to passing var=val
-# to "git -c var=val" each time git is called, or to adding the
-# configuration in .git/config (must come before instantiating the
-# environment) :
-#git_multimail.Config.add_config_parameters('multimailhook.commitEmailFormat=html')
-#git_multimail.Config.add_config_parameters(('user.name=foo', 'user.email=foo@example.com'))
-
-# Select the type of environment:
-try:
-    environment = git_multimail.GenericEnvironment(config=config)
-    #environment = git_multimail.GitoliteEnvironment(config=config)
-except git_multimail.ConfigurationException:
-    sys.stderr.write('*** %s\n' % sys.exc_info()[1])
-    sys.exit(1)
-
-
-# Choose the method of sending emails based on the git config:
-mailer = git_multimail.choose_mailer(config, environment)
-
-# Alternatively, you may hardcode the mailer using code like one of
-# the following:
-
-# Use "/usr/sbin/sendmail -oi -t" to send emails.  The envelopesender
-# argument is optional:
-#mailer = git_multimail.SendMailer(
-#    command=['/usr/sbin/sendmail', '-oi', '-t'],
-#    envelopesender='git-repo@example.com',
-#    )
-
-# Use Python's smtplib to send emails.  Both arguments are required.
-#mailer = git_multimail.SMTPMailer(
-#    environment=environment,
-#    envelopesender='git-repo@example.com',
-#    # The smtpserver argument can also include a port number; e.g.,
-#    #     smtpserver='mail.example.com:25'
-#    smtpserver='mail.example.com',
-#    )
-
-# OutputMailer is intended only for testing; it writes the emails to
-# the specified file stream.
-#mailer = git_multimail.OutputMailer(sys.stdout)
-
-
-# Read changes from stdin and send notification emails:
-git_multimail.run_as_post_receive_hook(environment, mailer)

base-commit: ebf3c04b262aa27fbb97f8a0156c2347fecafafb
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2021-06-10 16:00 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-06-10  8:29 [PATCH] multimail: stop shipping a copy Johannes Schindelin via GitGitGadget
2021-06-10 10:04 ` Ævar Arnfjörð Bjarmason
2021-06-10 14:03   ` Matthieu Moy
2021-06-10 15:40     ` Elijah Newren
2021-06-10 10:31 ` Johannes Schindelin
2021-06-10 16:00 ` Junio C Hamano

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).