Merge branch 'sb/submodule-parallel-update' into next
[gitweb.git] / contrib / hooks / multimail / git_multimail.py
index 54ab4a49429fd9a3a31cdc471befe4852db369a6..73fdda6b14e331871cf4879ba85906b09cc0add3 100755 (executable)
@@ -1,8 +1,8 @@
 #! /usr/bin/env python
 
-__version__ = '1.3.1'
+__version__ = '1.4.0'
 
-# Copyright (c) 2015 Matthieu Moy and others
+# 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
@@ -56,6 +56,7 @@
 import subprocess
 import shlex
 import optparse
+import logging
 import smtplib
 try:
     import ssl
@@ -86,8 +87,8 @@ def is_string(s):
     def str_to_bytes(s):
         return s.encode(ENCODING)
 
-    def bytes_to_str(s):
-        return s.decode(ENCODING)
+    def bytes_to_str(s, errors='strict'):
+        return s.decode(ENCODING, errors)
 
     unicode = str
 
@@ -98,6 +99,15 @@ def write_str(f, msg):
             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)
 else:
     def is_string(s):
         try:
@@ -108,12 +118,15 @@ def is_string(s):
     def str_to_bytes(s):
         return s
 
-    def bytes_to_str(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()
 
@@ -213,8 +226,8 @@ def next(it):
             \\
              O -- O -- O   (%(oldrev_short)s)
 
-Any revisions marked "omits" are not gone; other references still
-refer to them.  Any revisions marked "discards" are gone forever.
+Any revisions marked "omit" are not gone; other references still
+refer to them.  Any revisions marked "discard" are gone forever.
 """
 
 
@@ -233,8 +246,8 @@ def next(it):
 revisions, and so the following emails describe only the N revisions
 from the common base, B.
 
-Any revisions marked "omits" are not gone; other references still
-refer to them.  Any revisions marked "discards" are gone forever.
+Any revisions marked "omit" are not gone; other references still
+refer to them.  Any revisions marked "discard" are gone forever.
 """
 
 
@@ -258,22 +271,22 @@ def next(it):
 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 "adds" were already present in the repository and have only
+listed as "add" were already present in the repository and have only
 been added to this reference.
 
 """
 
 
 TAG_CREATED_TEMPLATE = """\
-        at  %(newrev_short)-9s (%(newrev_type)s)
+      at %(newrev_short)-8s (%(newrev_type)s)
 """
 
 
 TAG_UPDATED_TEMPLATE = """\
 *** WARNING: tag %(short_refname)s was modified! ***
 
-      from  %(oldrev_short)-9s (%(oldrev_type)s)
-        to  %(newrev_short)-9s (%(newrev_type)s)
+    from %(oldrev_short)-8s (%(oldrev_type)s)
+      to %(newrev_short)-8s (%(newrev_type)s)
 """
 
 
@@ -286,7 +299,7 @@ def next(it):
 # 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)10s  %(rev_short)-9s %(text)s
+%(action)8s %(rev_short)-8s %(text)s
 """
 
 
@@ -434,11 +447,16 @@ def read_output(cmd, input=None, keepends=False, **kw):
         input = str_to_bytes(input)
     else:
         stdin = None
+    errors = 'strict'
+    if 'errors' in kw:
+        errors = kw['errors']
+        del kw['errors']
     p = subprocess.Popen(
-        cmd, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw
+        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)
+    out = bytes_to_str(out, errors=errors)
     retcode = p.wait()
     if retcode:
         raise CommandError(cmd, retcode)
@@ -1020,7 +1038,9 @@ def generate_email(self, push, body_filter=None, extra_header_values={}):
         for line in footer:
             yield line
 
-    def get_alt_fromaddr(self):
+    def get_specific_fromaddr(self):
+        """For kinds of Changes which specify it, return the kind-specific
+        From address to use."""
         return None
 
 
@@ -1045,7 +1065,7 @@ def __init__(self, reference_change, rev, num, tot):
             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\n' % (self.cc_recipients, self.rev.sha1))
+                    'Add %s to CC for %s' % (self.cc_recipients, self.rev.sha1))
 
     def _cc_recipients(self):
         cc_recipients = []
