[pypy-svn] buildbot default: merge heads

arigo commits-noreply at bitbucket.org
Fri Apr 29 11:52:40 CEST 2011


Author: Armin Rigo <arigo at tunes.org>
Branch: 
Changeset: r496:c5111efe6dd9
Date: 2011-04-29 11:52 +0200
http://bitbucket.org/pypy/buildbot/changeset/c5111efe6dd9/

Log:	merge heads

diff --git a/bitbucket_hook/hook.py b/bitbucket_hook/hook.py
--- a/bitbucket_hook/hook.py
+++ b/bitbucket_hook/hook.py
@@ -1,307 +1,53 @@
 import os.path
 import py
-import smtplib
-import socket
 import subprocess
 import sys
-from subprocess import Popen, PIPE
+import time
 
-LOCAL_REPOS = py.path.local(__file__).dirpath('repos')
-REMOTE_BASE = 'http://bitbucket.org'
+from .main import app
+from . import scm
+#
+from . import stdoutlog
+from . import irc
+from . import mail
 
-if socket.gethostname() == 'viper':
-    # for debugging, antocuni's settings
-    SMTP_SERVER = "out.alice.it"
-    SMTP_PORT = 25
-    ADDRESS = 'anto.cuni at gmail.com'
-    #
-    CHANNEL = '#test'
-    BOT = '/tmp/commit-bot/message'
-else:
-    # real settings, (they works on codespeak at least)
-    SMTP_SERVER = 'localhost'
-    SMTP_PORT = 25
-    ADDRESS = 'pypy-svn at codespeak.net'
-    #
-    CHANNEL = '#pypy'
-    BOT = '/svn/hooks/commit-bot/message'
 
-hgexe = str(py.path.local.sysfind('hg'))
+HANDLERS = [
+    stdoutlog.handle_commit,
+    irc.handle_commit,
+    mail.handle_commit
+    ]
 
-TEMPLATE = u"""\
-Author: {author}
-Branch: {branches}
-Changeset: r{rev}:{node|short}
-Date: {date|isodate}
-%(url)s
+def check_for_local_repo(local_repo, remote_repo, owner):
+    if local_repo.check(dir=True):
+        return True
+    if owner == app.config['DEFAULT_USER']:
+        print >> sys.stderr, 'Automatic initial clone of %s' % remote_repo
+        scm.hg('clone', str(remote_repo), str(local_repo))
+        return True
+    return False
 
-Log:\t{desc|fill68|tabindent}
+def get_commits(payload, seen_nodes=set()):
+    import operator
+    commits = sorted(payload['commits'],
+                     key=operator.itemgetter('revision'))
+    for commit in commits:
+        node = commit['raw_node']
+        if node in seen_nodes:
+            continue
+        seen_nodes.add(node)
+        yield commit
 
-"""
 
