1
2
3
4
5
6
7 from twisted.python import log
8 from twisted.internet import defer, reactor, utils
9 from twisted.internet.task import LoopingCall
10
11 from buildbot import util
12 from buildbot.changes import base
13 from buildbot.changes.changes import Change
14
15 import xml.dom.minidom
16 import urllib
17
19 if condition:
20 return True
21 raise AssertionError(msg)
22
24 log.msg(myString)
25 return 1
26
27
28
31
33
34
35 pieces = path.split('/')
36 if pieces[0] == 'trunk':
37 return (None, '/'.join(pieces[1:]))
38 elif pieces[0] == 'branches':
39 return ('/'.join(pieces[0:2]), '/'.join(pieces[2:]))
40 else:
41 return None
42
43
44 -class SVNPoller(base.ChangeSource, util.ComparableMixin):
45 """This source will poll a Subversion repository for changes and submit
46 them to the change master."""
47
48 compare_attrs = ["svnurl", "split_file_function",
49 "svnuser", "svnpasswd",
50 "pollinterval", "histmax",
51 "svnbin", "category"]
52
53 parent = None
54 last_change = None
55 loop = None
56 working = False
57
58 - def __init__(self, svnurl, split_file=None,
59 svnuser=None, svnpasswd=None,
60 pollinterval=10*60, histmax=100,
61 svnbin='svn', revlinktmpl='', category=None):
62 """
63 @type svnurl: string
64 @param svnurl: the SVN URL that describes the repository and
65 subdirectory to watch. If this ChangeSource should
66 only pay attention to a single branch, this should
67 point at the repository for that branch, like
68 svn://svn.twistedmatrix.com/svn/Twisted/trunk . If it
69 should follow multiple branches, point it at the
70 repository directory that contains all the branches
71 like svn://svn.twistedmatrix.com/svn/Twisted and also
72 provide a branch-determining function.
73
74 Each file in the repository has a SVN URL in the form
75 (SVNURL)/(BRANCH)/(FILEPATH), where (BRANCH) could be
76 empty or not, depending upon your branch-determining
77 function. Only files that start with (SVNURL)/(BRANCH)
78 will be monitored. The Change objects that are sent to
79 the Schedulers will see (FILEPATH) for each modified
80 file.
81
82 @type split_file: callable or None
83 @param split_file: a function that is called with a string of the
84 form (BRANCH)/(FILEPATH) and should return a tuple
85 (BRANCH, FILEPATH). This function should match
86 your repository's branch-naming policy. Each
87 changed file has a fully-qualified URL that can be
88 split into a prefix (which equals the value of the
89 'svnurl' argument) and a suffix; it is this suffix
90 which is passed to the split_file function.
91
92 If the function returns None, the file is ignored.
93 Use this to indicate that the file is not a part
94 of this project.
95
96 For example, if your repository puts the trunk in
97 trunk/... and branches are in places like
98 branches/1.5/..., your split_file function could
99 look like the following (this function is
100 available as svnpoller.split_file_branches)::
101
102 pieces = path.split('/')
103 if pieces[0] == 'trunk':
104 return (None, '/'.join(pieces[1:]))
105 elif pieces[0] == 'branches':
106 return ('/'.join(pieces[0:2]),
107 '/'.join(pieces[2:]))
108 else:
109 return None
110
111 If instead your repository layout puts the trunk
112 for ProjectA in trunk/ProjectA/... and the 1.5
113 branch in branches/1.5/ProjectA/..., your
114 split_file function could look like::
115
116 pieces = path.split('/')
117 if pieces[0] == 'trunk':
118 branch = None
119 pieces.pop(0) # remove 'trunk'
120 elif pieces[0] == 'branches':
121 pieces.pop(0) # remove 'branches'
122 # grab branch name
123 branch = 'branches/' + pieces.pop(0)
124 else:
125 return None # something weird
126 projectname = pieces.pop(0)
127 if projectname != 'ProjectA':
128 return None # wrong project
129 return (branch, '/'.join(pieces))
130
131 The default of split_file= is None, which
132 indicates that no splitting should be done. This
133 is equivalent to the following function::
134
135 return (None, path)
136
137 If you wish, you can override the split_file
138 method with the same sort of function instead of
139 passing in a split_file= argument.
140
141
142 @type svnuser: string
143 @param svnuser: If set, the --username option will be added to
144 the 'svn log' command. You may need this to get
145 access to a private repository.
146 @type svnpasswd: string
147 @param svnpasswd: If set, the --password option will be added.
148
149 @type pollinterval: int
150 @param pollinterval: interval in seconds between polls. The default
151 is 600 seconds (10 minutes). Smaller values
152 decrease the latency between the time a change
153 is recorded and the time the buildbot notices
154 it, but it also increases the system load.
155
156 @type histmax: int
157 @param histmax: maximum number of changes to look back through.
158 The default is 100. Smaller values decrease
159 system load, but if more than histmax changes
160 are recorded between polls, the extra ones will
161 be silently lost.
162
163 @type svnbin: string
164 @param svnbin: path to svn binary, defaults to just 'svn'. Use
165 this if your subversion command lives in an
166 unusual location.
167
168 @type revlinktmpl: string
169 @param revlinktmpl: A format string to use for hyperlinks to revision
170 information. For example, setting this to
171 "http://reposerver/websvn/revision.php?rev=%s"
172 would create suitable links on the build pages
173 to information in websvn on each revision.
174
175 @type category: string
176 @param category: A single category associated with the changes that
177 could be used by schedulers watch for branches of a
178 certain name AND category.
179 """
180
181 if svnurl.endswith("/"):
182 svnurl = svnurl[:-1]
183 self.svnurl = svnurl
184 self.split_file_function = split_file or split_file_alwaystrunk
185 self.svnuser = svnuser
186 self.svnpasswd = svnpasswd
187
188 self.revlinktmpl = revlinktmpl
189
190 self.svnbin = svnbin
191 self.pollinterval = pollinterval
192 self.histmax = histmax
193 self._prefix = None
194 self.overrun_counter = 0
195 self.loop = LoopingCall(self.checksvn)
196 self.category = category
197
199
200
201 f = getattr(self, "split_file_function")
202 return f(path)
203
211
216
218 return "SVNPoller watching %s" % self.svnurl
219
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252 if self.working:
253 log.msg("SVNPoller(%s) overrun: timer fired but the previous "
254 "poll had not yet finished." % self.svnurl)
255 self.overrun_counter += 1
256 return defer.succeed(None)
257 self.working = True
258
259 log.msg("SVNPoller polling")
260 if not self._prefix:
261
262
263
264 d = self.get_root()
265 d.addCallback(self.determine_prefix)
266 else:
267 d = defer.succeed(self._prefix)
268
269 d.addCallback(self.get_logs)
270 d.addCallback(self.parse_logs)
271 d.addCallback(self.get_new_logentries)
272 d.addCallback(self.create_changes)
273 d.addCallback(self.submit_changes)
274 d.addCallbacks(self.finished_ok, self.finished_failure)
275 return d
276
281
283 args = ["info", "--xml", "--non-interactive", self.svnurl]
284 if self.svnuser:
285 args.extend(["--username=%s" % self.svnuser])
286 if self.svnpasswd:
287 args.extend(["--password=%s" % self.svnpasswd])
288 d = self.getProcessOutput(args)
289 return d
290
292 try:
293 doc = xml.dom.minidom.parseString(output)
294 except xml.parsers.expat.ExpatError:
295 dbgMsg("_process_changes: ExpatError in %s" % output)
296 log.msg("SVNPoller._determine_prefix_2: ExpatError in '%s'"
297 % output)
298 raise
299 rootnodes = doc.getElementsByTagName("root")
300 if not rootnodes:
301
302
303 self._prefix = ""
304 return self._prefix
305 rootnode = rootnodes[0]
306 root = "".join([c.data for c in rootnode.childNodes])
307
308 _assert(self.svnurl.startswith(root),
309 "svnurl='%s' doesn't start with <root>='%s'" %
310 (self.svnurl, root))
311 self._prefix = self.svnurl[len(root):]
312 if self._prefix.startswith("/"):
313 self._prefix = self._prefix[1:]
314 log.msg("SVNPoller: svnurl=%s, root=%s, so prefix=%s" %
315 (self.svnurl, root, self._prefix))
316 return self._prefix
317
318 - def get_logs(self, ignored_prefix=None):
319 args = []
320 args.extend(["log", "--xml", "--verbose", "--non-interactive"])
321 if self.svnuser:
322 args.extend(["--username=%s" % self.svnuser])
323 if self.svnpasswd:
324 args.extend(["--password=%s" % self.svnpasswd])
325 args.extend(["--limit=%d" % (self.histmax), self.svnurl])
326 d = self.getProcessOutput(args)
327 return d
328
330
331 try:
332 doc = xml.dom.minidom.parseString(output)
333 except xml.parsers.expat.ExpatError:
334 dbgMsg("_process_changes: ExpatError in %s" % output)
335 log.msg("SVNPoller._parse_changes: ExpatError in '%s'" % output)
336 raise
337 logentries = doc.getElementsByTagName("logentry")
338 return logentries
339
340
342
343
344
345 if not logentries:
346
347 return (None, [])
348
349 mostRecent = int(logentries[0].getAttribute("revision"))
350
351 if last_change is None:
352
353
354
355 log.msg('svnPoller: starting at change %s' % mostRecent)
356 return (mostRecent, [])
357
358 if last_change == mostRecent:
359
360 log.msg('svnPoller: _process_changes last %s mostRecent %s' % (
361 last_change, mostRecent))
362 return (mostRecent, [])
363
364 new_logentries = []
365 for el in logentries:
366 if last_change == int(el.getAttribute("revision")):
367 break
368 new_logentries.append(el)
369 new_logentries.reverse()
370 return (mostRecent, new_logentries)
371
373 last_change = self.last_change
374 (new_last_change,
375 new_logentries) = self._filter_new_logentries(logentries,
376 self.last_change)
377 self.last_change = new_last_change
378 log.msg('svnPoller: _process_changes %s .. %s' %
379 (last_change, new_last_change))
380 return new_logentries
381
382
383 - def _get_text(self, element, tag_name):
384 try:
385 child_nodes = element.getElementsByTagName(tag_name)[0].childNodes
386 text = "".join([t.data for t in child_nodes])
387 except:
388 text = "<unknown>"
389 return text
390
401
403 changes = []
404
405 for el in new_logentries:
406 branch_files = []
407 revision = str(el.getAttribute("revision"))
408
409 revlink=''
410
411 if self.revlinktmpl:
412 if revision:
413 revlink = self.revlinktmpl % urllib.quote_plus(revision)
414
415 dbgMsg("Adding change revision %s" % (revision,))
416
417
418 author = self._get_text(el, "author")
419 comments = self._get_text(el, "msg")
420
421
422
423
424
425
426
427
428 branches = {}
429 pathlist = el.getElementsByTagName("paths")[0]
430 for p in pathlist.getElementsByTagName("path"):
431 action = p.getAttribute("action")
432 path = "".join([t.data for t in p.childNodes])
433
434
435
436
437 path = path.encode("ascii")
438 if path.startswith("/"):
439 path = path[1:]
440 where = self._transform_path(path)
441
442
443
444 if where:
445 branch, filename = where
446 if not branch in branches:
447 branches[branch] = { 'files': []}
448 branches[branch]['files'].append(filename)
449
450 if not branches[branch].has_key('action'):
451 branches[branch]['action'] = action
452
453 for branch in branches.keys():
454 action = branches[branch]['action']
455 files = branches[branch]['files']
456 number_of_files_changed = len(files)
457
458 if action == u'D' and number_of_files_changed == 1 and files[0] == '':
459 log.msg("Ignoring deletion of branch '%s'" % branch)
460 else:
461 c = Change(who=author,
462 files=files,
463 comments=comments,
464 revision=revision,
465 branch=branch,
466 revlink=revlink,
467 category=self.category)
468 changes.append(c)
469
470 return changes
471
475
477 log.msg("SVNPoller finished polling")
478 dbgMsg('_finished : %s' % res)
479 assert self.working
480 self.working = False
481 return res
482
484 log.msg("SVNPoller failed")
485 dbgMsg('_finished : %s' % f)
486 assert self.working
487 self.working = False
488 return None
489