@@ -1065,6 +1085,10 @@ def _compute_values(self):
             ['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['rev_short'] = self.rev.short
         values['change_type'] = self.change_type
@@ -1121,7 +1145,7 @@ def generate_email_body(self, push):
         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:
@@ -1135,7 +1159,7 @@ 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_alt_fromaddr(self):
+    def get_specific_fromaddr(self):
         return self.environment.from_commit
 
 
@@ -1193,7 +1217,7 @@ def create(environment, oldrev, newrev, refname):
                 # Tracking branch:
                 environment.log_warning(
                     '*** Push-update of tracking branch %r\n'
-                    '***  - incomplete email generated.\n'
+                    '***  - incomplete email generated.'
                     % (refname,)
                     )
                 klass = OtherReferenceChange
@@ -1201,7 +1225,7 @@ def create(environment, oldrev, newrev, refname):
                 # Some other reference namespace:
                 environment.log_warning(
                     '*** Push-update of strange reference %r\n'
-                    '***  - incomplete email generated.\n'
+                    '***  - incomplete email generated.'
                     % (refname,)
                     )
                 klass = OtherReferenceChange
@@ -1209,7 +1233,7 @@ def create(environment, oldrev, newrev, refname):
             # Anything else (is there anything else?)
             environment.log_warning(
                 '*** Unknown type of update to %r (%s)\n'
-                '***  - incomplete email generated.\n'
+                '***  - incomplete email generated.'
                 % (refname, rev.type,)
                 )
             klass = OtherReferenceChange
@@ -1446,9 +1470,9 @@ def generate_revision_change_summary(self, push):
             if discards and adds:
                 for (sha1, subject) in discards:
                     if sha1 in discarded_commits:
-                        action = 'discards'
+                        action = 'discard'
                     else:
-                        action = 'omits'
+                        action = 'omit'
                     yield self.expand(
                         BRIEF_SUMMARY_TEMPLATE, action=action,
                         rev_short=sha1, text=subject,
@@ -1457,7 +1481,7 @@ def generate_revision_change_summary(self, push):
                     if sha1 in new_commits:
                         action = 'new'
                     else:
-                        action = 'adds'
+                        action = 'add'
                     yield self.expand(
                         BRIEF_SUMMARY_TEMPLATE, action=action,
                         rev_short=sha1, text=subject,
@@ -1469,9 +1493,9 @@ def generate_revision_change_summary(self, push):
             elif discards:
                 for (sha1, subject) in discards:
                     if sha1 in discarded_commits:
-                        action = 'discards'
+                        action = 'discard'
                     else:
-                        action = 'omits'
+                        action = 'omit'
                     yield self.expand(
                         BRIEF_SUMMARY_TEMPLATE, action=action,
                         rev_short=sha1, text=subject,
@@ -1490,7 +1514,7 @@ def generate_revision_change_summary(self, push):
                     if sha1 in new_commits:
                         action = 'new'
                     else:
-                        action = 'adds'
+                        action = 'add'
                     yield self.expand(
                         BRIEF_SUMMARY_TEMPLATE, action=action,
                         rev_short=sha1, text=subject,
@@ -1543,7 +1567,7 @@ def generate_revision_change_summary(self, push):
                 for r in discarded_revisions:
                     (sha1, subject) = r.rev.get_summary()
                     yield r.expand(
-                        BRIEF_SUMMARY_TEMPLATE, action='discards', text=subject,
+                        BRIEF_SUMMARY_TEMPLATE, action='discard', text=subject,
                         )
                 for line in self.generate_revision_change_graph(push):
                     yield line
@@ -1581,7 +1605,7 @@ def generate_delete_summary(self, push):
             )
         yield '\n'
 
-    def get_alt_fromaddr(self):
+    def get_specific_fromaddr(self):
         return self.environment.from_refchange
 
 
@@ -1791,13 +1815,13 @@ def describe_tag(self, push):
             except CommandError:
                 prevtag = None
             if prevtag:
-                yield '  replaces  %s\n' % (prevtag,)
+                yield ' replaces %s\n' % (prevtag,)
         else:
             prevtag = None
-            yield '    length  %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),)
+            yield '  length %s bytes\n' % (read_git_output(['cat-file', '-s', tagobject]),)
 
-        yield ' tagged by  %s\n' % (tagger,)
-        yield '        on  %s\n' % (tagged,)
+        yield '      by %s\n' % (tagger,)
+        yield '      on %s\n' % (tagged,)
         yield '\n'
 
         # Show the content of the tag message; this might contain a
@@ -1914,6 +1938,9 @@ def __init__(self, environment, refname, short_refname, old, new, rev):
 class Mailer(object):
     """An object that can send emails."""
 
+    def __init__(self, environment):
+        self.environment = environment
+
     def send(self, lines, to_addrs):
         """Send an email consisting of lines.
 
@@ -1948,14 +1975,14 @@ def find_sendmail():
                 'Try setting multimailhook.sendmailCommand.'
                 )
 
-    def __init__(self, command=None, envelopesender=None):
+    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:
@@ -1968,7 +1995,7 @@ def send(self, lines, to_addrs):
         try:
             p = subprocess.Popen(self.command, stdin=subprocess.PIPE)
         except OSError:
-            sys.stderr.write(
+            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' +
@@ -1979,15 +2006,16 @@ def send(self, lines, to_addrs):
             lines = (str_to_bytes(line) for line in lines)
             p.stdin.writelines(lines)
         except Exception:
-            sys.stderr.write(
+            self.environment.get_logger().error(
                 '*** Error while generating commit email\n'
                 '***  - mail sending aborted.\n'
                 )
-            try:
+            if hasattr(p, 'terminate'):
                 # subprocess.terminate() is not available in Python 2.4
                 p.terminate()
-            except AttributeError:
-                pass
+            else:
+                import signal
+                os.kill(p.pid, signal.SIGTERM)
             raise
         else:
             p.stdin.close()
@@ -1999,14 +2027,16 @@ def send(self, lines, to_addrs):
 class SMTPMailer(Mailer):
     """Send emails using Python's smtplib."""
 
-    def __init__(self, envelopesender, smtpserver,
+    def __init__(self, environment,
+                 envelopesender, smtpserver,
                  smtpservertimeout=10.0, smtpserverdebuglevel=0,
                  smtpencryption='none',
                  smtpuser='', smtppass='',
                  smtpcacerts=''
                  ):
+        super(SMTPMailer, self).__init__(environment)
         if not envelopesender:
-            sys.stderr.write(
+            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'
                 )
@@ -2041,7 +2071,7 @@ def call(klass, server, timeout):
                 self.smtp = call(smtplib.SMTP_SSL, self.smtpserver, timeout=self.smtpservertimeout)
             elif self.security == 'tls':
                 if 'ssl' not in sys.modules:
-                    sys.stderr.write(
+                    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'
@@ -2071,7 +2101,7 @@ def call(klass, server, timeout):
                         self.smtp.sock,
                         cert_reqs=ssl.CERT_NONE
                         )
-                    sys.stderr.write(
+                    self.environment.get_logger().error(
                         '*** Warning, the server certificat is not verified (smtp) ***\n'
                         '***          set the option smtpCACerts                   ***\n'
                         )
@@ -2094,10 +2124,10 @@ def call(klass, server, timeout):
                     % self.smtpserverdebuglevel)
                 self.smtp.set_debuglevel(self.smtpserverdebuglevel)
         except Exception:
-            sys.stderr.write(
+            self.environment.get_logger().error(
                 '*** Error establishing SMTP connection to %s ***\n'
-                % self.smtpserver)
-            sys.stderr.write('*** %s\n' % sys.exc_info()[1])
+                '*** %s\n'
+                % (self.smtpserver, sys.exc_info()[1]))
             sys.exit(1)
 
     def __del__(self):
@@ -2115,10 +2145,11 @@ def send(self, lines, to_addrs):
                 to_addrs = [email for (name, email) in getaddresses([to_addrs])]
             self.smtp.sendmail(self.envelopesender, to_addrs, msg)
         except smtplib.SMTPResponseException:
-            sys.stderr.write('*** Error sending email ***\n')
             err = sys.exc_info()[1]
-            sys.stderr.write('*** Error %d: %s\n' % (err.smtp_code,
-                                                     bytes_to_str(err.smtp_error)))
+            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
@@ -2126,9 +2157,10 @@ def send(self, lines, to_addrs):
                 del self.smtp
                 smtp.quit()
             except:
-                sys.stderr.write('*** Error closing the SMTP connection ***\n')
-                sys.stderr.write('*** Exiting anyway ... ***\n')
-                sys.stderr.write('*** %s\n' % sys.exc_info()[1])
+                self.environment.get_logger().error(
+                    '*** Error closing the SMTP connection ***\n'
+                    '*** Exiting anyway ... ***\n'
+                    '*** %s\n' % sys.exc_info()[1])
             sys.exit(1)
 
 
@@ -2250,6 +2282,11 @@ class Environment(object):
             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)
@@ -2324,6 +2361,15 @@ class Environment(object):
             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)$')
@@ -2346,6 +2392,7 @@ def __init__(self, osenv=None):
         self.quiet = False
         self.stdout = False
         self.combine_when_single_commit = True
+        self.logger = None
 
         self.COMPUTED_KEYS = [
             'administrator',
@@ -2360,6 +2407,12 @@ def __init__(self, osenv=None):
 
         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."""
 
@@ -2467,6 +2520,11 @@ def get_default_ref_ignore_regex(self):
         # 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.
 
@@ -2482,19 +2540,22 @@ 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."""
-        write_str(sys.stderr, msg)
+        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."""
-        write_str(sys.stderr, msg)
+        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."""
-        write_str(sys.stderr, msg)
+        self.get_logger().error(msg)
+
+    def check(self):
+        pass
 
 
 class ConfigEnvironmentMixin(Environment):
@@ -2613,6 +2674,14 @@ def __init__(self, config, **kw):
         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
@@ -2631,11 +2700,21 @@ def get_emailprefix(self):
         if emailprefix is not None:
             emailprefix = emailprefix.strip()
             if emailprefix:
-                return emailprefix + ' '
-            else:
-                return ''
+                emailprefix += ' '
         else:
-            return '[%s] ' % (self.get_repo_shortname(),)
+            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')
@@ -2656,9 +2735,9 @@ def process_addr(self, addr, change):
     def get_fromaddr(self, change=None):
         fromaddr = self.config.get('from')
         if change:
-            alt_fromaddr = change.get_alt_fromaddr()
-            if alt_fromaddr:
-                fromaddr = alt_fromaddr
+            specific_fromaddr = change.get_specific_fromaddr()
+            if specific_fromaddr:
+                fromaddr = specific_fromaddr
         if fromaddr:
             fromaddr = self.process_addr(fromaddr, change)
         if fromaddr:
@@ -2684,7 +2763,7 @@ def get_scancommitforcc(self):
 class FilterLinesEnvironmentMixin(Environment):
     """Handle encoding and maximum line length of body lines.
 
-        emailmaxlinelength (int or None)
+        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 ' [...]'
@@ -2699,10 +2778,13 @@ class FilterLinesEnvironmentMixin(Environment):
 
     """
 
-    def __init__(self, strict_utf8=True, emailmaxlinelength=500, **kw):
+    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.__emailmaxlinelength = emailmaxlinelength
+        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)
@@ -2711,15 +2793,18 @@ def filter_body(self, lines):
                 lines = (line.decode(ENCODING, 'replace') for line in lines)
             # Limit the line length in Unicode-space to avoid
             # splitting characters:
-            if self.__emailmaxlinelength:
-                lines = limit_linelength(lines, self.__emailmaxlinelength)
+            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.__emailmaxlinelength:
-            lines = limit_linelength(lines, self.__emailmaxlinelength)
+        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,
@@ -2732,9 +2817,13 @@ def __init__(self, config, **kw):
         if strict_utf8 is not None:
             kw['strict_utf8'] = strict_utf8
 
-        emailmaxlinelength = config.get('emailmaxlinelength')
-        if emailmaxlinelength is not None:
-            kw['emailmaxlinelength'] = int(emailmaxlinelength)
+        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
@@ -2750,7 +2839,7 @@ def __init__(self, emailmaxlines, **kw):
 
     def filter_body(self, lines):
         lines = super(MaxlinesEnvironmentMixin, self).filter_body(lines)
-        if self.__emailmaxlines:
+        if self.__emailmaxlines > 0:
             lines = limit_lines(lines, self.__emailmaxlines)
         return lines
 
@@ -2843,25 +2932,64 @@ def __init__(
         # 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:
-        if not (refchange_recipients or
-                announce_recipients or
-                revision_recipients or
-                scancommitforcc):
-            raise ConfigurationException('No email recipients configured!')
         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
@@ -2935,24 +3063,20 @@ def __init__(self, ref_filter_incl_regex, ref_filter_excl_regex,
         if ref_filter_do_send_regex and ref_filter_dont_send_regex:
             raise ConfigurationException(
                 "Cannot specify both a ref doSend and dontSend regex.")
-        if ref_filter_do_send_regex or ref_filter_dont_send_regex:
-            self.__is_do_send_filter = bool(ref_filter_do_send_regex)
-            if ref_filter_incl_regex:
-                ref_filter_send_regex = ref_filter_incl_regex
-            elif ref_filter_excl_regex:
-                ref_filter_send_regex = ref_filter_excl_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]))
+        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:
-            self.__send_compiled_regex = self.__compiled_regex
-            self.__is_do_send_filter = self.__is_inclusion_filter
+            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:
@@ -3023,34 +3147,21 @@ def get_pusher(self):
         return self.osenv.get('USER', self.osenv.get('USERNAME', 'unknown user'))
 
 
-class GenericEnvironment(
-        ProjectdescEnvironmentMixin,
-        ConfigMaxlinesEnvironmentMixin,
-        ComputeFQDNEnvironmentMixin,
-        ConfigFilterLinesEnvironmentMixin,
-        ConfigRecipientsEnvironmentMixin,
-        ConfigRefFilterEnvironmentMixin,
-        PusherDomainEnvironmentMixin,
-        ConfigOptionsEnvironmentMixin,
-        GenericEnvironmentMixin,
-        Environment,
-        ):
-    pass
+class GitoliteEnvironmentHighPrecMixin(Environment):
+    def get_pusher(self):
+        return self.osenv.get('GL_USER', 'unknown user')
 
 
-class GitoliteEnvironmentMixin(Environment):
+class GitoliteEnvironmentLowPrecMixin(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(GitoliteEnvironmentMixin, self).get_repo_shortname()
+            super(GitoliteEnvironmentLowPrecMixin, self).get_repo_shortname()
             )
 
-    def get_pusher(self):
-        return self.osenv.get('GL_USER', 'unknown user')
-
     def get_fromaddr(self, change=None):
         GL_USER = self.osenv.get('GL_USER')
         if GL_USER is not None:
@@ -3088,7 +3199,7 @@ def get_fromaddr(self, change=None):
                             return m.group(1)
                 finally:
                     f.close()
-        return super(GitoliteEnvironmentMixin, self).get_fromaddr(change)
+        return super(GitoliteEnvironmentLowPrecMixin, self).get_fromaddr(change)
 
 
 class IncrementalDateTime(object):
@@ -3109,67 +3220,43 @@ def __next__(self):
         return formatted
 
 
-class GitoliteEnvironment(
-        ProjectdescEnvironmentMixin,
-        ConfigMaxlinesEnvironmentMixin,
-        ComputeFQDNEnvironmentMixin,
-        ConfigFilterLinesEnvironmentMixin,
-        ConfigRecipientsEnvironmentMixin,
-        ConfigRefFilterEnvironmentMixin,
-        PusherDomainEnvironmentMixin,
-        ConfigOptionsEnvironmentMixin,
-        GitoliteEnvironmentMixin,
-        Environment,
-        ):
-    pass
-
-
-class StashEnvironmentMixin(Environment):
+class StashEnvironmentHighPrecMixin(Environment):
     def __init__(self, user=None, repo=None, **kw):
-        super(StashEnvironmentMixin, self).__init__(**kw)
+        super(StashEnvironmentHighPrecMixin,
+              self).__init__(user=user, repo=repo, **kw)
         self.__user = user
         self.__repo = repo
 
-    def get_repo_shortname(self):
-        return self.__repo
-
     def get_pusher(self):
         return re.match('(.*?)\s*<', self.__user).group(1)
 
     def get_pusher_email(self):
         return self.__user
 
-    def get_fromaddr(self, change=None):
-        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
 
-class StashEnvironment(
-        StashEnvironmentMixin,
-        ProjectdescEnvironmentMixin,
-        ConfigMaxlinesEnvironmentMixin,
-        ComputeFQDNEnvironmentMixin,
-        ConfigFilterLinesEnvironmentMixin,
-        ConfigRecipientsEnvironmentMixin,
-        ConfigRefFilterEnvironmentMixin,
-        PusherDomainEnvironmentMixin,
-        ConfigOptionsEnvironmentMixin,
-        Environment,
-        ):
-    pass
+    def get_repo_shortname(self):
+        return self.__repo
+
+    def get_fromaddr(self, change=None):
+        return self.__user
 
 
-class GerritEnvironmentMixin(Environment):
+class GerritEnvironmentHighPrecMixin(Environment):
     def __init__(self, project=None, submitter=None, update_method=None, **kw):
-        super(GerritEnvironmentMixin, self).__init__(**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_repo_shortname(self):
-        return self.__project
-
     def get_pusher(self):
         if self.__submitter:
             if self.__submitter.find('<') != -1:
@@ -3192,16 +3279,10 @@ def get_pusher_email(self):
         if self.__submitter:
             return self.__submitter
         else:
-            return super(GerritEnvironmentMixin, self).get_pusher_email()
-
-    def get_fromaddr(self, change=None):
-        if self.__submitter and self.__submitter.find('<') != -1:
-            return self.__submitter
-        else:
-            return super(GerritEnvironmentMixin, self).get_fromaddr(change)
+            return super(GerritEnvironmentHighPrecMixin, self).get_pusher_email()
 
     def get_default_ref_ignore_regex(self):
-        default = super(GerritEnvironmentMixin, self).get_default_ref_ignore_regex()
+        default = super(GerritEnvironmentHighPrecMixin, self).get_default_ref_ignore_regex()
         return default + '|^refs/changes/|^refs/cache-automerge/|^refs/meta/'
 
     def get_revision_recipients(self, revision):
@@ -3214,25 +3295,26 @@ def get_revision_recipients(self, revision):
         if committer == 'Gerrit Code Review':
             return []
         else:
-            return super(GerritEnvironmentMixin, self).get_revision_recipients(revision)
+            return super(GerritEnvironmentHighPrecMixin, self).get_revision_recipients(revision)
 
     def get_update_method(self):
         return self.__update_method
 
 
-class GerritEnvironment(
-        GerritEnvironmentMixin,
-        ProjectdescEnvironmentMixin,
-        ConfigMaxlinesEnvironmentMixin,
-        ComputeFQDNEnvironmentMixin,
-        ConfigFilterLinesEnvironmentMixin,
-        ConfigRecipientsEnvironmentMixin,
-        ConfigRefFilterEnvironmentMixin,
-        PusherDomainEnvironmentMixin,
-        ConfigOptionsEnvironmentMixin,
-        Environment,
-        ):
-    pass
+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):
@@ -3498,13 +3580,13 @@ def send_emails(self, mailer, body_filter=None):
             if not change.recipients:
                 change.environment.log_warning(
                     '*** no recipients configured so no email will be sent\n'
-                    '*** for %r update %s->%s\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\n' % (change.recipients,))
+                        'Sending notification emails to: %s' % (change.recipients,))
                 extra_values = {'send_date': next(send_date)}
 
                 rev = change.send_single_combined_email(sha1s)
@@ -3527,14 +3609,14 @@ def send_emails(self, mailer, body_filter=None):
                 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\n' % max_emails
+                    '*** 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 not rev.recipients and rev.cc_recipients:
-                    change.environment.log_msg('*** Replacing Cc: with To:\n')
+                    change.environment.log_msg('*** Replacing Cc: with To:')
                     rev.recipients = rev.cc_recipients
                     rev.cc_recipients = None
                 if rev.recipients:
@@ -3548,7 +3630,7 @@ def send_emails(self, mailer, body_filter=None):
         if unhandled_sha1s:
             change.environment.log_error(
                 'ERROR: No emails were sent for the following new commits:\n'
-                '    %s\n'
+                '    %s'
                 % ('\n    '.join(sorted(unhandled_sha1s)),)
                 )
 
@@ -3562,12 +3644,23 @@ def include_ref(refname, ref_filter_regex, is_inclusion_filter):
 
 
 def run_as_post_receive_hook(environment, mailer):
-    ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
+    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 = []
-    for line in sys.stdin:
+    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)
             )
