| #!/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) |