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, 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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
368 name = "SVN commit-email.pl"
369
370 - def parse(self, m, prefix=None):
371 """Parse messages sent by the svn 'commit-email.pl' trigger.
372 """
373
374
375
376
377
378 name, addr = parseaddr(m["from"])
379 if not addr:
380 return None
381 at = addr.find("@")
382 if at == -1:
383 who = addr
384 else:
385 who = addr[:at]
386
387
388
389
390
391
392
393
394
395 when = util.now()
396
397 files = []
398 comments = ""
399 isdir = 0
400 lines = list(body_line_iterator(m))
401 rev = None
402 while lines:
403 line = lines.pop(0)
404
405
406 match = re.search(r"^Author: (\S+)", line)
407 if match:
408 who = match.group(1)
409
410
411 match = re.search(r"^New Revision: (\d+)", line)
412 if match:
413 rev = match.group(1)
414
415
416
417
418
419
420
421
422 if (line == "Log:\n"):
423 break
424
425
426 while lines:
427 line = lines.pop(0)
428 if (line == "Modified:\n" or
429 line == "Added:\n" or
430 line == "Removed:\n"):
431 break
432 comments += line
433 comments = comments.rstrip() + "\n"
434
435 while lines:
436 line = lines.pop(0)
437 if line == "\n":
438 break
439 if line.find("Modified:\n") == 0:
440 continue
441 if line.find("Added:\n") == 0:
442 continue
443 if line.find("Removed:\n") == 0:
444 continue
445 line = line.strip()
446
447 thesefiles = line.split(" ")
448 for f in thesefiles:
449 if prefix:
450
451
452 if f.startswith(prefix):
453 f = f[len(prefix):]
454 else:
455 log.msg("ignored file from svn commit: prefix '%s' "
456 "does not match filename '%s'" % (prefix, f))
457 continue
458
459
460
461 files.append(f)
462
463 if not files:
464 log.msg("no matching files found, ignoring commit")
465 return None
466
467 return changes.Change(who, files, comments, when=when, revision=rev)
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
497 name = "Launchpad"
498
499 compare_attrs = MaildirSource.compare_attrs + ["branchMap", "defaultBranch"]
500
501 - def __init__(self, maildir, prefix=None, branchMap=None, defaultBranch=None, **kwargs):
505
506 - def parse(self, m, prefix=None):
507 """Parse branch notification messages sent by Launchpad.
508 """
509
510 subject = m["subject"]
511 match = re.search(r"^\s*\[Branch\s+([^]]+)\]", subject)
512 if match:
513 repository = match.group(1)
514 else:
515 repository = None
516
517
518
519 d = { 'files': [], 'comments': "" }
520 gobbler = None
521 rev = None
522 who = None
523 when = util.now()
524 def gobble_comment(s):
525 d['comments'] += s + "\n"
526 def gobble_removed(s):
527 d['files'].append('%s REMOVED' % s)
528 def gobble_added(s):
529 d['files'].append('%s ADDED' % s)
530 def gobble_modified(s):
531 d['files'].append('%s MODIFIED' % s)
532 def gobble_renamed(s):
533 match = re.search(r"^(.+) => (.+)$", s)
534 if match:
535 d['files'].append('%s RENAMED %s' % (match.group(1), match.group(2)))
536 else:
537 d['files'].append('%s RENAMED' % s)
538
539 lines = list(body_line_iterator(m, True))
540 rev = None
541 while lines:
542 line = lines.pop(0)
543
544
545 match = re.search(r"^revno: ([0-9.]+)", line)
546 if match:
547 rev = match.group(1)
548
549
550 match = re.search(r"^committer: (.*)$", line)
551 if match:
552 who = match.group(1)
553
554
555
556
557 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)
558 if match:
559 datestr = match.group(1)
560 tz_sign = match.group(2)
561 tz_hours = match.group(3)
562 tz_minutes = match.group(4)
563 when = parseLaunchpadDate(datestr, tz_sign, tz_hours, tz_minutes)
564
565 if re.search(r"^message:\s*$", line):
566 gobbler = gobble_comment
567 elif re.search(r"^removed:\s*$", line):
568 gobbler = gobble_removed
569 elif re.search(r"^added:\s*$", line):
570 gobbler = gobble_added
571 elif re.search(r"^renamed:\s*$", line):
572 gobbler = gobble_renamed
573 elif re.search(r"^modified:\s*$", line):
574 gobbler = gobble_modified
575 elif re.search(r"^ ", line) and gobbler:
576 gobbler(line[2:-1])
577
578
579 branch = None
580 if self.branchMap and repository:
581 if self.branchMap.has_key(repository):
582 branch = self.branchMap[repository]
583 elif self.branchMap.has_key('lp:' + repository):
584 branch = self.branchMap['lp:' + repository]
585 if not branch:
586 if self.defaultBranch:
587 branch = self.defaultBranch
588 else:
589 if repository:
590 branch = 'lp:' + repository
591 else:
592 branch = None
593
594
595 if rev and who:
596 return changes.Change(who, d['files'], d['comments'],
597 when=when, revision=rev, branch=branch,
598 repository=repository or '')
599 else:
600 return None
601
603 time_no_tz = calendar.timegm(time.strptime(datestr, "%Y-%m-%d %H:%M:%S"))
604 tz_delta = 60 * 60 * int(tz_sign + tz_hours) + 60 * int(tz_minutes)
605 return time_no_tz - tz_delta
606