@@ -3579,9 +3672,13 @@ def run_as_post_receive_hook(environment, mailer):
 
 
 def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=False):
-    ref_filter_regex, is_inclusion_filter = environment.get_ref_filter_regex(True)
+    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,
@@ -3596,6 +3693,75 @@ def run_as_update_hook(environment, mailer, refname, oldrev, newrev, force_send=
         mailer.__del__()
 
 
+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')
 
@@ -3608,6 +3774,7 @@ def choose_mailer(config, environment):
         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,
@@ -3620,43 +3787,41 @@ def choose_mailer(config, environment):
         command = config.get('sendmailcommand')
         if command:
             command = shlex.split(command)
-        mailer = SendMailer(command=command, envelopesender=environment.get_sender())
+        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".\n'
+            'please use one of "smtp" or "sendmail".'
             )
         sys.exit(1)
     return mailer
 
 
 KNOWN_ENVIRONMENTS = {
-    'generic': GenericEnvironmentMixin,
-    'gitolite': GitoliteEnvironmentMixin,
-    'stash': StashEnvironmentMixin,
-    'gerrit': GerritEnvironmentMixin,
+    '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
 
-    environment_mixins = [
-        ConfigRefFilterEnvironmentMixin,
-        ProjectdescEnvironmentMixin,
-        ConfigMaxlinesEnvironmentMixin,
-        ComputeFQDNEnvironmentMixin,
-        ConfigFilterLinesEnvironmentMixin,
-        PusherDomainEnvironmentMixin,
-        ConfigOptionsEnvironmentMixin,
-        ]
-    environment_kw = {
-        'osenv': osenv,
-        'config': config,
-        }
-
     if not env:
         env = config.get('environment')
 
@@ -3665,8 +3830,58 @@ def choose_environment(config, osenv=None, env=None, recipients=None,
             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() + 'Environement'
+    environment_klass = type(
+        klass_name,
+        tuple(environment_mixins),
+        {},
+        )
+    KNOWN_ENVIRONMENTS[env_name]['class'] = environment_klass
+    return environment_klass
+
 
-    environment_mixins.insert(0, KNOWN_ENVIRONMENTS[env])
+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']
@@ -3676,20 +3891,8 @@ def choose_environment(config, osenv=None, env=None, recipients=None,
         environment_kw['submitter'] = hook_info['submitter']
         environment_kw['update_method'] = hook_info['update_method']
 
-    if recipients:
-        environment_mixins.insert(0, StaticRecipientsEnvironmentMixin)
-        environment_kw['refchange_recipients'] = recipients
-        environment_kw['announce_recipients'] = recipients
-        environment_kw['revision_recipients'] = recipients
-        environment_kw['scancommitforcc'] = config.get('scancommitforcc')
-    else:
-        environment_mixins.insert(0, ConfigRecipientsEnvironmentMixin)
+    environment_kw['cli_recipients'] = recipients
 
-    environment_klass = type(
-        'EffectiveEnvironment',
-        tuple(environment_mixins) + (Environment,),
-        {},
-        )
     return environment_klass(**environment_kw)
 
 
@@ -3710,7 +3913,8 @@ def get_version():
     return __version__
 
 
-def compute_gerrit_options(options, args, required_gerrit_options):
+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.")
@@ -3727,24 +3931,11 @@ def compute_gerrit_options(options, args, required_gerrit_options):
     # 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, options.refname)) and
+    if (not os.path.exists(os.path.join(git_dir, raw_refname)) and
         os.path.exists(os.path.join(git_dir, 'refs', 'heads',
-                                    options.refname))):
+                                    raw_refname))):
         options.refname = 'refs/heads/' + 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)
