{"thread":{"id":"36359","subject":"[PATCH] git-multimail: update to version 1.0.0","startedAt":"2014-04-07T15:20:40Z","lastAt":"2014-04-09T16:35:40Z","messageCount":4,"participants":["Michael Haggerty","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"238470","messageId":"1396884040-26014-1-git-send-email-mhagger@alum.mit.edu","threadId":"36359","inReplyTo":null,"subject":"[PATCH] git-multimail: update to version 1.0.0","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2014-04-07T15:20:40Z","receivedAt":"2014-04-07T15:20:40Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"This commit contains the squashed changes from the upstream\ngit-multimail repository since the last code drop.  Highlights:\n\n* Fix encoding of non-ASCII email addresses in email headers.\n\n* Fix backwards-compatibility bugs for older Python 2.x versions.\n\n* Fix a backwards-compatibility bug for Git 1.7.1.\n\n* Add an option commitDiffOpts to customize logs for revisions.\n\n* Pass \"-oi\" to sendmail by default to prevent premature\n  termination\n  on a line containing only \".\".\n\n* Stagger email \"Date:\" values in an attempt to help mail clients\n  thread the emails in the right order.\n\n* If a mailing list setting is missing, just skip sending the\n  corresponding email (with a warning) instead of failing.\n\n* Add a X-Git-Host header that can be used for email filtering.\n\n* Allow the sender's fully-qualified domain name to be configured.\n\n* Minor documentation improvements.\n\n* Add a CHANGES file.\n\nContributions-by: Raphaël Hertzog <hertzog@debian.org>\nContributions-by: Eric Berberich <eric.berberich@gmail.com>\nContributions-by: Michiel Holtkamp <git@elfstone.nl>\nContributions-by: Malte Swart <mswart@devtation.de>\nSigned-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n---\nJunio, how would you like other people's contributions to be recorded\nwithin the Git project?  I have listed them above as\n\"Contributions-by\".  All of these people have signed off on their\ncontributions (recorded in my GitHub repo).  So should I also/instead\nadd \"Signed-off-by\" for those people?\n\n contrib/hooks/multimail/CHANGES          |  33 +++++\n contrib/hooks/multimail/README           |  46 ++++---\n contrib/hooks/multimail/README.Git       |   4 +-\n contrib/hooks/multimail/git_multimail.py | 218 ++++++++++++++++++++++++++-----\n contrib/hooks/multimail/post-receive     |   4 +-\n 5 files changed, 249 insertions(+), 56 deletions(-)\n create mode 100644 contrib/hooks/multimail/CHANGES\n\ndiff --git a/contrib/hooks/multimail/CHANGES b/contrib/hooks/multimail/CHANGES\nnew file mode 100644\nindex 0000000..3603d56\n--- /dev/null\n+++ b/contrib/hooks/multimail/CHANGES\n@@ -0,0 +1,33 @@\n+Release 1.0.0\n+=============\n+\n+* Fix encoding of non-ASCII email addresses in email headers.\n+\n+* Fix backwards-compatibility bugs for older Python 2.x versions.\n+\n+* Fix a backwards-compatibility bug for Git 1.7.1.\n+\n+* Add an option commitDiffOpts to customize logs for revisions.\n+\n+* Pass \"-oi\" to sendmail by default to prevent premature termination\n+  on a line containing only \".\".\n+\n+* Stagger email \"Date:\" values in an attempt to help mail clients\n+  thread the emails in the right order.\n+\n+* If a mailing list setting is missing, just skip sending the\n+  corresponding email (with a warning) instead of failing.\n+\n+* Add a X-Git-Host header that can be used for email filtering.\n+\n+* Allow the sender's fully-qualified domain name to be configured.\n+\n+* Minor documentation improvements.\n+\n+* Add this CHANGES file.\n+\n+\n+Release 0.9.0\n+=============\n+\n+* Initial release.\ndiff --git a/contrib/hooks/multimail/README b/contrib/hooks/multimail/README\nindex 9904396..477d65f 100644\n--- a/contrib/hooks/multimail/README\n+++ b/contrib/hooks/multimail/README\n@@ -91,9 +91,10 @@ Requirements\n   been tested; if you do so, please report your results.)\n \n * To send emails using the default configuration, a standard sendmail\n-  program must be located at '/usr/sbin/sendmail' and configured\n-  correctly to send emails.  If this is not the case, see the\n-  multimailhook.mailer configuration variable below for how to\n+  program must be located at '/usr/sbin/sendmail' or\n+  '/usr/lib/sendmail' and must be configured correctly to send emails.\n+  If this is not the case, set multimailhook.sendmailCommand, or see\n+  the multimailhook.mailer configuration variable below for how to\n   configure git-multimail to send emails via an SMTP server.\n \n \n@@ -169,7 +170,7 @@ multimailhook.repoName\n     for gitolite repositories, or otherwise to derive this value from\n     the repository path name.\n \n-multimailhook.mailinglist\n+multimailhook.mailingList\n \n     The list of email addresses to which notification emails should be\n     sent, as RFC 2822 email addresses separated by commas.  This\n@@ -184,26 +185,29 @@ multimailhook.refchangeList\n     reference changes should be sent, as RFC 2822 email addresses\n     separated by commas.  This configuration option can be\n     multivalued.  The default is the value in\n-    multimailhook.mailinglist.  Set this value to the empty string to\n-    prevent reference change emails from being sent.\n+    multimailhook.mailingList.  Set this value to the empty string to\n+    prevent reference change emails from being sent even if\n+    multimailhook.mailingList is set.\n \n multimailhook.announceList\n \n     The list of email addresses to which emails about new annotated\n     tags should be sent, as RFC 2822 email addresses separated by\n     commas.  This configuration option can be multivalued.  The\n-    default is the value in multimailhook.refchangelist or\n-    multimailhook.mailinglist.  Set this value to the empty string to\n-    prevent annotated tag announcement emails from being sent.\n+    default is the value in multimailhook.refchangeList or\n+    multimailhook.mailingList.  Set this value to the empty string to\n+    prevent annotated tag announcement emails from being sent even if\n+    one of the other values is set.\n \n multimailhook.commitList\n \n     The list of email addresses to which emails about individual new\n     commits should be sent, as RFC 2822 email addresses separated by\n     commas.  This configuration option can be multivalued.  The\n-    default is the value in multimailhook.mailinglist.  Set this value\n+    default is the value in multimailhook.mailingList.  Set this value\n     to the empty string to prevent notification emails about\n-    individual commits from being sent.\n+    individual commits from being sent even if\n+    multimailhook.mailingList is set.\n \n multimailhook.announceShortlog\n \n@@ -237,10 +241,11 @@ multimailhook.mailer\n            quoting is allowed in the value of this setting, but remember that\n            Git requires double-quotes to be escaped; e.g.,\n \n-             git config multimailhook.sendmailcommand '/usr/sbin/sendmail -t -F \\\"Git Repo\\\"'\n+             git config multimailhook.sendmailcommand '/usr/sbin/sendmail -oi -t -F \\\"Git Repo\\\"'\n \n-           Default is '/usr/sbin/sendmail -t' or '/usr/lib/sendmail\n-           -t' (depending on which file is present and executable).\n+           Default is '/usr/sbin/sendmail -oi -t' or\n+           '/usr/lib/sendmail -oi -t' (depending on which file is\n+           present and executable).\n \n        multimailhook.envelopeSender\n \n@@ -344,6 +349,14 @@ multimailhook.logOpts\n       [multimailhook]\n               logopts = --pretty=format:\\\"%h %aN <%aE>%n%s%n%n%b%n\\\"\n \n+multimailhook.commitLogOpts\n+\n+    Options passed to \"git log\" to generate additional info for\n+    revision change emails.  For example, adding --ignore-all-spaces\n+    will suppress whitespace changes.  The default options are \"-C\n+    --stat -p --cc\".  Shell quoting is allowed; see\n+    multimailhook.logOpts for details.\n+\n multimailhook.emailDomain\n \n     Domain name appended to the username of the person doing the push\n@@ -381,8 +394,8 @@ Email filtering aids\n \n All emails include extra headers to enable fine tuned filtering and\n give information for debugging.  All emails include the headers\n-\"X-Git-Repo\", \"X-Git-Refname\", and \"X-Git-Reftype\".  ReferenceChange\n-emails also include headers \"X-Git-Oldrev\" and \"X-Git-Newrev\";\n+\"X-Git-Host\", \"X-Git-Repo\", \"X-Git-Refname\", and \"X-Git-Reftype\".\n+ReferenceChange emails also include headers \"X-Git-Oldrev\" and \"X-Git-Newrev\";\n Revision emails also include header \"X-Git-Rev\".\n \n \n@@ -463,6 +476,7 @@ The git-multimail project itself is currently hosted on GitHub:\n We use the GitHub issue tracker to keep track of bugs and feature\n requests, and GitHub pull requests to exchange patches (though, if you\n prefer, you can send patches via the Git mailing list with cc to me).\n+Please sign off your patches as per the Git project practice.\n \n Please note that although a copy of git-multimail will probably be\n distributed in the \"contrib\" section of the main Git project,\ndiff --git a/contrib/hooks/multimail/README.Git b/contrib/hooks/multimail/README.Git\nindex 9c2e66a..129b771 100644\n--- a/contrib/hooks/multimail/README.Git\n+++ b/contrib/hooks/multimail/README.Git\n@@ -6,10 +6,10 @@ website:\n     https://github.com/mhagger/git-multimail\n \n The version in this directory was obtained from the upstream project\n-on 2013-07-14 and consists of the \"git-multimail\" subdirectory from\n+on 2014-04-07 and consists of the \"git-multimail\" subdirectory from\n revision\n \n-    1a5cb09c698a74d15a715a86b09ead5f56bf4b06\n+    1b32653bafc4f902535b9fc1cd9cae911325b870\n \n Please see the README file in this directory for information about how\n to report bugs or contribute to git-multimail.\ndiff --git a/contrib/hooks/multimail/git_multimail.py b/contrib/hooks/multimail/git_multimail.py\nindex 81c6a51..8b58ed6 100755\n--- a/contrib/hooks/multimail/git_multimail.py\n+++ b/contrib/hooks/multimail/git_multimail.py\n@@ -1,6 +1,6 @@\n #! /usr/bin/env python2\n \n-# Copyright (c) 2012,2013 Michael Haggerty\n+# Copyright (c) 2012-2014 Michael Haggerty and others\n # Derived from contrib/hooks/post-receive-email, which is\n # Copyright (c) 2007 Andy Parkins\n # and also includes contributions by other authors.\n@@ -49,21 +49,25 @@ import sys\n import os\n import re\n import bisect\n+import socket\n import subprocess\n import shlex\n import optparse\n import smtplib\n+import time\n \n try:\n     from email.utils import make_msgid\n     from email.utils import getaddresses\n     from email.utils import formataddr\n+    from email.utils import formatdate\n     from email.header import Header\n except ImportError:\n     # Prior to Python 2.5, the email module used different names:\n     from email.Utils import make_msgid\n     from email.Utils import getaddresses\n     from email.Utils import formataddr\n+    from email.Utils import formatdate\n     from email.Header import Header\n \n \n@@ -73,6 +77,7 @@ ZEROS = '0' * 40\n LOGBEGIN = '- Log -----------------------------------------------------------------\\n'\n LOGEND = '-----------------------------------------------------------------------\\n'\n \n+ADDR_HEADERS = set(['from', 'to', 'cc', 'bcc', 'reply-to', 'sender'])\n \n # It is assumed in many places that the encoding is uniformly UTF-8,\n # so changing these constants is unsupported.  But define them here\n@@ -95,6 +100,7 @@ REF_DELETED_SUBJECT_TEMPLATE = (\n     )\n \n REFCHANGE_HEADER_TEMPLATE = \"\"\"\\\n+Date: %(send_date)s\n To: %(recipients)s\n Subject: %(subject)s\n MIME-Version: 1.0\n@@ -103,6 +109,7 @@ Content-Transfer-Encoding: 8bit\n Message-ID: %(msgid)s\n From: %(fromaddr)s\n Reply-To: %(reply_to)s\n+X-Git-Host: %(fqdn)s\n X-Git-Repo: %(repo_shortname)s\n X-Git-Refname: %(refname)s\n X-Git-Reftype: %(refname_type)s\n@@ -221,6 +228,7 @@ how to provide full information about this reference change.\n \n \n REVISION_HEADER_TEMPLATE = \"\"\"\\\n+Date: %(send_date)s\n To: %(recipients)s\n Subject: %(emailprefix)s%(num)02d/%(tot)02d: %(oneline)s\n MIME-Version: 1.0\n@@ -230,6 +238,7 @@ From: %(fromaddr)s\n Reply-To: %(reply_to)s\n In-Reply-To: %(reply_to_msgid)s\n References: %(reply_to_msgid)s\n+X-Git-Host: %(fqdn)s\n X-Git-Repo: %(repo_shortname)s\n X-Git-Refname: %(refname)s\n X-Git-Reftype: %(refname_type)s\n@@ -263,13 +272,43 @@ class ConfigurationException(Exception):\n     pass\n \n \n+# The \"git\" program (this could be changed to include a full path):\n+GIT_EXECUTABLE = 'git'\n+\n+\n+# How \"git\" should be invoked (including global arguments), as a list\n+# of words.  This variable is usually initialized automatically by\n+# read_git_output() via choose_git_command(), but if a value is set\n+# here then it will be used unconditionally.\n+GIT_CMD = None\n+\n+\n+def choose_git_command():\n+    \"\"\"Decide how to invoke git, and record the choice in GIT_CMD.\"\"\"\n+\n+    global GIT_CMD\n+\n+    if GIT_CMD is None:\n+        try:\n+            # Check to see whether the \"-c\" option is accepted (it was\n+            # only added in Git 1.7.2).  We don't actually use the\n+            # output of \"git --version\", though if we needed more\n+            # specific version information this would be the place to\n+            # do it.\n+            cmd = [GIT_EXECUTABLE, '-c', 'foo.bar=baz', '--version']\n+            read_output(cmd)\n+            GIT_CMD = [GIT_EXECUTABLE, '-c', 'i18n.logoutputencoding=%s' % (ENCODING,)]\n+        except CommandError:\n+            GIT_CMD = [GIT_EXECUTABLE]\n+\n+\n def read_git_output(args, input=None, keepends=False, **kw):\n     \"\"\"Read the output of a Git command.\"\"\"\n \n-    return read_output(\n-        ['git', '-c', 'i18n.logoutputencoding=%s' % (ENCODING,)] + args,\n-        input=input, keepends=keepends, **kw\n-        )\n+    if GIT_CMD is None:\n+        choose_git_command()\n+\n+    return read_output(GIT_CMD + args, input=input, keepends=keepends, **kw)\n \n \n def read_output(cmd, input=None, keepends=False, **kw):\n@@ -297,6 +336,31 @@ def read_git_lines(args, keepends=False, **kw):\n     return read_git_output(args, keepends=True, **kw).splitlines(keepends)\n \n \n+def header_encode(text, header_name=None):\n+    \"\"\"Encode and line-wrap the value of an email header field.\"\"\"\n+\n+    try:\n+        if isinstance(text, str):\n+            text = text.decode(ENCODING, 'replace')\n+        return Header(text, header_name=header_name).encode()\n+    except UnicodeEncodeError:\n+        return Header(text, header_name=header_name, charset=CHARSET,\n+                      errors='replace').encode()\n+\n+\n+def addr_header_encode(text, header_name=None):\n+    \"\"\"Encode and line-wrap the value of an email header field containing\n+    email addresses.\"\"\"\n+\n+    return Header(\n+        ', '.join(\n+            formataddr((header_encode(name), emailaddr))\n+            for name, emailaddr in getaddresses([text])\n+            ),\n+        header_name=header_name\n+        ).encode()\n+\n+\n class Config(object):\n     def __init__(self, section, git_config=None):\n         \"\"\"Represent a section of the git configuration.\n@@ -578,11 +642,11 @@ class Change(object):\n                         % (e.args[0], line,)\n                         )\n             else:\n-                try:\n-                    h = Header(value, header_name=name)\n-                except UnicodeDecodeError:\n-                    h = Header(value, header_name=name, charset=CHARSET, errors='replace')\n-                for splitline in ('%s: %s\\n' % (name, h.encode(),)).splitlines(True):\n+                if name.lower() in ADDR_HEADERS:\n+                    value = addr_header_encode(value, name)\n+                else:\n+                    value = header_encode(value, name)\n+                for splitline in ('%s: %s\\n' % (name, value)).splitlines(True):\n                     yield splitline\n \n     def generate_email_header(self):\n@@ -616,15 +680,19 @@ class Change(object):\n \n         raise NotImplementedError()\n \n-    def generate_email(self, push, body_filter=None):\n+    def generate_email(self, push, body_filter=None, extra_header_values={}):\n         \"\"\"Generate an email describing this change.\n \n         Iterate over the lines (including the header lines) of an\n         email describing this change.  If body_filter is not None,\n         then use it to filter the lines that are intended for the\n-        email body.\"\"\"\n+        email body.\n+\n+        The extra_header_values field is received as a dict and not as\n+        **kwargs, to allow passing other keyword arguments in the\n+        future (e.g. passing extra values to generate_email_intro()\"\"\"\n \n-        for line in self.generate_email_header():\n+        for line in self.generate_email_header(**extra_header_values):\n             yield line\n         yield '\\n'\n         for line in self.generate_email_intro():\n@@ -680,8 +748,10 @@ class Revision(Change):\n \n         return values\n \n-    def generate_email_header(self):\n-        for line in self.expand_header_lines(REVISION_HEADER_TEMPLATE):\n+    def generate_email_header(self, **extra_values):\n+        for line in self.expand_header_lines(\n+            REVISION_HEADER_TEMPLATE, **extra_values\n+            ):\n             yield line\n \n     def generate_email_intro(self):\n@@ -692,11 +762,7 @@ class Revision(Change):\n         \"\"\"Show this revision.\"\"\"\n \n         return read_git_lines(\n-            [\n-                'log', '-C',\n-                 '--stat', '-p', '--cc',\n-                '-1', self.rev.sha1,\n-                ],\n+            ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1],\n             keepends=True,\n             )\n \n@@ -800,6 +866,7 @@ class ReferenceChange(Change):\n         self.msgid = make_msgid()\n         self.diffopts = environment.diffopts\n         self.logopts = environment.logopts\n+        self.commitlogopts = environment.commitlogopts\n         self.showlog = environment.refchange_showlog\n \n     def _compute_values(self):\n@@ -835,9 +902,12 @@ class ReferenceChange(Change):\n             }[self.change_type]\n         return self.expand(template)\n \n-    def generate_email_header(self):\n+    def generate_email_header(self, **extra_values):\n+        if 'subject' not in extra_values:\n+            extra_values['subject'] = self.get_subject()\n+\n         for line in self.expand_header_lines(\n-            REFCHANGE_HEADER_TEMPLATE, subject=self.get_subject(),\n+            REFCHANGE_HEADER_TEMPLATE, **extra_values\n             ):\n             yield line\n \n@@ -1273,7 +1343,7 @@ class Mailer(object):\n \n \n class SendMailer(Mailer):\n-    \"\"\"Send emails using 'sendmail -t'.\"\"\"\n+    \"\"\"Send emails using 'sendmail -oi -t'.\"\"\"\n \n     SENDMAIL_CANDIDATES = [\n         '/usr/sbin/sendmail',\n@@ -1302,7 +1372,7 @@ class SendMailer(Mailer):\n         if command:\n             self.command = command[:]\n         else:\n-            self.command = [self.find_sendmail(), '-t']\n+            self.command = [self.find_sendmail(), '-oi', '-t']\n \n         if envelopesender:\n             self.command.extend(['-f', envelopesender])\n@@ -1495,6 +1565,12 @@ class Environment(object):\n             'git log' when generating the detailed log for a set of\n             commits (see refchange_showlog)\n \n+        commitlogopts (list of strings)\n+\n+            The options that should be passed to 'git log' for each\n+            commit mail.  The value should be a list of strings\n+            representing words to be passed to the command.\n+\n     \"\"\"\n \n     REPO_NAME_RE = re.compile(r'^(?P<name>.+?)(?:\\.git)$')\n@@ -1506,6 +1582,7 @@ class Environment(object):\n         self.diffopts = ['--stat', '--summary', '--find-copies-harder']\n         self.logopts = []\n         self.refchange_showlog = False\n+        self.commitlogopts = ['-C', '--stat', '-p', '--cc']\n \n         self.COMPUTED_KEYS = [\n             'administrator',\n@@ -1672,6 +1749,10 @@ class ConfigOptionsEnvironmentMixin(ConfigEnvironmentMixin):\n         if logopts is not None:\n             self.logopts = shlex.split(logopts)\n \n+        commitlogopts = config.get('commitlogopts')\n+        if commitlogopts is not None:\n+            self.commitlogopts = shlex.split(commitlogopts)\n+\n         reply_to = config.get('replyTo')\n         self.__reply_to_refchange = config.get('replyToRefchange', default=reply_to)\n         if (\n@@ -1829,6 +1910,47 @@ class ConfigMaxlinesEnvironmentMixin(\n             )\n \n \n+class FQDNEnvironmentMixin(Environment):\n+    \"\"\"A mixin that sets the host's FQDN to its constructor argument.\"\"\"\n+\n+    def __init__(self, fqdn, **kw):\n+        super(FQDNEnvironmentMixin, self).__init__(**kw)\n+        self.COMPUTED_KEYS += ['fqdn']\n+        self.__fqdn = fqdn\n+\n+    def get_fqdn(self):\n+        \"\"\"Return the fully-qualified domain name for this host.\n+\n+        Return None if it is unavailable or unwanted.\"\"\"\n+\n+        return self.__fqdn\n+\n+\n+class ConfigFQDNEnvironmentMixin(\n+    ConfigEnvironmentMixin,\n+    FQDNEnvironmentMixin,\n+    ):\n+    \"\"\"Read the FQDN from the config.\"\"\"\n+\n+    def __init__(self, config, **kw):\n+        fqdn = config.get('fqdn')\n+        super(ConfigFQDNEnvironmentMixin, self).__init__(\n+            config=config,\n+            fqdn=fqdn,\n+            **kw\n+            )\n+\n+\n+class ComputeFQDNEnvironmentMixin(FQDNEnvironmentMixin):\n+    \"\"\"Get the FQDN by calling socket.getfqdn().\"\"\"\n+\n+    def __init__(self, **kw):\n+        super(ComputeFQDNEnvironmentMixin, self).__init__(\n+            fqdn=socket.getfqdn(),\n+            **kw\n+            )\n+\n+\n class PusherDomainEnvironmentMixin(ConfigEnvironmentMixin):\n     \"\"\"Deduce pusher_email from pusher by appending an emaildomain.\"\"\"\n \n@@ -1861,6 +1983,10 @@ class StaticRecipientsEnvironmentMixin(Environment):\n         # actual *contents* of the change being reported, we only\n         # choose based on the *type* of the change.  Therefore we can\n         # compute them once and for all:\n+        if not (refchange_recipients\n+                or announce_recipients\n+                or revision_recipients):\n+            raise ConfigurationException('No email recipients configured!')\n         self.__refchange_recipients = refchange_recipients\n         self.__announce_recipients = announce_recipients\n         self.__revision_recipients = revision_recipients\n@@ -1911,17 +2037,8 @@ class ConfigRecipientsEnvironmentMixin(\n             retval = config.get_recipients(name)\n             if retval is not None:\n                 return retval\n-        if len(names) == 1:\n-            hint = 'Please set \"%s.%s\"' % (config.section, name)\n         else:\n-            hint = (\n-                'Please set one of the following:\\n    \"%s\"'\n-                % ('\"\\n    \"'.join('%s.%s' % (config.section, name) for name in names))\n-                )\n-\n-        raise ConfigurationException(\n-            'The list of recipients for %s is not configured.\\n%s' % (names[0], hint)\n-            )\n+            return ''\n \n \n class ProjectdescEnvironmentMixin(Environment):\n@@ -1956,6 +2073,7 @@ class GenericEnvironmentMixin(Environment):\n class GenericEnvironment(\n     ProjectdescEnvironmentMixin,\n     ConfigMaxlinesEnvironmentMixin,\n+    ComputeFQDNEnvironmentMixin,\n     ConfigFilterLinesEnvironmentMixin,\n     ConfigRecipientsEnvironmentMixin,\n     PusherDomainEnvironmentMixin,\n@@ -1980,9 +2098,27 @@ class GitoliteEnvironmentMixin(Environment):\n         return self.osenv.get('GL_USER', 'unknown user')\n \n \n+class IncrementalDateTime(object):\n+    \"\"\"Simple wrapper to give incremental date/times.\n+\n+    Each call will result in a date/time a second later than the\n+    previous call.  This can be used to falsify email headers, to\n+    increase the likelihood that email clients sort the emails\n+    correctly.\"\"\"\n+\n+    def __init__(self):\n+        self.time = time.time()\n+\n+    def next(self):\n+        formatted = formatdate(self.time, True)\n+        self.time += 1\n+        return formatted\n+\n+\n class GitoliteEnvironment(\n     ProjectdescEnvironmentMixin,\n     ConfigMaxlinesEnvironmentMixin,\n+    ComputeFQDNEnvironmentMixin,\n     ConfigFilterLinesEnvironmentMixin,\n     ConfigRecipientsEnvironmentMixin,\n     PusherDomainEnvironmentMixin,\n@@ -2187,6 +2323,7 @@ class Push(object):\n         # guarantee that one (and only one) email is generated for\n         # each new commit.\n         unhandled_sha1s = set(self.get_new_commits())\n+        send_date = IncrementalDateTime()\n         for change in self.changes:\n             # Check if we've got anyone to send to\n             if not change.recipients:\n@@ -2197,7 +2334,11 @@ class Push(object):\n                     )\n             else:\n                 sys.stderr.write('Sending notification emails to: %s\\n' % (change.recipients,))\n-                mailer.send(change.generate_email(self, body_filter), change.recipients)\n+                extra_values = {'send_date' : send_date.next()}\n+                mailer.send(\n+                    change.generate_email(self, body_filter, extra_values),\n+                    change.recipients,\n+                    )\n \n             sha1s = []\n             for sha1 in reversed(list(self.get_new_commits(change))):\n@@ -2217,7 +2358,11 @@ class Push(object):\n             for (num, sha1) in enumerate(sha1s):\n                 rev = Revision(change, GitObject(sha1), num=num+1, tot=len(sha1s))\n                 if rev.recipients:\n-                    mailer.send(rev.generate_email(self, body_filter), rev.recipients)\n+                    extra_values = {'send_date' : send_date.next()}\n+                    mailer.send(\n+                        rev.generate_email(self, body_filter, extra_values),\n+                        rev.recipients,\n+                        )\n \n         # Consistency check:\n         if unhandled_sha1s:\n@@ -2288,6 +2433,7 @@ def choose_environment(config, osenv=None, env=None, recipients=None):\n     environment_mixins = [\n         ProjectdescEnvironmentMixin,\n         ConfigMaxlinesEnvironmentMixin,\n+        ComputeFQDNEnvironmentMixin,\n         ConfigFilterLinesEnvironmentMixin,\n         PusherDomainEnvironmentMixin,\n         ConfigOptionsEnvironmentMixin,\ndiff --git a/contrib/hooks/multimail/post-receive b/contrib/hooks/multimail/post-receive\nindex 93ebb43..4d46828 100755\n--- a/contrib/hooks/multimail/post-receive\n+++ b/contrib/hooks/multimail/post-receive\n@@ -66,10 +66,10 @@ mailer = git_multimail.choose_mailer(config, environment)\n # Alternatively, you may hardcode the mailer using code like one of\n # the following:\n \n-# Use \"/usr/sbin/sendmail -t\" to send emails.  The envelopesender\n+# Use \"/usr/sbin/sendmail -oi -t\" to send emails.  The envelopesender\n # argument is optional:\n #mailer = git_multimail.SendMailer(\n-#    command=['/usr/sbin/sendmail', '-t'],\n+#    command=['/usr/sbin/sendmail', '-oi', '-t'],\n #    envelopesender='git-repo@example.com',\n #    )\n \n-- \n1.9.1\n"},{"id":"238496","messageId":"xmqqd2gtm0id.fsf@gitster.dls.corp.google.com","threadId":"36359","inReplyTo":"1396884040-26014-1-git-send-email-mhagger@alum.mit.edu","subject":"Re: [PATCH] git-multimail: update to version 1.0.0","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-04-07T18:56:42Z","receivedAt":"2014-04-07T18:56:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Michael Haggerty <mhagger@alum.mit.edu> writes:\n\n> ...\n> Contributions-by: Raphaël Hertzog <hertzog@debian.org>\n> Contributions-by: Eric Berberich <eric.berberich@gmail.com>\n> Contributions-by: Michiel Holtkamp <git@elfstone.nl>\n> Contributions-by: Malte Swart <mswart@devtation.de>\n> Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n> ---\n> Junio, how would you like other people's contributions to be recorded\n> within the Git project?  I have listed them above as\n> \"Contributions-by\".  All of these people have signed off on their\n> contributions (recorded in my GitHub repo).  So should I also/instead\n> add \"Signed-off-by\" for those people?\n\nEither is fine, as long as somewhere in that directory:\n\n - we make it clear that the copy we have in contrib/ is merely for\n   \"batteries included\" convenience;\n\n - we refer to the canonical source that is your repository;\n\n - we tell readers to go there to get the authoritative and up to\n   date copy, as what we have in contrib/ is possibly stale.\n\nIn the longer term, I have a feeling that we may be better off to\nmake the \"git core\" tree not be the \"batteris included\" convenience\ntree, though.  In the early days, Linus's rationale for including\n\"gitk\" held true: having tools that are not quite core is a good way\nto get people (especially those without C background) involved in\nthe still-small project in its infancy to help nurture the developer\ncommunity.  The same reasoning stood behind the merging of \"gitweb\".\n\nWe already are beyond that stage, and good tools like iMerge and\nmultimail that can stand on its own may be better off flourishing\noutside \"git core\" tree, still within the same developer community.\n"},{"id":"238583","messageId":"53456EC8.7090109@alum.mit.edu","threadId":"36359","inReplyTo":"xmqqd2gtm0id.fsf@gitster.dls.corp.google.com","subject":"Re: [PATCH] git-multimail: update to version 1.0.0","fromName":"Michael Haggerty","fromEmail":"mhagger@alum.mit.edu","sentAt":"2014-04-09T16:01:12Z","receivedAt":"2014-04-09T16:01:12Z","isPatch":true,"sender":{"key":"mhagger@alum.mit.edu","avatar":"https://avatars.githubusercontent.com/u/119718?v=4"},"body":"On 04/07/2014 08:56 PM, Junio C Hamano wrote:\n> Michael Haggerty <mhagger@alum.mit.edu> writes:\n> \n>> ...\n>> Contributions-by: Raphaël Hertzog <hertzog@debian.org>\n>> Contributions-by: Eric Berberich <eric.berberich@gmail.com>\n>> Contributions-by: Michiel Holtkamp <git@elfstone.nl>\n>> Contributions-by: Malte Swart <mswart@devtation.de>\n>> Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu>\n>> ---\n>> Junio, how would you like other people's contributions to be recorded\n>> within the Git project?  I have listed them above as\n>> \"Contributions-by\".  All of these people have signed off on their\n>> contributions (recorded in my GitHub repo).  So should I also/instead\n>> add \"Signed-off-by\" for those people?\n> \n> Either is fine, as long as somewhere in that directory:\n> \n>  - we make it clear that the copy we have in contrib/ is merely for\n>    \"batteries included\" convenience;\n> \n>  - we refer to the canonical source that is your repository;\n> \n>  - we tell readers to go there to get the authoritative and up to\n>    date copy, as what we have in contrib/ is possibly stale.\n\nThis information is already present in README.Git, though it doesn't say\nexplicitly that the copy within the Git source tree might be stale.  I\ncan add that if you like.\n\n> In the longer term, I have a feeling that we may be better off to\n> make the \"git core\" tree not be the \"batteris included\" convenience\n> tree, though.  In the early days, Linus's rationale for including\n> \"gitk\" held true: having tools that are not quite core is a good way\n> to get people (especially those without C background) involved in\n> the still-small project in its infancy to help nurture the developer\n> community.  The same reasoning stood behind the merging of \"gitweb\".\n> \n> We already are beyond that stage, and good tools like iMerge and\n> multimail that can stand on its own may be better off flourishing\n> outside \"git core\" tree, still within the same developer community.\n\nI was going by what you said when multimail was originally added to your\nrepo [1]:\n\n> The second category will be in a separate hierarchy (perhaps\n> addons/, hooks/, ..., but I am fine if we decide to keep them in\n> contrib/addons, contrib/hooks, etc.).\n> [...]\n> The multimail tool can be in the second category.  It helps use of\n> Git more than it is helped by using Git.\n\nTell me if/when you want to transition to omitting git-multimail (and\npresumably post-receive-email and maybe others) from the Git source\ntree.  I suppose in that case we would replace the scripts with pointers\nto where they can be obtained.\n\nMichael\n\n[1] http://article.gmane.org/gmane.comp.version-control.git/226644\n\n-- \nMichael Haggerty\nmhagger@alum.mit.edu\nhttp://softwareswirl.blogspot.com/\n"},{"id":"238584","messageId":"xmqqwqey1mw3.fsf@gitster.dls.corp.google.com","threadId":"36359","inReplyTo":"53456EC8.7090109@alum.mit.edu","subject":"Re: [PATCH] git-multimail: update to version 1.0.0","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-04-09T16:35:40Z","receivedAt":"2014-04-09T16:35:40Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Michael Haggerty <mhagger@alum.mit.edu> writes:\n\n>> In the longer term, I have a feeling that we may be better off to\n>> make the \"git core\" tree not be the \"batteris included\" convenience\n>> tree, though....\n> ...\n> Tell me if/when you want to transition to omitting git-multimail (and\n> presumably post-receive-email and maybe others) from the Git source\n> tree.  I suppose in that case we would replace the scripts with pointers\n> to where they can be obtained.\n\nYes, that would be a longer-term thing, not related to this \"one\nround of periodical updates\" patch.  I do not have any specific\ntimeframe in mind at this moment.\n"}]}