1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 """
17 Parse various kinds of 'CVS notify' email.
18 """
19 import re
20 import time, calendar
21 import datetime
22 from email import message_from_file
23 from email.Utils import parseaddr, parsedate_tz, mktime_tz
24 from email.Iterators import body_line_iterator
25
26 from zope.interface import implements
27 from twisted.python import log
28 from twisted.internet import defer
29 from buildbot import util
30 from buildbot.interfaces import IChangeSource
31 from buildbot.util.maildir import MaildirService
32
34 """Generic base class for Maildir-based change sources"""
35 implements(IChangeSource)
36
37 compare_attrs = ["basedir", "pollinterval", "prefix"]
38
39 - def __init__(self, maildir, prefix=None, category='', repository=''):
47
49 return "%s watching maildir '%s'" % (self.__class__.__name__, self.basedir)
50
56 d.addCallback(parse_file)
57
58 def add_change(chtuple):
59 src, chdict = None, None
60 if chtuple:
61 src, chdict = chtuple
62 if chdict:
63 return self.master.addChange(src=src, **chdict)
64 else:
65 log.msg("no change found in maildir file '%s'" % filename)
66 d.addCallback(add_change)
67
68 return d
69
71 m = message_from_file(fd)
72 return self.parse(m, prefix)
73
75 name = "CVSMaildirSource"
76
77 - def __init__(self, maildir, prefix=None, category='',
78 repository='', properties={}):
81
82 - def parse(self, m, prefix=None):
83 """Parse messages sent by the 'buildbot-cvs-mail' program.
84 """
85
86
87
88
89 name, addr = parseaddr(m["from"])
90 if not addr:
91 return None
92 at = addr.find("@")
93 if at == -1:
94 author = addr
95 else:
96 author = addr[:at]
97
98
99
100
101
102
103
104
105 log.msg('Processing CVS mail')
106 dateTuple = parsedate_tz(m["date"])
107 if dateTuple == None:
108 when = util.now()
109 else:
110 when = mktime_tz(dateTuple)
111
112 theTime = datetime.datetime.utcfromtimestamp(float(when))
113 rev = theTime.strftime('%Y-%m-%d %H:%M:%S')
114
115 catRE = re.compile( '^Category:\s*(\S.*)')
116 cvsRE = re.compile( '^CVSROOT:\s*(\S.*)')
117 cvsmodeRE = re.compile( '^Cvsmode:\s*(\S.*)')
118 filesRE = re.compile( '^Files:\s*(\S.*)')
119 modRE = re.compile( '^Module:\s*(\S.*)')
120 pathRE = re.compile( '^Path:\s*(\S.*)')
121 projRE = re.compile( '^Project:\s*(\S.*)')
122 singleFileRE = re.compile( '(.*) (NONE|\d(\.|\d)+) (NONE|\d(\.|\d)+)')
123 tagRE = re.compile( '^\s+Tag:\s*(\S.*)')
124 updateRE = re.compile( '^Update of:\s*(\S.*)')
125 comments = ""
126 branch = None
127 cvsroot = None
128 fileList = None
129 files = []
130 isdir = 0
131 path = None
132 project = None
133
134 lines = list(body_line_iterator(m))
135 while lines:
136 line = lines.pop(0)
137 m = catRE.match(line)
138 if m:
139 category = m.group(1)
140 continue
141 m = cvsRE.match(line)
142 if m:
143 cvsroot = m.group(1)
144 continue
145 m = cvsmodeRE.match(line)
146 if m:
147 cvsmode = m.group(1)
148 continue
149 m = filesRE.match(line)
150 if m:
151 fileList = m.group(1)
152 continue
153 m = modRE.match(line)
154 if m:
155
156
157 continue
158 m = pathRE.match(line)
159 if m:
160 path = m.group(1)
161 continue
162 m = projRE.match(line)
163 if m:
164 project = m.group(1)
165 continue
166 m = tagRE.match(line)
167 if m:
168 branch = m.group(1)
169 continue
170 m = updateRE.match(line)
171 if m:
172
173
174 continue
175 if line == "Log Message:\n":
176 break
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199 if fileList is None:
200 log.msg('CVSMaildirSource Mail with no files. Ignoring')
201 return None
202
203 if cvsmode == '1.11':
204
205 m = re.search('([^ ]*) ', fileList)
206 if m:
207 path = m.group(1)
208 else:
209 log.msg('CVSMaildirSource can\'t get path from file list. Ignoring mail')
210 return
211 fileList = fileList[len(path):].strip()
212 singleFileRE = re.compile( '(.+?),(NONE|(?:\d+\.(?:\d+\.\d+\.)*\d+)),(NONE|(?:\d+\.(?:\d+\.\d+\.)*\d+))(?: |$)')
213 elif cvsmode == '1.12':
214 singleFileRE = re.compile( '(.+?) (NONE|(?:\d+\.(?:\d+\.\d+\.)*\d+)) (NONE|(?:\d+\.(?:\d+\.\d+\.)*\d+))(?: |$)')
215 if path is None:
216 raise ValueError('CVSMaildirSource cvs 1.12 require path. Check cvs loginfo config')
217 else:
218 raise ValueError('Expected cvsmode 1.11 or 1.12. got: %s' % cvsmode)
219
220 log.msg("CVSMaildirSource processing filelist: %s" % fileList)
221 links = []
222 while(fileList):
223 m = singleFileRE.match(fileList)
224 if m:
225 curFile = path + '/' + m.group(1)
226 files.append( curFile )
227 fileList = fileList[m.end():]
228 else:
229 log.msg('CVSMaildirSource no files matched regex. Ignoring')
230 return None
231
232 while lines:
233 line = lines.pop(0)
234 comments += line
235
236 comments = comments.rstrip() + "\n"
237 if comments == '\n':
238 comments = None
239 return ('cvs', dict(author=author, files=files, comments=comments,
240 isdir=isdir, when=when, branch=branch,
241 revision=rev, category=category,
242 repository=cvsroot, project=project,
243 links=links, properties=self.properties))
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
271 name = "SVN commit-email.pl"
272
273 - def parse(self, m, prefix=None):
274 """Parse messages sent by the svn 'commit-email.pl' trigger.
275 """
276
277
278
279
280
281 name, addr = parseaddr(m["from"])
282 if not addr:
283 return None
284 at = addr.find("@")
285 if at == -1:
286 author = addr
287 else:
288 author = addr[:at]
289
290
291
292
293
294
295
296
297
298 when = util.now()
299
300 files = []
301 comments = ""
302 lines = list(body_line_iterator(m))
303 rev = None
304 while lines:
305 line = lines.pop(0)
306
307
308 match = re.search(r"^Author: (\S+)", line)
309 if match:
310 author = match.group(1)
311
312
313 match = re.search(r"^New Revision: (\d+)", line)
314 if match:
315 rev = match.group(1)
316
317
318
319
320
321
322
323
324 if (line == "Log:\n"):
325 break
326
327
328 while lines:
329 line = lines.pop(0)
330 if (line == "Modified:\n" or
331 line == "Added:\n" or
332 line == "Removed:\n"):
333 break
334 comments += line
335 comments = comments.rstrip() + "\n"
336
337 while lines:
338 line = lines.pop(0)
339 if line == "\n":
340 break
341 if line.find("Modified:\n") == 0:
342 continue
343 if line.find("Added:\n") == 0:
344 continue
345 if line.find("Removed:\n") == 0:
346 continue
347 line = line.strip()
348
349 thesefiles = line.split(" ")
350 for f in thesefiles:
351 if prefix:
352
353
354 if f.startswith(prefix):
355 f = f[len(prefix):]
356 else:
357 log.msg("ignored file from svn commit: prefix '%s' "
358 "does not match filename '%s'" % (prefix, f))
359 continue
360
361
362
363 files.append(f)
364
365 if not files:
366 log.msg("no matching files found, ignoring commit")
367 return None
368
369 return ('svn', dict(author=author, files=files, comments=comments,
370 when=when, revision=rev))
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
400 name = "Launchpad"
401
402 compare_attrs = MaildirSource.compare_attrs + ["branchMap", "defaultBranch"]
403
404 - def __init__(self, maildir, prefix=None, branchMap=None, defaultBranch=None, **kwargs):
408
409 - def parse(self, m, prefix=None):
410 """Parse branch notification messages sent by Launchpad.
411 """
412
413 subject = m["subject"]
414 match = re.search(r"^\s*\[Branch\s+([^]]+)\]", subject)
415 if match:
416 repository = match.group(1)
417 else:
418 repository = None
419
420
421
422 d = { 'files': [], 'comments': u"" }
423 gobbler = None
424 rev = None
425 author = None
426 when = util.now()
427 def gobble_comment(s):
428 d['comments'] += s + "\n"
429 def gobble_removed(s):
430 d['files'].append('%s REMOVED' % s)
431 def gobble_added(s):
432 d['files'].append('%s ADDED' % s)
433 def gobble_modified(s):
434 d['files'].append('%s MODIFIED' % s)
435 def gobble_renamed(s):
436 match = re.search(r"^(.+) => (.+)$", s)
437 if match:
438 d['files'].append('%s RENAMED %s' % (match.group(1), match.group(2)))
439 else:
440 d['files'].append('%s RENAMED' % s)
441
442 lines = list(body_line_iterator(m, True))
443 rev = None
444 while lines:
445 line = unicode(lines.pop(0), "utf-8", errors="ignore")
446
447
448 match = re.search(r"^revno: ([0-9.]+)", line)
449 if match:
450 rev = match.group(1)
451
452
453 match = re.search(r"^committer: (.*)$", line)
454 if match:
455 author = match.group(1)
456
457
458
459
460 match = re.search(r"^timestamp: [a-zA-Z]{3} (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([-+])(\d{2})(\d{2})$", line)
461 if match:
462 datestr = match.group(1)
463 tz_sign = match.group(2)
464 tz_hours = match.group(3)
465 tz_minutes = match.group(4)
466 when = parseLaunchpadDate(datestr, tz_sign, tz_hours, tz_minutes)
467
468 if re.search(r"^message:\s*$", line):
469 gobbler = gobble_comment
470 elif re.search(r"^removed:\s*$", line):
471 gobbler = gobble_removed
472 elif re.search(r"^added:\s*$", line):
473 gobbler = gobble_added
474 elif re.search(r"^renamed:\s*$", line):
475 gobbler = gobble_renamed
476 elif re.search(r"^modified:\s*$", line):
477 gobbler = gobble_modified
478 elif re.search(r"^ ", line) and gobbler:
479 gobbler(line[2:-1])
480
481
482 branch = None
483 if self.branchMap and repository:
484 if self.branchMap.has_key(repository):
485 branch = self.branchMap[repository]
486 elif self.branchMap.has_key('lp:' + repository):
487 branch = self.branchMap['lp:' + repository]
488 if not branch:
489 if self.defaultBranch:
490 branch = self.defaultBranch
491 else:
492 if repository:
493 branch = 'lp:' + repository
494 else:
495 branch = None
496
497 if rev and author:
498 return ('bzr', dict(author=author, files=d['files'],
499 comments=d['comments'],
500 when=when, revision=rev,
501 branch=branch, repository=repository or ''))
502 else:
503 return None
504
506 time_no_tz = calendar.timegm(time.strptime(datestr, "%Y-%m-%d %H:%M:%S"))
507 tz_delta = 60 * 60 * int(tz_sign + tz_hours) + 60 * int(tz_minutes)
508 return time_no_tz - tz_delta
509