-
     # 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
@@ -3784,6 +3975,20 @@ def compute_gerrit_options(options, args, required_gerrit_options):
 
 
 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 "
@@ -3797,12 +4002,78 @@ def check_hook_specific_args(options, args):
     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)
+        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):
+        for l in self.loggers:
+            l.info(msg)
+
+    def debug(self, msg):
+        for l in self.loggers:
+            l.debug(msg)
+
+    def warning(self, msg):
+        for l in self.loggers:
+            l.warning(msg)
+
+    def error(self, msg):
+        for l in self.loggers:
+            l.error(msg)
+
+
 def main(args):
     parser = optparse.OptionParser(
         description=__doc__,
@@ -3829,7 +4100,7 @@ def main(args):
         '--show-env', action='store_true', default=False,
         help=(
             'Write to stderr the values determined for the environment '
-            '(intended for debugging purposes).'
+            '(intended for debugging purposes), then proceed normally.'
             ),
         )
     parser.add_option(
@@ -3854,6 +4125,22 @@ def main(args):
             "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
@@ -3880,11 +4167,16 @@ def main(args):
         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,
@@ -3894,38 +4186,52 @@ def main(args):
             )
 
         if options.show_env:
-            sys.stderr.write('Environment values:\n')
-            for (k, v) in sorted(environment.get_values().items()):
-                sys.stderr.write('    %s : %r\n' % (k, v))
-            sys.stderr.write('\n')
+            show_env(environment, sys.stderr)
 
         if options.stdout or environment.stdout:
             mailer = OutputMailer(sys.stdout)
         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.
-        if args:
+        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.stdout.write('\n')
-        sys.stdout.write('Exception \'' + t.__name__ +
-                         '\' raised. Please report this as a bug to\n')
-        sys.stdout.write('https://github.com/git-multimail/git-multimail/issues\n')
-        sys.stdout.write('with the information below:\n\n')
-        sys.stdout.write('git-multimail version ' + get_version() + '\n')
-        sys.stdout.write('Python version ' + sys.version + '\n')
-        traceback.print_exc(file=sys.stdout)
+        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__':