1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 from __future__ import with_statement
17
18
19
20
21
22
23 from twisted.python import log
24 from twisted.internet import defer, utils
25
26 from buildbot import util
27 from buildbot.changes import base
28
29 import xml.dom.minidom
30 import os, urllib
36
38
39
40
41
42
43
44 pieces = path.split('/')
45 if len(pieces) > 1 and pieces[0] == 'trunk':
46 return (None, '/'.join(pieces[1:]))
47 elif len(pieces) > 2 and pieces[0] == 'branches':
48 return ('/'.join(pieces[0:2]), '/'.join(pieces[2:]))
49 else:
50 return None
51
64
65 -class SVNPoller(base.PollingChangeSource, util.ComparableMixin):
66 """
67 Poll a Subversion repository for changes and submit them to the change
68 master.
69 """
70
71 compare_attrs = ["svnurl", "split_file",
72 "svnuser", "svnpasswd", "project",
73 "pollInterval", "histmax",
74 "svnbin", "category", "cachepath"]
75
76 parent = None
77 last_change = None
78 loop = None
79
80 - def __init__(self, svnurl, split_file=None,
81 svnuser=None, svnpasswd=None,
82 pollInterval=10*60, histmax=100,
83 svnbin='svn', revlinktmpl='', category=None,
84 project='', cachepath=None, pollinterval=-2,
85 extra_args=None):
86
87
88 if pollinterval != -2:
89 pollInterval = pollinterval
90
91 base.PollingChangeSource.__init__(self, name=svnurl, pollInterval=pollInterval)
92
93 if svnurl.endswith("/"):
94 svnurl = svnurl[:-1]
95 self.svnurl = svnurl
96 self.extra_args = extra_args
97 self.split_file = split_file or split_file_alwaystrunk
98 self.svnuser = svnuser
99 self.svnpasswd = svnpasswd
100
101 self.revlinktmpl = revlinktmpl
102
103 self.environ = os.environ.copy()
104
105
106 self.svnbin = svnbin
107 self.histmax = histmax
108 self._prefix = None
109 self.category = category
110 self.project = project
111
112 self.cachepath = cachepath
113 if self.cachepath and os.path.exists(self.cachepath):
114 try:
115 with open(self.cachepath, "r") as f:
116 self.last_change = int(f.read().strip())
117 log.msg("SVNPoller: SVNPoller(%s) setting last_change to %s" % (self.svnurl, self.last_change))
118
119 with open(self.cachepath, "w") as f:
120 f.write(str(self.last_change))
121 except:
122 self.cachepath = None
123 log.msg(("SVNPoller: SVNPoller(%s) cache file corrupt or unwriteable; " +
124 "skipping and not using") % self.svnurl)
125 log.err()
126
128 return "SVNPoller: watching %s" % self.svnurl
129
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162 if self.project:
163 log.msg("SVNPoller: polling " + self.project)
164 else:
165 log.msg("SVNPoller: polling")
166
167 d = defer.succeed(None)
168 if not self._prefix:
169 d.addCallback(lambda _ : self.get_prefix())
170 def set_prefix(prefix):
171 self._prefix = prefix
172 d.addCallback(set_prefix)
173
174 d.addCallback(self.get_logs)
175 d.addCallback(self.parse_logs)
176 d.addCallback(self.get_new_logentries)
177 d.addCallback(self.create_changes)
178 d.addCallback(self.submit_changes)
179 d.addCallback(self.finished_ok)
180 d.addErrback(log.err, 'SVNPoller: Error in while polling')
181 return d
182
187
189 args = ["info", "--xml", "--non-interactive", self.svnurl]
190 if self.svnuser:
191 args.extend(["--username=%s" % self.svnuser])
192 if self.svnpasswd:
193 args.extend(["--password=%s" % self.svnpasswd])
194 if self.extra_args:
195 args.extend(self.extra_args)
196 d = self.getProcessOutput(args)
197 def determine_prefix(output):
198 try:
199 doc = xml.dom.minidom.parseString(output)
200 except xml.parsers.expat.ExpatError:
201 log.msg("SVNPoller: SVNPoller._determine_prefix_2: ExpatError in '%s'"
202 % output)
203 raise
204 rootnodes = doc.getElementsByTagName("root")
205 if not rootnodes:
206
207
208 self._prefix = ""
209 return self._prefix
210 rootnode = rootnodes[0]
211 root = "".join([c.data for c in rootnode.childNodes])
212
213 assert self.svnurl.startswith(root), \
214 ("svnurl='%s' doesn't start with <root>='%s'" %
215 (self.svnurl, root))
216 prefix = self.svnurl[len(root):]
217 if prefix.startswith("/"):
218 prefix = prefix[1:]
219 log.msg("SVNPoller: svnurl=%s, root=%s, so prefix=%s" %
220 (self.svnurl, root, prefix))
221 return prefix
222 d.addCallback(determine_prefix)
223 return d
224
226 args = []
227 args.extend(["log", "--xml", "--verbose", "--non-interactive"])
228 if self.svnuser:
229 args.extend(["--username=%s" % self.svnuser])
230 if self.svnpasswd:
231 args.extend(["--password=%s" % self.svnpasswd])
232 if self.extra_args:
233 args.extend(self.extra_args)
234 args.extend(["--limit=%d" % (self.histmax), self.svnurl])
235 d = self.getProcessOutput(args)
236 return d
237
239
240 try:
241 doc = xml.dom.minidom.parseString(output)
242 except xml.parsers.expat.ExpatError:
243 log.msg("SVNPoller: SVNPoller.parse_logs: ExpatError in '%s'" % output)
244 raise
245 logentries = doc.getElementsByTagName("logentry")
246 return logentries
247
248
250 last_change = old_last_change = self.last_change
251
252
253
254
255
256 new_last_change = None
257 new_logentries = []
258 if logentries:
259 new_last_change = int(logentries[0].getAttribute("revision"))
260
261 if last_change is None:
262
263
264
265 log.msg('SVNPoller: starting at change %s' % new_last_change)
266 elif last_change == new_last_change:
267
268 log.msg('SVNPoller: no changes')
269 else:
270 for el in logentries:
271 if last_change == int(el.getAttribute("revision")):
272 break
273 new_logentries.append(el)
274 new_logentries.reverse()
275
276 self.last_change = new_last_change
277 log.msg('SVNPoller: _process_changes %s .. %s' %
278 (old_last_change, new_last_change))
279 return new_logentries
280
281
282 - def _get_text(self, element, tag_name):
283 try:
284 child_nodes = element.getElementsByTagName(tag_name)[0].childNodes
285 text = "".join([t.data for t in child_nodes])
286 except:
287 text = "<unknown>"
288 return text
289
304
306 changes = []
307
308 for el in new_logentries:
309 revision = str(el.getAttribute("revision"))
310
311 revlink=''
312
313 if self.revlinktmpl:
314 if revision:
315 revlink = self.revlinktmpl % urllib.quote_plus(revision)
316
317 log.msg("Adding change revision %s" % (revision,))
318 author = self._get_text(el, "author")
319 comments = self._get_text(el, "msg")
320
321
322
323
324
325 branches = {}
326 try:
327 pathlist = el.getElementsByTagName("paths")[0]
328 except IndexError:
329 log.msg("ignoring commit with no paths")
330 continue
331
332 for p in pathlist.getElementsByTagName("path"):
333 kind = p.getAttribute("kind")
334 action = p.getAttribute("action")
335 path = "".join([t.data for t in p.childNodes])
336
337
338
339
340 path = path.encode("ascii")
341 if path.startswith("/"):
342 path = path[1:]
343 if kind == "dir" and not path.endswith("/"):
344 path += "/"
345 where = self._transform_path(path)
346
347
348
349 if where:
350 branch = where.get("branch", None)
351 filename = where["path"]
352 if not branch in branches:
353 branches[branch] = { 'files': [], 'number_of_directories': 0}
354 if filename == "":
355
356 branches[branch]['files'].append(filename)
357 branches[branch]['number_of_directories'] += 1
358 elif filename.endswith("/"):
359
360 branches[branch]['files'].append(filename[:-1])
361 branches[branch]['number_of_directories'] += 1
362 else:
363 branches[branch]['files'].append(filename)
364
365 if not branches[branch].has_key('action'):
366 branches[branch]['action'] = action
367
368 for key in ("repository", "project", "codebase"):
369 if key in where:
370 branches[branch][key] = where[key]
371
372 for branch in branches.keys():
373 action = branches[branch]['action']
374 files = branches[branch]['files']
375
376 number_of_directories_changed = branches[branch]['number_of_directories']
377 number_of_files_changed = len(files)
378
379 if action == u'D' and number_of_directories_changed == 1 and number_of_files_changed == 1 and files[0] == '':
380 log.msg("Ignoring deletion of branch '%s'" % branch)
381 else:
382 chdict = dict(
383 author=author,
384 files=files,
385 comments=comments,
386 revision=revision,
387 branch=branch,
388 revlink=revlink,
389 category=self.category,
390 repository=branches[branch].get('repository', self.svnurl),
391 project=branches[branch].get('project', self.project),
392 codebase=branches[branch].get('codebase', None))
393 changes.append(chdict)
394
395 return changes
396
397 @defer.inlineCallbacks
401
403 if self.cachepath:
404 with open(self.cachepath, "w") as f:
405 f.write(str(self.last_change))
406
407 log.msg("SVNPoller: finished polling %s" % res)
408 return res
409