#!/usr/bin/env python # # This is a CIA client script for Subversion repositories, written in python. # It generates commit messages using CIA's XML format, and can deliver them # using either XML-RPC or email. See below for usage and cuztomization # information. # # -------------------------------------------------------------------------- # # Copyright (c) 2004-2007, Micah Dowty # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # -------------------------------------------------------------------------- # # This script is cleaner and much more featureful than the shell # script version, but won't work on systems without Python. # # To use the CIA bot in your Subversion 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_svn.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_svn.py "$REPOS" "$REV" "ProjectName" & # ############# 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. # # NOTE: This shouldn't be a long description of your project. Ideally # it is a short identifier with no spaces, punctuation, or # unnecessary capitalization. This will be used in URLs related # to your project, as an internal identifier, and in IRC messages. # If you want a longer name shown for your project on the web # interface, please use the "title" metadata key rather than # putting that here. # project = "Mageia" # Subversion's normal directory hierarchy is powerful enough that # it doesn't have special methods of specifying modules, tags, or # branches like CVS does. Most projects do use a naming # convention though that works similarly to CVS's modules, tags, # and branches. # # This is a list of regular expressions that are tested against # paths in the order specified. If a regex matches, the 'branch' # and 'module' groups are stored and the matching section of the # path is removed. # # Several common directory structure styles are below as defaults. # Uncomment the ones you're using, or add your own regexes. # Whitespace in the each regex are ignored. pathRegexes = [ # r"^ trunk/ (?P[^/]+)/ ", # r"^ (branches|tags)/ (?P[^/]+)/ ", # r"^ (branches|tags)/ (?P[^/]+)/ (?P[^/]+)/ ", ] # If your repository is accessible over the web, put its base URL here # and 'uri' attributes will be given to all 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 # If your repository is accessible over the web via a tool like ViewVC # that allows viewing information about a full revision, put a format string # for its URL here. You can specify various substitution keys in the Python # syntax: "%(project)s" is replaced by the project name, and likewise # "%(revision)s" and "%(author)s" are replaced by the revision / author. # The resulting URI is added to the data sent to CIA. After this, in CIA's # online message viewer, the commit will link directly to the corresponding # revision page. revisionURI = None # Example (works for ViewVC as used by SourceForge.net): #revisionURI = "https://svn.sourceforge.net/viewcvs.cgi/%(project)s?view=rev&rev=%(revision)s" # 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, getopt class File: """A file in a Subversion repository. According to our current configuration, this may have a module, branch, and URI in addition to a path.""" # Map svn's status letters to our action names actionMap = { 'U': 'modify', 'A': 'add', 'D': 'remove', } def __init__(self, fullPath, status=None): self.fullPath = fullPath self.path = fullPath self.action = self.actionMap.get(status) 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) if self.action: attrs['action'] = self.action attrString = ''.join([' %s="%s"' % (key, escapeToXml(value,1)) for key, value in attrs.items()]) return "%s" % (attrString, escapeToXml(self.path)) class SvnClient: """A CIA client for Subversion repositories. Uses svnlook to gather information""" name = 'Python Subversion client for CIA' version = '1.20' 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 import xmlrpclib xmlrpclib.ServerProxy(server).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() self.deliver("" + self.makeGeneratorTag() + self.makeSourceTag() + self.makeBodyTag() + "") 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: # Recent Pythons don't need this, but Python 2.1 # at least can't convert other types directly # to Unicode. We have to take an intermediate step. if type(v) not in (type(''), type(u'')): v = str(v) s += "<%s>%s" % (name, escapeToXml(v), name) return s def makeGeneratorTag(self): return "%s" % self.makeAttrTags( 'name', 'version', ) def makeSourceTag(self): return "%s" % self.makeAttrTags( 'project', 'module', 'branch', ) def makeBodyTag(self): return "%s%s" % ( self.makeAttrTags( 'revision', 'author', 'log', 'diffLines', 'url', ), self.makeFileTags(), ) def makeFileTags(self): """Return XML tags for our file list""" return "%s" % ''.join([file.makeTag(self.config) for file in self.files]) def svnlook(self, command): """Run the given svnlook command on our current repository and revision, returning all output""" # We have to set LC_ALL to force svnlook to give us UTF-8 output, # then we explicitly slurp that into a unicode object. return unicode(os.popen( 'LC_ALL="en_US.UTF-8" svnlook %s -r "%s" "%s"' % (command, self.revision, self.repository)).read(), 'utf-8', 'replace') def collectData(self): self.author = self.svnlook('author').strip() self.project = self.config.project self.log = self.svnlook('log') self.diffLines = len(self.svnlook('diff').split('\n')) self.files = self.collectFiles() if self.config.revisionURI is not None: self.url = self.config.revisionURI % self.__dict__ else: self.url = None def collectFiles(self): # Extract all the files from the output of 'svnlook changed' files = [] for line in self.svnlook('changed').split('\n'): path = line[2:].strip() if path: status = line[0] files.append(File(path, status)) # Try each of our several regexes. To be applied, the same # regex must mach every file under consideration and they must # all return the same results. If we find one matching regex, # or we try all regexes without a match, we're done. matchDict = None for regex in self.config.pathRegexes: matchDict = matchAgainstFiles(regex, files) if matchDict is not None: self.__dict__.update(matchDict) break return files def matchAgainstFiles(regex, files): """Try matching a regex against all File objects in the provided list. If the regex returns the same matches for every file, the matches are returned in a dict and the matched portions are filtered out. If not, returns None. """ prevMatchDict = None compiled = re.compile(regex, re.VERBOSE) for f in files: match = compiled.match(f.fullPath) if not match: # Give up, it must match every file return None matchDict = match.groupdict() if prevMatchDict is not None and prevMatchDict != matchDict: # Give up, we got conflicting matches return None prevMatchDict = matchDict # If we got this far, the regex matched every file with # the same results. Now filter the matched portion out of # each file and store the matches we found. for f in files: f.path = compiled.sub('', f.fullPath) return prevMatchDict def escapeToXml(text, isAttrib=0): text = unicode(text) text = text.replace("&", "&") text = text.replace("<", "<") text = text.replace(">", ">") if isAttrib == 1: text = text.replace("'", "'") text = text.replace("\"", """) return text def usage(): """Print a short usage description of this script and exit""" sys.stderr.write("Usage: %s [OPTIONS] REPOS-PATH REVISION [PROJECTNAME]\n" % sys.argv[0]) def version(): """Print out the version of this script""" sys.stderr.write("%s %s\n" % (sys.argv[0], SvnClient.version)) def main(): try: options = [ "version" ] for key in config.__dict__: if not key.startswith("_"): options.append(key + "="); opts, args = getopt.getopt(sys.argv[1:], "", options) except getopt.GetoptError: usage() sys.exit(2) for o, a in opts: if o == "--version": version() sys.exit() else: # Everything else maps straight to a config key. Just have # to remove the "--" prefix from the option name. config.__dict__[o[2:]] = a # Print a usage message when not enough parameters are provided. if not len(args) in (2,3): sys.stderr.write("%s: incorrect number of arguments\n" % sys.argv[0]) usage(); sys.exit(2); # If a project name was provided, override the default project name. if len(args) == 3: config.project = args[2] # Go do the real work. SvnClient(args[0], args[1], config).main() if __name__ == "__main__": main() ### The End ###