From 297d906213901919950052d91375e5de046efed7 Mon Sep 17 00:00:00 2001 From: Nicolas Vigier Date: Fri, 7 Jan 2011 17:43:33 +0000 Subject: add cia bot svn hook --- modules/subversion/manifests/init.pp | 12 + modules/subversion/templates/ciabot_svn.py | 398 +++++++++++++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 modules/subversion/templates/ciabot_svn.py (limited to 'modules') diff --git a/modules/subversion/manifests/init.pp b/modules/subversion/manifests/init.pp index 815e5103..f6b5db1c 100644 --- a/modules/subversion/manifests/init.pp +++ b/modules/subversion/manifests/init.pp @@ -123,6 +123,7 @@ class subversion { define repository ($group = "svn", $public = true, $commit_mail = '', + $cia_post = true, $syntax_check = '', $extract_dir = '') { # check permissions @@ -177,6 +178,17 @@ class subversion { } } + if $cia_post { + file { "$name/hooks/post-commit.d/cia.vc": + ensure => present, + owner => root, + group => root, + mode => 755, + content => template("subversion/ciabot_svn.py"), + } + + } + if $extract_dir { file { "$name/hooks/post-commit.d/extract_dir": ensure => present, diff --git a/modules/subversion/templates/ciabot_svn.py b/modules/subversion/templates/ciabot_svn.py new file mode 100644 index 00000000..ab7828b4 --- /dev/null +++ b/modules/subversion/templates/ciabot_svn.py @@ -0,0 +1,398 @@ +#!/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 ### -- cgit v1.2.1