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.")