blob: 505b59a7556981c2cb605f2f6c1e2c8345331105 [file] [log] [blame]
#!/usr/bin/python
# This is a program which will poll a (remote) SVN repository, looking for
# new revisions. It then uses the 'buildbot sendchange' command to deliver
# information about the Change to a (remote) buildmaster. It can be run from
# a cron job on a periodic basis, or can be told (with the 'watch' option) to
# automatically repeat its check every 10 minutes.
# This script does not store any state information, so to avoid spurious
# changes you must use the 'watch' option and let it run forever.
# You will need to provide it with the location of the buildmaster's
# PBChangeSource port (in the form hostname:portnum), and the svnurl of the
# repository to watch.
# 15.03.06 by John Pye
# 29.03.06 by Niklaus Giger, added support to run under windows,
# added invocation option
# 22.03.10 by Johnnie Pittman, added support for category and interval
# options.
import subprocess
import xml.dom.minidom
from xml.parsers.expat import ExpatError
import sys
import time
from optparse import OptionParser
import os
if sys.platform == 'win32':
import win32pipe
def getoutput(cmd):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
return p.stdout.read()
def sendchange_cmd(master, revisionData):
cmd = [
"buildbot",
"sendchange",
"--master=%s" % master,
"--revision=%s" % revisionData['revision'],
"--username=%s" % revisionData['author'],
"--comments=%s" % revisionData['comments'],
]
if opts.category:
cmd.append("--category=%s" % opts.category)
for path in revisionData['paths']:
cmd.append(path)
if opts.verbose == True:
print cmd
return cmd
def parseChangeXML(raw_xml):
"""Parse the raw xml and return a dict with key pairs set.
Commmand we're parsing:
svn log --non-interactive --xml --verbose --limit=1 <repo url>
With an output that looks like this:
<?xml version="1.0"?>
<log>
<logentry revision="757">
<author>mwiggins</author>
<date>2009-11-11T17:16:48.012357Z</date>
<paths>
<path kind="" copyfrom-path="/trunk" copyfrom-rev="756" action="A">/tags/Latest</path>
</paths>
<msg>Updates/latest</msg>
</logentry>
</log>
"""
data = dict()
# parse the xml string and grab the first log entry.
try:
doc = xml.dom.minidom.parseString(raw_xml)
except ExpatError:
print "\nError: Got an empty response with an empty changeset.\n"
raise
log_entry = doc.getElementsByTagName("logentry")[0]
# grab the appropriate meta data we need
data['revision'] = log_entry.getAttribute("revision")
data['author'] = "".join([t.data for t in
log_entry.getElementsByTagName("author")[0].childNodes])
data['comments'] = "".join([t.data for t in
log_entry.getElementsByTagName("msg")[0].childNodes])
# grab the appropriate file paths that changed.
pathlist = log_entry.getElementsByTagName("paths")[0]
paths = []
for path in pathlist.getElementsByTagName("path"):
paths.append("".join([t.data for t in path.childNodes]))
data['paths'] = paths
return data
def checkChanges(repo, master, oldRevision=-1):
cmd = ["svn", "log", "--non-interactive", "--xml", "--verbose",
"--limit=1", repo]
if opts.verbose == True:
print "Getting last revision of repository: " + repo
if sys.platform == 'win32':
f = win32pipe.popen(cmd)
xml1 = ''.join(f.readlines())
f.close()
else:
xml1 = getoutput(cmd)
if opts.verbose == True:
print "XML\n-----------\n"+xml1+"\n\n"
revisionData = parseChangeXML(xml1)
if opts.verbose == True:
print "PATHS"
print revisionData['paths']
if revisionData['revision'] != oldRevision:
cmd = sendchange_cmd(master, revisionData)
if sys.platform == 'win32':
f = win32pipe.popen(cmd)
pretty_time = time.strftime("%H.%M.%S ")
print "%s Revision %s: %s" % (pretty_time, revisionData['revision'],
''.join(f.readlines()))
f.close()
else:
xml1 = getoutput(cmd)
else:
pretty_time = time.strftime("%H.%M.%S ")
print "%s nothing has changed since revision %s" % (pretty_time,
revisionData['revision'])
return revisionData['revision']
def build_parser():
usagestr = "%prog [options] <repo url> <buildbot master:port>"
parser = OptionParser(usage=usagestr)
parser.add_option(
"-c", "--category", dest="category", action="store", default="",
help="""Store a category name to be associated with sendchange msg."""
)
parser.add_option(
"-i", "--interval", dest="interval", action="store", default=0,
help="Implies watch option and changes the time in minutes to the value specified.",
)
parser.add_option(
"-v", "--verbose", dest="verbose", action="store_true", default=False,
help="Enables more information to be presented on the command line.",
)
parser.add_option(
"", "--watch", dest="watch", action="store_true", default=False,
help="Automatically check the repo url every 10 minutes.",
)
return parser
def validate_args(args):
"""Validate our arguments and exit if we don't have what we want."""
if not args:
print "\nError: No arguments were specified.\n"
parser.print_help()
sys.exit(1)
elif len(args) > 2:
print "\nToo many arguments specified.\n"
parser.print_help()
sys.exit(2)
if __name__ == '__main__':
# build our parser and validate our args
parser = build_parser()
(opts, args) = parser.parse_args()
validate_args(args)
if opts.interval:
try:
int(opts.interval)
except ValueError:
print "\nError: Value of the interval option must be a number."
parser.print_help()
sys.exit(3)
# grab what we need
repo_url = args[0]
bbmaster = args[1]
# if watch is specified, run until stopped
if opts.watch or opts.interval:
oldRevision = -1
print "Watching for changes in repo %s for master %s." % (repo_url, bbmaster)
while 1:
try:
oldRevision = checkChanges(repo_url, bbmaster, oldRevision)
except ExpatError:
# had an empty changeset. Trapping the exception and moving on.
pass
try:
if opts.interval:
# Check the repository every interval in minutes the user specified.
time.sleep(int(opts.interval) * 60)
else:
# Check the repository every 10 minutes
time.sleep(10*60)
except KeyboardInterrupt:
print "\nReceived interrupt via keyboard. Shutting Down."
sys.exit(0)
# default action if watch isn't specified
checkChanges(repo_url, bbmaster)