rename old extra_args in commands.py to svn_args to avoid confusion
[buildbot.git] / buildbot / changes / bonsaipoller.py
blob2e319bb4c4e40a3cfe7a19f4fd775f336541d055
1 import time
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"):
13 self.value = value
14 def __str__(self):
15 return repr(self.value)
17 class EmptyResult(Exception):
18 pass
20 class NoMoreCiNodes(Exception):
21 pass
23 class NoMoreFileNodes(Exception):
24 pass
26 class BonsaiResult:
27 """I hold a list of CiNodes"""
28 def __init__(self, nodes=[]):
29 self.nodes = nodes
31 def __cmp__(self, other):
32 if len(self.nodes) != len(other.nodes):
33 return False
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):
39 return -1
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:
46 return -1
48 return 0
50 class CiNode:
51 """I hold information baout one <ci> node, including a list of files"""
52 def __init__(self, log="", who="", date=0, files=[]):
53 self.log = log
54 self.who = who
55 self.date = date
56 self.files = files
58 class FileNode:
59 """I hold information about one <f> node"""
60 def __init__(self, revision="", filename=""):
61 self.revision = revision
62 self.filename = filename
64 class BonsaiParser:
65 """I parse the XML result from a bonsai cvsquery."""
67 def __init__(self, data):
68 try:
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
72 # the broadest base
73 data = data.decode("latin1")
74 data = data.encode("ascii", "replace")
75 self.dom = minidom.parseString(data)
76 log.msg(data)
77 except:
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()
86 def getData(self):
87 return self.bonsaiResult
89 def _parseData(self):
90 """Returns data from a Bonsai cvsquery in a BonsaiResult object"""
91 nodes = []
92 try:
93 while self._nextCiNode():
94 files = []
95 try:
96 while self._nextFileNode():
97 files.append(FileNode(self._getRevision(),
98 self._getFilename()))
99 except NoMoreFileNodes:
100 pass
101 except InvalidResultError:
102 raise
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
111 else:
112 nodes.append(cinode)
114 except NoMoreCiNodes:
115 pass
116 except InvalidResultError, EmptyResult:
117 raise
119 return BonsaiResult(nodes)
122 def _nextCiNode(self):
123 """Iterates to the next <ci> node and fills self.fileNodes with
124 child <f> nodes"""
125 try:
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")
131 except IndexError:
132 # if there was zero <ci> nodes in the result
133 if not self.currentCiNode:
134 raise EmptyResult
135 else:
136 raise NoMoreCiNodes
138 return True
140 def _nextFileNode(self):
141 """Iterates to the next <f> node"""
142 try:
143 self.currentFileNode = self.fileNodes.pop(0)
144 except IndexError:
145 raise NoMoreFileNodes
147 return True
149 def _getLog(self):
150 """Returns the log of the current <ci> node"""
151 logs = self.currentCiNode.getElementsByTagName("log")
152 if len(logs) < 1:
153 raise InvalidResultError("No log present")
154 elif len(logs) > 1:
155 raise InvalidResultError("Multiple logs present")
157 # catch empty check-in comments
158 if logs[0].firstChild:
159 return logs[0].firstChild.data
160 return ''
162 def _getWho(self):
163 """Returns the e-mail address of the commiter"""
164 # convert unicode string to regular string
165 return str(self.currentCiNode.getAttribute("who"))
167 def _getDate(self):
168 """Returns the date (unix time) of the commit"""
169 # convert unicode number to regular one
170 try:
171 commitDate = int(self.currentCiNode.getAttribute("date"))
172 except ValueError:
173 raise InvalidResultError
175 return commitDate
177 def _getFilename(self):
178 """Returns the filename of the current <f> node"""
179 try:
180 filename = self.currentFileNode.firstChild.data
181 except AttributeError:
182 raise InvalidResultError("Missing filename")
184 return 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
198 loop = None
199 volatile = ['loop']
200 working = False
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)
208 @type module: string
209 @param module: The module to look for changes in. Commonly
210 this is 'all'
211 @type branch: string
212 @param branch: The branch to look for changes in. This must
213 match the
214 'branch' option for the Scheduler.
215 @type tree: string
216 @param tree: The tree to look for changes in. Commonly this
217 is 'all'
218 @type cvsroot: string
219 @param cvsroot: The cvsroot of the repository. Usually this is
220 '/cvsroot'
221 @type pollInterval: int
222 @param pollInterval: The time (in seconds) between queries for
223 changes
226 self.bonsaiURL = bonsaiURL
227 self.module = module
228 self.branch = branch
229 self.tree = tree
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):
242 self.loop.stop()
243 return base.ChangeSource.stopService(self)
245 def describe(self):
246 str = ""
247 str += "Getting changes from the Bonsai service running at %s " \
248 % self.bonsaiURL
249 str += "<br>Using tree: %s, branch: %s, and module: %s" % (self.tree, \
250 self.branch, self.module)
251 return str
253 def poll(self):
254 if self.working:
255 log.msg("Not polling Bonsai because last poll is still working")
256 else:
257 self.working = True
258 d = self._get_changes()
259 d.addCallback(self._process_changes)
260 d.addCallbacks(self._finished_ok, self._finished_failure)
261 return
263 def _finished_ok(self, res):
264 assert self.working
265 self.working = False
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)
271 return res
273 def _finished_failure(self, res):
274 log.msg("Bonsai poll failed: %s" % res)
275 assert self.working
276 self.working = False
277 return None # eat the failure
279 def _make_url(self):
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
287 url = self.bonsaiURL
288 url += "/cvsquery.cgi?"
289 url += "&".join(args)
291 return url
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):
302 try:
303 bp = BonsaiParser(query)
304 result = bp.getData()
305 except InvalidResultError, e:
306 log.msg("Could not process Bonsai query: " + e.value)
307 return
308 except EmptyResult:
309 return
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,
315 files = files,
316 comments = cinode.log,
317 when = cinode.date,
318 branch = self.branch)
319 self.parent.addChange(c)
320 self.lastChange = self.lastPoll