bzr-email-0.0.1~bzr58/0000755000175000017500000000000012000015517013140 5ustar kurakurabzr-email-0.0.1~bzr58/__init__.py0000644000175000017500000001401512000015474015254 0ustar kurakura# Copyright (C) 2005, 2006, 2007 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Sending emails for commits and branch changes.
To have bzr send an email you need to configure an address to send mail
to for that branch. To do this set the configuration option ``post_commit_to``
and the address to send the mail from is read from the configuration option
``post_commit_sender`` (if not supplied defaults to the email address reported
by ``bzr whoami``).
By default, the diff for the commit will be included in the email if the
length is less than 1000 lines. This limit can be changed by setting the
configuration option 'post_commit_difflimit' to the number of lines you wish
it to be limited to. Set it to 0 to unconditionally disable sending of diffs.
By default bzr-email only emails when a commit occurs, not when a push or
pull operation occurs. To email on push or pull set post_commit_push_pull=True
in the configuration.
If you are using a bzr release from before 0.15, you need to manually tell
bzr about the commit action, by setting
post_commit=bzrlib.plugins.email.post_commit in bazaar.conf or locations.conf.
The URL of the branch is determined from the following checks (in order):
- If the configuration value 'post_commit_url' is set, it is used.
- If the configuration value 'public_branch' is set, it is used.
- The URL of the branch itself.
Setting public_branch is highly recommended if you commit via a protocol which
has a private address (e.g. bzr+ssh but anonymous access might be bzr:// or
http://).
How emails are sent is determined by the value of the configuration option
'post_commit_mailer':
- Unset: use ``/usr/bin/mail``.
- ``smtplib``: Use python's smtplib to send the mail. If you use 'smtplib' you
can also configure the settings "smtp_server=host[:port]",
"smtp_username=userid", "smtp_password". If "smtp_username" is set but
"smtp_password" is not, you will be prompted for a password.
Also, if using 'smtplib', the messages will be sent as a UTF-8 text message,
with a 8-bit text diff attached (rather than all-as-one). Work has also been
done to make sure usernames do not have to be ascii.
- Any other value: Run the value expecting it to behave like ``/usr/bin/mail``
- in particular supporting the -s and -a options.
When using smtplib, you can specify additional headers to be included in the
mail by setting the 'revision_mail_headers' configuration option - something like::
revision_mail_headers=X-Cheese: to the rescue!
"""
from __future__ import absolute_import
from bzrlib.config import option_registry
from bzrlib.lazy_import import lazy_import
# lazy_import emailer so that it doesn't get loaded if it isn't used
lazy_import(globals(), """\
from bzrlib.plugins.email import emailer as _emailer
""")
def post_commit(branch, revision_id):
"""This is the post_commit hook that should get run after commit."""
_emailer.EmailSender(branch, revision_id, branch.get_config_stack()).send_maybe()
def branch_commit_hook(local, master, old_revno, old_revid, new_revno, new_revid):
"""This is the post_commit hook that runs after commit."""
_emailer.EmailSender(master, new_revid, master.get_config_stack(),
local_branch=local).send_maybe()
def branch_post_change_hook(params):
"""This is the post_change_branch_tip hook."""
# (branch, old_revno, new_revno, old_revid, new_revid)
_emailer.EmailSender(params.branch, params.new_revid,
params.branch.get_config_stack(), local_branch=None, op='change').send_maybe()
def test_suite():
from unittest import TestSuite
import bzrlib.plugins.email.tests
result = TestSuite()
result.addTest(bzrlib.plugins.email.tests.test_suite())
return result
option_registry.register_lazy("post_commit_body",
"bzrlib.plugins.email.emailer", "opt_post_commit_body")
option_registry.register_lazy("post_commit_subject",
"bzrlib.plugins.email.emailer", "opt_post_commit_subject")
option_registry.register_lazy("post_commit_log_format",
"bzrlib.plugins.email.emailer", "opt_post_commit_log_format")
option_registry.register_lazy("post_commit_difflimit",
"bzrlib.plugins.email.emailer", "opt_post_commit_difflimit")
option_registry.register_lazy("post_commit_push_pull",
"bzrlib.plugins.email.emailer", "opt_post_commit_push_pull")
option_registry.register_lazy("post_commit_diffoptions",
"bzrlib.plugins.email.emailer", "opt_post_commit_diffoptions")
option_registry.register_lazy("post_commit_sender",
"bzrlib.plugins.email.emailer", "opt_post_commit_sender")
option_registry.register_lazy("post_commit_to",
"bzrlib.plugins.email.emailer", "opt_post_commit_to")
option_registry.register_lazy("post_commit_mailer",
"bzrlib.plugins.email.emailer", "opt_post_commit_mailer")
option_registry.register_lazy("revision_mail_headers",
"bzrlib.plugins.email.emailer", "opt_revision_mail_headers")
try:
from bzrlib.hooks import install_lazy_named_hook
except ImportError:
from bzrlib.branch import Branch
Branch.hooks.install_named_hook('post_commit', branch_commit_hook, 'bzr-email')
Branch.hooks.install_named_hook('post_change_branch_tip', branch_post_change_hook, 'bzr-email')
else:
install_lazy_named_hook("bzrlib.branch", "Branch.hooks", 'post_commit',
branch_commit_hook, 'bzr-email')
install_lazy_named_hook("bzrlib.branch", "Branch.hooks",
'post_change_branch_tip', branch_post_change_hook, 'bzr-email')
bzr-email-0.0.1~bzr58/setup.py0000755000175000017500000000131212000015474014654 0ustar kurakura#!/usr/bin/env python2.4
from distutils.core import setup
if __name__ == '__main__':
setup(name='bzr-email',
description='Email plugin for Bazaar',
keywords='plugin bzr email',
version='0.0.1',
url='http://launchpad.net/bzr-email',
download_url='http://launchpad.net/bzr-email',
license='GPL',
author='Robert Collins',
author_email='robertc@robertcollins.net',
long_description="""
Hooks into Bazaar and sends commit notification emails.
""",
package_dir={'bzrlib.plugins.email':'.',
'bzrlib.plugins.email.tests':'tests'},
packages=['bzrlib.plugins.email',
'bzrlib.plugins.email.tests']
)
bzr-email-0.0.1~bzr58/README0000644000175000017500000000167412000015474014032 0ustar kurakuraThis is a plugin which implements post commit emails for bzr.
The plugin is activated by:
- installing it
- configuring an address to send emails to (see ``bzr help email``).
Installation
------------
The simplest way to install it for a single user is to do
``bzr checkout https://launchpad.net/bzr-email ~/.bazaar/plugins/email``.
If you need to install it system wide, or are packing the plugin for non-source
distribution (e.g. as a .deb or .rpm) then there is a setup.py file that should
do the right thing when called with the values you use elsewhere when
installing python software.
After installing it you can use 'bzr help email' for documentation (for bzr >=
0.16), or read the docstring at the top of __init__.py (for bzr < 0.16)
TODO
----
- support format specifiers / email templates.
Feedback/Contributions
----------------------
Feedback and contributions should be sent to the Bazaar mailing list:
bazaar@lists.canonical.com.
bzr-email-0.0.1~bzr58/tests/0000755000175000017500000000000012000015474014304 5ustar kurakurabzr-email-0.0.1~bzr58/tests/__init__.py0000644000175000017500000000176512000015474016426 0ustar kurakura# Copyright (C) 2005 by Canonical Ltd
# Authors: Robert Collins
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from unittest import TestLoader, TestSuite
def test_suite():
result = TestSuite()
import testemail
loader = TestLoader()
result.addTests(loader.loadTestsFromModule(testemail))
return result
bzr-email-0.0.1~bzr58/tests/testemail.py0000644000175000017500000002166512000015474016657 0ustar kurakura# Copyright (C) 2005-2008, 2010 by Canonical Ltd
# Authors: Robert Collins
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from unittest import TestLoader
from bzrlib import (
config,
tests,
)
from bzrlib.bzrdir import BzrDir
from bzrlib.tests import TestCaseInTempDir
from bzrlib.plugins.email.emailer import EmailSender
def test_suite():
return TestLoader().loadTestsFromName(__name__)
sample_config=("[DEFAULT]\n"
"post_commit_to=demo@example.com\n"
"post_commit_sender=Sample \n"
"revision_mail_headers=X-Cheese: to the rescue!\n")
unconfigured_config=("[DEFAULT]\n"
"email=Robert \n")
sender_configured_config=("[DEFAULT]\n"
"post_commit_sender=Sample \n")
to_configured_config=("[DEFAULT]\n"
"post_commit_to=Sample \n")
multiple_to_configured_config=("[DEFAULT]\n"
"post_commit_sender=Sender \n"
"post_commit_to=Sample , Other \n")
customized_mail_config=("[DEFAULT]\n"
"post_commit_to=demo@example.com\n"
"post_commit_sender=Sample \n"
"post_commit_subject=[commit] $message\n"
"post_commit_body='''$committer has committed "
"revision 1 at $url.\n\n'''\n")
push_config=("[DEFAULT]\n"
"post_commit_to=demo@example.com\n"
"post_commit_push_pull=True\n")
with_url_config=("[DEFAULT]\n"
"post_commit_url=http://some.fake/url/\n"
"post_commit_to=demo@example.com\n"
"post_commit_sender=Sample \n")
# FIXME: this should not use a literal log, rather grab one from bzrlib.log
sample_log=('------------------------------------------------------------\n'
'revno: 1\n'
'revision-id: A\n'
'committer: Sample \n'
'branch nick: work\n'
'timestamp: Thu 1970-01-01 00:00:01 +0000\n'
'message:\n'
' foo bar baz\n'
' fuzzy\n'
' wuzzy\n')
class TestGetTo(TestCaseInTempDir):
def test_body(self):
sender = self.get_sender()
self.assertEqual('At %s\n\n%s' % (sender.url(), sample_log),
sender.body())
def test_custom_body(self):
sender = self.get_sender(customized_mail_config)
self.assertEqual('%s has committed revision 1 at %s.\n\n%s' %
(sender.revision.committer, sender.url(), sample_log),
sender.body())
def test_command_line(self):
sender = self.get_sender()
self.assertEqual(['mail', '-s', sender.subject(), '-a',
'From: ' + sender.from_address()] + sender.to(),
sender._command_line())
def test_to(self):
sender = self.get_sender()
self.assertEqual(['demo@example.com'], sender.to())
def test_from(self):
sender = self.get_sender()
self.assertEqual('Sample ', sender.from_address())
def test_from_default(self):
sender = self.get_sender(unconfigured_config)
self.assertEqual('Robert ', sender.from_address())
def test_should_send(self):
sender = self.get_sender()
self.assertEqual(True, sender.should_send())
def test_should_not_send(self):
sender = self.get_sender(unconfigured_config)
self.assertEqual(False, sender.should_send())
def test_should_not_send_sender_configured(self):
sender = self.get_sender(sender_configured_config)
self.assertEqual(False, sender.should_send())
def test_should_not_send_to_configured(self):
sender = self.get_sender(to_configured_config)
self.assertEqual(True, sender.should_send())
def test_send_to_multiple(self):
sender = self.get_sender(multiple_to_configured_config)
self.assertEqual([u'Sample ', u'Other '],
sender.to())
self.assertEqual([u'Sample ', u'Other '],
sender._command_line()[-2:])
def test_url_set(self):
sender = self.get_sender(with_url_config)
self.assertEqual(sender.url(), 'http://some.fake/url/')
def test_public_url_set(self):
config=("[DEFAULT]\n"
"public_branch=http://the.publication/location/\n")
sender = self.get_sender(config)
self.assertEqual(sender.url(), 'http://the.publication/location/')
def test_url_precedence(self):
config=("[DEFAULT]\n"
"post_commit_url=http://some.fake/url/\n"
"public_branch=http://the.publication/location/\n")
sender = self.get_sender(config)
self.assertEqual(sender.url(), 'http://some.fake/url/')
def test_url_unset(self):
sender = self.get_sender()
self.assertEqual(sender.url(), sender.branch.base)
def test_subject(self):
sender = self.get_sender()
self.assertEqual("Rev 1: foo bar baz in %s" %
sender.branch.base,
sender.subject())
def test_custom_subject(self):
sender = self.get_sender(customized_mail_config)
self.assertEqual("[commit] %s" %
sender.revision.get_summary(),
sender.subject())
def test_diff_filename(self):
sender = self.get_sender()
self.assertEqual('patch-1.diff', sender.diff_filename())
def test_headers(self):
sender = self.get_sender()
self.assertEqual({'X-Cheese': 'to the rescue!'}, sender.extra_headers())
def get_sender(self, text=sample_config):
my_config = config.MemoryStack(text)
self.branch = BzrDir.create_branch_convenience('.')
tree = self.branch.bzrdir.open_workingtree()
tree.commit('foo bar baz\nfuzzy\nwuzzy', rev_id='A',
allow_pointless=True,
timestamp=1,
timezone=0,
committer="Sample ",
)
sender = EmailSender(self.branch, 'A', my_config)
# This is usually only done after the EmailSender has locked the branch
# and repository during send(), however, for testing, we need to do it
# earlier, since send() is not called.
sender._setup_revision_and_revno()
return sender
class TestEmailerWithLocal(tests.TestCaseWithTransport):
"""Test that Emailer will use a local branch if supplied."""
def test_local_has_revision(self):
master_tree = self.make_branch_and_tree('master')
self.build_tree(['master/a'])
master_tree.add('a')
master_tree.commit('a')
child_tree = master_tree.bzrdir.sprout('child').open_workingtree()
child_tree.branch.bind(master_tree.branch)
self.build_tree(['child/b'])
child_tree.add(['b'])
revision_id = child_tree.commit('b')
sender = EmailSender(master_tree.branch, revision_id,
master_tree.branch.get_config(),
local_branch=child_tree.branch)
# Make sure we are using the 'local_branch' repository, and not the
# remote one.
self.assertIs(child_tree.branch.repository, sender.repository)
def test_local_missing_revision(self):
master_tree = self.make_branch_and_tree('master')
self.build_tree(['master/a'])
master_tree.add('a')
master_tree.commit('a')
child_tree = master_tree.bzrdir.sprout('child').open_workingtree()
child_tree.branch.bind(master_tree.branch)
self.build_tree(['master/c'])
master_tree.add(['c'])
revision_id = master_tree.commit('c')
self.failIf(child_tree.branch.repository.has_revision(revision_id))
sender = EmailSender(master_tree.branch, revision_id,
master_tree.branch.get_config(),
local_branch=child_tree.branch)
# We should be using the master repository here, because the child
# repository doesn't contain the revision.
self.assertIs(master_tree.branch.repository, sender.repository)
bzr-email-0.0.1~bzr58/emailer.py0000644000175000017500000002561012000015474015136 0ustar kurakura# Copyright (C) 2005-2011 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import subprocess
import tempfile
from bzrlib import (
errors,
revision as _mod_revision,
)
from bzrlib.config import (
ListOption,
Option,
bool_from_store,
int_from_store,
)
from bzrlib.smtp_connection import SMTPConnection
from bzrlib.email_message import EmailMessage
class EmailSender(object):
"""An email message sender."""
_smtplib_implementation = SMTPConnection
def __init__(self, branch, revision_id, config, local_branch=None,
op='commit'):
self.config = config
self.branch = branch
self.repository = branch.repository
if (local_branch is not None and
local_branch.repository.has_revision(revision_id)):
self.repository = local_branch.repository
self._revision_id = revision_id
self.revision = None
self.revno = None
self.op = op
def _setup_revision_and_revno(self):
self.revision = self.repository.get_revision(self._revision_id)
self.revno = self.branch.revision_id_to_revno(self._revision_id)
def _format(self, text):
fields = {
'committer': self.revision.committer,
'message': self.revision.get_summary(),
'revision': '%d' % self.revno,
'url': self.url()
}
for name, value in fields.items():
text = text.replace('$%s' % name, value)
return text
def body(self):
from bzrlib import log
rev1 = rev2 = self.revno
if rev1 == 0:
rev1 = None
rev2 = None
# use 'replace' so that we don't abort if trying to write out
# in e.g. the default C locale.
# We must use StringIO.StringIO because we want a Unicode string that
# we can pass to send_email and have that do the proper encoding.
from StringIO import StringIO
outf = StringIO()
_body = self.config.get('post_commit_body')
if _body is None:
_body = 'At %s\n\n' % self.url()
outf.write(self._format(_body))
log_format = self.config.get('post_commit_log_format')
lf = log.log_formatter(log_format,
show_ids=True,
to_file=outf
)
if len(self.revision.parent_ids) <= 1:
# This is not a merge, so we can special case the display of one
# revision, and not have to encur the show_log overhead.
lr = log.LogRevision(self.revision, self.revno, 0, None)
lf.log_revision(lr)
else:
# let the show_log code figure out what revisions need to be
# displayed, as this is a merge
log.show_log(self.branch,
lf,
start_revision=rev1,
end_revision=rev2,
verbose=True
)
return outf.getvalue()
def get_diff(self):
"""Add the diff from the commit to the output.
If the diff has more than difflimit lines, it will be skipped.
"""
difflimit = self.difflimit()
if not difflimit:
# No need to compute a diff if we aren't going to display it
return
from bzrlib.diff import show_diff_trees
# optionally show the diff if its smaller than the post_commit_difflimit option
revid_new = self.revision.revision_id
if self.revision.parent_ids:
revid_old = self.revision.parent_ids[0]
tree_new, tree_old = self.repository.revision_trees((revid_new, revid_old))
else:
# revision_trees() doesn't allow None or 'null:' to be passed as a
# revision. So we need to call revision_tree() twice.
revid_old = _mod_revision.NULL_REVISION
tree_new = self.repository.revision_tree(revid_new)
tree_old = self.repository.revision_tree(revid_old)
# We can use a cStringIO because show_diff_trees should only write
# 8-bit strings. It is an error to write a Unicode string here.
from cStringIO import StringIO
diff_content = StringIO()
diff_options = self.config.get('post_commit_diffoptions')
show_diff_trees(tree_old, tree_new, diff_content, None, diff_options)
numlines = diff_content.getvalue().count('\n')+1
if numlines <= difflimit:
return diff_content.getvalue()
else:
return ("\nDiff too large for email"
" (%d lines, the limit is %d).\n"
% (numlines, difflimit))
def difflimit(self):
"""Maximum number of lines of diff to show."""
return self.config.get('post_commit_difflimit')
def mailer(self):
"""What mail program to use."""
return self.config.get('post_commit_mailer')
def _command_line(self):
cmd = [self.mailer(), '-s', self.subject(), '-a',
"From: " + self.from_address()]
cmd.extend(self.to())
return cmd
def to(self):
"""What is the address the mail should go to."""
return self.config.get('post_commit_to')
def url(self):
"""What URL to display in the subject of the mail"""
url = self.config.get('post_commit_url')
if url is None:
url = self.config.get('public_branch')
if url is None:
url = self.branch.base
return url
def from_address(self):
"""What address should I send from."""
result = self.config.get('post_commit_sender')
if result is None:
result = self.config.get('email')
return result
def extra_headers(self):
"""Additional headers to include when sending."""
result = {}
headers = self.config.get('revision_mail_headers')
if not headers:
return
for line in headers:
key, value = line.split(": ", 1)
result[key] = value
return result
def send(self):
"""Send the email.
Depending on the configuration, this will either use smtplib, or it
will call out to the 'mail' program.
"""
self.branch.lock_read()
self.repository.lock_read()
try:
# Do this after we have locked, to make things faster.
self._setup_revision_and_revno()
mailer = self.mailer()
if mailer == 'smtplib':
self._send_using_smtplib()
else:
self._send_using_process()
finally:
self.repository.unlock()
self.branch.unlock()
def _send_using_process(self):
"""Spawn a 'mail' subprocess to send the email."""
# TODO think up a good test for this, but I think it needs
# a custom binary shipped with. RBC 20051021
msgfile = tempfile.NamedTemporaryFile()
try:
msgfile.write(self.body().encode('utf8'))
diff = self.get_diff()
if diff:
msgfile.write(diff)
msgfile.flush()
msgfile.seek(0)
process = subprocess.Popen(self._command_line(),
stdin=msgfile.fileno())
rc = process.wait()
if rc != 0:
raise errors.BzrError("Failed to send email: exit status %s" % (rc,))
finally:
msgfile.close()
def _send_using_smtplib(self):
"""Use python's smtplib to send the email."""
body = self.body()
diff = self.get_diff()
subject = self.subject()
from_addr = self.from_address()
to_addrs = self.to()
if isinstance(to_addrs, basestring):
to_addrs = [to_addrs]
header = self.extra_headers()
msg = EmailMessage(from_addr, to_addrs, subject, body)
if diff:
msg.add_inline_attachment(diff, self.diff_filename())
# Add revision_mail_headers to the headers
if header != None:
for k, v in header.items():
msg[k] = v
smtp = self._smtplib_implementation(self.config)
smtp.send_email(msg)
def should_send(self):
post_commit_push_pull = self.config.get('post_commit_push_pull')
if post_commit_push_pull and self.op == 'commit':
# We will be called again with a push op, send the mail then.
return False
if not post_commit_push_pull and self.op != 'commit':
# Mailing on commit only, and this is a push/pull operation.
return False
return bool(self.to() and self.from_address())
def send_maybe(self):
if self.should_send():
self.send()
def subject(self):
_subject = self.config.get('post_commit_subject')
if _subject is None:
_subject = ("Rev %d: %s in %s" %
(self.revno,
self.revision.get_summary(),
self.url()))
return self._format(_subject)
def diff_filename(self):
return "patch-%s.diff" % (self.revno,)
opt_post_commit_body = Option("post_commit_body",
help="Body for post commit emails.")
opt_post_commit_subject = Option("post_commit_subject",
help="Subject for post commit emails.")
opt_post_commit_log_format = Option('post_commit_log_format',
default='long', help="Log format for option.")
opt_post_commit_difflimit = Option('post_commit_difflimit',
default=1000, from_unicode=int_from_store,
help="Maximum number of lines in diffs.")
opt_post_commit_push_pull = Option('post_commit_push_pull',
from_unicode=bool_from_store,
help="Whether to send emails on push and pull.")
opt_post_commit_diffoptions = Option('post_commit_diffoptions',
help="Diff options to use.")
opt_post_commit_sender = Option('post_commit_sender',
help='From address to use for emails.')
opt_post_commit_to = ListOption('post_commit_to',
help='Address to send commit emails to.')
opt_post_commit_mailer = Option('post_commit_mailer',
help='Mail client to use.', default='mail')
opt_post_commit_url = Option('post_commit_url',
help='URL to mention for branch in post commit messages.')
opt_revision_mail_headers = ListOption('revision_mail_headers',
help="Extra revision headers.")