[hipl-commit] [trunk] Rev 4692: added pyton scripts used at the bzr server

  • From: Rene Hummen <rene.hummen@xxxxxxxxxxxxxxxxx>
  • To: hipl-commit@xxxxxxxxxxxxx
  • Date: Tue, 1 Jun 2010 19:02:13 +0300

Committer: Rene Hummen <rene.hummen@xxxxxxxxxxxxxxxxx>
Date: 01/06/2010 at 19:02:13
Revision: 4692
Revision-id: rene.hummen@xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Branch nick: trunk

Log:
  added pyton scripts used at the bzr server
  
  email - email notification system (actively used)
  syntaxchecker - checks commits for tabs, whitespace, etc (actively used)
  tagchecker - checks commits subjects for tags (currently unused)

Modified:
  A  tools/bzrhooks/
  A  tools/bzrhooks/email/
  A  tools/bzrhooks/email/README
  A  tools/bzrhooks/email/__init__.py
  A  tools/bzrhooks/email/customlogformatter.py
  A  tools/bzrhooks/email/emailer.py
  A  tools/bzrhooks/email/setup.py
  A  tools/bzrhooks/email/tests/
  A  tools/bzrhooks/email/tests/__init__.py
  A  tools/bzrhooks/email/tests/test_smtp_connection.py
  A  tools/bzrhooks/email/tests/testemail.py
  A  tools/bzrhooks/syntaxchecker/
  A  tools/bzrhooks/syntaxchecker/__init__.py
  A  tools/bzrhooks/syntaxchecker/syntaxchecker.py
  A  tools/bzrhooks/tagchecker/
  A  tools/bzrhooks/tagchecker/__init__.py
  A  tools/bzrhooks/tagchecker/tagchecker.py

=== added directory 'tools/bzrhooks'
=== added directory 'tools/bzrhooks/email'
=== added file 'tools/bzrhooks/email/README'
--- tools/bzrhooks/email/README 1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/email/README 2010-06-01 16:02:05 +0000
@@ -0,0 +1,44 @@
+This is a plugin which implements post commmit emails for bzr.
+
+The plugin is activated by:
+ - installing it
+ - configuring an address to send emails to (see ``bzr help email``).
+
+Installation
+------------
+
+To install this plug in system wide, copy it into the plugins directory of the 
bzrlib.
+You can find out the directory of the bzrlib by executing
+    bzr version | grep bzrlib
+This should be something like
+    /usr/lib/python2.6/dist-packages/bzrlib
+Then you put the plugin into
+    /usr/lib/python2.6/dist-packages/bzrlib/plugins/email
+That's all.
+
+You can check the installation by executing
+    bzr plugins
+(email plugin should be displayed with a short message)
+or
+    bzr hooks
+(post_change_branch_tip, pre_change_branch_tip should have entries from 
email-notification now)
+
+Configuration
+------------
+
+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
+    commit_notification_to
+in the bazaar.conf, locations.conf or branch.conf. If there is no such address
+configured for a specific branch, no email-notifications will be sent when 
changes
+in this branch occur. Preferably, this option should be set in the branch.conf 
of each branch
+separately.
+
+The address from which the mail is sent is read from the configuration option
+    ``commit_notification_sender``
+If not supplied the name of the committer is taken as the originator.
+
+Help
+------------
+All this should be displayed too, by executing
+bzr email help

