Package buildbot :: Package changes :: Module mail
[frames] | no frames]

Source Code for Module buildbot.changes.mail

  1  # -*- test-case-name: buildbot.test.test_mailparse -*- 
  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   
20 -class MaildirSource(MaildirService, util.ComparableMixin):
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=''):
30 MaildirService.__init__(self, maildir) 31 self.prefix = prefix 32 self.category = category 33 self.repository = repository 34 if prefix and not prefix.endswith("/"): 35 log.msg("%s: you probably want your prefix=('%s') to end with " 36 "a slash")
37
38 - def describe(self):
39 return "%s mailing list in maildir %s" % (self.name, self.basedir)
40
41 - def messageReceived(self, filename):
42 path = os.path.join(self.basedir, "new", filename) 43 change = self.parse_file(open(path, "r"), self.prefix) 44 if change: 45 self.parent.addChange(change) 46 os.rename(os.path.join(self.basedir, "new", filename), 47 os.path.join(self.basedir, "cur", filename))
48
49 - def parse_file(self, fd, prefix=None):
50 m = message_from_file(fd) 51 return self.parse(m, prefix)
52
53 -class FCMaildirSource(MaildirSource):
54 name = "FreshCVS" 55
56 - def parse(self, m, prefix=None):
57 """Parse mail sent by FreshCVS""" 58 59 # FreshCVS sets From: to "user CVS <user>", but the <> part may be 60 # modified by the MTA (to include a local domain) 61 name, addr = parseaddr(m["from"]) 62 if not name: 63 return None # no From means this message isn't from FreshCVS 64 cvs = name.find(" CVS") 65 if cvs == -1: 66 return None # this message isn't from FreshCVS 67 who = name[:cvs] 68 69 # we take the time of receipt as the time of checkin. Not correct, 70 # but it avoids the out-of-order-changes issue. See the comment in 71 # parseSyncmail about using the 'Date:' header 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 # insist that the file start with the prefix: FreshCVS sends 91 # changes we don't care about too 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 # message is terminated by "ViewCVS links:" or "Index:..." (patch) 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
122 -class SyncmailMaildirSource(MaildirSource):
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 # pretty much the same as freshcvs mail, not surprising since CVS is 131 # the one creating most of the text 132 133 # The mail is sent from the person doing the checkin. Assume that the 134 # local username is enough to identify them (this assumes a one-server 135 # cvs-over-rsh environment rather than the server-dirs-shared-over-NFS 136 # model) 137 name, addr = parseaddr(m["from"]) 138 if not addr: 139 return None # no From means this message isn't from FreshCVS 140 at = addr.find("@") 141 if at == -1: 142 who = addr # might still be useful 143 else: 144 who = addr[:at] 145 146 # we take the time of receipt as the time of checkin. Not correct (it 147 # depends upon the email latency), but it avoids the 148 # out-of-order-changes issue. Also syncmail doesn't give us anything 149 # better to work with, unless you count pulling the v1-vs-v2 150 # timestamp out of the diffs, which would be ugly. TODO: Pulling the 151 # 'Date:' header from the mail is a possibility, and 152 # email.Utils.parsedate_tz may be useful. It should be configurable, 153 # however, because there are a lot of broken clocks out there. 154 when = util.now() 155 156 # calculate a "revision" based on that timestamp 157 theCurrentTime = datetime.datetime.utcfromtimestamp(float(when)) 158 rev = theCurrentTime.strftime('%Y-%m-%d %H:%M:%S') 159 160 subject = m["subject"] 161 # syncmail puts the repository-relative directory in the subject: 162 # mprefix + "%(dir)s %(file)s,%(oldversion)s,%(newversion)s", where 163 # 'mprefix' is something that could be added by a mailing list 164 # manager. 165 # this is the only reasonable way to determine the directory name 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 # note: syncmail will send one email per directory involved in a 196 # commit, with multiple files if they were in the same directory. 197 # Unlike freshCVS, it makes no attempt to collect all related 198 # commits into a single message. 199 200 # note: syncmail will report a Tag underneath the ... Files: line 201 # e.g.: Tag: BRANCH-DEVEL 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 # insist that the file start with the prefix: we may get 212 # changes we don't care about too 213 if f.startswith(prefix): 214 f = f[len(prefix):] 215 else: 216 continue 217 break 218 # TODO: figure out how new directories are described, set 219 # .isdir 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 # message is terminated by "Index:..." (patch) or "--- NEW FILE.." 230 # or "--- filename DELETED ---". Sigh. 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 # Bonsai mail parser by Stephen Davis. 250 # 251 # This handles changes for CVS repositories that are watched by Bonsai 252 # (http://www.mozilla.org/bonsai.html) 253 254 # A Bonsai-formatted email message looks like: 255 # 256 # C|1071099907|stephend|/cvs|Sources/Scripts/buildbot|bonsai.py|1.2|||18|7 257 # A|1071099907|stephend|/cvs|Sources/Scripts/buildbot|master.cfg|1.1|||18|7 258 # R|1071099907|stephend|/cvs|Sources/Scripts/buildbot|BuildMaster.py||| 259 # LOGCOMMENT 260 # Updated bonsai parser and switched master config to buildbot-0.4.1 style. 261 # 262 # :ENDLOGCOMMENT 263 # 264 # In the first example line, stephend is the user, /cvs the repository, 265 # buildbot the directory, bonsai.py the file, 1.2 the revision, no sticky 266 # and branch, 18 lines added and 7 removed. All of these fields might not be 267 # present (during "removes" for example). 268 # 269 # There may be multiple "control" lines or even none (imports, directory 270 # additions) but there is one email per directory. We only care about actual 271 # changes since it is presumed directory additions don't actually affect the 272 # build. At least one file should need to change (the makefile, say) to 273 # actually make a new directory part of the build process. That's my story 274 # and I'm sticking to it. 275
276 -class BonsaiMaildirSource(MaildirSource):
277 name = "Bonsai" 278
279 - def parse(self, m, prefix=None):
280 """Parse mail sent by the Bonsai cvs loginfo script.""" 281 282 # we don't care who the email came from b/c the cvs user is in the 283 # msg text 284 285 who = "unknown" 286 timestamp = None 287 files = [] 288 lines = list(body_line_iterator(m)) 289 290 # read the control lines (what/who/where/file/etc.) 291 while lines: 292 line = lines.pop(0) 293 if line == "LOGCOMMENT\n": 294 break; 295 line = line.rstrip("\n") 296 297 # we'd like to do the following but it won't work if the number of 298 # items doesn't match so... 299 # what, timestamp, user, repo, module, file = line.split( '|' ) 300 items = line.split('|') 301 if len(items) < 6: 302 # not a valid line, assume this isn't a bonsai message 303 return None 304 305 try: 306 # just grab the bottom-most timestamp, they're probably all the 307 # same. TODO: I'm assuming this is relative to the epoch, but 308 # this needs testing. 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 # if no files changed, return nothing 326 if not files: 327 return None 328 329 # read the comments 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 # return buildbot Change object 339 return changes.Change(who, files, comments, when=timestamp, 340 branch=branch)
341
342 -class CVSMaildirSource(MaildirSource):
343 name = "CVSMaildirSource" 344
345 - def __init__(self, maildir, prefix=None, category='', 346 repository='', urlmaker=None, properties={}):
347 """If urlmaker is defined, it will be called with three arguments: 348 filename, previous version, new version. It returns a url for that 349 file.""" 350 MaildirSource.__init__(self, maildir, prefix, category, repository) 351 self.urlmaker = urlmaker 352 self.properties = properties
353
354 - def parse(self, m, prefix=None):
355 """Parse messages sent by the 'buildbot-cvs-mail' program. 356 """ 357 # The mail is sent from the person doing the checkin. Assume that the 358 # local username is enough to identify them (this assumes a one-server 359 # cvs-over-rsh environment rather than the server-dirs-shared-over-NFS 360 # model) 361 name, addr = parseaddr(m["from"]) 362 if not addr: 363 return None # no From means this message isn't from buildbot-cvs-mail 364 at = addr.find("@") 365 if at == -1: 366 who = addr # might still be useful 367 else: 368 who = addr[:at] 369 370 # CVS accecpts RFC822 dates. buildbot-cvs-mail adds the date as 371 # part of the mail header, so use that. 372 # This assumes cvs is being access via ssh or pserver, so the time 373 # will be the CVS server's time. 374 375 # calculate a "revision" based on that timestamp, or the current time 376 # if we're unable to parse the date. 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 # CVS 1.11 lists files as: 449 # repo/path file,old-version,new-version file2,old-version,new-version 450 # Version 1.12 lists files as: 451 # file1 old-version new-version file2 old-version new-version 452 # 453 # files consists of tuples of 'file-name old-version new-version' 454 # The versions are either dotted-decimal version numbers, ie 1.1 455 # or NONE. New files are of the form 'NONE NUMBER', while removed 456 # files are 'NUMBER NONE'. 'NONE' is a literal string 457 # Parsing this instead of files list in 'Added File:' etc 458 # makes it possible to handle files with embedded spaces, though 459 # it could fail if the filename was 'bad 1.1 1.2' 460 # For cvs version 1.11, we expect 461 # my_module new_file.c,NONE,1.1 462 # my_module removed.txt,1.2,NONE 463 # my_module modified_file.c,1.1,1.2 464 # While cvs version 1.12 gives us 465 # new_file.c NONE 1.1 466 # removed.txt 1.2 NONE 467 # modified_file.c 1.1,1.2 468 469 if fileList is None: 470 log.msg('CVSMaildirSource Mail with no files. Ignoring') 471 return None # We don't have any files. Email not from CVS 472 473 if cvsmode == '1.11': 474 # Please, no repo paths with spaces! 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 # bail - we couldn't parse the files that changed 505 # Now get comments 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 # svn "commit-email.pl" handler. The format is very similar to freshcvs mail; 523 # here's a sample: 524 525 # From: username [at] apache.org [slightly obfuscated to avoid spam here] 526 # To: commits [at] spamassassin.apache.org 527 # Subject: svn commit: r105955 - in spamassassin/trunk: . lib/Mail 528 # ... 529 # 530 # Author: username 531 # Date: Sat Nov 20 00:17:49 2004 [note: TZ = local tz on server!] 532 # New Revision: 105955 533 # 534 # Modified: [also Removed: and Added:] 535 # [filename] 536 # ... 537 # Log: 538 # [log message] 539 # ... 540 # 541 # 542 # Modified: spamassassin/trunk/lib/Mail/SpamAssassin.pm 543 # [unified diff] 544 # 545 # [end of mail] 546
547 -class SVNCommitEmailMaildirSource(MaildirSource):
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 # The mail is sent from the person doing the checkin. Assume that the 555 # local username is enough to identify them (this assumes a one-server 556 # cvs-over-rsh environment rather than the server-dirs-shared-over-NFS 557 # model) 558 name, addr = parseaddr(m["from"]) 559 if not addr: 560 return None # no From means this message isn't from FreshCVS 561 at = addr.find("@") 562 if at == -1: 563 who = addr # might still be useful 564 else: 565 who = addr[:at] 566 567 # we take the time of receipt as the time of checkin. Not correct (it 568 # depends upon the email latency), but it avoids the 569 # out-of-order-changes issue. Also syncmail doesn't give us anything 570 # better to work with, unless you count pulling the v1-vs-v2 571 # timestamp out of the diffs, which would be ugly. TODO: Pulling the 572 # 'Date:' header from the mail is a possibility, and 573 # email.Utils.parsedate_tz may be useful. It should be configurable, 574 # however, because there are a lot of broken clocks out there. 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 # "Author: jmason" 586 match = re.search(r"^Author: (\S+)", line) 587 if match: 588 who = match.group(1) 589 590 # "New Revision: 105955" 591 match = re.search(r"^New Revision: (\d+)", line) 592 if match: 593 rev = match.group(1) 594 595 # possible TODO: use "Date: ..." data here instead of time of 596 # commit message receipt, above. however, this timestamp is 597 # specified *without* a timezone, in the server's local TZ, so to 598 # be accurate buildbot would need a config setting to specify the 599 # source server's expected TZ setting! messy. 600 601 # this stanza ends with the "Log:" 602 if (line == "Log:\n"): 603 break 604 605 # commit message is terminated by the file-listing section 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 # ignore this line 621 if line.find("Added:\n") == 0: 622 continue # ignore this line 623 if line.find("Removed:\n") == 0: 624 continue # ignore this line 625 line = line.strip() 626 627 thesefiles = line.split(" ") 628 for f in thesefiles: 629 if prefix: 630 # insist that the file start with the prefix: we may get 631 # changes we don't care about too 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 # TODO: figure out how new directories are described, set 640 # .isdir 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 # bzr Launchpad branch subscription mails. Sample mail: 650 # 651 # From: noreply@launchpad.net 652 # Subject: [Branch ~knielsen/maria/tmp-buildbot-test] Rev 2701: test add file 653 # To: Joe <joe@acme.com> 654 # ... 655 # 656 # ------------------------------------------------------------ 657 # revno: 2701 658 # committer: Joe <joe@acme.com> 659 # branch nick: tmpbb 660 # timestamp: Fri 2009-05-15 10:35:43 +0200 661 # message: 662 # test add file 663 # added: 664 # test-add-file 665 # 666 # 667 # -- 668 # 669 # https://code.launchpad.net/~knielsen/maria/tmp-buildbot-test 670 # 671 # You are subscribed to branch lp:~knielsen/maria/tmp-buildbot-test. 672 # To unsubscribe from this branch go to https://code.launchpad.net/~knielsen/maria/tmp-buildbot-test/+edit-subscription. 673 # 674 # [end of mail] 675
676 -class BzrLaunchpadEmailMaildirSource(MaildirSource):
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):
682 self.branchMap = branchMap 683 self.defaultBranch = defaultBranch 684 MaildirSource.__init__(self, maildir, prefix, **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 # Put these into a dictionary, otherwise we cannot assign them 698 # from nested function definitions. 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 # revno: 101 725 match = re.search(r"^revno: ([0-9.]+)", line) 726 if match: 727 rev = match.group(1) 728 729 # committer: Joe <joe@acme.com> 730 match = re.search(r"^committer: (.*)$", line) 731 if match: 732 who = match.group(1) 733 734 # timestamp: Fri 2009-05-15 10:35:43 +0200 735 # datetime.strptime() is supposed to support %z for time zone, but 736 # it does not seem to work. So handle the time zone manually. 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]) # Use :-1 to gobble trailing newline 757 758 # Determine the name of the branch. 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 #log.msg("parse(): rev=%s who=%s files=%s comments='%s' when=%s branch=%s" % (rev, who, d['files'], d['comments'], time.asctime(time.localtime(when)), branch)) 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
782 -def parseLaunchpadDate(datestr, tz_sign, tz_hours, tz_minutes):
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