diff options
Diffstat (limited to 'buildbot/contrib/svn_buildbot.py')
-rwxr-xr-x | buildbot/contrib/svn_buildbot.py | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/buildbot/contrib/svn_buildbot.py b/buildbot/contrib/svn_buildbot.py new file mode 100755 index 0000000..5a671dc --- /dev/null +++ b/buildbot/contrib/svn_buildbot.py @@ -0,0 +1,260 @@ +#!/usr/bin/python + +# this requires python >=2.3 for the 'sets' module. + +# The sets.py from python-2.3 appears to work fine under python2.2 . To +# install this script on a host with only python2.2, copy +# /usr/lib/python2.3/sets.py from a newer python into somewhere on your +# PYTHONPATH, then edit the #! line above to invoke python2.2 + +# python2.1 is right out + +# If you run this program as part of your SVN post-commit hooks, it will +# deliver Change notices to a buildmaster that is running a PBChangeSource +# instance. + +# edit your svn-repository/hooks/post-commit file, and add lines that look +# like this: + +''' +# set up PYTHONPATH to contain Twisted/buildbot perhaps, if not already +# installed site-wide +. ~/.environment + +/path/to/svn_buildbot.py --repository "$REPOS" --revision "$REV" \ +--bbserver localhost --bbport 9989 +''' + +import commands +import sys +import os +import re +import sets + +# We have hackish "-d" handling here rather than in the Options +# subclass below because a common error will be to not have twisted in +# PYTHONPATH; we want to be able to print that error to the log if +# debug mode is on, so we set it up before the imports. + +DEBUG = None + +if '-d' in sys.argv: + i = sys.argv.index('-d') + DEBUG = sys.argv[i+1] + del sys.argv[i] + del sys.argv[i] + +if DEBUG: + f = open(DEBUG, 'a') + sys.stderr = f + sys.stdout = f + + +from twisted.internet import defer, reactor +from twisted.python import usage +from twisted.spread import pb +from twisted.cred import credentials + + +class Options(usage.Options): + optParameters = [ + ['repository', 'r', None, + "The repository that was changed."], + ['revision', 'v', None, + "The revision that we want to examine (default: latest)"], + ['bbserver', 's', 'localhost', + "The hostname of the server that buildbot is running on"], + ['bbport', 'p', 8007, + "The port that buildbot is listening on"], + ['include', 'f', None, + '''\ +Search the list of changed files for this regular expression, and if there is +at least one match notify buildbot; otherwise buildbot will not do a build. +You may provide more than one -f argument to try multiple +patterns. If no filter is given, buildbot will always be notified.'''], + ['filter', 'f', None, "Same as --include. (Deprecated)"], + ['exclude', 'F', None, + '''\ +The inverse of --filter. Changed files matching this expression will never +be considered for a build. +You may provide more than one -F argument to try multiple +patterns. Excludes override includes, that is, patterns that match both an +include and an exclude will be excluded.'''], + ] + optFlags = [ + ['dryrun', 'n', "Do not actually send changes"], + ] + + def __init__(self): + usage.Options.__init__(self) + self._includes = [] + self._excludes = [] + self['includes'] = None + self['excludes'] = None + + def opt_include(self, arg): + self._includes.append('.*%s.*' % (arg, )) + + opt_filter = opt_include + + def opt_exclude(self, arg): + self._excludes.append('.*%s.*' % (arg, )) + + def postOptions(self): + if self['repository'] is None: + raise usage.error("You must pass --repository") + if self._includes: + self['includes'] = '(%s)' % ('|'.join(self._includes), ) + if self._excludes: + self['excludes'] = '(%s)' % ('|'.join(self._excludes), ) + + +def split_file_dummy(changed_file): + """Split the repository-relative filename into a tuple of (branchname, + branch_relative_filename). If you have no branches, this should just + return (None, changed_file). + """ + return (None, changed_file) + + +# this version handles repository layouts that look like: +# trunk/files.. -> trunk +# branches/branch1/files.. -> branches/branch1 +# branches/branch2/files.. -> branches/branch2 +# + + +def split_file_branches(changed_file): + pieces = changed_file.split(os.sep) + if pieces[0] == 'branches': + return (os.path.join(*pieces[:2]), + os.path.join(*pieces[2:])) + if pieces[0] == 'trunk': + return (pieces[0], os.path.join(*pieces[1:])) + ## there are other sibilings of 'trunk' and 'branches'. Pretend they are + ## all just funny-named branches, and let the Schedulers ignore them. + #return (pieces[0], os.path.join(*pieces[1:])) + + raise RuntimeError("cannot determine branch for '%s'" % changed_file) + + +split_file = split_file_dummy + + +class ChangeSender: + + def getChanges(self, opts): + """Generate and stash a list of Change dictionaries, ready to be sent + to the buildmaster's PBChangeSource.""" + + # first we extract information about the files that were changed + repo = opts['repository'] + print "Repo:", repo + rev_arg = '' + if opts['revision']: + rev_arg = '-r %s' % (opts['revision'], ) + changed = commands.getoutput('svnlook changed %s "%s"' % ( + rev_arg, repo)).split('\n') + # the first 4 columns can contain status information + changed = [x[4:] for x in changed] + + message = commands.getoutput('svnlook log %s "%s"' % (rev_arg, repo)) + who = commands.getoutput('svnlook author %s "%s"' % (rev_arg, repo)) + revision = opts.get('revision') + if revision is not None: + revision = int(revision) + + # see if we even need to notify buildbot by looking at filters first + changestring = '\n'.join(changed) + fltpat = opts['includes'] + if fltpat: + included = sets.Set(re.findall(fltpat, changestring)) + else: + included = sets.Set(changed) + + expat = opts['excludes'] + if expat: + excluded = sets.Set(re.findall(expat, changestring)) + else: + excluded = sets.Set([]) + if len(included.difference(excluded)) == 0: + print changestring + print """\ + Buildbot was not interested, no changes matched any of these filters:\n %s + or all the changes matched these exclusions:\n %s\ + """ % (fltpat, expat) + sys.exit(0) + + # now see which branches are involved + files_per_branch = {} + for f in changed: + branch, filename = split_file(f) + if branch in files_per_branch.keys(): + files_per_branch[branch].append(filename) + else: + files_per_branch[branch] = [filename] + + # now create the Change dictionaries + changes = [] + for branch in files_per_branch.keys(): + d = {'who': who, + 'branch': branch, + 'files': files_per_branch[branch], + 'comments': message, + 'revision': revision} + changes.append(d) + + return changes + + def sendChanges(self, opts, changes): + pbcf = pb.PBClientFactory() + reactor.connectTCP(opts['bbserver'], int(opts['bbport']), pbcf) + d = pbcf.login(credentials.UsernamePassword('change', 'changepw')) + d.addCallback(self.sendAllChanges, changes) + return d + + def sendAllChanges(self, remote, changes): + dl = [remote.callRemote('addChange', change) + for change in changes] + return defer.DeferredList(dl) + + def run(self): + opts = Options() + try: + opts.parseOptions() + except usage.error, ue: + print opts + print "%s: %s" % (sys.argv[0], ue) + sys.exit() + + changes = self.getChanges(opts) + if opts['dryrun']: + for i, c in enumerate(changes): + print "CHANGE #%d" % (i+1) + keys = c.keys() + keys.sort() + for k in keys: + print "[%10s]: %s" % (k, c[k]) + print "*NOT* sending any changes" + return + + d = self.sendChanges(opts, changes) + + def quit(*why): + print "quitting! because", why + reactor.stop() + + def failed(f): + print "FAILURE" + print f + reactor.stop() + + d.addCallback(quit, "SUCCESS") + d.addErrback(failed) + reactor.callLater(60, quit, "TIMEOUT") + reactor.run() + + +if __name__ == '__main__': + s = ChangeSender() + s.run() |