-def getpaths(files, listfiles=False):
-
-    # Handle empty input
-    if not files:
-        return '', ''
-    files = [f['file'] for f in files]
-    if not any(files):
-        return '', ''
-
-    dirname = os.path.dirname
-    basename = os.path.basename
-
-    common_prefix = [dirname(f) for f in files]
-
-    # Single file, show its full path
-    if len(files) == 1:
-        common_prefix = files[0]
-        listfiles = False
-
-    else:
-        common_prefix = [path.split(os.sep) for path in common_prefix]
-        common_prefix = os.sep.join(os.path.commonprefix(common_prefix))
-        if common_prefix and not common_prefix.endswith('/'):
-            common_prefix += '/'
-
-    if listfiles:
-        # XXX Maybe should return file paths relative to prefix? Or TMI?
-        filenames = [basename(f) for f in files if f and basename(f)]
-        filenames = ' M(%s)' % ', '.join(filenames)
-    else:
-        filenames = ''
-    return common_prefix, filenames
-
-
-class BitbucketHookHandler(object):
-    Popen, PIPE = Popen, PIPE
-    def _hgexe(self, argv):
-        proc = self.Popen([hgexe] + list(argv), stdout=self.PIPE,
-                          stderr=self.PIPE)
-        stdout, stderr = proc.communicate()
-        ret = proc.wait()
-        return stdout, stderr, ret
-
-    def hg(self, *argv):
-        argv = map(str, argv)
-        stdout, stderr, ret = self._hgexe(argv)
-        if ret != 0:
-            print >> sys.stderr, 'error: hg', ' '.join(argv)
-            print >> sys.stderr, stderr
-            raise Exception('error when executing hg')
-        return unicode(stdout, encoding='utf-8', errors='replace')
-
-    SMTP = smtplib.SMTP
-    def send(self, from_, to, subject, body, test=False):
-        from email.mime.text import MIMEText
-        # Is this a valid workaround for unicode errors?
-        body = body.encode('ascii', 'xmlcharrefreplace')
-        msg = MIMEText(body, _charset='utf-8')
-        msg['From'] = from_
-        msg['To'] = to
-        msg['Subject'] = subject
-        if test:
-            print '#' * 20
-            print "Email contents:\n"
-            print from_
-            print to
-            print msg.get_payload(decode=True)
-        else:
-            smtp = self.SMTP(SMTP_SERVER, SMTP_PORT)
-            smtp.sendmail(from_, [to], msg.as_string())
-
-    call_subprocess = staticmethod(subprocess.call)
-
-    def send_irc_message(self, message, test=False):
-        if test:
-            print message + '\n'
-        else:
-            return self.call_subprocess([BOT, CHANNEL, message])
-
-    def handle(self, payload, test=False):
-        path = payload['repository']['absolute_url']
-        self.payload = payload
-        self.local_repo = LOCAL_REPOS.join(path)
-        self.remote_repo = REMOTE_BASE + path
-        if not self.local_repo.check(dir=True):
-            print >> sys.stderr, 'Ignoring unknown repo', path
-            return
-        self.hg('pull', '-R', self.local_repo)
-        self.handle_irc_message(test)
-        self.handle_diff_email(test)
-
-    USE_COLOR_CODES = True
-    LISTFILES = False
-    def handle_irc_message(self, test=False):
-        import operator
-        commits = sorted(self.payload['commits'],
-                         key=operator.itemgetter('revision'))
-        if test:
-            print "#" * 20
-            print "IRC messages:"
-
-        for commit in commits:
-            author = commit['author']
-            branch = commit['branch']
-            node = commit['node']
-
-            files = commit.get('files', [])
-            common_prefix, filenames = getpaths(files, self.LISTFILES)
-            pathlen = len(common_prefix) + len(filenames) + 2
-            common_prefix = '/' + common_prefix
-
-            if self.USE_COLOR_CODES:
-                author = '\x0312%s\x0F' % author   # in blue
-                branch = '\x02%s\x0F'   % branch   # in bold
-                node = '\x0311%s\x0F'   % node     # in azure
-                common_prefix = '\x0315%s\x0F' % common_prefix # in gray
-
-            message = commit['message'].replace('\n', ' ')
-            fields = (author, branch, node, common_prefix, filenames)
-            part1 = '%s %s %s %s%s: ' % fields
-            totallen = 160 + pathlen
-            if len(message) + len(part1) <= totallen:
-                irc_msg = part1 + message
-            else:
-                maxlen = totallen - (len(part1) + 3)
-                irc_msg = part1 + message[:maxlen] + '...'
-            self.send_irc_message(irc_msg, test)
-
-    def handle_diff_email(self, test=False):
-        import operator
-        commits = sorted(self.payload['commits'],
-                         key=operator.itemgetter('revision'))
-        for commit in commits:
-            self.send_diff_for_commit(commit, test)
-
-    def send_diff_for_commit(self, commit, test=False):
-        hgid = commit['raw_node']
-        sender = commit['author'] + ' <commits-noreply at bitbucket.org>'
-        lines = commit['message'].splitlines()
-        line0 = lines and lines[0] or ''
-        reponame = self.payload['repository']['name']
-        # TODO: maybe include the modified paths in the subject line?
-        url = self.remote_repo + 'changeset/' + commit['node'] + '/'
-        template = TEMPLATE % {'url': url}
-        subject = '%s %s: %s' % (reponame, commit['branch'], line0)
-        body = self.hg('-R', self.local_repo, 'log', '-r', hgid,
-                 '--template', template)
-        diff = self.get_diff(hgid, commit['files'])
-        body = body+diff
-        self.send(sender, ADDRESS, subject, body, test)
-
-    def get_diff(self, hgid, files):
-        import re
-        binary = re.compile('^GIT binary patch$', re.MULTILINE)
-        files = [item['file'] for item in files]
-        lines = []
-        for filename in files:
-            out = self.hg('-R', self.local_repo, 'diff', '--git', '-c', hgid,
-                          self.local_repo.join(filename))
-            match = binary.search(out)
-            if match:
-                # it's a binary patch, omit the content
-                out = out[:match.end()]
-                out += u'\n[cut]'
-            lines.append(out)
-        return u'\n'.join(lines)
-
-
-if __name__ == '__main__':
-    import hook as hookfile
-    repopath = os.path.dirname(os.path.dirname(hookfile.__file__))
-    print 'Repository path:', repopath
-    test_payload = {u'repository': {u'absolute_url': '',
-                                    u'name': u'test',
-                                    u'owner': u'antocuni',
-                                    u'slug': u'test',
-                                    u'website': u''},
-                    u'user': u'antocuni'}
-
-    commits = [{u'author': u'arigo',
-                u'branch': u'default',
-                u'files': [],
-                u'message': u'Merge heads.',
-                u'node': u'00ae063c6b8c',
-                u'parents': [u'278760e9c560', u'29f1ff96548d'],
-                u'raw_author': u'Armin Rigo <arigo at tunes.org>',
-                u'raw_node': u'00ae063c6b8c13d873d92afc5485671f6a944077',
-                u'revision': 403,
-                u'size': 0,
-                u'timestamp': u'2011-01-09 13:07:24'},
-
-               {u'author': u'antocuni',
-                u'branch': u'default',
-                u'files': [{u'file': u'bitbucket_hook/hook.py', u'type': u'modified'}],
-                u'message': u"don't send newlines to irc",
-                u'node': u'e17583fbfa5c',
-                u'parents': [u'69e9eac01cf6'],
-                u'raw_author': u'Antonio Cuni <anto.cuni at gmail.com>',
-                u'raw_node': u'e17583fbfa5c5636b5375a5fc81f3d388ce1b76e',
-                u'revision': 399,
-                u'size': 19,
-                u'timestamp': u'2011-01-07 17:42:13'},
-
-               {u'author': u'antocuni',
-                u'branch': u'default',
-                u'files': [{u'file': u'.hgignore', u'type': u'added'}],
-                u'message': u'ignore irrelevant files',
-                u'node': u'5cbd6e289c04',
-                u'parents': [u'3a7c89443fc8'],
-                u'raw_author': u'Antonio Cuni <anto.cuni at gmail.com>',
-                u'raw_node': u'5cbd6e289c043c4dd9b6f55b5ec1c8d05711c6ad',
-                u'revision': 362,
-                u'size': 658,
-                u'timestamp': u'2010-11-04 16:34:31'},
-
-               {u'author': u'antocuni',
-                u'branch': u'default',
-                u'files': [{u'file': u'bitbucket_hook/hook.py', u'type': u'modified'},
-                           {u'file': u'bitbucket_hook/__init__.py', u'type': u'added'},
-                           {u'file': u'bitbucket_hook/test/__init__.py',
-                            u'type': u'added'},
-                           {u'file': u'bitbucket_hook/test/test_hook.py',
-                            u'type': u'added'}],
-                u'message': u'partially refactor the hook to be more testable, and write a test for the fix in 12cc0caf054d',
-                u'node': u'9c7bc068df88',
-                u'parents': [u'12cc0caf054d'],
-                u'raw_author': u'Antonio Cuni <anto.cuni at gmail.com>',
-                u'raw_node': u'9c7bc068df8850f4102c610d2bee3cdef67b30e6',
-                u'revision': 391,
-                u'size': 753,
-                u'timestamp': u'2010-12-19 14:45:44'}]
-
-
-    test_payload[u'commits']  = commits
-
-##    # To regenerate:
-##    try:
-##        from json import loads # 2.6
-##    except ImportError:
-##        from simplejson import loads
-##
-##    from urllib2 import urlopen
-##    url = ("https://api.bitbucket.org/1.0/repositories/pypy/buildbot/"
-##           "changesets/%s/")
-##
-##    # Representative changesets
-##    mergeheads = u'00ae063c6b8c'
-##    singlefilesub = u'e17583fbfa5c'
-##    root = u'5cbd6e289c04'
-##    multiadd = u'9c7bc068df88'
-##    test_nodes = mergeheads, singlefilesub, root, multiadd
-##
-##    commits = []
-##    for commit in test_nodes:
-##        req = urlopen(url % commit)
-##        payload = req.read()
-##        req.close()
-##        commits.append(loads(payload))
-##
-##    test_payload['commits'] = commits
-
-    LOCAL_REPOS = py.path.local(repopath)
-
-    hook = BitbucketHookHandler()
-    hook.USE_COLOR_CODES = False
-    hook.handle(test_payload, test=True)
+def handle(payload, test=False):
+    path = payload['repository']['absolute_url']
+    owner = payload['repository']['owner']
+    local_repo = app.config['LOCAL_REPOS'].join(path)
+    remote_repo = app.config['REMOTE_BASE'] + path
+    if not check_for_local_repo(local_repo, remote_repo, owner):
+        print >> sys.stderr, 'Ignoring unknown repo', path
+        return
+    scm.hg('pull', '-R', local_repo)
+    for commit in get_commits(payload):
+        for handler in HANDLERS:
+            handler(payload, commit, test)

diff --git a/bitbucket_hook/test/test_hook.py b/bitbucket_hook/test/test_hook.py
--- a/bitbucket_hook/test/test_hook.py
+++ b/bitbucket_hook/test/test_hook.py
@@ -1,144 +1,31 @@
 # -*- encoding: utf-8 -*-
+import py
+import pytest
+from bitbucket_hook import hook, scm, mail, irc
 
