diff options
-rw-r--r-- | BitKeeper/triggers/ciabot_bk.py | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/BitKeeper/triggers/ciabot_bk.py b/BitKeeper/triggers/ciabot_bk.py index e69de29bb2..fe055b41b4 100644 --- a/BitKeeper/triggers/ciabot_bk.py +++ b/BitKeeper/triggers/ciabot_bk.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# CIA bot client script for Bitkeeper repositories, written in python. +# This generates commit messages using CIA's XML commit format, and can +# deliver them using either XML-RPC or email. +# +# -- Micah Dowty <micah@navi.cx> +# +# This script is cleaner, more featureful, and faster than the shell +# script version, but won't work on systems without Python or that don't +# allow outgoing HTTP connections. +# +# To use the CIA bot in your Bitkeeper repository... +# +# 1. Customize the parameters below +# +# 2. This script should be called from your repository's post-commit +# hook with the repository and revision as arguments. For example, +# you could copy this script into your repository's "hooks" directory +# and add something like the following to the "post-commit" script, +# also in the repository's "hooks" directory: +# +# REPOS="$1" +# REV="$2" +# $REPOS/hooks/ciabot_bk.py "$REPOS" "$REV" & +# +# Or, if you have multiple project hosted, you can add each +# project's name to the commandline in that project's post-commit +# hook: +# +# $REPOS/hooks/ciabot_bk.py "$REPOS" "$REV" "My Project" & +# +############# There are some parameters for this script that you can customize: + +class config: + # Replace this with your project's name, or always provide a + # project name on the commandline. + project = "openembedded" + + # If your repository is accessable over the web, put its base URL here + # and 'uri' attributes will be given to all <file> elements. This means + # that in CIA's online message viewer, each file in the tree will link + # directly to the file in your repository + repositoryURI = None + + # This can be the http:// URI of the CIA server to deliver commits over + # XML-RPC, or it can be an email address to deliver using SMTP. The + # default here should work for most people. If you need to use e-mail + # instead, you can replace this with "cia@cia.navi.cx" + server = "http://cia.navi.cx" + + # The SMTP server to use, only used if the CIA server above is an + # email address + smtpServer = "localhost" + + # The 'from' address to use. If you're delivering commits via email, set + # this to the address you would normally send email from on this host. + fromAddress = "cia-user@localhost" + + # When nonzero, print the message to stdout instead of delivering it to CIA + debug = 0 + + +############# Normally the rest of this won't need modification + +import sys, os, re, urllib, xmlrpclib + +class UrllibTransport(xmlrpclib.Transport): + '''Handles an HTTP transaction to an XML-RPC server via urllib + (urllib includes proxy-server support) + jjk 07/02/99''' + + def __init__(self): + self.verbose = 0 + + def request(self, host, handler, request_body, verbose=0): + '''issue XML-RPC request + jjk 07/02/99''' + import urllib + urlopener = urllib.FancyURLopener() + urlopener.addheaders = [('User-agent', self.user_agent)] + # probably should use appropriate 'join' methods instead of 'http://'+host+handler + f = urlopener.open('http://'+host+handler, request_body) + return(self.parse_response(f)) + +class File: + """A file in a Bitkeeper repository. According to our current + configuration, this may have a module, branch, and URI in addition + to a path.""" + + def __init__(self, fullPath): + self.fullPath = fullPath + self.path = fullPath + + def getURI(self, repo): + """Get the URI of this file, given the repository's URI. This + encodes the full path and joins it to the given URI.""" + quotedPath = urllib.quote(self.fullPath) + if quotedPath[0] == '/': + quotedPath = quotedPath[1:] + if repo[-1] != '/': + repo = repo + '/' + return repo + quotedPath + + def makeTag(self, config): + """Return an XML tag for this file, using the given config""" + attrs = {} + + if config.repositoryURI is not None: + attrs['uri'] = self.getURI(config.repositoryURI) + + attrString = ''.join([' %s="%s"' % (key, escapeToXml(value,1)) + for key, value in attrs.iteritems()]) + return "<file%s>%s</file>" % (attrString, escapeToXml(self.path)) + + +class CIAClient: + """Base CIA client class""" + name = 'Python client for CIA' + version = '1.0' + + def __init__(self, repository, revision, config): + self.repository = repository + self.revision = revision + self.config = config + + def deliver(self, message): + if config.debug: + print message + else: + server = self.config.server + if server.startswith('http:') or server.startswith('https:'): + # Deliver over XML-RPC + proxy = os.environ.get('http_proxy') + if proxy: + os.environ['HTTP_PROXY'] = proxy + s = xmlrpclib.ServerProxy(server, UrllibTransport()) + else: + s = xmlrpclib.ServerProxy(server) + s.hub.deliver(message) + else: + # Deliver over email + import smtplib + smtp = smtplib.SMTP(self.config.smtpServer) + smtp.sendmail(self.config.fromAddress, server, + "From: %s\r\nTo: %s\r\n" + "Subject: DeliverXML\r\n\r\n%s" % + (self.config.fromAddress, server, message)) + + def main(self): + self.collectData() + import socket + try: + self.deliver("<message>" + + self.makeGeneratorTag() + + self.makeSourceTag() + + self.makeBodyTag() + + "</message>") + return 0 + except socket.error, e: + print "ERROR: socket: %s" % e + return 1 + + def makeAttrTags(self, *names): + """Given zero or more attribute names, generate XML elements for + those attributes only if they exist and are non-None. + """ + s = '' + for name in names: + if hasattr(self, name): + v = getattr(self, name) + if v is not None: + s += "<%s>%s</%s>" % (name, escapeToXml(str(v)), name) + return s + + def makeGeneratorTag(self): + return "<generator>%s</generator>" % self.makeAttrTags( + 'name', + 'version', + ) + + def makeSourceTag(self): + self.project = self.config.project + return "<source>%s</source>" % self.makeAttrTags( + 'project', + 'module', + 'branch', + ) + + def makeBodyTag(self): + return "<body><commit>%s%s</commit></body>" % ( + self.makeAttrTags( + 'revision', + 'author', + 'log', + 'diffLines', + ), + self.makeFileTags(), + ) + + def makeFileTags(self): + """Return XML tags for our file list""" + return "<files>%s</files>" % ''.join([file.makeTag(self.config) + for file in self.files]) + + def collectData(self): + raise NotImplementedError("collectData method not implemented in the base CIA client class.") + +def escapeToXml(text, isAttrib=0): + text = text.replace("&", "&") + text = text.replace("<", "<") + text = text.replace(">", ">") + if isAttrib == 1: + text = text.replace("'", "'") + text = text.replace("\"", """) + return text + +class BKClient(CIAClient): + """A CIA client for Bitkeeper repositories.""" + name = 'Python Bitkeeper client for CIA' + version = '1.0' + + def __init__(self, repository, revision, config): + CIAClient.__init__(self, repository, revision, config) + os.chdir(self.repository) + + def bkchanges(self, command): + """Run the given bkchanges command on our current repository and + revision, returning all output""" + return os.popen('bk changes %s -r"%s"' % \ + (command, self.revision)).read() + + def collectData(self): + self.author = self.bkchanges('-d\':P:\'').strip() + self.log = self.bkchanges('-d\'$if(:C:){$each(:C:){:C: \\\\n}}\'').strip() + self.diffLines = len(os.popen('bk export -tpatch -r"%s"|grep -v \'^#\'' % self.revision).read().split('\n')) + self.files = self.collectFiles() + self.module = os.path.basename(os.environ.get('BKD_ROOT') or '') + self.branch = self.bkchanges('-d\':TAG:\'') + + def collectFiles(self): + # Extract all the files from the output of 'bkchanges changed' + files = [] + for line in self.bkchanges('-n -v -d\'$unless(:GFILE:=ChangeSet){:GFILE:}\'').strip().split('\n'): + files.append(File(line)) + return files + + +if __name__ == "__main__": + # Print a usage message when not enough parameters are provided. + if len(sys.argv) < 3: + sys.stderr.write("USAGE: %s REPOS-PATH REVISION [PROJECTNAME]\n" % + sys.argv[0]) + sys.exit(1) + + # If a project name was provided, override the default project name. + if len(sys.argv) > 3: + config.project = sys.argv[3] + + # Go do the real work. + BKClient(sys.argv[1], sys.argv[2], config).main() |