=== added file 'tools/bzrhooks/email/__init__.py'
--- tools/bzrhooks/email/__init__.py    1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/email/__init__.py    2010-06-01 16:02:05 +0000
@@ -0,0 +1,134 @@
+# 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 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
+    ``commit_notification_to``
+in the bazaar.conf, locations.conf or branch.conf. If there is no such address
+configured for a specific branch, no email-notifications will be sent when 
changes
+in this branch occur.
+
+The address from which the mail is sent is read from the configuration option
+    ``commit_notification_sender``
+If not supplied the name of the committer is taken as the originator.
+
+The plugin sends email-notifications on commits and uncommits. Merges are
+displayed in a special format.
+
+Emails are sent using python's smtplib.
+
+To install this plug in system wide copy it into the plugins directory
+of the bzrlib. You can find out the directory of the bzrlib by executing
+    `` bzr version | grep bzrlib ``
+This should be something like
+    `` /usr/lib/python2.6/dist-packages/bzrlib ``
+Then you put the plugin into
+    `` /usr/lib/python2.6/dist-packages/bzrlib/plugins/email ``
+That's all.
+
+You can check the installation by executing
+    `` bzr plugins ``
+(email plugin should be displayed with a short message)
+or
+    `` bzr hooks ``
+(post_change_branch_tip, pre_change_branch_tip should have entries from 
email-notification now)
+"""
+
+
+if __name__ != 'bzrlib.plugins.email':
+    raise ImportError('The email plugin must be installed as'
+                      ' bzrlib.plugins.email not %s'
+                      % __name__)
+
+
+# These three are used during import: No point lazy_importing them.
+from bzrlib import errors
+from bzrlib.branch import Branch
+from bzrlib.smart.server import SmartTCPServer
+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 server_started_hook(backing_urls, public_urls):
+    """Detect if running as server to prevent clients from sending email 
notifications"""
+    _emailer.EmailSender.server_mode = True
+
+def post_change_branch_tip_hook(params):
+    """This hook will be called on the server's side after a change."""
+    # (branch, old_revno, new_revno, old_revid, new_revid)
+    _emailer.EmailSender(params, op='post_change').send_maybe()
+
+def pre_change_branch_tip_hook(params):
+    """This hook will be called on the server's side before a change."""
+    # (branch, old_revno, new_revno, old_revid, new_revid)
+    _emailer.EmailSender(params, op='pre_change').send_maybe()
+
+def install_hook(bzr_hook, callback, name):
+    """Install the given hook with the given name """
+    if bzr_hook in Branch.hooks:
+        install_named_hook = getattr(Branch.hooks, 'install_named_hook', None)
+        if install_named_hook is not None:
+            install_named_hook(bzr_hook, callback, name)
+        else:
+            Branch.hooks.install_hook(bzr_hook, callback)
+            if getattr(Branch.hooks, 'name_hook', None) is not None:
+                Branch.hooks.name_hook(callback, name)
+    else:
+        raise errors.BzrError("Bazaar version does not support " + bzr_hook + 
" hooks.")
+
+def install_server_hook():
+    if 'server_started' in SmartTCPServer.hooks:
+        install_named_hook = getattr(SmartTCPServer.hooks, 
'install_named_hook', None)
+        if install_named_hook is not None:
+            if 'server_started' in SmartTCPServer.hooks:
+                install_named_hook('server_started', server_started_hook, 'bzr 
email-notification')
+    else:
+        raise errors.BzrError("Bazaar version does not support server_started 
hooks.")
+
+# Install all hooks here
+def install_hooks():
+    """Install post_change_branch_tip hook """
+    install_hook('post_change_branch_tip', post_change_branch_tip_hook, 'bzr 
email-notification')
+
+    """Install pre_change_branch_tip hook """
+    install_hook('pre_change_branch_tip', pre_change_branch_tip_hook, 'bzr 
email-notification')
+
+    """ Install server_started hook """
+    install_server_hook()
+
+def test_suite():
+    from unittest import TestSuite
+    import bzrlib.plugins.email.tests
+    result = TestSuite()
+    result.addTest(bzrlib.plugins.email.tests.test_suite())
+    return result
+
+
+# setup the email plugin with > 0.15 hooks.
+try:
+    install_hooks()
+    use_legacy = False
+except AttributeError:
+    # bzr < 0.15 - no Branch.hooks
+    use_legacy = True
+except errors.UnknownHook:
+    # bzr 0.15 dev before post_commit was added
+    use_legacy = True

=== added file 'tools/bzrhooks/email/customlogformatter.py'
--- tools/bzrhooks/email/customlogformatter.py  1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/email/customlogformatter.py  2010-06-01 16:02:05 +0000
@@ -0,0 +1,120 @@
+from bzrlib import log, builtins, status
+import codecs
+
+""" A custom built subclass of log.LogFormatter.
+    Displays log of newly committed revisions in pisa style. """
+class CustomLogFormatter(log.LogFormatter):
+
+    supports_merge_revisions = True
+    preferred_levels = 1
+    supports_delta = True
+    supports_tags = True
+    supports_diff = True
+
+    def log_revision(self, revision):
+        import time
+
+        charset = 'utf-8'
+
+        """Log a revision, either merged or not."""
+        indent = u'    ' * revision.merge_depth
+        to_file = self.to_file
+
+        # Write author / commiter
+        # Only commiters are relevant for email-notification,
+        # authors are omitted.
+        try:
+            unicodestr = revision.rev.committer.decode(charset, 'replace')
+        except UnicodeError:
+            unicodestr = u"n/a due to unicode issues"
+        to_file.write(unicode(indent + 'Committer: %s\n' % (unicodestr,)))
+
+        # Write date
+        try:
+        #    date_str = osutils.format_date(revision.rev.timestamp, 
revision.rev.timezone or 0, self.show_timezone, date_fmt)
+            unicodestr = time.strftime("%d/%m/%Y at %H:%M:%S").decode(charset, 
'replace')
+        except UnicodeError:
+            unicodestr = u"n/a due to unicode issues"
+        to_file.write(unicode(indent + 'Date: %s\n' % (unicodestr,)))
+
+        # Write new revision number
+        if revision.revno is not None:
+            try:
+                unicodestr = revision.revno.decode(charset, 'replace')
+            except UnicodeError:
+                unicodestr = u"(n/a due to unicode issues)"
+            to_file.write(unicode(indent + 'Revision: %s%s\n' % (unicodestr, 
self.merge_marker(revision))))
+
+        # Always show revision id..
+        #if self.show_ids:
+        try:
+            revidstr = revision.rev.revision_id.decode(charset, 'replace')
+        except UnicodeError:
+            revidstr = u"(n/a due to unicode issues)"
+        to_file.write(unicode(indent + 'Revision-id: ' + revidstr))
+        to_file.write('\n')
+
+        # Write branch nick
+        branch_nick = revision.rev.properties.get('branch-nick', None)
+        if branch_nick is not None:
+            try:
+                unicodestr = branch_nick.decode(charset, 'replace')
+            except UnicodeError:
+                unicodestr = u"(n/a due to unicode issues)"
+            to_file.write(unicode(indent + 'Branch nick: %s\n' % 
(unicodestr,)))
+        to_file.write('\n')
+
+        # Write log message if available
+        to_file.write(indent + 'Log:\n')
+        if not revision.rev.message:
+            to_file.write(indent + '  (no log message available)\n')
+        else:
+            message = revision.rev.message.rstrip('\r\n')
+            for l in message.split('\n'):
+                try:
+                    unicodestr = l.decode(charset, 'replace')
+                except UnicodeError:
+                    unicodestr = u" xx (there were encoding issues in this 
line) xx"
+                to_file.write(indent + '  %s\n' % (unicodestr,))
+        to_file.write('\n')
+
+        # Write summary of modified files if available
+        if revision.delta is not None:
+            to_file.write(indent + 'Modified:\n')
+            revision.delta.show(to_file, self.show_ids, indent=indent+'  ', 
short_status=True)
+        else:
+            to_file.write('   ' + "revision delta not available \n")
+        to_file.write('\n')
+
+        # Write diff, only if this is not a merge
+        # If this is a merge further information will be provided by the 
emailer class
+        if revision.diff is not None and len(revision.rev.parent_ids) <= 1:
+            self.show_diff(to_file, revision.diff, indent)
+
+    def show_diff(self, to_file, diff, indent):
+        """ Show diff for files that were added and modified.
+            Diffs for files that were removed are not displayed. """
+        leaveout = False
+        charset = 'utf-8'
+        for line in diff.rstrip().split('\n'):
+            try:
+                unicodestr = line.decode(charset, 'replace')
+            except UnicodeError:
+                unicodestr = u" xx (there were encoding issues in this line) 
xx"
+
+            # Not remove/renamed case, not new file
+            if not leaveout and not line.startswith("==="):
+                to_file.write(unicode(indent + '%s\n' % (unicodestr,)))
+
+            # Start of new file, maybe remove/renamed case
+            elif line.startswith("==="):
+                parts = line.split()
+                if (parts[1] == "removed" or parts[1] == "renamed") and 
parts[2] == "file":
+                    leaveout = True
+                else:
+                    leaveout = False
+                    to_file.write(indent + '%s\n' % (unicodestr,))
+
+    def get_advice_separator(self):
+        """Get the text separating the log from the closing advice."""
+        return '-' * 60 + '\n'

=== added file 'tools/bzrhooks/email/emailer.py'
--- tools/bzrhooks/email/emailer.py     1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/email/emailer.py     2010-06-01 16:02:05 +0000
@@ -0,0 +1,254 @@
+# 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
+
+import errno
+import subprocess
+import customlogformatter as clf
+import datetime
+import time
+import sys
+import warnings
+import codecs
+
+from bzrlib import (
+    errors,
+    revision as _mod_revision,
+    log
+    )
+
+class EmailSender(object):
+    """An email message sender."""
+
+    server_mode = False
+
+    def __init__(self, params, op, local_branch=None):
+        # Extract useful information from params
+        self.config = params.branch.get_config()
+        self.branch = params.branch
+        self.new_revid = params.new_revid
+        self.old_revid = params.old_revid
+        self.new_revno = params.new_revno
+        self.old_revno = params.old_revno
+        if (local_branch is not None and 
local_branch.repository.has_revision(self.new_rev_id)):
+            self.repository = local_branch.repository
+        else:
+            self.repository = self.branch.repository
+
+        # Which hook was exectued
+        self.op = op
+
+        # Server mode?
+        self.server_mode = EmailSender.server_mode
+
+        # Which charset
+        self.charset = 'utf-8'
+
+    def setup_revision_and_revno(self):
+        """Get the revision and revision number from the branch."""
+        if self.uncommit:
+            # We want to notify about the revision that is being uncommited
+            self.revision = self.repository.get_revision(self.old_revid)
+            tmp = self.new_revid
+            self.new_revid = self.old_revid
+            self.old_revid = tmp
+            tmp = self.new_revno
+            self.new_revno = self.old_revno
+            self.old_revno = tmp
+        else:
+            self.revision = self.repository.get_revision(self.new_revid)
+
+    def get_merge_info(self):
+        """ Display some additional information if this revision is a merge """
+
+        # Get new working tree
+        tree_new = self.repository.revision_tree(self.new_revid)
+
+        # Get parent ids.. if this is a merge there are more than one.
+        parent_ids = self.repository.get_revision(self.new_revid).parent_ids
+        if len(parent_ids) <= 1:
+            return ""
+
+        # Create merge information
+        res = "Merge from following parent revisions and branches:\n"
+        for merge in parent_ids:
+            try:
+                rev = self.repository.get_revisions([merge])[0]
+                res += "  - " + rev.properties.get('branch-nick', '(branch 
nick n/a)') +"\t  (parent-id: " + merge + ")    \n"
+            except errors.NoSuchRevision:
+                # If we are missing a revision, just print out the revision id
+                res += ' (ghost) ' + merge + '\n'
+                continue
+
+        return res + "\n"
+
+    def body(self):
+        """Create the email body"""
+        from bzrlib import log
+
+        # Determine start and end revision to display
+        rev1 = rev2 = self.new_revno
+        if rev1 == 0:
+            rev1 = None
+            rev2 = None
+
+        # 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()
+        try:
+            enc, dec, reader, writer = codecs.lookup(self.charset)
+            writebuffer = writer(outf, 'replace')
+        except LookupError:
+            warnings.warn("Failed to lookup " + charset + "-codec. You might 
encounter further errors regarding encoding..")
+            writebuffer = outf
+
+        try:
+            # Use custom log formatter, which displays logrevisions in 
pisa-style...
+            lf = clf.CustomLogFormatter(show_ids=False, to_file=writebuffer)
+
+            # Let the show_log code do all the work
+            # set verbose = True to provide a treedelta
+            log.show_log(self.branch, lf, start_revision=rev1, 
end_revision=rev2, verbose=True, show_diff=True)
+
+            # This could be a merge, too...
+            merge_info = self.get_merge_info()
+        except UnicodeError:
+            warnings.warn("There was a unicode error...")
+
+        # If this an uncommit, write a short note
+        additional_info = ""
+        if self.uncommit:
+            additional_info = " ------- Log of revision UNCOMMITTED on " + 
time.strftime("%y/%m/%d at %H:%M:%S") + " ------- \n \n"
+
+        try:
+            logmessage = outf.getvalue()
+        except UnicodeError:
+            logmessage = "(empty due to UnicodeDecodeError) \n"
+            warnings.warn("Error while creating logmessage. Notification with 
empty revisionlog will be sent!")
+
+        return additional_info + logmessage + merge_info
+
+    def to_address(self):
+        """What is the address the mail should go to."""
+        return self.config.get_user_option('commit_notification_to')
+
+    def from_address(self):
+        """What address should I send from."""
+        # On default the commiter is the sender,
+        # overwrite this by setting post_commit_sender in bazaar.conf or 
branch.conf
+        result = self.config.get_user_option('commit_notification_sender')
+        if result is None:
+            result = self.repository.get_revision(self.new_revid).committer
+
+        return result
+
+    def send(self):
+        """Send the email."""
+        self.branch.lock_read()
+        self.repository.lock_read()
+        try:
+            self._send_using_smtplib()
+        finally:
+            self.repository.unlock()
+            self.branch.unlock()
+
+    def _send_using_smtplib(self):
+        """Use python's smtplib to send the email."""
+        import smtplib
+        from email.mime.text import MIMEText
+
+
+        # Encode message body
+        try:
+            body = self.body().encode(self.charset)
+        except UnicodeEncodeError:
+            warnings.warn("Could not encode email body. No notification sent!")
+            return
+
+        # Create the email
+        try:
+            message = MIMEText(body, 'plain', self.charset)
+        except TypeError:
+            warnings.warn("Could not create email. No notification sent!")
+            return
+
+        message['Subject'] = self.subject()
+        message['From'] = self.from_address()
+        message['To'] = self.to_address()
+
+        # Send the message via our own SMTP server, but don't include the
+        # envelope header.
+        try:
+            server = smtplib.SMTP('localhost')
+        except:
+            warnings.warn("Could not connect to local mailserver. No 
notification sent!")
+            return
+
+        # Send the mail
+        server.sendmail(self.from_address(), self.to_address(), 
message.as_string())
+        server.quit()
+
+    def should_send(self):
+        """Determine if a notification should be sent at the present point.
+
+           Only send if running as server.
+
+           If called from pre_change_branch_tip hook we only want to send a 
mail if this is an uncommit.
+           If called from post_change_branch_tip hook we want to send a mail 
if this is a commit or merge.
+        """
+
+        # Determine whether this is an uncommit and set up revision, revision 
ids and revision numbers accordingly
+        if (self.new_revno - self.old_revno) > 0:
+            self.uncommit = False
+        else:
+            self.uncommit = True
+        self.setup_revision_and_revno()
+
+        # Server mode is not running as the server_started hook doesn't get 
fired,
+        # so this feature is disabled
+        # A client should not send mails, as this is supposed to run on a 
server.
+        # if not self.server_mode:
+        #    return False
+
+        # If pre_change_branch_tip hook is exectued on something not an 
uncommit
+        if (self.op == 'pre_change' and not self.uncommit):
+            return False
+
+        # If post_change_branch_tip hook is exectued on an uncommit
+        if (self.op == 'post_change' and self.uncommit):
+            return False
+
+        # Only send if to- and from-address is known
+        # Don't set at least one of these in order to disable 
email-notification.
+        return bool(self.to_address() and self.from_address())
+
+    def send_maybe(self):
+        if self.should_send():
+            self.send()
+
+
+    def subject(self):
+        """Create the subject of the email notification"""
+        branch_nick = self.revision.properties.get('branch-nick','(branch nick 
n/a)')
+
+        additional_info = ""
+        if self.uncommit:
+            additional_info = "[UNCOMMIT]"
+
+        return ("[" + branch_nick + "] Rev %d: %s %s" %
+                (self.new_revno,
+                 self.revision.get_summary(),
+                 additional_info))

=== added file 'tools/bzrhooks/email/setup.py'
--- tools/bzrhooks/email/setup.py       1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/email/setup.py       2010-06-01 16:02:05 +0000
@@ -0,0 +1,21 @@
+#!/usr/bin/env python2.4
+
+from distutils.core import setup
+
+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@xxxxxxxxxxxxxxxxx',
+      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']
+      )

=== added directory 'tools/bzrhooks/email/tests'
=== added file 'tools/bzrhooks/email/tests/__init__.py'
--- tools/bzrhooks/email/tests/__init__.py      1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/email/tests/__init__.py      2010-06-01 16:02:05 +0000
@@ -0,0 +1,29 @@
+# Copyright (C) 2005 by Canonical Ltd
+#   Authors: Robert Collins <robert.collins@xxxxxxxxxxxxx>
+#
+# 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
+    import test_smtp_connection
+
+    loader = TestLoader()
+    result.addTests(loader.loadTestsFromModule(testemail))
+    result.addTests(loader.loadTestsFromModule(test_smtp_connection))
+    return result
+

=== added file 'tools/bzrhooks/email/tests/test_smtp_connection.py'
--- tools/bzrhooks/email/tests/test_smtp_connection.py  1970-01-01 00:00:00 
+0000
+++ tools/bzrhooks/email/tests/test_smtp_connection.py  2010-06-01 16:02:05 
+0000
@@ -0,0 +1,299 @@
+# Copyright (C) 2005 by Canonical Ltd
+#   Authors: Robert Collins <robert.collins@xxxxxxxxxxxxx>
+#
+# 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 cStringIO import StringIO
+
+from bzrlib import (
+    config,
+    __version__ as _bzrlib_version,
+    )
+from bzrlib.tests import TestCase
+from bzrlib.plugins.email.smtp_connection import SMTPConnection
+
+
+class InstrumentedSMTPConnection(SMTPConnection):
+    """Instrument SMTPConnection.
+
+    We don't want to actually connect or send messages, so this just
+    fakes it.
+    """
+
+    class FakeSMTP(object):
+        """Fakes an SMTP connection."""
+
+        def __init__(self, actions):
+            self.actions = actions
+
+        def sendmail(self, from_addr, to_addrs, msg):
+            self.actions.append(('sendmail', from_addr, to_addrs, msg))
+
+        def login(self, username, password):
+            self.actions.append(('login', username, password))
+
+    def __init__(self, config):
+        super(InstrumentedSMTPConnection, self).__init__(config)
+        self.actions = []
+
+    def _create_connection(self):
+        self.actions.append(('create_connection',))
+        self._connection = InstrumentedSMTPConnection.FakeSMTP(self.actions)
+
+    def _basic_message(self, *args, **kwargs):
+        """Override to force the boundary for easier testing."""
+        msg, from_email, to_emails = super(InstrumentedSMTPConnection,
+        self)._basic_message(*args, **kwargs)
+        msg.set_boundary('=====123456==')
+        return msg, from_email, to_emails
+
+
+class TestSMTPConnection(TestCase):
+
+    def get_connection(self, text):
+        my_config = config.GlobalConfig()
+        config_file = StringIO(text)
+        my_config._get_parser(config_file)
+        return InstrumentedSMTPConnection(my_config)
+
+    def test_defaults(self):
+        conn = self.get_connection('')
+        self.assertEqual('localhost', conn._smtp_server)
+        self.assertEqual(None, conn._smtp_username)
+        self.assertEqual(None, conn._smtp_password)
+
+    def test_smtp_server(self):
+        conn = self.get_connection('[DEFAULT]\nsmtp_server=host:10\n')
+        self.assertEqual('host:10', conn._smtp_server)
+
+    def test_smtp_username(self):
+        conn = self.get_connection('')
+        self.assertIs(None, conn._smtp_username)
+
+        conn = self.get_connection('[DEFAULT]\nsmtp_username=joebody\n')
+        self.assertEqual(u'joebody', conn._smtp_username)
+
+    def test_smtp_password(self):
+        conn = self.get_connection('')
+        self.assertIs(None, conn._smtp_password)
+
+        conn = self.get_connection('[DEFAULT]\nsmtp_password=mypass\n')
+        self.assertEqual(u'mypass', conn._smtp_password)
+
+    def assertSplitEquals(self, username, email, address):
+        actual = SMTPConnection._split_address(address)
+        self.assertEqual((username, email), actual)
+
+    def test__split_address(self):
+        self.assertSplitEquals(u'Joe Foo', 'joe@xxxxxxx',
+                               u'Joe Foo <joe@xxxxxxx>')
+        self.assertSplitEquals(u'Joe F\xb5', 'joe@xxxxxxx',
+                               u'Joe F\xb5 <joe@xxxxxxx>')
+        self.assertSplitEquals('', 'joe', 'joe')
+
+    def test_simple_send(self):
+        """Test that we build up a reasonable looking email.
+
+        This also tests that we extract the right email addresses, etc, and it
+        gets passed to sendmail() with the right parameters.
+        """
+        conn = self.get_connection('')
+        from_addr = u'Jerry F\xb5z <jerry@xxxxxxxx>'
+        to_addr = u'Biz N\xe5 <biz@xxxxxx>'
+        subject = u'Hello Biz N\xe5'
+        message=(u'Hello Biz N\xe5\n'
+                 u'I haven\'t heard\n'
+                 u'from you in a while\n')
+        conn.send_text_email(from_addr, [to_addr], subject, message)
+        self.assertEqual(('create_connection',), conn.actions[0])
+        self.assertEqual(('sendmail', 'jerry@xxxxxxxx', ['biz@xxxxxx']),
+                         conn.actions[1][:3])
+        self.assertEqualDiff((
+   'Content-Type: multipart/mixed; boundary="=====123456=="\n'
+   'MIME-Version: 1.0\n'
+   'From: =?utf-8?q?Jerry_F=C2=B5z?= <jerry@xxxxxxxx>\n'
+   'User-Agent: bzr/%s\n'
+   'To: =?utf-8?q?Biz_N=C3=A5?= <biz@xxxxxx>\n'
+   'Subject: =?utf-8?q?Hello_Biz_N=C3=A5?=\n'
+   '\n'
+   '--=====123456==\n'
+   'Content-Type: text/plain; charset="utf-8"\n'
+   'MIME-Version: 1.0\n'
+   'Content-Transfer-Encoding: base64\n'
+   '\n'
+   'SGVsbG8gQml6IE7DpQpJIGhhdmVuJ3QgaGVhcmQKZnJvbSB5b3UgaW4gYSB3aGlsZQo=\n'
+   '\n'
+   '--=====123456==--'
+   ) % _bzrlib_version, conn.actions[1][3])
+
+    def test_send_text_and_attachment_email(self):
+        conn = self.get_connection('')
+        from_addr = u'Jerry F\xb5z <jerry@xxxxxxxx>'
+        to_addr = u'Biz N\xe5 <biz@xxxxxx>'
+        subject = u'Hello Biz N\xe5'
+        message=(u'Hello Biz N\xe5\n'
+                 u'See my attached patch\n')
+        diff_txt = ('=== diff contents\n'
+                    '--- old\n'
+                    '+++ new\n'
+                    ' unchanged\n'
+                    '-old binary\xb5\n'
+                    '-new binary\xe5\n'
+                    ' unchanged\n')
+        conn.send_text_and_attachment_email(from_addr, [to_addr], subject,
+                                            message, diff_txt, 'test.diff')
+        self.assertEqual(('create_connection',), conn.actions[0])
+        self.assertEqual(('sendmail', 'jerry@xxxxxxxx', ['biz@xxxxxx']),
+                         conn.actions[1][:3])
+        self.assertEqualDiff((
+   'Content-Type: multipart/mixed; boundary="=====123456=="\n'
+   'MIME-Version: 1.0\n'
+   'From: =?utf-8?q?Jerry_F=C2=B5z?= <jerry@xxxxxxxx>\n'
+   'User-Agent: bzr/%s\n'
+   'To: =?utf-8?q?Biz_N=C3=A5?= <biz@xxxxxx>\n'
+   'Subject: =?utf-8?q?Hello_Biz_N=C3=A5?=\n'
+   '\n'
+   '--=====123456==\n'
+   'Content-Type: text/plain; charset="utf-8"\n'
+   'MIME-Version: 1.0\n'
+   'Content-Transfer-Encoding: base64\n'
+   '\n'
+   'SGVsbG8gQml6IE7DpQpTZWUgbXkgYXR0YWNoZWQgcGF0Y2gK\n'
+   '\n'
+   '--=====123456==\n'
+   'Content-Type: text/plain; charset="8-bit"; name="test.diff"\n'
+   'MIME-Version: 1.0\n'
+   'Content-Transfer-Encoding: base64\n'
+   'Content-Disposition: inline; filename="test.diff"\n'
+   '\n'
+   
'PT09IGRpZmYgY29udGVudHMKLS0tIG9sZAorKysgbmV3CiB1bmNoYW5nZWQKLW9sZCBiaW5hcnm1\n'
+   'Ci1uZXcgYmluYXJ55QogdW5jaGFuZ2VkCg==\n'
+   '\n'
+   '--=====123456==--'
+   ) % _bzrlib_version, conn.actions[1][3])
+
+    def test_create_and_send(self):
+        """Test that you can create a custom email, and send it."""
+        conn = self.get_connection('')
+        email_msg, from_email, to_emails = conn.create_email(
+            'Joe Foo <joe@xxxxxxx>',
+            ['Jane Foo <jane@xxxxxxx>', 'Barry Foo <barry@xxxxxxx>'],
+            'Hi Jane and Barry',
+            'Check out the attachment\n')
+        self.assertEqual('joe@xxxxxxx', from_email)
+        self.assertEqual(['jane@xxxxxxx', 'barry@xxxxxxx'], to_emails)
+
+        try:
+            # python 2.5
+            from email.mime.nonmultipart import MIMENonMultipart
+            from email.encoders import encode_base64
+        except ImportError:
+            # python 2.4
+            from email.MIMENonMultipart import MIMENonMultipart
+            from email.Encoders import encode_base64
+
+        attachment_txt = '\x00foo\xff\xff\xff\xff'
+        attachment = MIMENonMultipart('application', 'octet-stream')
+        attachment.set_payload(attachment_txt)
+        encode_base64(attachment)
+
+        email_msg.attach(attachment)
+
+        # This will add someone to send to, but not include it in the To list.
+        to_emails.append('b@xxxxxx')
+        conn.send_email(email_msg, from_email, to_emails)
+
+        self.assertEqual(('create_connection',), conn.actions[0])
+        self.assertEqual(('sendmail', 'joe@xxxxxxx',
+                          ['jane@xxxxxxx', 'barry@xxxxxxx', 'b@xxxxxx']),
+                         conn.actions[1][:3])
+        self.assertEqualDiff((
+   'Content-Type: multipart/mixed; boundary="=====123456=="\n'
+   'MIME-Version: 1.0\n'
+   'From: Joe Foo <joe@xxxxxxx>\n'
+   'User-Agent: bzr/%s\n'
+   'To: Jane Foo <jane@xxxxxxx>, Barry Foo <barry@xxxxxxx>\n'
+   'Subject: Hi Jane and Barry\n'
+   '\n'
+   '--=====123456==\n'
+   'Content-Type: text/plain; charset="utf-8"\n'
+   'MIME-Version: 1.0\n'
+   'Content-Transfer-Encoding: base64\n'
+   '\n'
+   'Q2hlY2sgb3V0IHRoZSBhdHRhY2htZW50Cg==\n'
+   '\n'
+   '--=====123456==\n'
+   'Content-Type: application/octet-stream\n'
+   'MIME-Version: 1.0\n'
+   'Content-Transfer-Encoding: base64\n'
+   '\n'
+   'AGZvb/////8=\n'
+   '--=====123456==--'
+   ) % _bzrlib_version, conn.actions[1][3])
+
+    def test_email_parse(self):
+        """Check that python's email can parse our emails."""
+        conn = self.get_connection('')
+        from_addr = u'Jerry F\xb5z <jerry@xxxxxxxx>'
+        to_addr = u'Biz N\xe5 <biz@xxxxxx>'
+        subject = u'Hello Biz N\xe5'
+        message=(u'Hello Biz N\xe5\n'
+                 u'See my attached patch\n')
+        diff_txt = ('=== diff contents\n'
+                    '--- old\n'
+                    '+++ new\n'
+                    ' unchanged\n'
+                    '-old binary\xb5\n'
+                    '-new binary\xe5\n'
+                    ' unchanged\n')
+        conn.send_text_and_attachment_email(from_addr, [to_addr], subject,
+                                            message, diff_txt, 'test.diff')
+        self.assertEqual(('create_connection',), conn.actions[0])
+        self.assertEqual(('sendmail', 'jerry@xxxxxxxx', ['biz@xxxxxx']),
+                         conn.actions[1][:3])
+        email_message_text = conn.actions[1][3]
+
+        try:
+            # python 2.5
+            from email.parser import Parser
+            from email.header import decode_header
+        except ImportError:
+            # python 2.4
+            from email.Parser import Parser
+            from email.Header import decode_header
+
+        def decode(s):
+            """Convert a header string to a unicode string.
+
+            This handles '=?utf-8?q?foo=C2=B5?=' => u'Foo\\xb5'
+            """
+            return ' '.join([chunk.decode(encoding or 'ascii')
+                             for chunk, encoding in decode_header(s)])
+
+        p = Parser()
+        email_message = p.parsestr(email_message_text)
+
+        self.assertEqual(from_addr, decode(email_message['From']))
+        self.assertEqual(to_addr, decode(email_message['To']))
+        self.assertEqual(subject, decode(email_message['Subject']))
+        text_payload = email_message.get_payload(0)
+        diff_payload = email_message.get_payload(1)
+        # I haven't found a way to have python's email read the charset=""
+        # portion of the Content-Type header. So I'm doing it manually
+        # The 'decode=True' here means to decode from base64 => 8-bit text.
+        # text_payload.get_charset() returns None
+        text = text_payload.get_payload(decode=True).decode('utf-8')
+        self.assertEqual(message, text)
+        self.assertEqual(diff_txt, diff_payload.get_payload(decode=True))

=== added file 'tools/bzrhooks/email/tests/testemail.py'
--- tools/bzrhooks/email/tests/testemail.py     1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/email/tests/testemail.py     2010-06-01 16:02:05 +0000
@@ -0,0 +1,216 @@
+# Copyright (C) 2005 by Canonical Ltd
+#   Authors: Robert Collins <robert.collins@xxxxxxxxxxxxx>
+#
+# 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 cStringIO import StringIO
+from unittest import TestLoader
+
+from bzrlib import (
+    config,
+    tests,
+    )
+from bzrlib.bzrdir import BzrDir
+from bzrlib.tests import TestCaseInTempDir
+from bzrlib.plugins.email import post_commit
+from bzrlib.plugins.email.emailer import EmailSender
+
+
+def test_suite():
+    return TestLoader().loadTestsFromName(__name__)
+
+
+sample_config=("[DEFAULT]\n"
+               "post_commit_to=demo@xxxxxxxxxxx\n"
+               "post_commit_sender=Sample <foo@xxxxxxxxxxx>\n")
+
+unconfigured_config=("[DEFAULT]\n"
+                     "email=Robert <foo@xxxxxxxxxxx>\n")
+
+sender_configured_config=("[DEFAULT]\n"
+                          "post_commit_sender=Sample <foo@xxxxxxxxxxx>\n")
+
+to_configured_config=("[DEFAULT]\n"
+                      "post_commit_to=Sample <foo@xxxxxxxxxxx>\n")
+
+multiple_to_configured_config=("[DEFAULT]\n"
+              "post_commit_sender=Sender <from@xxxxxxxxxxx>\n"
+              "post_commit_to=Sample <foo@xxxxxxxxxxx>, Other <baz@xxxxxxx>\n")
+
+push_config=("[DEFAULT]\n"
+    "post_commit_to=demo@xxxxxxxxxxx\n"
+    "post_commit_push_pull=True\n")
+
+with_url_config=("[DEFAULT]\n"
+                 "post_commit_url=http://some.fake/url/\n";
+                 "post_commit_to=demo@xxxxxxxxxxx\n"
+                 "post_commit_sender=Sample <foo@xxxxxxxxxxx>\n")
+
+
+class TestGetTo(TestCaseInTempDir):
+
+    def test_body(self):
+        sender = self.get_sender()
+        # FIXME: this should not use a literal log, rather grab one from 
bzrlib.log
+        self.assertEqual(
+            'At %s\n'
+            '\n'
+            '------------------------------------------------------------\n'
+            'revno: 1\n'
+            'revision-id: A\n'
+            'committer: Sample <john@xxxxxxxxxxx>\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' % sender.url(), 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@xxxxxxxxxxx', sender.to())
+
+    def test_from(self):
+        sender = self.get_sender()
+        self.assertEqual('Sample <foo@xxxxxxxxxxx>', sender.from_address())
+
+    def test_from_default(self):
+        sender = self.get_sender(unconfigured_config)
+        self.assertEqual('Robert <foo@xxxxxxxxxxx>', 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 <foo@xxxxxxxxxxx>', u'Other <baz@xxxxxxx>'],
+                         sender.to())
+        self.assertEqual([u'Sample <foo@xxxxxxxxxxx>', u'Other <baz@xxxxxxx>'],
+                         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_diff_filename(self):
+        sender = self.get_sender()
+        self.assertEqual('patch-1.diff', sender.diff_filename())
+
+    def get_sender(self, text=sample_config):
+        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 <john@xxxxxxxxxxx>",
+            )
+        my_config = self.branch.get_config()
+        config_file = StringIO(text)
+        (my_config._get_global_config()._get_parser(config_file))
+        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)

=== added directory 'tools/bzrhooks/syntaxchecker'
=== added file 'tools/bzrhooks/syntaxchecker/__init__.py'
--- tools/bzrhooks/syntaxchecker/__init__.py    1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/syntaxchecker/__init__.py    2010-06-01 16:02:05 +0000
@@ -0,0 +1,119 @@
+# 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
+
+""" Plugin to avoid committing undesired text.
+
+* The plugin will reject all patches that contain tabs, trailing whitespaces 
or long lines.
+
+* Files that are allowed to contain these patterns can be specified in the 
user options
+    - syntax_dontcheck_tabs
+    - syntax_dontcheck_longlines
+    - syntax_dontcheck_whitespaces
+    - syntax_dontcheck_newlines
+F.e.
+    - syntax_ignorelist_tabs = file1.c,file2.c
+    - syntax_ignorelist_longlines = *.c
+    - syntax_ignorelist_whitespaces =
+    - syntax_ignorelist_newlines = *.c,Makefile.am
+  If one of the option is not set, this test will be disabled
+  Attention: On the contrary, specifying an empty list, will result in every 
file being checked.
+  In the patterns you can use unix shell-style wildcards, which are not the 
same as regular expressions
+  The special characters used in shell-style wildcards are:
+    *  which matches everything
+    ?  which matches any single character
+    [seq] which matches any character in seq
+    [!seq] which matches any character not in seq
+  Note that the filename separator ('/' on Unix) is not special here.
+  Neither are filenames starting with a period. They are equally matched by 
the * and ? patterns.
+  (for further details see documentation of pythons fnmatch module)
+
+* The maximal length of lines is read from useroption
+    - syntax_longline_length
+  It is set to 80 if not specified.
+
+* To install this plug in system wide copy it into the plugins directory
+  of the bzrlib. You can find out the directory of the bzrlib by executing
+    `` bzr version | grep bzrlib ``
+  This should be something like
+    `` /usr/lib/python2.6/dist-packages/bzrlib ``
+  Then you put the plugin into
+    `` /usr/lib/python2.6/dist-packages/bzrlib/plugins/syntaxchecker ``
+  That's all.
+
+* You can check the installation by executing
+    `` bzr plugins ``
+  (syntaxchecker plugin should be displayed with a short message)
+  or
+    `` bzr hooks ``
+  (pre_change_branch_tip should have an entry from syntaxchecker now)
+"""
+
+
+if __name__ != 'bzrlib.plugins.syntaxchecker':
+    raise ImportError('The syntaxchecker plugin must be installed as'
+                      ' bzrlib.plugins.syntaxchecker not %s'
+                      % __name__)
+
+
+# These three are used during import: No point lazy_importing them.
+from bzrlib import errors
+from bzrlib.branch import Branch
+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.syntaxchecker import syntaxchecker as _syntaxchecker
+""")
+
+def pre_change_branch_tip_hook(params):
+    """This hook will be called on the server's side before a change."""
+    # (branch, old_revno, new_revno, old_revid, new_revid)
+    _syntaxchecker.SyntaxChecker(params).check()
+
+def install_hook(bzr_hook, callback, name):
+    """Install the given hook with the given name """
+    if bzr_hook in Branch.hooks:
+        install_named_hook = getattr(Branch.hooks, 'install_named_hook', None)
+        if install_named_hook is not None:
+            install_named_hook(bzr_hook, callback, name)
+        else:
+            Branch.hooks.install_hook(bzr_hook, callback)
+            if getattr(Branch.hooks, 'name_hook', None) is not None:
+                Branch.hooks.name_hook(callback, name)
+    else:
+        raise errors.BzrError("Bazaar version does not support " + bzr_hook + 
" hooks.")
+
+# Install all hooks here
+def install_hooks():
+
+    """Install pre_change_branch_tip hook """
+    install_hook('pre_change_branch_tip', pre_change_branch_tip_hook, 'bzr 
syntaxchecker')
+
+
+def test_suite():
+    return
+
+
+# setup the email plugin with > 0.15 hooks.
+try:
+    install_hooks()
+    use_legacy = False
+except AttributeError:
+    # bzr < 0.15 - no Branch.hooks
+    use_legacy = True
+except errors.UnknownHook:
+    # bzr 0.15 dev before post_commit was added
+    use_legacy = True

=== added file 'tools/bzrhooks/syntaxchecker/syntaxchecker.py'
--- tools/bzrhooks/syntaxchecker/syntaxchecker.py       1970-01-01 00:00:00 
+0000
+++ tools/bzrhooks/syntaxchecker/syntaxchecker.py       2010-06-01 16:02:05 
+0000
@@ -0,0 +1,222 @@
+# 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
+
+from bzrlib import errors
+from StringIO import StringIO
+import fnmatch
+
+class SyntaxChecker(object):
+
+    def __init__(self, params, local_branch=None):
+        """Get the revision ids, revision numbers, config and repository from 
the branch."""
+
+        self.config = params.branch.get_config()
+        self.branch = params.branch
+        self.new_revno = params.new_revno
+        self.old_revno = params.old_revno
+
+        # Get repository for new revision
+        if (local_branch is not None and 
local_branch.repository.has_revision(params.new_revid)):
+            self.repository = local_branch.repository
+        else:
+            self.repository = params.branch.repository
+
+        # Get the revision
+        self.revision = self.repository.get_revision(params.new_revid)
+
+        # Get old and new working tree
+        self.new_tree =  self.repository.revision_tree(params.new_revid)
+        self.old_tree = self.branch.basis_tree()
+
+        # Initialize ignore lists
+        self._init_ignore_lists()
+
+        # Get maximal line length
+        self.longline_length = 
self.config.get_user_option('syntax_longline_length')
+        if self.longline_length is None:
+            self.longline_length = 80
+
+    def _init_ignore_list(self, user_option):
+        """ Initialize the whitelist from the user option """
+        list = self.config.get_user_option(user_option)
+
+        # if no list is provided
+        if list is None:
+            return None
+
+        # if only one pattern is provided
+        if type(list) != type([]):
+            list = [list]
+        return list
+
+    def _init_ignore_lists(self):
+        """ Read the ignore for the different checks from the user config """
+        self.tabs_ignorelist = self._init_ignore_list('syntax_dontcheck_tabs')
+        self.whitespaces_ignorelist = 
self._init_ignore_list('syntax_dontcheck_whitespaces')
+        self.newlines_ignorelist = 
self._init_ignore_list('syntax_dontcheck_newlines')
+        self.longlines_ignorelist = 
self._init_ignore_list('syntax_dontcheck_longlines')
+        self.dosbreaks_ignorelist = 
self._init_ignore_list('syntax_dontcheck_doslinebreaks')
+
+    def _match(self, patterns, file):
+        """ Check if file matches one of the patterns in the given list of 
patterns.
+            If no list is provided then the file does match e.g. will be 
ignored"""
+
+        # If no list was provided, file is ignored per default, e.g. matches
+        if patterns is None:
+            return True
+
+        # Use unix shell-style wildcards for filename-pattern-matching
+        for pattern in patterns:
+            if fnmatch.fnmatch(file, pattern):
+                return True
+
+        return False
+
+    def _get_tests(self, path):
+        """ Return a list of tests to perform on the given file """
+        tests = {}
+        if not self._match(self.tabs_ignorelist, path):
+            tests['tabs'] = self._check_tabs
+        if not self._match(self.whitespaces_ignorelist, path):
+            tests['whitespaces'] = self._check_trailing_whitespaces
+        if not self._match(self.longlines_ignorelist, path):
+            tests['longlines'] = self._check_longline
+        if not self._match(self.dosbreaks_ignorelist, path):
+            tests['doslinebreaks'] = self._check_dosbreaks
+        return tests
+
+    def _check_tabs(self, line):
+        """ Checks if given line contains tabs """
+        if '\t' in line:
+            return True
+        else:
+            return False
+
+    def _check_dosbreaks(self, line):
+        if '\r' in line:
+            return True
+        else:
+            return False
+
+    def _check_trailing_whitespaces(self, line):
+        """ Checks if line has trailing whitespaces.
+            This is copied from text_checker plugin """
+        import re
+        trailing_ws_match = re.match(r'^((.*?)([\t ]*))(\r?\n)?$', line)
+        if trailing_ws_match:
+            return bool(trailing_ws_match.group(3))
+        else:
+            return False
+
+    def _check_longline(self, line):
+        """ Checks if given line is longer than 80 chars """
+        if len(line) > self.longline_length:
+            return True
+        else:
+            return False
+
+    def _check_file(self, file_id, path):
+        """ Apply tests for given file """
+
+        # Get the lines of the file in a list
+        file_lines = self.new_tree.get_file(file_id).readlines()
+
+        # Log problems and line numbers
+        problems = {'tabs' : [], 'whitespaces' : [], 'longlines' : [], 
'newline' : [], 'doslinebreaks' : []}
+
+        # Dont check empty files or files on ignorelist
+        if len(file_lines) > 0 and not self._match(self.newlines_ignorelist, 
path):
+            # Check for newline at eof
+            if not file_lines[-1].endswith('\n'):
+                problems['newline'] = [len(file_lines)]
+
+        # Get further tests to perform
+        tests = self._get_tests(path)
+
+        # If there are no further tests to perform, return
+        if len(tests.keys()) == 0:
+            return problems
+
+        # Keep track of line numbers where errors occur
+        counter = 0
+
+         # Perform tests on every line of every and log problems
+        for line in file_lines:
+            counter += 1
+            for type, test in tests.iteritems():
+                if test(line):
+                    problems[type].append(counter)
+
+        # Return problems
+        return problems
+
+    def check(self):
+        """ Checks for each touched file in this patch if it is whitelisted,
+            and if not, if it contains tabs."""
+
+        # If this is an uncommit, there's nothing to be done
+        if (self.new_revno - self.old_revno) < 0:
+            return
+
+        # Lock the trees
+        self.old_tree.lock_read()
+        self.new_tree.lock_read()
+
+        try:
+            # Check all modified files in new working tree
+            problems = {}
+            iterator = self.new_tree.iter_changes(self.old_tree)
+            for (file_id, paths, changed_content, versioned, parent, name, 
kind, executable) in iterator:
+                # paths contains two paths: the old one (index 0) and the new 
path (index 1) of the modified file
+                # they are different if file was added, renamed or removed
+                NEW_PATH = 1
+                # Omit checking for files that are being removed
+                if not paths[NEW_PATH] is None:
+                    problems[paths[NEW_PATH]] = self._check_file(file_id, 
paths[NEW_PATH])
+
+            # Construct possible error message
+            fmt_string = ""
+            arglist = []
+            for file, problems_of_file in problems.iteritems():
+                # display filename only once
+                disply_filename = True
+                for type, linenumbers in problems_of_file.iteritems():
+                    if len(linenumbers) > 0:
+                        if disply_filename:
+                            fmt_string += "%12s failed check\t(type = %-15s in 
lines %s \n"
+                            arglist += [file, type+")", str(linenumbers)]
+                            disply_filename = False
+                        else:
+                            fmt_string += "\t\t\t\t(type = %-15s in lines %s 
\n"
+                            arglist += [type+")", str(linenumbers)]
+
+            # Report the errors if there were any
+            errormessage = str(fmt_string % tuple(arglist))
+            if errormessage != "":
+                # Append the old log-message
+                errormessage += "\nYou entered the following log-message:\n"
+                if not self.revision.message:
+                    errormessage += "  (no log message available)\n"
+                else:
+                    errormessage += self.revision.message.rstrip('\r\n')
+                errormessage += "\n\n"
+
+                # Report the errors
+                raise errors.TipChangeRejected("\nThe commit was rejected, 
because there were errors.\n" + errormessage)
+
+        finally:
+            self.old_tree.unlock()
+            self.new_tree.unlock()

=== added directory 'tools/bzrhooks/tagchecker'
=== added file 'tools/bzrhooks/tagchecker/__init__.py'
--- tools/bzrhooks/tagchecker/__init__.py       1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/tagchecker/__init__.py       2010-06-01 16:02:05 +0000
@@ -0,0 +1,97 @@
+# 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
+
+"""Checking commit messages for whitelisted tags.
+
+The plugin will reject all commits whose commit messages don't contain the 
following pattern:
+"[taglist]", where tags is a comma-separated whitelist of tags.
+The allowed tags are specified in option `tagchecker_whitelist` in the 
branch.conf of every branch,
+the tagchecker should control.
+e.g. add line
+    tagchecker_whitelist = tag1,tag2,...,tagn
+to the branch.conf.
+
+To install this plug in system wide copy it into the plugins directory
+of the bzrlib. You can find out the directory of the bzrlib by executing
+    `` bzr version | grep bzrlib ``
+This should be something like
+    `` /usr/lib/python2.6/dist-packages/bzrlib ``
+Then you put the plugin into
+    `` /usr/lib/python2.6/dist-packages/bzrlib/plugins/tagchecker ``
+That's all.
+
+You can check the installation by executing
+    `` bzr plugins ``
+(tagchecker plugin should be displayed with a short message)
+or
+    `` bzr hooks ``
+(pre_change_branch_tip should have an entry from tagchecker now)
+"""
+
+
+if __name__ != 'bzrlib.plugins.tagchecker':
+    raise ImportError('The email plugin must be installed as'
+                      ' bzrlib.plugins.tagchecker not %s'
+                      % __name__)
+
+
+# These three are used during import: No point lazy_importing them.
+from bzrlib import errors
+from bzrlib.branch import Branch
+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.tagchecker import tagchecker as _tagchecker
+""")
+
+def pre_change_branch_tip_hook(params):
+    """This hook will be called on the server's side before a change."""
+    # (branch, old_revno, new_revno, old_revid, new_revid)
+    _tagchecker.TagChecker(params).check()
+
+def install_hook(bzr_hook, callback, name):
+    """Install the given hook with the given name """
+    if bzr_hook in Branch.hooks:
+        install_named_hook = getattr(Branch.hooks, 'install_named_hook', None)
+        if install_named_hook is not None:
+            install_named_hook(bzr_hook, callback, name)
+        else:
+            Branch.hooks.install_hook(bzr_hook, callback)
+            if getattr(Branch.hooks, 'name_hook', None) is not None:
+                Branch.hooks.name_hook(callback, name)
+    else:
+        raise errors.BzrError("Bazaar version does not support " + bzr_hook + 
" hooks.")
+
+# Install all hooks here
+def install_hooks():
+    """Install pre_change_branch_tip hook """
+    install_hook('pre_change_branch_tip', pre_change_branch_tip_hook, 'bzr 
tagchecker')
+
+def test_suite():
+    return
+
+
+# setup the email plugin with > 0.15 hooks.
+try:
+    install_hooks()
+    use_legacy = False
+except AttributeError:
+    # bzr < 0.15 - no Branch.hooks
+    use_legacy = True
+except errors.UnknownHook:
+    # bzr 0.15 dev before post_commit was added
+    use_legacy = True

=== added file 'tools/bzrhooks/tagchecker/tagchecker.py'
--- tools/bzrhooks/tagchecker/tagchecker.py     1970-01-01 00:00:00 +0000
+++ tools/bzrhooks/tagchecker/tagchecker.py     2010-06-01 16:02:05 +0000
@@ -0,0 +1,91 @@
+# 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
+
+from bzrlib import errors
+
+class TagChecker(object):
+
+    def __init__(self, params, local_branch=None):
+        """ The only thing we need is the revision and the config"""
+
+        self.config = params.branch.get_config()
+
+        if (local_branch is not None and 
local_branch.repository.has_revision(self.new_rev_id)):
+            repository = local_branch.repository
+        else:
+            repository = params.branch.repository
+
+        self.revision = repository.get_revision(params.new_revid)
+
+    def _errormessage(self, tag_list, reason):
+        res = ""
+        if reason == "notagfound":
+            res += "\n" \
+                "Commit was rejected because the commit-message did not 
contain any tag \n" \
+                "The commit-message should contain at least one tag from the 
following list: \n"
+
+            # Append list of allowed tags
+            for tag in tag_list:
+                res += "    - " + tag + "\n"
+
+        elif reason == "wrongformat":
+            res += "\n" \
+                "Commit was rejected because tags were not given in the right 
format \n" \
+                "Tags should appear immediately at the beginning of the commit 
message enclosed in brackets.\n" \
+                "F.e. [tag1, tag2, tag3, ... ] logsummary ... logmessage...\n" 
\
+
+        # Append the old log-message
+        res += "\n" \
+            "You entered the following log-message:\n"
+        if not self.revision.message:
+            res += "  (no log message found)\n"
+        else:
+            res += self.revision.message.rstrip('\r\n')
+            res += "\n\n"
+
+        return res
+
+    def check(self):
+        """ Check if the commit message begins with at
+            least one tag from a whitelist, given by
+            commit_notification_tags option. """
+
+        # This is the tag white list
+        # Each commit message should begin with at least one of those tags
+        # tag_white_list = ["test","bugfix","docu"]
+        tag_white_list = self.config.get_user_option('tagchecker_whitelist')
+
+        # No tag checking...
+        if tag_white_list is None:
+            return
+
+        # Get commit message
+        message = self.revision.message
+
+        # We assume that tags are enclosed in brackets
+        tags_start = message.find('[')
+        tags_end = message.find(']')
+        if tags_start != 0 or tags_end == -1:
+            raise errors.TipChangeRejected(self._errormessage(tag_white_list, 
"wrongformat"))
+
+        # Check for each tag from the white list if it appears in the message 
tag area
+        # and return on first match
+        for tag in tag_white_list:
+            if message.find(tag, tags_start, tags_end) != -1:
+                return
+
+        # No match found
+        raise errors.TipChangeRejected(self._errormessage(tag_white_list, 
"notagfound"))

Other related posts:

  • » [hipl-commit] [trunk] Rev 4692: added pyton scripts used at the bzr server - Rene Hummen