1
2
3 """
4 Parse various kinds of 'CVS notify' email.
5 """
6 import os, re
7 import time, calendar
8 from email import message_from_file
9 from email.Utils import parseaddr
10 from email.Iterators import body_line_iterator
11
12 from zope.interface import implements
13 from twisted.python import log
14 from buildbot import util
15 from buildbot.interfaces import IChangeSource
16 from buildbot.changes import changes
17 from buildbot.changes.maildir import MaildirService
18
20 """This source will watch a maildir that is subscribed to a FreshCVS
21 change-announcement mailing list.
22 """
23 implements(IChangeSource)
24
25 compare_attrs = ["basedir", "pollinterval", "prefix"]
26 name = None
27
28 - def __init__(self, maildir, prefix=None):
29 MaildirService.__init__(self, maildir)
30 self.prefix = prefix
31 if prefix and not prefix.endswith("/"):
32 log.msg("%s: you probably want your prefix=('%s') to end with "
33 "a slash")
34
36 return "%s mailing list in maildir %s" % (self.name, self.basedir)
37
45
47 m = message_from_file(fd)
48 return self.parse(m, prefix)
49
51 name = "FreshCVS"
52
53 - def parse(self, m, prefix=None):
54 """Parse mail sent by FreshCVS"""
55
56
57
58 name, addr = parseaddr(m["from"])
59 if not name:
60 return None
61 cvs = name.find(" CVS")
62 if cvs == -1:
63 return None
64 who = name[:cvs]
65
66
67
68
69 when = util.now()
70
71 files = []
72 comments = ""
73 isdir = 0
74 lines = list(body_line_iterator(m))
75 while lines:
76 line = lines.pop(0)
77 if line == "Modified files:\n":
78 break
79 while lines:
80 line = lines.pop(0)
81 if line == "\n":
82 break
83 line = line.rstrip("\n")
84 linebits = line.split(None, 1)
85 file = linebits[0]
86 if prefix:
87
88
89 if file.startswith(prefix):
90 file = file[len(prefix):]
91 else:
92 continue
93 if len(linebits) == 1:
94 isdir = 1
95 elif linebits[1] == "0 0":
96 isdir = 1
97 files.append(file)
98 while lines:
99 line = lines.pop(0)
100 if line == "Log message:\n":
101 break
102
103 while lines:
104 line = lines.pop(0)
105 if line == "ViewCVS links:\n":
106 break
107 if line.find("Index: ") == 0:
108 break
109 comments += line
110 comments = comments.rstrip() + "\n"
111
112 if not files:
113 return None
114
115 change = changes.Change(who, files, comments, isdir, when=when)
116
117 return change
118
120 name = "Syncmail"
121
122 - def parse(self, m, prefix=None):
123 """Parse messages sent by the 'syncmail' program, as suggested by the
124 sourceforge.net CVS Admin documentation. Syncmail is maintained at
125 syncmail.sf.net .
126 """
127
128
129
130
131
132
133
134 name, addr = parseaddr(m["from"])
135 if not addr:
136 return None
137 at = addr.find("@")
138 if at == -1:
139 who = addr
140 else:
141 who = addr[:at]
142
143
144
145
146
147
148
149
150
151 when = util.now()
152
153 subject = m["subject"]
154
155
156
157
158
159 space = subject.find(" ")
160 if space != -1:
161 directory = subject[:space]
162 else:
163 directory = subject
164
165 files = []
166 comments = ""
167 isdir = 0
168 branch = None
169
170 lines = list(body_line_iterator(m))
171 while lines:
172 line = lines.pop(0)
173
174 if (line == "Modified Files:\n" or
175 line == "Added Files:\n" or
176 line == "Removed Files:\n"):
177 break
178
179 while lines:
180 line = lines.pop(0)
181 if line == "\n":
182 break
183 if line == "Log Message:\n":
184 lines.insert(0, line)
185 break
186 line = line.lstrip()
187 line = line.rstrip()
188
189
190
191
192
193
194
195
196 if line.startswith('Tag:'):
197 branch = line.split(' ')[-1].rstrip()
198 continue
199
200 thesefiles = line.split(" ")
201 for f in thesefiles:
202 f = directory + "/" + f
203 if prefix:
204
205
206 if f.startswith(prefix):
207 f = f[len(prefix):]
208 else:
209 continue
210 break
211
212
213 files.append(f)
214
215 if not files:
216 return None
217
218 while lines:
219 line = lines.pop(0)
220 if line == "Log Message:\n":
221 break
222
223
224 while lines:
225 line = lines.pop(0)
226 if line.find("Index: ") == 0:
227 break
228 if re.search(r"^--- NEW FILE", line):
229 break
230 if re.search(r" DELETED ---$", line):
231 break
232 comments += line
233 comments = comments.rstrip() + "\n"
234
235 change = changes.Change(who, files, comments, isdir, when=when,
236 branch=branch)
237
238 return change
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
268 name = "Bonsai"
269
270 - def parse(self, m, prefix=None):
271 """Parse mail sent by the Bonsai cvs loginfo script."""
272
273
274
275
276 who = "unknown"
277 timestamp = None
278 files = []
279 lines = list(body_line_iterator(m))
280
281
282 while lines:
283 line = lines.pop(0)
284 if line == "LOGCOMMENT\n":
285 break;
286 line = line.rstrip("\n")
287
288
289
290
291 items = line.split('|')
292 if len(items) < 6:
293
294 return None
295
296 try:
297
298
299
300 timestamp = int(items[1])
301 except ValueError:
302 pass
303
304 user = items[2]
305 if user:
306 who = user
307
308 module = items[4]
309 file = items[5]
310 if module and file:
311 path = "%s/%s" % (module, file)
312 files.append(path)
313 sticky = items[7]
314 branch = items[8]
315
316
317 if not files:
318 return None
319
320
321 comments = ""
322 while lines:
323 line = lines.pop(0)
324 if line == ":ENDLOGCOMMENT\n":
325 break
326 comments += line
327 comments = comments.rstrip() + "\n"
328
329
330 return changes.Change(who, files, comments, when=timestamp,
331 branch=branch)
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
359 name = "SVN commit-email.pl"
360
361 - def parse(self, m, prefix=None):
362 """Parse messages sent by the svn 'commit-email.pl' trigger.
363 """
364
365
366
367
368
369 name, addr = parseaddr(m["from"])
370 if not addr:
371 return None
372 at = addr.find("@")
373 if at == -1:
374 who = addr
375 else:
376 who = addr[:at]
377
378
379
380
381
382
383
384
385
386 when = util.now()
387
388 files = []
389 comments = ""
390 isdir = 0
391 lines = list(body_line_iterator(m))
392 rev = None
393 while lines:
394 line = lines.pop(0)
395
396
397 match = re.search(r"^Author: (\S+)", line)
398 if match:
399 who = match.group(1)
400
401
402 match = re.search(r"^New Revision: (\d+)", line)
403 if match:
404 rev = match.group(1)
405
406
407
408
409
410
411
412
413 if (line == "Log:\n"):
414 break
415
416
417 while lines:
418 line = lines.pop(0)
419 if (line == "Modified:\n" or
420 line == "Added:\n" or
421 line == "Removed:\n"):
422 break
423 comments += line
424 comments = comments.rstrip() + "\n"
425
426 while lines:
427 line = lines.pop(0)
428 if line == "\n":
429 break
430 if line.find("Modified:\n") == 0:
431 continue
432 if line.find("Added:\n") == 0:
433 continue
434 if line.find("Removed:\n") == 0:
435 continue
436 line = line.strip()
437
438 thesefiles = line.split(" ")
439 for f in thesefiles:
440 if prefix:
441
442
443 if f.startswith(prefix):
444 f = f[len(prefix):]
445 else:
446 log.msg("ignored file from svn commit: prefix '%s' "
447 "does not match filename '%s'" % (prefix, f))
448 continue
449
450
451
452 files.append(f)
453
454 if not files:
455 log.msg("no matching files found, ignoring commit")
456 return None
457
458 return changes.Change(who, files, comments, when=when, revision=rev)
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
488 name = "Launchpad"
489
490 compare_attrs = MaildirSource.compare_attrs + ["branchMap", "defaultBranch"]
491
492 - def __init__(self, maildir, prefix=None, branchMap=None, defaultBranch=None, **kwargs):
496
497 - def parse(self, m, prefix=None):
498 """Parse branch notification messages sent by Launchpad.
499 """
500
501 subject = m["subject"]
502 match = re.search(r"^\s*\[Branch\s+([^]]+)\]", subject)
503 if match:
504 repository = match.group(1)
505 else:
506 repository = None
507
508
509
510 d = { 'files': [], 'comments': "" }
511 gobbler = None
512 rev = None
513 who = None
514 when = util.now()
515 def gobble_comment(s):
516 d['comments'] += s + "\n"
517 def gobble_removed(s):
518 d['files'].append('%s REMOVED' % s)
519 def gobble_added(s):
520 d['files'].append('%s ADDED' % s)
521 def gobble_modified(s):
522 d['files'].append('%s MODIFIED' % s)
523 def gobble_renamed(s):
524 match = re.search(r"^(.+) => (.+)$", s)
525 if match:
526 d['files'].append('%s RENAMED %s' % (match.group(1), match.group(2)))
527 else:
528 d['files'].append('%s RENAMED' % s)
529
530 lines = list(body_line_iterator(m, True))
531 rev = None
532 while lines:
533 line = lines.pop(0)
534
535
536 match = re.search(r"^revno: ([0-9.]+)", line)
537 if match:
538 rev = match.group(1)
539
540
541 match = re.search(r"^committer: (.*)$", line)
542 if match:
543 who = match.group(1)
544
545
546
547
548 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)
549 if match:
550 datestr = match.group(1)
551 tz_sign = match.group(2)
552 tz_hours = match.group(3)
553 tz_minutes = match.group(4)
554 when = parseLaunchpadDate(datestr, tz_sign, tz_hours, tz_minutes)
555
556 if re.search(r"^message:\s*$", line):
557 gobbler = gobble_comment
558 elif re.search(r"^removed:\s*$", line):
559 gobbler = gobble_removed
560 elif re.search(r"^added:\s*$", line):
561 gobbler = gobble_added
562 elif re.search(r"^renamed:\s*$", line):
563 gobbler = gobble_renamed
564 elif re.search(r"^modified:\s*$", line):
565 gobbler = gobble_modified
566 elif re.search(r"^ ", line) and gobbler:
567 gobbler(line[2:-1])
568
569
570 branch = None
571 if self.branchMap and repository:
572 if self.branchMap.has_key(repository):
573 branch = self.branchMap[repository]
574 elif self.branchMap.has_key('lp:' + repository):
575 branch = self.branchMap['lp:' + repository]
576 if not branch:
577 if self.defaultBranch:
578 branch = self.defaultBranch
579 else:
580 if repository:
581 branch = 'lp:' + repository
582 else:
583 branch = None
584
585
586 if rev and who:
587 return changes.Change(who, d['files'], d['comments'],
588 when=when, revision=rev, branch=branch)
589 else:
590 return None
591
593 time_no_tz = calendar.timegm(time.strptime(datestr, "%Y-%m-%d %H:%M:%S"))
594 tz_delta = 60 * 60 * int(tz_sign + tz_hours) + 60 * int(tz_minutes)
595 return time_no_tz - tz_delta
596