-from bitbucket_hook.hook import BitbucketHookHandler, getpaths
+#XXX
+hook.app.config['USE_COLOR_CODES'] = False
 
-class BaseHandler(BitbucketHookHandler):
-
-    def __init__(self):
-        self.mails = []
-
-    def send(self, from_, to, subject, body, test=False):
-        self.mails.append((from_, to, subject, body))
-
-
-def test_non_ascii_encoding_guess_utf8():
-    class MyHandler(BaseHandler):
-        def _hgexe(self, argv):
-            return u'sp&#228;m'.encode('utf-8'), '', 0
-    #
-    handler = MyHandler()
-    stdout = handler.hg('foobar')
-    assert type(stdout) is unicode
-    assert stdout == u'sp&#228;m'
-
-def test_non_ascii_encoding_invalid_utf8():
-    class MyHandler(BaseHandler):
-        def _hgexe(self, argv):
-            return '\xe4aa', '', 0 # invalid utf-8 string
-    #
-    handler = MyHandler()
-    stdout = handler.hg('foobar')
-    assert type(stdout) is unicode
-    assert stdout == u'\ufffdaa'
 
 def test_sort_commits():
-    class MyHandler(BaseHandler):
-        def __init__(self):
-            self.sent_commits = []
-        def send_diff_for_commit(self, commit, test=False):
-            self.sent_commits.append(commit['node'])
     #
-    handler = MyHandler()
-    handler.payload = {
-        'commits': [{'revision': 43, 'node': 'second'},
-                    {'revision': 42, 'node': 'first'}]
-        }
-    handler.handle_diff_email()
-    assert handler.sent_commits == ['first', 'second']
+    seen_nodes = set()
+    payload = {
+        'commits': [
+            {'revision': 43, 'node': 'second', 'raw_node': 'first'},
+            {'revision': 42, 'node': 'first', 'raw_node': 'second'},
+        ],
+    }
+    commits = hook.get_commits(payload, seen_nodes)
+    commits = [x['node'] for x in commits]
 
-def test_getpaths():
-    d = dict
+    assert commits == ['first', 'second']
 
-    barefile = [d(file='file')]
-    distinct = [d(file='path1/file1'), d(file='path2/file2'),
-                d(file='path3/file')]
-    shared = [d(file='path/file1'), d(file='path/file2'),
-              d(file='path/file')]
 
-    deepfile = [d(file='a/long/path/to/deepfile.py')]
-    slashesfile = [d(file='/slashesfile/')]
-    slashleft = [d(file='/slashleft')]
-    slashright = [d(file='slashright/')]
+LONG_MESSAGE = u'This is a test with a long message: ' + 'x' * 1000
+LONG_CUT = LONG_MESSAGE[:160 - 29]
 
 
-    nocommon = distinct + [d(file='path4/file')]
-    nocommonplusroot = distinct + barefile
-
-    common = [d(file='some/path/to/file'), d(file='some/path/to/deeper/file'),
-              d(file='some/path/to/anotherfile'), d(file='some/path/to/afile')]
-    commonplusroot = shared + barefile
-
-    empty = d(file='')
-    nocommonplusempty = distinct + [empty]
-    commonplusempty = shared + [empty]
-    nocommonplusslash = distinct + [d(file='path4/dir/')]
-    commonplusslash = shared + [d(file='path/dir/')]
-
-    pypydoubleslash = [d(file='pypy/jit/metainterp/opt/u.py'),
-                       d(file='pypy/jit/metainterp/test/test_c.py'),
-                       d(file='pypy/jit/metainterp/test/test_o.py')]
-
-    pypyempty = [d(file='pypy/rlib/rdtoa.py'),
-                 d(file='pypy/rlib/test/test_rdtoa.py')]
-
-    nothing = ('', '')
-
-    # (input, expected output) for listfiles=False
-    files_expected = [([], nothing),
-                      ([empty], nothing),
-                      ([empty, empty], nothing),
-                      (barefile, ('file', '')),
-                      (deepfile, ('a/long/path/to/deepfile.py', '')),
-                      (slashesfile, ('/slashesfile/', '')),
-                      (slashleft, ('/slashleft', '')),
-                      (slashright, ('slashright/', '')),
-                      (nocommon, nothing),
-                      (nocommonplusroot, nothing),
-                      (nocommonplusempty, nothing),
-                      (common, ('some/path/to/', '')),
-                      (commonplusroot, nothing),
-                      (commonplusempty, nothing),
-                      (nocommonplusslash, nothing),
-                      (commonplusslash, ('path/', '')),
-                      (pypydoubleslash, ('pypy/jit/metainterp/', '')),
-                      (pypyempty, ('pypy/rlib/', '')),
-                      ]
-
-    for f, wanted in files_expected:
-        assert getpaths(f) == wanted
-
-    # (input, expected output) for listfiles=True
-    files_expected = [([], nothing),
-                      ([empty], nothing),
-                      ([empty, empty], nothing),
-                      (barefile, ('file', '')),
-                      (deepfile, ('a/long/path/to/deepfile.py', '')),
-                      (slashesfile, ('/slashesfile/', '')),
-                      (slashleft, ('/slashleft', '')),
-                      (slashright, ('slashright/', '')),
-                      (nocommon, ('', ' M(file1, file2, file, file)')),
-                      (nocommonplusroot, ('', ' M(file1, file2, file, file)')),
-                      (nocommonplusempty, ('',' M(file1, file2, file)')),
-                      (common, ('some/path/to/',
-                                ' M(file, file, anotherfile, afile)')),
-                      (commonplusroot, ('', ' M(file1, file2, file, file)')),
-                      (commonplusempty, ('',' M(file1, file2, file)')),
-                      (nocommonplusslash, ('',' M(file1, file2, file)')),
-                      (commonplusslash, ('path/',' M(file1, file2, file)')),
-                      (pypydoubleslash, ('pypy/jit/metainterp/',
-                                         ' M(u.py, test_c.py, test_o.py)')),
-                      (pypyempty, ('pypy/rlib/',
-                                   ' M(rdtoa.py, test_rdtoa.py)')),
-                      ]
-
-    for f, wanted in files_expected:
-        assert getpaths(f, listfiles=True) == wanted
-
-
-
-LONG_MESSAGE = u'This is a test with a long message: ' + 'x'*1000
-LONG_CUT = LONG_MESSAGE[:160-29]
-
 def irc_cases(payload=None):
 
     if payload is None:
@@ -154,12 +41,12 @@
                                   d(file='my/file3')]
     single_file_deep = [d(file='path/to/single')]
 
