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