2 from xml
.dom
import minidom
4 from twisted
.python
import log
, failure
5 from twisted
.internet
import reactor
6 from twisted
.internet
.task
import LoopingCall
7 from twisted
.web
.client
import getPage
9 from buildbot
.changes
import base
, changes
11 class InvalidResultError(Exception):
12 def __init__(self
, value
="InvalidResultError"):
15 return repr(self
.value
)
17 class EmptyResult(Exception):
20 class NoMoreCiNodes(Exception):
23 class NoMoreFileNodes(Exception):
27 """I hold a list of CiNodes"""
28 def __init__(self
, nodes
=[]):
31 def __cmp__(self
, other
):
32 if len(self
.nodes
) != len(other
.nodes
):
34 for i
in range(len(self
.nodes
)):
35 if self
.nodes
[i
].log
!= other
.nodes
[i
].log \
36 or self
.nodes
[i
].who
!= other
.nodes
[i
].who \
37 or self
.nodes
[i
].date
!= other
.nodes
[i
].date \
38 or len(self
.nodes
[i
].files
) != len(other
.nodes
[i
].files
):
41 for j
in range(len(self
.nodes
[i
].files
)):
42 if self
.nodes
[i
].files
[j
].revision \
43 != other
.nodes
[i
].files
[j
].revision \
44 or self
.nodes
[i
].files
[j
].filename \
45 != other
.nodes
[i
].files
[j
].filename
:
51 """I hold information baout one <ci> node, including a list of files"""
52 def __init__(self
, log
="", who
="", date
=0, files
=[]):
59 """I hold information about one <f> node"""
60 def __init__(self
, revision
="", filename
=""):
61 self
.revision
= revision
62 self
.filename
= filename
65 """I parse the XML result from a bonsai cvsquery."""
67 def __init__(self
, data
):
69 # this is a fix for non-ascii characters
70 # because bonsai does not give us an encoding to work with
71 # it impossible to be 100% sure what to decode it as but latin1 covers
73 data
= data
.decode("latin1")
74 data
= data
.encode("ascii", "replace")
75 self
.dom
= minidom
.parseString(data
)
78 raise InvalidResultError("Malformed XML in result")
80 self
.ciNodes
= self
.dom
.getElementsByTagName("ci")
81 self
.currentCiNode
= None # filled in by _nextCiNode()
82 self
.fileNodes
= None # filled in by _nextCiNode()
83 self
.currentFileNode
= None # filled in by _nextFileNode()
84 self
.bonsaiResult
= self
._parseData
()
87 return self
.bonsaiResult
90 """Returns data from a Bonsai cvsquery in a BonsaiResult object"""
93 while self
._nextCiNode
():
96 while self
._nextFileNode
():
97 files
.append(FileNode(self
._getRevision
(),
99 except NoMoreFileNodes
:
101 except InvalidResultError
:
103 cinode
= CiNode(self
._getLog
(), self
._getWho
(),
104 self
._getDate
(), files
)
105 # hack around bonsai xml output bug for empty check-in comments
106 if not cinode
.log
and nodes
and \
107 not nodes
[-1].log
and \
108 cinode
.who
== nodes
[-1].who
and \
109 cinode
.date
== nodes
[-1].date
:
110 nodes
[-1].files
+= cinode
.files
114 except NoMoreCiNodes
:
116 except InvalidResultError
, EmptyResult
:
119 return BonsaiResult(nodes
)
122 def _nextCiNode(self
):
123 """Iterates to the next <ci> node and fills self.fileNodes with
126 self
.currentCiNode
= self
.ciNodes
.pop(0)
127 if len(self
.currentCiNode
.getElementsByTagName("files")) > 1:
128 raise InvalidResultError("Multiple <files> for one <ci>")
130 self
.fileNodes
= self
.currentCiNode
.getElementsByTagName("f")
132 # if there was zero <ci> nodes in the result
133 if not self
.currentCiNode
:
140 def _nextFileNode(self
):
141 """Iterates to the next <f> node"""
143 self
.currentFileNode
= self
.fileNodes
.pop(0)
145 raise NoMoreFileNodes
150 """Returns the log of the current <ci> node"""
151 logs
= self
.currentCiNode
.getElementsByTagName("log")
153 raise InvalidResultError("No log present")
155 raise InvalidResultError("Multiple logs present")
157 # catch empty check-in comments
158 if logs
[0].firstChild
:
159 return logs
[0].firstChild
.data
163 """Returns the e-mail address of the commiter"""
164 # convert unicode string to regular string
165 return str(self
.currentCiNode
.getAttribute("who"))
168 """Returns the date (unix time) of the commit"""
169 # convert unicode number to regular one
171 commitDate
= int(self
.currentCiNode
.getAttribute("date"))
173 raise InvalidResultError
177 def _getFilename(self
):
178 """Returns the filename of the current <f> node"""
180 filename
= self
.currentFileNode
.firstChild
.data
181 except AttributeError:
182 raise InvalidResultError("Missing filename")
186 def _getRevision(self
):
187 return self
.currentFileNode
.getAttribute("rev")
190 class BonsaiPoller(base
.ChangeSource
):
191 """This source will poll a bonsai server for changes and submit
192 them to the change master."""
194 compare_attrs
= ["bonsaiURL", "pollInterval", "tree",
195 "module", "branch", "cvsroot"]
197 parent
= None # filled in when we're added
202 def __init__(self
, bonsaiURL
, module
, branch
, tree
="default",
203 cvsroot
="/cvsroot", pollInterval
=30):
205 @type bonsaiURL: string
206 @param bonsaiURL: The base URL of the Bonsai server
207 (ie. http://bonsai.mozilla.org)
209 @param module: The module to look for changes in. Commonly
212 @param branch: The branch to look for changes in. This must
214 'branch' option for the Scheduler.
216 @param tree: The tree to look for changes in. Commonly this
218 @type cvsroot: string
219 @param cvsroot: The cvsroot of the repository. Usually this is
221 @type pollInterval: int
222 @param pollInterval: The time (in seconds) between queries for
226 self
.bonsaiURL
= bonsaiURL
230 self
.cvsroot
= cvsroot
231 self
.pollInterval
= pollInterval
232 self
.lastChange
= time
.time()
233 self
.lastPoll
= time
.time()
235 def startService(self
):
236 self
.loop
= LoopingCall(self
.poll
)
237 base
.ChangeSource
.startService(self
)
239 reactor
.callLater(0, self
.loop
.start
, self
.pollInterval
)
241 def stopService(self
):
243 return base
.ChangeSource
.stopService(self
)
247 str += "Getting changes from the Bonsai service running at %s " \
249 str += "<br>Using tree: %s, branch: %s, and module: %s" % (self
.tree
, \
250 self
.branch
, self
.module
)
255 log
.msg("Not polling Bonsai because last poll is still working")
258 d
= self
._get
_changes
()
259 d
.addCallback(self
._process
_changes
)
260 d
.addCallbacks(self
._finished
_ok
, self
._finished
_failure
)
263 def _finished_ok(self
, res
):
267 # check for failure -- this is probably never hit but the twisted docs
268 # are not clear enough to be sure. it is being kept "just in case"
269 if isinstance(res
, failure
.Failure
):
270 log
.msg("Bonsai poll failed: %s" % res
)
273 def _finished_failure(self
, res
):
274 log
.msg("Bonsai poll failed: %s" % res
)
277 return None # eat the failure
280 args
= ["treeid=%s" % self
.tree
, "module=%s" % self
.module
,
281 "branch=%s" % self
.branch
, "branchtype=match",
282 "sortby=Date", "date=explicit",
283 "mindate=%d" % self
.lastChange
,
284 "maxdate=%d" % int(time
.time()),
285 "cvsroot=%s" % self
.cvsroot
, "xml=1"]
286 # build the bonsai URL
288 url
+= "/cvsquery.cgi?"
289 url
+= "&".join(args
)
293 def _get_changes(self
):
294 url
= self
._make
_url
()
295 log
.msg("Polling Bonsai tree at %s" % url
)
297 self
.lastPoll
= time
.time()
298 # get the page, in XML format
299 return getPage(url
, timeout
=self
.pollInterval
)
301 def _process_changes(self
, query
):
303 bp
= BonsaiParser(query
)
304 result
= bp
.getData()
305 except InvalidResultError
, e
:
306 log
.msg("Could not process Bonsai query: " + e
.value
)
311 for cinode
in result
.nodes
:
312 files
= [file.filename
+ ' (revision '+file.revision
+')'
313 for file in cinode
.files
]
314 c
= changes
.Change(who
= cinode
.who
,
316 comments
= cinode
.log
,
318 branch
= self
.branch
)
319 self
.parent
.addChange(c
)
320 self
.lastChange
= self
.lastPoll