-    cases = [(no_file,  ''), # No diff
-             (single_file,'single'), # Single file
+    cases = [(no_file,  ''),  # No diff
+             (single_file, 'single'),  # Single file
              (multiple_files,   ''),  # No common prefix
-             (multiple_files_subdir, 'path/'), # Common prefix
-             (multiple_files_subdir_root, ''), # No common subdir, file in root
-             (single_file_deep,'path/to/single') # Single file in deep path
+             (multiple_files_subdir, 'path/'),  # Common prefix
+             (multiple_files_subdir_root, ''),  # No common subdir file in root
+             (single_file_deep, 'path/to/single'),  # Single file in deep path
             ]
 
     author = u'antocuni'
@@ -171,7 +58,7 @@
 
     for i, (case, snippet) in enumerate(cases):
         rev = 44 + i
-        node = chr(97+i) + 'xxyyy'
+        node = chr(97 + i) + 'xxyyy'
         raw_node = node * 2
         expected.append(expected_template % (node, snippet, LONG_CUT))
         commits.append(d(revision=rev, files=case, author=author,
@@ -181,51 +68,64 @@
     return payload, expected
 
 
-def test_irc_message():
-    class MyHandler(BaseHandler):
-        USE_COLOR_CODES = False
-        def __init__(self):
-            self.messages = []
-        def send_irc_message(self, message, test=False):
-            self.messages.append(message)
+def test_irc_message(monkeypatch, messages):
+    payload = {
+        'repository': {
+            'owner': 'pypy',
+            'name': 'pypy',
+            },
+        'commits': [
+            {
+                'revision': 42,
+                'branch': u'default',
+                'author': u'antocuni',
+                'message': u'this is a test',
+                'node': 'abcdef',
+                'raw_node': 'abcdef',
+            },
+            {
+                'revision': 43,
+                'author': u'antocuni',
+                'branch': u'mybranch',
+                'message': LONG_MESSAGE,
+                'node': 'xxxyyy',
+                'raw_node': 'xxxyyy',
+            },
+        ]
+    }
 
-    handler = MyHandler()
-    handler.payload = {
-        'commits': [{'revision': 42,
-                     'branch': u'default',
-                     'author': u'antocuni',
-                     'message': u'this is a test',
-                     'node': 'abcdef'
-                     },
-                    {'revision': 43,
-                     'author': u'antocuni',
-                     'branch': u'mybranch',
-                     'message': LONG_MESSAGE,
-                     'node': 'xxxyyy'
-                     }
-                    ]}
+    payload, expected = irc_cases(payload)
+    commits = payload['commits']
+    irc.handle_commit(payload, commits[0])
+    irc.handle_commit(payload, commits[1])
 
-    handler.payload, expected = irc_cases(handler.payload)
-    handler.handle_irc_message()
-
-    msg1, msg2 = handler.messages[:2]
+    msg1, msg2 = messages[:2]
 
     assert msg1 == 'antocuni default abcdef /: this is a test'
     x = 'antocuni mybranch xxxyyy /: %s...' % LONG_CUT
     assert msg2 == x
 
-    for got, wanted in zip(handler.messages[2:], expected):
+    for got, wanted in zip(messages[2:], expected):
         assert got == wanted
 
-def noop(*args, **kwargs): pass
+
+def noop(*args, **kwargs):
+    pass
+
+
 class mock:
     __init__ = noop
-    def communicate(*args, **kwargs): return '1', 2
-    def wait(*args, **kwargs): return 0
+
+    def communicate(*args, **kwargs):
+        return '1', 2
+
+    def wait(*args, **kwargs):
+        return 0
+
     sendmail = noop
 
-def test_handle():
-    handler = BitbucketHookHandler()
+
+def test_handle(monkeypatch):
     commits, _ = irc_cases()
     test_payload = {u'repository': {u'absolute_url': '',
                                     u'name': u'test',
@@ -235,14 +135,57 @@
                     u'user': u'antocuni',
                     'commits': commits['commits']}
 
-    handler.call_subprocess = noop
-    handler.Popen = mock
-    handler.SMTP = mock
+    monkeypatch.setattr(scm, 'Popen', mock)
+    monkeypatch.setattr(irc.subprocess, 'call', noop)
+    monkeypatch.setattr(mail, 'SMTP', mock)
 
-    handler.handle(test_payload)
-    handler.handle(test_payload, test=True)
+    hook.handle(test_payload)
+    hook.handle(test_payload, test=True)
 
-    handler.LISTFILES = True
-    handler.handle(test_payload)
-    handler.handle(test_payload, test=True)
+    hook.app.config['LISTFILES'] = True
+    hook.handle(test_payload)
+    hook.handle(test_payload, test=True)
 
+
+def test_handle_unknown(monkeypatch):
+    hgcalls = []
+    def hgraise(*args):
+        hgcalls.append(args)
+
+    monkeypatch.setattr(scm, 'hg', hgraise)
+    hook.handle({
+        u'repository': {
+            u'absolute_url': '/foobar/myrepo',
+            u'owner': 'foobar',
+        },
+    })
+    assert hgcalls == []
+
+    hook.handle({
+        u'repository': {
+            u'absolute_url': '/pypy/myrepo',
+            u'owner': 'pypy'
+        },
+        u'commits': [],
+    })
+    assert hgcalls[0][:2] == ('clone', 'http://bitbucket.org/pypy/myrepo',)
+    local_repo = hgcalls[0][-1]
+    assert hgcalls[1] == ('pull', '-R', local_repo)
+
+
+def test_ignore_duplicate_commits(monkeypatch, mails, messages):
+    commits, _ = irc_cases()
+    payload = {u'repository': {u'absolute_url': '',
+                               u'name': u'test',
+                               u'owner': u'antocuni',
+                               u'slug': u'test',
+                               u'website': u''},
+               u'user': u'antocuni',
+               'commits': commits['commits']}
+    seen_nodes = set()
+    commits_listed = list(hook.get_commits(payload, seen_nodes))
+    commits_again = list(hook.get_commits(payload, seen_nodes))
+    num_commits = len(commits['commits'])
+    assert len(commits_listed) == num_commits
+    assert not commits_again
+

diff --git a/bitbucket_hook/test/test_main.py b/bitbucket_hook/test/test_main.py
new file mode 100644
--- /dev/null
+++ b/bitbucket_hook/test/test_main.py
@@ -0,0 +1,32 @@
+from bitbucket_hook.main import app
+from bitbucket_hook import hook
+
+def test_get():
+    client = app.test_client()
+    response = client.get('/')
+
+
+def test_post(monkeypatch):
+    client = app.test_client()
+    def handle(payload, test):
+        assert payload=={}
+        assert test==app.config['TESTING']
+    monkeypatch.setattr(hook, 'handle', handle)
+
+    app.config['TESTING'] = True
+    response = client.post('/', data={'payload':"{}"})
+
+    app.config['TESTING'] = False
+    response = client.post('/', data={'payload':"{}"})
+
+    assert response.status_code == 200
+
+def test_post_error(monkeypatch):
+    client = app.test_client()
+    def handle(payload, test):
+        raise Exception('omg')
+    monkeypatch.setattr(hook, 'handle', handle)
+    response = client.post('/', data={'payload':"{}"})
+    assert response.status_code == 500
+
+

diff --git a/bitbucket_hook/test/conftest.py b/bitbucket_hook/test/conftest.py
new file mode 100644
--- /dev/null
+++ b/bitbucket_hook/test/conftest.py
@@ -0,0 +1,27 @@
+#XXX imports in conftest globals dont sow in coverage reports
+
+
+def pytest_funcarg__mails(request):
+    return []
+
+
+def pytest_funcarg__messages(request):
+    return []
+
+
+def pytest_funcarg__monkeypatch(request):
+    from bitbucket_hook import irc, mail
+    mp = request.getfuncargvalue('monkeypatch')
+    mails = request.getfuncargvalue('mails')
+
+    def send(from_, to, subject, body, test=False, mails=mails):
+        mails.append((from_, to, subject, body))
+    mp.setattr(mail, 'send', send)
+
+    messages = request.getfuncargvalue('messages')
+
+    def send_irc_message(message, test=False):
+        messages.append(message)
+    mp.setattr(irc, 'send_message', send_irc_message)
+
+    return mp

diff --git a/bitbucket_hook/test_hook_testcall.py b/bitbucket_hook/test_hook_testcall.py
new file mode 100644
--- /dev/null
+++ b/bitbucket_hook/test_hook_testcall.py
@@ -0,0 +1,109 @@
+import os
+import py
+
+
+def test_handlecall():
+    from bitbucket_hook.hook import handle
+    from bitbucket_hook.main import app
+    repopath = os.path.dirname(os.path.dirname(__file__))
+    print 'Repository path:', repopath
+    test_payload = {u'repository': {u'absolute_url': '',
+                                    u'name': u'test',
+                                    u'owner': u'antocuni',
+                                    u'slug': u'test',
+                                    u'website': u''},
+                    u'user': u'antocuni'}
+
+    commits = [{u'author': u'arigo',
+                u'branch': u'default',
+                u'files': [],
+                u'message': u'Merge heads.',
+                u'node': u'00ae063c6b8c',
+                u'parents': [u'278760e9c560', u'29f1ff96548d'],
+                u'raw_author': u'Armin Rigo <arigo at tunes.org>',
+                u'raw_node': u'00ae063c6b8c13d873d92afc5485671f6a944077',
+                u'revision': 403,
+                u'size': 0,
+                u'timestamp': u'2011-01-09 13:07:24'},
+
+               {u'author': u'antocuni',
+                u'branch': u'default',
+                u'files': [{u'file': u'bitbucket_hook/hook.py',
+                            u'type': u'modified'}],
+                u'message': u"don't send newlines to irc",
+                u'node': u'e17583fbfa5c',
+                u'parents': [u'69e9eac01cf6'],
+                u'raw_author': u'Antonio Cuni <anto.cuni at gmail.com>',
+                u'raw_node': u'e17583fbfa5c5636b5375a5fc81f3d388ce1b76e',
+                u'revision': 399,
+                u'size': 19,
+                u'timestamp': u'2011-01-07 17:42:13'},
+
+               {u'author': u'antocuni',
+                u'branch': u'default',
+                u'files': [{u'file': u'.hgignore', u'type': u'added'}],
+                u'message': u'ignore irrelevant files',
+                u'node': u'5cbd6e289c04',
+                u'parents': [u'3a7c89443fc8'],
+                u'raw_author': u'Antonio Cuni <anto.cuni at gmail.com>',
+                u'raw_node': u'5cbd6e289c043c4dd9b6f55b5ec1c8d05711c6ad',
+                u'revision': 362,
+                u'size': 658,
+                u'timestamp': u'2010-11-04 16:34:31'},
+
+               {u'author': u'antocuni',
+                u'branch': u'default',
+                u'files': [{u'file': u'bitbucket_hook/hook.py',
+                            u'type': u'modified'},
+                           {u'file': u'bitbucket_hook/__init__.py',
+                            u'type': u'added'},
+                           {u'file': u'bitbucket_hook/test/__init__.py',
+                            u'type': u'added'},
+                           {u'file': u'bitbucket_hook/test/test_hook.py',
+                            u'type': u'added'}],
+                u'message': u'partially refactor the hook to be more testable,'
+                            u' and write a test for the fix in 12cc0caf054d',
+                u'node': u'9c7bc068df88',
+                u'parents': [u'12cc0caf054d'],
+                u'raw_author': u'Antonio Cuni <anto.cuni at gmail.com>',
+                u'raw_node': u'9c7bc068df8850f4102c610d2bee3cdef67b30e6',
+                u'revision': 391,
+                u'size': 753,
+                u'timestamp': u'2010-12-19 14:45:44'}]
+
+    test_payload[u'commits'] = commits
+
+##    # To regenerate:
+##    try:
+##        from json import loads # 2.6
+##    except ImportError:
+##        from simplejson import loads
+##
+##    from urllib2 import urlopen
+##    url = ("https://api.bitbucket.org/1.0/repositories/pypy/buildbot/"
+##           "changesets/%s/")
+##
+##    # Representative changesets
+##    mergeheads = u'00ae063c6b8c'
+##    singlefilesub = u'e17583fbfa5c'
+##    root = u'5cbd6e289c04'
+##    multiadd = u'9c7bc068df88'
+##    test_nodes = mergeheads, singlefilesub, root, multiadd
+##
+##    commits = []
+##    for commit in test_nodes:
+##        req = urlopen(url % commit)
+##        payload = req.read()
+##        req.close()
+##        commits.append(loads(payload))
+##
+##    test_payload['commits'] = commits
+
+    app.config['LOCAL_REPOS'] = py.path.local(repopath)
+    app.config['USE_COLOR_CODES'] = False
+
+    handle(test_payload, test=True)
+
+
+if __name__ == '__main__':
+    test_handlecall()

diff --git a/bitbucket_hook/test/test_scm.py b/bitbucket_hook/test/test_scm.py
new file mode 100644
--- /dev/null
+++ b/bitbucket_hook/test/test_scm.py
@@ -0,0 +1,34 @@
+# -*- encoding: utf-8 -*-
+import py
+import pytest
+
+from bitbucket_hook import scm
+
+
+def test_non_ascii_encoding_guess_utf8(monkeypatch):
+
+    def _hgexe(argv):
+        return u'sp&#228;m'.encode('utf-8'), '', 0
+    monkeypatch.setattr(scm, '_hgexe', _hgexe)
+    stdout = scm.hg('foobar')
+    assert type(stdout) is unicode
+    assert stdout == u'sp&#228;m'
+
+
+def test_non_ascii_encoding_invalid_utf8(monkeypatch):
+
+    def _hgexe(argv):
+        return '\xe4aa', '', 0  # invalid utf-8 string
+    monkeypatch.setattr(scm, '_hgexe', _hgexe)
+    stdout = scm.hg('foobar')
+    assert type(stdout) is unicode
+    assert stdout == u'\ufffdaa'
+
+
+ at pytest.mark.skip_if("not py.path.local.sysfind('hg')",
+                     reason='hg binary missing')
+def test_hg():
+    scm.hg('help')
+    with pytest.raises(Exception):
+        print scm.hg
+        scm.hg('uhmwrong')

diff --git a/bitbucket_hook/run.py b/bitbucket_hook/run.py
new file mode 100755
--- /dev/null
+++ b/bitbucket_hook/run.py
@@ -0,0 +1,22 @@
+#!/usr/bin/python
+
+"""
+To start the server in production mode, run this command::
+
+    ./run.py deploy
+"""
+
+import py
+import sys
+import argparse
+main = py.path.local(__file__).dirpath().join('main.py').pyimport()
+
+
+if __name__ == '__main__':
+    HOST_NAME = 'codespeak.net'
+    PORT_NUMBER = 9237
+    main.app.run(
+        host = HOST_NAME if 'deploy' in sys.argv else 'localhost',
+        debug = 'debug' in sys.argv,
+        port=PORT_NUMBER)
+

diff --git a/bitbucket_hook/irc.py b/bitbucket_hook/irc.py
new file mode 100644
--- /dev/null
+++ b/bitbucket_hook/irc.py
@@ -0,0 +1,108 @@
+'''
+utilities for interacting with the irc bot (via cli)
+'''
+
+import os
+import subprocess
+
+def getpaths(files, listfiles=False):
+
+    # Handle empty input
+    if not files:
+        return '', ''
+    files = [f['file'] for f in files]
+    if not any(files):
+        return '', ''
+
+    dirname = os.path.dirname
+    basename = os.path.basename
+
+    common_prefix = [dirname(f) for f in files]
+
+    # Single file, show its full path
+    if len(files) == 1:
+        common_prefix = files[0]
+        listfiles = False
+
+    else:
+        common_prefix = [path.split(os.sep) for path in common_prefix]
+        common_prefix = os.sep.join(os.path.commonprefix(common_prefix))
+        if common_prefix and not common_prefix.endswith('/'):
+            common_prefix += '/'
+
+    if listfiles:
+        # XXX Maybe should return file paths relative to prefix? Or TMI?
+        filenames = [basename(f) for f in files if f and basename(f)]
+        filenames = ' M(%s)' % ', '.join(filenames)
+    else:
+        filenames = ''
+    return common_prefix, filenames
+
+
+def send_message(message, test=False):
+    if test:
+        print message + '\n'
+    else:
+        from .main import app
+        return subprocess.call([
+            app.config['BOT'],
+            app.config['CHANNEL'],
+            message,
+        ])
+
+def get_short_id(owner, repo, branch):
+    """
+    Custom rules to get a short string that identifies a repo/branch in a
+    useful way, for IRC messages.  Look at test_irc.test_get_short_id for what
+    we expect.
+    """
+    from .main import app
+    repo_parts = []
+    if owner != app.config['DEFAULT_USER']:
+        repo_parts.append('%s' % owner)
+    if repo_parts or repo != app.config['DEFAULT_REPO']:
+        repo_parts.append(repo)
+    repo_id = '/'.join(repo_parts)
+    #
+    if repo_id == '':
+        return branch
+    elif branch == 'default':
+        return repo_id
+    elif repo_id == branch:
+        return repo_id # e.g., pypy/extradoc has a branch extradoc, just return 'extradoc'
+    else:
+        return '%s[%s]' % (repo_id, branch)
+    return branch
+
+
+def handle_commit(payload, commit, test=False):
+    from .main import app
+
+    repo_owner = payload['repository']['owner']
+    repo_name = payload['repository']['name']
+    author = commit['author']
+    branch = commit['branch']
+    node = commit['node']
+    short_id = get_short_id(repo_owner, repo_name, branch)
+
+    files = commit.get('files', [])
+    common_prefix, filenames = getpaths(files, app.config['LISTFILES'])
+    pathlen = len(common_prefix) + len(filenames) + 2
+    common_prefix = '/' + common_prefix
+
+    if app.config['USE_COLOR_CODES']:
+        author = '\x0312%s\x0F' % author   # in blue
+        short_id = '\x02%s\x0F' % short_id   # in bold
+        node = '\x0311%s\x0F' % node     # in azure
+        common_prefix = '\x0315%s\x0F' % common_prefix  # in gray
+
+    message = commit['message'].replace('\n', ' ')
+    fields = (author, short_id, node, common_prefix, filenames)
+    part1 = '%s %s %s %s%s: ' % fields
+    totallen = 160 + pathlen
+    if len(message) + len(part1) <= totallen:
+        irc_msg = part1 + message
+    else:
+        maxlen = totallen - (len(part1) + 3)
+        irc_msg = part1 + message[:maxlen] + '...'
+    send_message(irc_msg, test)

diff --git a/bitbucket_hook/stdoutlog.py b/bitbucket_hook/stdoutlog.py
new file mode 100644
--- /dev/null
+++ b/bitbucket_hook/stdoutlog.py
@@ -0,0 +1,21 @@
+import time
+
+RED = 31
+GREEN = 32
+YELLOW = 33
+BLUE = 34
+MAGENTA = 35
+CYAN = 36
+GRAY = 37
+
+def color(s, fg=1, bg=1):
+    template = '\033[%02d;%02dm%s\033[0m'
+    return template % (bg, fg, s)
+
+def handle_commit(payload, commit, test=False):
+    author = commit['author']
+    node = commit['node']
+    timestamp = commit.get('timestamp')
+    curtime = time.strftime('[%Y-%m-%d %H:%M]')
+    log = '%s %s %s %s' % (curtime, node, timestamp, author)
+    print color(log, fg=GREEN)

diff --git a/bitbucket_hook/mail.py b/bitbucket_hook/mail.py
new file mode 100644
--- /dev/null
+++ b/bitbucket_hook/mail.py
@@ -0,0 +1,57 @@
+from . import scm
+from smtplib import SMTP
+
+
+TEMPLATE = u"""\
+Author: {author}
+Branch: {branches}
+Changeset: r{rev}:{node|short}
+Date: {date|isodate}
+%(url)s
+
+Log:\t{desc|fill68|tabindent}
+
+"""
+
+def handle_commit(payload, commit, test=False):
+    from .main import app
+
+    path = payload['repository']['absolute_url']
+    local_repo = app.config['LOCAL_REPOS'].join(path)
+    remote_repo = app.config['REMOTE_BASE'] + path
+
+    hgid = commit['raw_node']
+    sender = commit['author'] + ' <commits-noreply at bitbucket.org>'
+    lines = commit['message'].splitlines()
+    line0 = lines and lines[0] or ''
+    reponame = payload['repository']['name']
+    # TODO: maybe include the modified paths in the subject line?
+    url = remote_repo + 'changeset/' + commit['node'] + '/'
+    template = TEMPLATE % {'url': url}
+    subject = '%s %s: %s' % (reponame, commit['branch'], line0)
+    body = scm.hg('-R', local_repo, 'log', '-r', hgid,
+             '--template', template)
+    diff = scm.get_diff(local_repo, hgid, commit['files'])
+    body = body + diff
+    send(sender, app.config['ADDRESS'], subject, body, test)
+
+
+def send(from_, to, subject, body, test=False):
+    from .main import app
+    from email.mime.text import MIMEText
+    # Is this a valid workaround for unicode errors?
+    body = body.encode('ascii', 'xmlcharrefreplace')
+    msg = MIMEText(body, _charset='utf-8')
+    msg['From'] = from_
+    msg['To'] = to
+    msg['Subject'] = subject
+    if test:
+        print '#' * 20
+        print "Email contents:\n"
+        print from_
+        print to
+        print msg.get_payload(decode=True)
+    else:
+        smtp = SMTP(app.config['SMTP_SERVER'], app.config['SMTP_PORT'])
+        smtp.sendmail(from_, [to], msg.as_string())
+

diff --git a/bot2/pypybuildbot/builds.py b/bot2/pypybuildbot/builds.py
--- a/bot2/pypybuildbot/builds.py
+++ b/bot2/pypybuildbot/builds.py
@@ -202,7 +202,9 @@
                 logfiles={'pytestLog': 'cpython.log'}))
 
         if pypyjit:
-            # upload nightly build, if we're running jit tests
+            # kill this step when the transition to test_pypy_c_new has been
+            # completed
+            # "old" test_pypy_c
             self.addStep(PytestCmd(
                 description="pypyjit tests",
                 command=["python", "pypy/test_all.py",
@@ -210,6 +212,15 @@
                          "--resultlog=pypyjit.log",
                          "pypy/module/pypyjit/test"],
                 logfiles={'pytestLog': 'pypyjit.log'}))
+            #
+            # "new" test_pypy_c
+            self.addStep(PytestCmd(
+                description="pypyjit tests",
+                command=["pypy/translator/goal/pypy-c", "pypy/test_all.py",
+                         "--resultlog=pypyjit_new.log",
+                         "pypy/module/pypyjit/test_pypy_c"],
+                logfiles={'pytestLog': 'pypyjit_new.log'}))
+
         if pypyjit:
             kind = 'jit'
         else:

diff --git a/bitbucket_hook/test/test_irc.py b/bitbucket_hook/test/test_irc.py
new file mode 100644
--- /dev/null
+++ b/bitbucket_hook/test/test_irc.py
@@ -0,0 +1,103 @@
+from bitbucket_hook import irc
+import subprocess
+
+def fl(*paths):
+    return [{'file': x} for x in paths]
+
+
+def pytest_generate_tests(metafunc):
+
+    barefile = fl('file')
+    distinct = fl('path1/file1', 'path2/file2', 'path3/file')
+    shared = fl('path/file1', 'path/file2', 'path/file')
+
+    deepfile = fl('a/long/path/to/deepfile.py')
+    slashesfile = fl('/slashesfile/')
+    slashleft = fl('/slashleft')
+    slashright = fl('slashright/')
+
+    nocommon = distinct + fl('path4/file')
+    nocommonplusroot = distinct + barefile
+
+    common = fl('some/path/to/file', 'some/path/to/deeper/file',
+                'some/path/to/anotherfile', 'some/path/to/afile')
+    commonplusroot = shared + barefile
+
+    empty = fl('')
+    nocommonplusempty = distinct + empty
+    commonplusempty = shared + empty
+    nocommonplusslash = distinct + fl('path4/dir/')
+    commonplusslash = shared + fl('path/dir/')
+
+    pypydoubleslash = fl('pypy/jit/metainterp/opt/u.py',
+                         'pypy/jit/metainterp/test/test_c.py',
+                         'pypy/jit/metainterp/test/test_o.py')
+
+    pypyempty = fl('pypy/rlib/rdtoa.py', 'pypy/rlib/test/test_rdtoa.py')
+
+    nothing = ('', '')
+
+    expectations = [
+        ('null', [], nothing),
+        ('empty', empty, nothing),
+        ('empty*2', empty * 2, nothing),
+        ('bare', barefile, ('file', '')),
+        ('deep', deepfile, ('a/long/path/to/deepfile.py', '')),
+        ('slashes', slashesfile, ('/slashesfile/', '')),
+        ('slashleft', slashleft, ('/slashleft', '')),
+        ('slashright', slashright, ('slashright/', '')),
+        ('nocommon', nocommon, ('', ' M(file1, file2, file, file)')),
+        ('nocommon+root', nocommonplusroot,
+                          ('', ' M(file1, file2, file, file)')),
+        ('nocommon+empty', nocommonplusempty, ('', ' M(file1, file2, file)')),
+        ('common', common, ('some/path/to/',
+                ' M(file, file, anotherfile, afile)')),
+        ('common+root', commonplusroot, ('', ' M(file1, file2, file, file)')),
+        ('common+empty', commonplusempty, ('', ' M(file1, file2, file)')),
+        ('nocommon+slash', nocommonplusslash, ('', ' M(file1, file2, file)')),
+        ('common+slash', commonplusslash, ('path/', ' M(file1, file2, file)')),
+        ('pypydoubledash', pypydoubleslash, ('pypy/jit/metainterp/',
+                         ' M(u.py, test_c.py, test_o.py)')),
+        ('pypyempty', pypyempty, ('pypy/rlib/',
+                   ' M(rdtoa.py, test_rdtoa.py)')),
+        ]
+
+    if metafunc.function.__name__ == 'test_getpaths':
+        for name, files, (common, listfiles) in expectations:
+            metafunc.addcall(id='list/' + name, funcargs={
+                'files': files,
+                'expected_common': common,
+                'expected_listfiles': listfiles,
+            })
+            metafunc.addcall(id='nolist/' + name, funcargs={
+                'files': files,
+                'expected_common': common,
+                'expected_listfiles': listfiles,
+            })
+
+
+def test_getpaths(files, expected_common, expected_listfiles):
+    common, files = irc.getpaths(files, listfiles=bool(expected_listfiles))
+    assert common == expected_common
+    assert files == expected_listfiles
+
+def test_send_message(monkeypatch):
+    monkeypatch.undo()  # hack to get at the functions
+
+    # gets called in normal mode
+    monkeypatch.setattr(subprocess, 'call', lambda *k, **kw: None)
+    irc.send_message('test')
+
+    # doesnt get called in test mode
+    monkeypatch.setattr(subprocess, 'call', lambda: None)
+    irc.send_message('test', test=True)
+
+def test_get_short_id():
+    assert irc.get_short_id('pypy', 'pypy', 'default') == 'default'
+    assert irc.get_short_id('pypy', 'pypy', 'mybranch') == 'mybranch'
+    assert irc.get_short_id('pypy', 'buildbot', 'default') == 'buildbot'
+    assert irc.get_short_id('pypy', 'buildbot', 'mybranch') == 'buildbot[mybranch]'
+    assert irc.get_short_id('pypy', 'extradoc', 'extradoc') == 'extradoc'
+    #
+    assert irc.get_short_id('anto', 'pypy', 'default') == 'anto/pypy'
+    assert irc.get_short_id('anto', 'pypy', 'mybranch') == 'anto/pypy[mybranch]'

diff --git a/bitbucket_hook/scm.py b/bitbucket_hook/scm.py
new file mode 100644
--- /dev/null
+++ b/bitbucket_hook/scm.py
@@ -0,0 +1,36 @@
+import sys
+from subprocess import Popen, PIPE
+
+
+def _hgexe(argv):
+    proc = Popen(['hg'] + list(argv), stdout=PIPE, stderr=PIPE)
+    stdout, stderr = proc.communicate()
+    ret = proc.wait()
+    return stdout, stderr, ret
+
+
+def hg(*argv):
+    argv = map(str, argv)
+    stdout, stderr, ret = _hgexe(argv)
+    if ret != 0:
+        print >> sys.stderr, 'error: hg', ' '.join(argv)
+        print >> sys.stderr, stderr
+        raise Exception('error when executing hg')
+    return unicode(stdout, encoding='utf-8', errors='replace')
+
+
+def get_diff(local_repo, hgid, files):
+    import re
+    binary = re.compile('^GIT binary patch$', re.MULTILINE)
+    files = [item['file'] for item in files]
+    lines = []
+    for filename in files:
+        out = hg('-R', local_repo, 'diff', '--git', '-c', hgid,
+                      local_repo.join(filename))
+        match = binary.search(out)
+        if match:
+            # it's a binary patch, omit the content
+            out = out[:match.end()]
+            out += u'\n[cut]'
+        lines.append(out)
+    return u'\n'.join(lines)

diff --git a/.hgignore b/.hgignore
--- a/.hgignore
+++ b/.hgignore
@@ -2,6 +2,9 @@
 *.pyc
 *~
 
+# test coverage files
+.coverage
+
 # master/slaveinfo.py contains the passwords, so it should never be tracked
 master/slaveinfo.py
 

diff --git a/bot2/pypybuildbot/summary.py b/bot2/pypybuildbot/summary.py
--- a/bot2/pypybuildbot/summary.py
+++ b/bot2/pypybuildbot/summary.py
@@ -67,12 +67,18 @@
     def populate_one(self, name, shortrepr, longrepr=None):
         if shortrepr == '!':
             namekey = [name, '']
-        else:        
-            namekey = name.split(':', 1)
+        else:
+            # pytest2 and pytest1 use different separators/test id
+            # syntax support both here for now
+            if '.py::' in name:
+                namekey = name.split('::', 1)
+            else:
+                namekey = name.split(':', 1)
             if namekey[0].endswith('.py'):
                 namekey[0] = namekey[0][:-3].replace('/', '.')
             if len(namekey) == 1:
                 namekey.append('')
+            namekey[1] = namekey[1].replace("::", ".")
 
         namekey = tuple(namekey)
         self._outcomes[namekey] = shortrepr
@@ -106,7 +112,7 @@
         kind = None
         def add_one():
             if kind is not None:
-                self.populate_one(name, kind, ''.join(longrepr))        
+                self.populate_one(name, kind, ''.join(longrepr))
         for line in log.readlines():
             first = line[0]
             if first == ' ':
@@ -570,7 +576,7 @@
         mod, testname = self.get_namekey(request)
         if mod is None:
             return "no such test"
-        return "%s %s" % (mod, testname)        
+        return "%s %s" % (mod, testname)
 
     def body(self, request):
         t0 = time.time()
@@ -660,7 +666,7 @@
             request.site.buildbot_service.head_elements = old_head_elements
 
     def getTitle(self, request):
-        status = self.getStatus(request)        
+        status = self.getStatus(request)
         return "%s: summaries of last %d revisions" % (status.getProjectName(),
                                                        N)
 

diff --git a/bitbucket_hook/main.py b/bitbucket_hook/main.py
--- a/bitbucket_hook/main.py
+++ b/bitbucket_hook/main.py
@@ -7,57 +7,81 @@
 codespeak.
 """
 
-import time
-import BaseHTTPServer
 import json
-import cgi
 import traceback
 import pprint
 import sys
+import flask
+import py
 
-from hook import BitbucketHookHandler
 
-HOST_NAME = 'codespeak.net'
-PORT_NUMBER = 9237
+app = flask.Flask(__name__)
 
-class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
 
-    def do_GET(self):
-        """Respond to a GET request."""
-        self.send_response(200)
-        self.send_header("Content-type", "text/html")
-        self.end_headers()
-        self.wfile.write("""
-            <html>
-                <p>This is the pypy bitbucket hook. Use the following form only for testing</p>
-                <form method=post>
-                    payload: <input name=payload> <br>
-                    submit: <input type=submit>
-                </form>
-            </html>
-        """)
 
-    def do_POST(self):
-        length = int(self.headers['Content-Length'])
-        query_string = self.rfile.read(length)
-        data = dict(cgi.parse_qsl(query_string))
-        payload = json.loads(data['payload'])
-        handler = BitbucketHookHandler()
-        try:
-            handler.handle(payload)
-        except:
-            traceback.print_exc()
-            print >> sys.stderr, 'payload:'
-            pprint.pprint(payload, sys.stderr)
-            print >> sys.stderr
+ at app.route('/', methods=['GET'])
+def test_form():
+    """Respond to a GET request."""
+    return """
+        <html>
+            <p>
+                This is the pypy bitbucket hook.
+                Use the following form only for testing
+            </p>
+            <form method=post>
+                payload: <input name=payload> <br>
+                submit: <input type=submit>
+            </form>
+        </html>
+    """
 
-if __name__ == '__main__':
-    server_class = BaseHTTPServer.HTTPServer
-    httpd = server_class((HOST_NAME, PORT_NUMBER), MyHandler)
-    print time.asctime(), "Server Starts - %s:%s" % (HOST_NAME, PORT_NUMBER)
+
+ at app.route('/', methods=['POST'])
+def handle_payload():
+    payload = json.loads(flask.request.form['payload'])
     try:
-        httpd.serve_forever()
-    except KeyboardInterrupt:
-        pass
-    httpd.server_close()
-    print time.asctime(), "Server Stops - %s:%s" % (HOST_NAME, PORT_NUMBER)
+        from . import hook
+        hook.handle(payload, test=app.testing)
+    except:
+        traceback.print_exc()
+        print >> sys.stderr, 'payload:'
+        pprint.pprint(payload, sys.stderr)
+        print >> sys.stderr
+        raise
+    return 'ok'
+
+
+class DefaultConfig(object):
+    LOCAL_REPOS = py.path.local(__file__).dirpath('repos')
+    REMOTE_BASE = 'http://bitbucket.org'
+    USE_COLOR_CODES = True
+    LISTFILES = False
+    #
+    DEFAULT_USER = 'pypy'
+    DEFAULT_REPO = 'pypy'
+
+
+class CodeSpeakConfig(DefaultConfig):
+    SMTP_SERVER = 'localhost'
+    SMTP_PORT = 25
+    ADDRESS = 'pypy-svn at codespeak.net'
+    #
+    CHANNEL = '#pypy'
+    BOT = '/svn/hooks/commit-bot/message'
+
+
+class ViperConfig(DefaultConfig):
+    SMTP_SERVER = "out.alice.it"
+    SMTP_PORT = 25
+    ADDRESS = 'anto.cuni at gmail.com'
+    #
+    CHANNEL = '#test'
+    BOT = '/tmp/commit-bot/message'
+
+
+if py.std.socket.gethostname() == 'viper':
+    # for debugging, antocuni's settings
+    app.config.from_object(ViperConfig)
+else:
+    # real settings, (they works on codespeak at least)
+    app.config.from_object(CodeSpeakConfig)


More information about the Pypy-commit mailing list