1
2
3 """
4 Parse various kinds of 'CVS notify' email.
5 """
6 import os, re
7 import time, calendar
8 import datetime
9 from email import message_from_file
10 from email.Utils import parseaddr, parsedate_tz, mktime_tz
11 from email.Iterators import body_line_iterator
12
13 from zope.interface import implements
14 from twisted.python import log
15 from buildbot import util
16 from buildbot.interfaces import IChangeSource
17 from buildbot.changes import changes
18 from buildbot.changes.maildir import MaildirService
19
21 """This source will watch a maildir that is subscribed to a FreshCVS
22 change-announcement mailing list.
23 """
24 implements(IChangeSource)
25
26 compare_attrs = ["basedir", "pollinterval", "prefix"]
27 name = None
28
29 - def __init__(self, maildir, prefix=None, category='', repository=''):
37
39 return "%s mailing list in maildir %s" % (self.name, self.basedir)
40
48
50 m = message_from_file(fd)
51 return self.parse(m, prefix)
52
54 name = "FreshCVS"
55
56 - def parse(self, m, prefix=None):
57 """Parse mail sent by FreshCVS"""
58
59
60
61 name, addr = parseaddr(m["from"])
62 if not name:
63 return None
64 cvs = name.find(" CVS")
65 if cvs == -1:
66 return None
67 who = name[:cvs]
68
69
70
71
72 when = util.now()
73
74 files = []
75 comments = ""
76 isdir = 0
77 lines = list(body_line_iterator(m))
78 while lines:
79 line = lines.pop(0)
80 if line == "Modified files:\n":
81 break
82 while lines:
83 line = lines.pop(0)
84 if line == "\n":
85 break
86 line = line.rstrip("\n")
87 linebits = line.split(None, 1)
88 file = linebits[0]
89 if prefix:
90
91
92 if file.startswith(prefix):
93 file = file[len(prefix):]
94 else:
95 continue
96 if len(linebits) == 1:
97 isdir = 1
98 elif linebits[1] == "0 0":
99 isdir = 1
100 files.append(file)
101 while lines:
102 line = lines.pop(0)
103 if line == "Log message:\n":
104 break
105
106 while lines:
107 line = lines.pop(0)
108 if line == "ViewCVS links:\n":
109 break
110 if line.find("Index: ") == 0:
111 break
112 comments += line
113 comments = comments.rstrip() + "\n"
114
115 if not files:
116 return None
117
118 change = changes.Change(who, files, comments, isdir, when=when)
119
120 return change
121
123 name = "Syncmail"
124
125 - def parse(self, m, prefix=None):
126 """Parse messages sent by the 'syncmail' program, as suggested by the
127 sourceforge.net CVS Admin documentation. Syncmail is maintained at
128 syncmail.sf.net .
129 """
130
131
132
133
134
135
136
137 name, addr = parseaddr(m["from"])
138 if not addr:
139 return None
140 at = addr.find("@")
141 if at == -1:
142 who = addr
143 else:
144 who = addr[:at]
145
146
147
148
149
150
151
152
153
154 when = util.now()
155
156
157 theCurrentTime = datetime.datetime.utcfromtimestamp(float(when))
158 rev = theCurrentTime.strftime('%Y-%m-%d %H:%M:%S')
159
160 subject = m["subject"]
161
162
163
164
165
166 space = subject.find(" ")
167 if space != -1:
168 directory = subject[:space]
169 else:
170 directory = subject
171
172 files = []
173 comments = ""
174 isdir = 0
175 branch = None
176
177 lines = list(body_line_iterator(m))
178 while lines:
179 line = lines.pop(0)
180
181 if (line == "Modified Files:\n" or
182 line == "Added Files:\n" or
183 line == "Removed Files:\n"):
184 break
185
186 while lines:
187 line = lines.pop(0)
188 if line == "\n":
189 break
190 if line == "Log Message:\n":
191 lines.insert(0, line)
192 break
193 line = line.lstrip()
194 line = line.rstrip()
195
196
197
198
199
200
201
202
203 if line.startswith('Tag:'):
204 branch = line.split(' ')[-1].rstrip()
205 continue
206
207 thesefiles = line.split(" ")
208 for f in thesefiles:
209 f = directory + "/" + f
210 if prefix:
211
212
213 if f.startswith(prefix):
214 f = f[len(prefix):]
215 else:
216 continue
217 break
218
219
220 files.append(f)
221
222 if not files:
223 return None
224
225 while lines:
226 line = lines.pop(0)
227 if line == "Log Message:\n":
228 break
229
230
231 while lines:
232 line = lines.pop(0)
233 if line.find("Index: ") == 0:
234 break
235 if re.search(r"^--- NEW FILE", line):
236 break
237 if re.search(r" DELETED ---$", line):
238 break
239 comments += line
240 comments = comments.rstrip() + "\n"
241
242 change = changes.Change(who, files, comments, isdir, when=when,
243 branch=branch, revision=rev,
244 category=self.category,
245 repository=self.repository)
246
247 return change
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
277 name = "Bonsai"
278
279 - def parse(self, m, prefix=None):
280 """Parse mail sent by the Bonsai cvs loginfo script."""
281
282
283
284
285 who = "unknown"
286 timestamp = None
287 files = []
288 lines = list(body_line_iterator(m))
289
290
291 while lines:
292 line = lines.pop(0)
293 if line == "LOGCOMMENT\n":
294 break;
295 line = line.rstrip("\n")
296
297
298
299
300 items = line.split('|')
301 if len(items) < 6:
302
303 return None
304
305 try:
306
307
308
309 timestamp = int(items[1])
310 except ValueError:
311 pass
312
313 user = items[2]
314 if user:
315 who = user
316
317 module = items[4]
318 file = items[5]
319 if module and file:
320 path = "%s/%s" % (module, file)
321 files.append(path)
322 sticky = items[7]
323 branch = items[8]
324
325
326 if not files:
327 return None
328
329
330 comments = ""
331 while lines:
332 line = lines.pop(0)
333 if line == ":ENDLOGCOMMENT\n":
334 break
335 comments += line
336 comments = comments.rstrip() + "\n"
337
338
339 return changes.Change(who, files, comments, when=timestamp,
340 branch=branch)
341
343 name = "CVSMaildirSource"
344
345 - def __init__(self, maildir, prefix=None, category='',
346 repository='', urlmaker=None, properties={}):
353
354 - def parse(self, m, prefix=None):
355 """Parse messages sent by the 'buildbot-cvs-mail' program.
356 """
357
358
359
360
361 name, addr = parseaddr(m["from"])
362 if not addr:
363 return None
364 at = addr.find("@")
365 if at == -1:
366 who = addr
367 else:
368 who = addr[:at]
369
370
371
372
373
374
375
376
377 log.msg('Processing CVS mail')
378 dateTuple = parsedate_tz(m["date"])
379 if dateTuple == None:
380 when = util.now()
381 else:
382 when = mktime_tz(dateTuple)
383
384 theTime = datetime.datetime.utcfromtimestamp(float(when))
385 rev = theTime.strftime('%Y-%m-%d %H:%M:%S')
386
387 catRE = re.compile( '^Category:\s*(\S.*)')
388 cvsRE = re.compile( '^CVSROOT:\s*(\S.*)')
389 cvsmodeRE = re.compile( '^Cvsmode:\s*(\S.*)')
390 filesRE = re.compile( '^Files:\s*(\S.*)')
391 modRE = re.compile( '^Module:\s*(\S.*)')
392 pathRE = re.compile( '^Path:\s*(\S.*)')
393 projRE = re.compile( '^Project:\s*(\S.*)')
394 singleFileRE = re.compile( '(.*) (NONE|\d(\.|\d)+) (NONE|\d(\.|\d)+)')
395 tagRE = re.compile( '^\s+Tag:\s*(\S.*)')
396 updateRE = re.compile( '^Update of:\s*(\S.*)')
397 comments = ""
398 branch = None
399 cvsroot = None
400 fileList = None
401 files = []
402 isdir = 0
403 path = None
404 project = None
405
406 lines = list(body_line_iterator(m))
407 while lines:
408 line = lines.pop(0)
409 m = catRE.match(line)
410 if m:
411 category = m.group(1)
412 continue
413 m = cvsRE.match(line)
414 if m:
415 cvsroot = m.group(1)
416 continue
417 m = cvsmodeRE.match(line)
418 if m:
419 cvsmode = m.group(1)
420 continue
421 m = filesRE.match(line)
422 if m:
423 fileList = m.group(1)
424 continue
425 m = modRE.match(line)
426 if m:
427 module = m.group(1)
428 continue
429 m = pathRE.match(line)
430 if m:
431 path = m.group(1)
432 continue
433 m = projRE.match(line)
434 if m:
435 project = m.group(1)
436 continue
437 m = tagRE.match(line)
438 if m:
439 branch = m.group(1)
440 continue
441 m = updateRE.match(line)
442 if m:
443 updateof = m.group(1)
444 continue
445 if line == "Log Message:\n":
446 break
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469 if fileList is None:
470 log.msg('CVSMaildirSource Mail with no files. Ignoring')
471 return None
472
473 if cvsmode == '1.11':
474
475 m = re.search('([^ ]*) ', fileList)
476 if m:
477 path = m.group(1)
478 else:
479 log.msg('CVSMaildirSource can\'t get path from file list. Ignoring mail')
480 return
481 fileList = fileList[len(path):].strip()
482 singleFileRE = re.compile( '(.+?),(NONE|(?:\d+\.(?:\d+\.\d+\.)*\d+)),(NONE|(?:\d+\.(?:\d+\.\d+\.)*\d+))(?: |$)')
483 elif cvsmode == '1.12':
484 singleFileRE = re.compile( '(.+?) (NONE|(?:\d+\.(?:\d+\.\d+\.)*\d+)) (NONE|(?:\d+\.(?:\d+\.\d+\.)*\d+))(?: |$)')
485 if path is None:
486 raise ValueError('CVSMaildirSource cvs 1.12 require path. Check cvs loginfo config')
487 else:
488 raise ValueError('Expected cvsmode 1.11 or 1.12. got: %s' % cvsmode)
489
490 log.msg("CVSMaildirSource processing filelist: %s" % fileList)
491 links = []
492 while(fileList):
493 m = singleFileRE.match(fileList)
494 if m:
495 curFile = path + '/' + m.group(1)
496 oldRev = m.group(2)
497 newRev = m.group(3)
498 files.append( curFile )
499 if self.urlmaker:
500 links.append(self.urlmaker(curFile, oldRev, newRev ))
501 fileList = fileList[m.end():]
502 else:
503 log.msg('CVSMaildirSource no files matched regex. Ignoring')
504 return None
505
506 while lines:
507 line = lines.pop(0)
508 comments += line
509
510 comments = comments.rstrip() + "\n"
511 if comments == '\n':
512 comments = None
513 change = changes.Change(who, files, comments, isdir, when=when,
514 branch=branch, revision=rev,
515 category=category,
516 repository=cvsroot,
517 project=project,
518 links=links,
519 properties=self.properties)
520 return change
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
548 name = "SVN commit-email.pl"
549
550 - def parse(self, m, prefix=None):
551 """Parse messages sent by the svn 'commit-email.pl' trigger.
552 """
553
554
555
556
557
558 name, addr = parseaddr(m["from"])
559 if not addr:
560 return None
561 at = addr.find("@")
562 if at == -1:
563 who = addr
564 else:
565 who = addr[:at]
566
567
568
569
570
571
572
573
574
575 when = util.now()
576
577 files = []
578 comments = ""
579 isdir = 0
580 lines = list(body_line_iterator(m))
581 rev = None
582 while lines:
583 line = lines.pop(0)
584
585
586 match = re.search(r"^Author: (\S+)", line)
587 if match:
588 who = match.group(1)
589
590
591 match = re.search(r"^New Revision: (\d+)", line)
592 if match:
593 rev = match.group(1)
594
595
596
597
598
599
600
601
602 if (line == "Log:\n"):
603 break
604
605
606 while lines:
607 line = lines.pop(0)
608 if (line == "Modified:\n" or
609 line == "Added:\n" or
610 line == "Removed:\n"):
611 break
612 comments += line
613 comments = comments.rstrip() + "\n"
614
615 while lines:
616 line = lines.pop(0)
617 if line == "\n":
618 break
619 if line.find("Modified:\n") == 0:
620 continue
621 if line.find("Added:\n") == 0:
622 continue
623 if line.find("Removed:\n") == 0:
624 continue
625 line = line.strip()
626
627 thesefiles = line.split(" ")
628 for f in thesefiles:
629 if prefix:
630
631
632 if f.startswith(prefix):
633 f = f[len(prefix):]
634 else:
635 log.msg("ignored file from svn commit: prefix '%s' "
636 "does not match filename '%s'" % (prefix, f))
637 continue
638
639
640
641 files.append(f)
642
643 if not files:
644 log.msg("no matching files found, ignoring commit")
645 return None
646
647 return changes.Change(who, files, comments, when=when, revision=rev)
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
677 name = "Launchpad"
678
679 compare_attrs = MaildirSource.compare_attrs + ["branchMap", "defaultBranch"]
680
681 - def __init__(self, maildir, prefix=None, branchMap=None, defaultBranch=None, **kwargs):
685
686 - def parse(self, m, prefix=None):
687 """Parse branch notification messages sent by Launchpad.
688 """
689
690 subject = m["subject"]
691 match = re.search(r"^\s*\[Branch\s+([^]]+)\]", subject)
692 if match:
693 repository = match.group(1)
694 else:
695 repository = None
696
697
698
699 d = { 'files': [], 'comments': "" }
700 gobbler = None
701 rev = None
702 who = None
703 when = util.now()
704 def gobble_comment(s):
705 d['comments'] += s + "\n"
706 def gobble_removed(s):
707 d['files'].append('%s REMOVED' % s)
708 def gobble_added(s):
709 d['files'].append('%s ADDED' % s)
710 def gobble_modified(s):
711 d['files'].append('%s MODIFIED' % s)
712 def gobble_renamed(s):
713 match = re.search(r"^(.+) => (.+)$", s)
714 if match:
715 d['files'].append('%s RENAMED %s' % (match.group(1), match.group(2)))
716 else:
717 d['files'].append('%s RENAMED' % s)
718
719 lines = list(body_line_iterator(m, True))
720 rev = None
721 while lines:
722 line = lines.pop(0)
723
724
725 match = re.search(r"^revno: ([0-9.]+)", line)
726 if match:
727 rev = match.group(1)
728
729
730 match = re.search(r"^committer: (.*)$", line)
731 if match:
732 who = match.group(1)
733
734
735
736
737 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)
738 if match:
739 datestr = match.group(1)
740 tz_sign = match.group(2)
741 tz_hours = match.group(3)
742 tz_minutes = match.group(4)
743 when = parseLaunchpadDate(datestr, tz_sign, tz_hours, tz_minutes)
744
745 if re.search(r"^message:\s*$", line):
746 gobbler = gobble_comment
747 elif re.search(r"^removed:\s*$", line):
748 gobbler = gobble_removed
749 elif re.search(r"^added:\s*$", line):
750 gobbler = gobble_added
751 elif re.search(r"^renamed:\s*$", line):
752 gobbler = gobble_renamed
753 elif re.search(r"^modified:\s*$", line):
754 gobbler = gobble_modified
755 elif re.search(r"^ ", line) and gobbler:
756 gobbler(line[2:-1])
757
758
759 branch = None
760 if self.branchMap and repository:
761 if self.branchMap.has_key(repository):
762 branch = self.branchMap[repository]
763 elif self.branchMap.has_key('lp:' + repository):
764 branch = self.branchMap['lp:' + repository]
765 if not branch:
766 if self.defaultBranch:
767 branch = self.defaultBranch
768 else:
769 if repository:
770 branch = 'lp:' + repository
771 else:
772 branch = None
773
774
775 if rev and who:
776 return changes.Change(who, d['files'], d['comments'],
777 when=when, revision=rev, branch=branch,
778 repository=repository or '')
779 else:
780 return None
781
783 time_no_tz = calendar.timegm(time.strptime(datestr, "%Y-%m-%d %H:%M:%S"))
784 tz_delta = 60 * 60 * int(tz_sign + tz_hours) + 60 * int(tz_minutes)
785 return time_no_tz - tz_delta
786