Index: third_party/buildbot_7_12/buildbot/changes/svnpoller.py |
diff --git a/third_party/buildbot_7_12/buildbot/changes/svnpoller.py b/third_party/buildbot_7_12/buildbot/changes/svnpoller.py |
deleted file mode 100644 |
index 6012088949714ba4acb7858ef7b2294139b859ea..0000000000000000000000000000000000000000 |
--- a/third_party/buildbot_7_12/buildbot/changes/svnpoller.py |
+++ /dev/null |
@@ -1,501 +0,0 @@ |
-# -*- test-case-name: buildbot.test.test_svnpoller -*- |
- |
-# Based on the work of Dave Peticolas for the P4poll |
-# Changed to svn (using xml.dom.minidom) by Niklaus Giger |
-# Hacked beyond recognition by Brian Warner |
- |
-from twisted.python import log |
-from twisted.internet import defer, reactor, utils |
-from twisted.internet.task import LoopingCall |
- |
-from buildbot import util |
-from buildbot.changes import base |
-from buildbot.changes.changes import Change |
- |
-import xml.dom.minidom |
-import urllib |
- |
-def _assert(condition, msg): |
- if condition: |
- return True |
- raise AssertionError(msg) |
- |
-def dbgMsg(myString): |
- log.msg(myString) |
- return 1 |
- |
-# these split_file_* functions are available for use as values to the |
-# split_file= argument. |
-def split_file_alwaystrunk(path): |
- return (None, path) |
- |
-def split_file_branches(path): |
- # turn trunk/subdir/file.c into (None, "subdir/file.c") |
- # and branches/1.5.x/subdir/file.c into ("branches/1.5.x", "subdir/file.c") |
- pieces = path.split('/') |
- if pieces[0] == 'trunk': |
- return (None, '/'.join(pieces[1:])) |
- elif pieces[0] == 'branches': |
- return ('/'.join(pieces[0:2]), '/'.join(pieces[2:])) |
- else: |
- return None |
- |
- |
-class SVNPoller(base.ChangeSource, util.ComparableMixin): |
- """This source will poll a Subversion repository for changes and submit |
- them to the change master.""" |
- |
- compare_attrs = ["svnurl", "split_file_function", |
- "svnuser", "svnpasswd", |
- "pollinterval", "histmax", |
- "svnbin", "category"] |
- |
- parent = None # filled in when we're added |
- last_change = None |
- loop = None |
- working = False |
- |
- def __init__(self, svnurl, split_file=None, |
- svnuser=None, svnpasswd=None, |
- pollinterval=10*60, histmax=100, |
- svnbin='svn', revlinktmpl='', category=None): |
- """ |
- @type svnurl: string |
- @param svnurl: the SVN URL that describes the repository and |
- subdirectory to watch. If this ChangeSource should |
- only pay attention to a single branch, this should |
- point at the repository for that branch, like |
- svn://svn.twistedmatrix.com/svn/Twisted/trunk . If it |
- should follow multiple branches, point it at the |
- repository directory that contains all the branches |
- like svn://svn.twistedmatrix.com/svn/Twisted and also |
- provide a branch-determining function. |
- |
- Each file in the repository has a SVN URL in the form |
- (SVNURL)/(BRANCH)/(FILEPATH), where (BRANCH) could be |
- empty or not, depending upon your branch-determining |
- function. Only files that start with (SVNURL)/(BRANCH) |
- will be monitored. The Change objects that are sent to |
- the Schedulers will see (FILEPATH) for each modified |
- file. |
- |
- @type split_file: callable or None |
- @param split_file: a function that is called with a string of the |
- form (BRANCH)/(FILEPATH) and should return a tuple |
- (BRANCH, FILEPATH). This function should match |
- your repository's branch-naming policy. Each |
- changed file has a fully-qualified URL that can be |
- split into a prefix (which equals the value of the |
- 'svnurl' argument) and a suffix; it is this suffix |
- which is passed to the split_file function. |
- |
- If the function returns None, the file is ignored. |
- Use this to indicate that the file is not a part |
- of this project. |
- |
- For example, if your repository puts the trunk in |
- trunk/... and branches are in places like |
- branches/1.5/..., your split_file function could |
- look like the following (this function is |
- available as svnpoller.split_file_branches):: |
- |
- pieces = path.split('/') |
- if pieces[0] == 'trunk': |
- return (None, '/'.join(pieces[1:])) |
- elif pieces[0] == 'branches': |
- return ('/'.join(pieces[0:2]), |
- '/'.join(pieces[2:])) |
- else: |
- return None |
- |
- If instead your repository layout puts the trunk |
- for ProjectA in trunk/ProjectA/... and the 1.5 |
- branch in branches/1.5/ProjectA/..., your |
- split_file function could look like:: |
- |
- pieces = path.split('/') |
- if pieces[0] == 'trunk': |
- branch = None |
- pieces.pop(0) # remove 'trunk' |
- elif pieces[0] == 'branches': |
- pieces.pop(0) # remove 'branches' |
- # grab branch name |
- branch = 'branches/' + pieces.pop(0) |
- else: |
- return None # something weird |
- projectname = pieces.pop(0) |
- if projectname != 'ProjectA': |
- return None # wrong project |
- return (branch, '/'.join(pieces)) |
- |
- The default of split_file= is None, which |
- indicates that no splitting should be done. This |
- is equivalent to the following function:: |
- |
- return (None, path) |
- |
- If you wish, you can override the split_file |
- method with the same sort of function instead of |
- passing in a split_file= argument. |
- |
- |
- @type svnuser: string |
- @param svnuser: If set, the --username option will be added to |
- the 'svn log' command. You may need this to get |
- access to a private repository. |
- @type svnpasswd: string |
- @param svnpasswd: If set, the --password option will be added. |
- |
- @type pollinterval: int |
- @param pollinterval: interval in seconds between polls. The default |
- is 600 seconds (10 minutes). Smaller values |
- decrease the latency between the time a change |
- is recorded and the time the buildbot notices |
- it, but it also increases the system load. |
- |
- @type histmax: int |
- @param histmax: maximum number of changes to look back through. |
- The default is 100. Smaller values decrease |
- system load, but if more than histmax changes |
- are recorded between polls, the extra ones will |
- be silently lost. |
- |
- @type svnbin: string |
- @param svnbin: path to svn binary, defaults to just 'svn'. Use |
- this if your subversion command lives in an |
- unusual location. |
- |
- @type revlinktmpl: string |
- @param revlinktmpl: A format string to use for hyperlinks to revision |
- information. For example, setting this to |
- "http://reposerver/websvn/revision.php?rev=%s" |
- would create suitable links on the build pages |
- to information in websvn on each revision. |
- |
- @type category: string |
- @param category: A single category associated with the changes that |
- could be used by schedulers watch for branches of a |
- certain name AND category. |
- """ |
- |
- if svnurl.endswith("/"): |
- svnurl = svnurl[:-1] # strip the trailing slash |
- self.svnurl = svnurl |
- self.split_file_function = split_file or split_file_alwaystrunk |
- self.svnuser = svnuser |
- self.svnpasswd = svnpasswd |
- |
- self.revlinktmpl = revlinktmpl |
- |
- self.svnbin = svnbin |
- self.pollinterval = pollinterval |
- self.histmax = histmax |
- self._prefix = None |
- self.overrun_counter = 0 |
- self.loop = LoopingCall(self.checksvn) |
- self.category = category |
- |
- def split_file(self, path): |
- # use getattr() to avoid turning this function into a bound method, |
- # which would require it to have an extra 'self' argument |
- f = getattr(self, "split_file_function") |
- return f(path) |
- |
- def startService(self): |
- log.msg("SVNPoller(%s) starting" % self.svnurl) |
- base.ChangeSource.startService(self) |
- # Don't start the loop just yet because the reactor isn't running. |
- # Give it a chance to go and install our SIGCHLD handler before |
- # spawning processes. |
- reactor.callLater(0, self.loop.start, self.pollinterval) |
- |
- def stopService(self): |
- log.msg("SVNPoller(%s) shutting down" % self.svnurl) |
- self.loop.stop() |
- return base.ChangeSource.stopService(self) |
- |
- def describe(self): |
- return "SVNPoller watching %s" % self.svnurl |
- |
- def checksvn(self): |
- # Our return value is only used for unit testing. |
- |
- # we need to figure out the repository root, so we can figure out |
- # repository-relative pathnames later. Each SVNURL is in the form |
- # (ROOT)/(PROJECT)/(BRANCH)/(FILEPATH), where (ROOT) is something |
- # like svn://svn.twistedmatrix.com/svn/Twisted (i.e. there is a |
- # physical repository at /svn/Twisted on that host), (PROJECT) is |
- # something like Projects/Twisted (i.e. within the repository's |
- # internal namespace, everything under Projects/Twisted/ has |
- # something to do with Twisted, but these directory names do not |
- # actually appear on the repository host), (BRANCH) is something like |
- # "trunk" or "branches/2.0.x", and (FILEPATH) is a tree-relative |
- # filename like "twisted/internet/defer.py". |
- |
- # our self.svnurl attribute contains (ROOT)/(PROJECT) combined |
- # together in a way that we can't separate without svn's help. If the |
- # user is not using the split_file= argument, then self.svnurl might |
- # be (ROOT)/(PROJECT)/(BRANCH) . In any case, the filenames we will |
- # get back from 'svn log' will be of the form |
- # (PROJECT)/(BRANCH)/(FILEPATH), but we want to be able to remove |
- # that (PROJECT) prefix from them. To do this without requiring the |
- # user to tell us how svnurl is split into ROOT and PROJECT, we do an |
- # 'svn info --xml' command at startup. This command will include a |
- # <root> element that tells us ROOT. We then strip this prefix from |
- # self.svnurl to determine PROJECT, and then later we strip the |
- # PROJECT prefix from the filenames reported by 'svn log --xml' to |
- # get a (BRANCH)/(FILEPATH) that can be passed to split_file() to |
- # turn into separate BRANCH and FILEPATH values. |
- |
- # whew. |
- |
- if self.working: |
- log.msg("SVNPoller(%s) overrun: timer fired but the previous " |
- "poll had not yet finished." % self.svnurl) |
- self.overrun_counter += 1 |
- return defer.succeed(None) |
- self.working = True |
- |
- log.msg("SVNPoller polling") |
- if not self._prefix: |
- # this sets self._prefix when it finishes. It fires with |
- # self._prefix as well, because that makes the unit tests easier |
- # to write. |
- d = self.get_root() |
- d.addCallback(self.determine_prefix) |
- else: |
- d = defer.succeed(self._prefix) |
- |
- d.addCallback(self.get_logs) |
- d.addCallback(self.parse_logs) |
- d.addCallback(self.get_new_logentries) |
- d.addCallback(self.create_changes) |
- d.addCallback(self.submit_changes) |
- d.addCallbacks(self.finished_ok, self.finished_failure) |
- return d |
- |
- def getProcessOutput(self, args): |
- # this exists so we can override it during the unit tests |
- d = utils.getProcessOutput(self.svnbin, args, {}) |
- return d |
- |
- def get_root(self): |
- args = ["info", "--xml", "--non-interactive", self.svnurl] |
- if self.svnuser: |
- args.extend(["--username=%s" % self.svnuser]) |
- if self.svnpasswd: |
- args.extend(["--password=%s" % self.svnpasswd]) |
- d = self.getProcessOutput(args) |
- return d |
- |
- def determine_prefix(self, output): |
- try: |
- doc = xml.dom.minidom.parseString(output) |
- except xml.parsers.expat.ExpatError: |
- dbgMsg("_process_changes: ExpatError in %s" % output) |
- log.msg("SVNPoller._determine_prefix_2: ExpatError in '%s'" |
- % output) |
- raise |
- rootnodes = doc.getElementsByTagName("root") |
- if not rootnodes: |
- # this happens if the URL we gave was already the root. In this |
- # case, our prefix is empty. |
- self._prefix = "" |
- return self._prefix |
- rootnode = rootnodes[0] |
- root = "".join([c.data for c in rootnode.childNodes]) |
- # root will be a unicode string |
- _assert(self.svnurl.startswith(root), |
- "svnurl='%s' doesn't start with <root>='%s'" % |
- (self.svnurl, root)) |
- self._prefix = self.svnurl[len(root):] |
- if self._prefix.startswith("/"): |
- self._prefix = self._prefix[1:] |
- log.msg("SVNPoller: svnurl=%s, root=%s, so prefix=%s" % |
- (self.svnurl, root, self._prefix)) |
- return self._prefix |
- |
- def get_logs(self, ignored_prefix=None): |
- args = [] |
- args.extend(["log", "--xml", "--verbose", "--non-interactive"]) |
- if self.svnuser: |
- args.extend(["--username=%s" % self.svnuser]) |
- if self.svnpasswd: |
- args.extend(["--password=%s" % self.svnpasswd]) |
- args.extend(["--limit=%d" % (self.histmax), self.svnurl]) |
- d = self.getProcessOutput(args) |
- return d |
- |
- def parse_logs(self, output): |
- # parse the XML output, return a list of <logentry> nodes |
- try: |
- doc = xml.dom.minidom.parseString(output) |
- except xml.parsers.expat.ExpatError: |
- dbgMsg("_process_changes: ExpatError in %s" % output) |
- log.msg("SVNPoller._parse_changes: ExpatError in '%s'" % output) |
- raise |
- logentries = doc.getElementsByTagName("logentry") |
- return logentries |
- |
- |
- def _filter_new_logentries(self, logentries, last_change): |
- # given a list of logentries, return a tuple of (new_last_change, |
- # new_logentries), where new_logentries contains only the ones after |
- # last_change |
- if not logentries: |
- # no entries, so last_change must stay at None |
- return (None, []) |
- |
- mostRecent = int(logentries[0].getAttribute("revision")) |
- |
- if last_change is None: |
- # if this is the first time we've been run, ignore any changes |
- # that occurred before now. This prevents a build at every |
- # startup. |
- log.msg('svnPoller: starting at change %s' % mostRecent) |
- return (mostRecent, []) |
- |
- if last_change == mostRecent: |
- # an unmodified repository will hit this case |
- log.msg('svnPoller: _process_changes last %s mostRecent %s' % ( |
- last_change, mostRecent)) |
- return (mostRecent, []) |
- |
- new_logentries = [] |
- for el in logentries: |
- if last_change == int(el.getAttribute("revision")): |
- break |
- new_logentries.append(el) |
- new_logentries.reverse() # return oldest first |
- |
- # If the newest commit's author is chrome-bot, skip this commit. This |
- # is a guard to ensure that we don't poll on our mirror while it could |
- # be mid-sync. In that case, the author data could be wrong and would |
- # look like it was a commit by chrome-bot@google.com. A downside: the |
- # chrome-bot account may have a legitimate commit. This should not |
- # happen generally, so we're okay waiting to see it until there's a |
- # later commit with a non-chrome-bot author. |
- if len(new_logentries) > 0: |
- if new_logentries[-1].getAttribute("author") == 'chrome-bot@google.com': |
- new_logentries.pop(-1) |
- mostRecent = int(logentries[1].getAttribute("revision")) |
- |
- return (mostRecent, new_logentries) |
- |
- def get_new_logentries(self, logentries): |
- last_change = self.last_change |
- (new_last_change, |
- new_logentries) = self._filter_new_logentries(logentries, |
- self.last_change) |
- self.last_change = new_last_change |
- log.msg('svnPoller: _process_changes %s .. %s' % |
- (last_change, new_last_change)) |
- return new_logentries |
- |
- |
- def _get_text(self, element, tag_name): |
- try: |
- child_nodes = element.getElementsByTagName(tag_name)[0].childNodes |
- text = "".join([t.data for t in child_nodes]) |
- except: |
- text = "<unknown>" |
- return text |
- |
- def _transform_path(self, path): |
- _assert(path.startswith(self._prefix), |
- "filepath '%s' should start with prefix '%s'" % |
- (path, self._prefix)) |
- relative_path = path[len(self._prefix):] |
- if relative_path.startswith("/"): |
- relative_path = relative_path[1:] |
- where = self.split_file(relative_path) |
- # 'where' is either None or (branch, final_path) |
- return where |
- |
- def create_changes(self, new_logentries): |
- changes = [] |
- |
- for el in new_logentries: |
- branch_files = [] # get oldest change first |
- revision = str(el.getAttribute("revision")) |
- |
- revlink='' |
- |
- if self.revlinktmpl: |
- if revision: |
- revlink = self.revlinktmpl % urllib.quote_plus(revision) |
- |
- dbgMsg("Adding change revision %s" % (revision,)) |
- # TODO: the rest of buildbot may not be ready for unicode 'who' |
- # values |
- author = self._get_text(el, "author") |
- comments = self._get_text(el, "msg") |
- # there is a "date" field, but it provides localtime in the |
- # repository's timezone, whereas we care about buildmaster's |
- # localtime (since this will get used to position the boxes on |
- # the Waterfall display, etc). So ignore the date field and use |
- # our local clock instead. |
- #when = self._get_text(el, "date") |
- #when = time.mktime(time.strptime("%.19s" % when, |
- # "%Y-%m-%dT%H:%M:%S")) |
- branches = {} |
- pathlist = el.getElementsByTagName("paths")[0] |
- for p in pathlist.getElementsByTagName("path"): |
- action = p.getAttribute("action") |
- path = "".join([t.data for t in p.childNodes]) |
- # the rest of buildbot is certaily not yet ready to handle |
- # unicode filenames, because they get put in RemoteCommands |
- # which get sent via PB to the buildslave, and PB doesn't |
- # handle unicode. |
- path = path.encode("ascii") |
- if path.startswith("/"): |
- path = path[1:] |
- where = self._transform_path(path) |
- |
- # if 'where' is None, the file was outside any project that |
- # we care about and we should ignore it |
- if where: |
- branch, filename = where |
- if not branch in branches: |
- branches[branch] = { 'files': []} |
- branches[branch]['files'].append(filename) |
- |
- if not branches[branch].has_key('action'): |
- branches[branch]['action'] = action |
- |
- for branch in branches.keys(): |
- action = branches[branch]['action'] |
- files = branches[branch]['files'] |
- number_of_files_changed = len(files) |
- |
- if action == u'D' and number_of_files_changed == 1 and files[0] == '': |
- log.msg("Ignoring deletion of branch '%s'" % branch) |
- else: |
- c = Change(who=author, |
- files=files, |
- comments=comments, |
- revision=revision, |
- branch=branch, |
- revlink=revlink, |
- category=self.category) |
- changes.append(c) |
- |
- return changes |
- |
- def submit_changes(self, changes): |
- for c in changes: |
- self.parent.addChange(c) |
- |
- def finished_ok(self, res): |
- log.msg("SVNPoller finished polling") |
- dbgMsg('_finished : %s' % res) |
- assert self.working |
- self.working = False |
- return res |
- |
- def finished_failure(self, f): |
- log.msg("SVNPoller failed") |
- dbgMsg('_finished : %s' % f) |
- assert self.working |
- self.working = False |
- return None # eat the failure |