| | 1 | import time |
|---|
| | 2 | import rfc822 |
|---|
| | 3 | from urllib import urlopen |
|---|
| | 4 | from xml.dom import minidom, Node |
|---|
| | 5 | |
|---|
| | 6 | from twisted.python import log, failure |
|---|
| | 7 | from twisted.internet import defer, reactor |
|---|
| | 8 | from twisted.internet.task import LoopingCall |
|---|
| | 9 | |
|---|
| | 10 | from buildbot.changes import base, changes |
|---|
| | 11 | |
|---|
| | 12 | class HgPoller(base.ChangeSource): |
|---|
| | 13 | """This source will poll a Mercurial server over HTTP using |
|---|
| | 14 | the built-in RSS feed for changes and submit them to the |
|---|
| | 15 | change master.""" |
|---|
| | 16 | |
|---|
| | 17 | compare_attrs = ['hgURL', 'branch', 'pollInterval'] |
|---|
| | 18 | parent = None |
|---|
| | 19 | loop = None |
|---|
| | 20 | volatile = ['loop'] |
|---|
| | 21 | working = False |
|---|
| | 22 | |
|---|
| | 23 | def __init__(self, hgURL, branch, pollInterval=30): |
|---|
| | 24 | """ |
|---|
| | 25 | @type hgURL: string |
|---|
| | 26 | @param hgURL: The base URL of the Hg repo |
|---|
| | 27 | (e.g. http://hg.mozilla.org/) |
|---|
| | 28 | @type branch: string |
|---|
| | 29 | @param branch: The branch to check (e.g. mozilla-central) |
|---|
| | 30 | @type pollInterval: int |
|---|
| | 31 | @param pollInterval: The time (in seconds) between queries for |
|---|
| | 32 | changes |
|---|
| | 33 | """ |
|---|
| | 34 | |
|---|
| | 35 | self.hgURL = hgURL |
|---|
| | 36 | self.branch = branch |
|---|
| | 37 | self.pollInterval = pollInterval |
|---|
| | 38 | self.lastChange = time.time() |
|---|
| | 39 | self.lastPoll = time.time() |
|---|
| | 40 | |
|---|
| | 41 | def startService(self): |
|---|
| | 42 | self.loop = LoopingCall(self.poll) |
|---|
| | 43 | base.ChangeSource.startService(self) |
|---|
| | 44 | reactor.callLater(0, self.loop.start, self.pollInterval) |
|---|
| | 45 | |
|---|
| | 46 | def stopService(self): |
|---|
| | 47 | self.loop.stop() |
|---|
| | 48 | return base.ChangeSource.stopService(self) |
|---|
| | 49 | |
|---|
| | 50 | def describe(self): |
|---|
| | 51 | return "Getting changes from the Mercurial repo at %s%s" % \ |
|---|
| | 52 | (self.hgURL, self.branch) |
|---|
| | 53 | |
|---|
| | 54 | def poll(self): |
|---|
| | 55 | if self.working: |
|---|
| | 56 | log.msg("Not polling because last poll is still working") |
|---|
| | 57 | else: |
|---|
| | 58 | self.working = True |
|---|
| | 59 | d = self._get_changes() |
|---|
| | 60 | d.addCallback(self._process_changes) |
|---|
| | 61 | d.addCallbacks(self._finished_ok, self._finished_failure) |
|---|
| | 62 | |
|---|
| | 63 | def _finished_ok(self, res): |
|---|
| | 64 | assert self.working |
|---|
| | 65 | self.working = False |
|---|
| | 66 | |
|---|
| | 67 | return res |
|---|
| | 68 | |
|---|
| | 69 | def _finished_failure(self, res): |
|---|
| | 70 | log.msg("Hg poll failed: %s" % res) |
|---|
| | 71 | assert self.working |
|---|
| | 72 | self.working = False |
|---|
| | 73 | return None |
|---|
| | 74 | |
|---|
| | 75 | def _make_url(self): |
|---|
| | 76 | return "%s%s/?rss-log" % (self.hgURL, self.branch) |
|---|
| | 77 | |
|---|
| | 78 | def _get_changes(self): |
|---|
| | 79 | url = self._make_url() |
|---|
| | 80 | log.msg("Polling Hg server at %s" % url) |
|---|
| | 81 | |
|---|
| | 82 | self.lastPoll = time.time() |
|---|
| | 83 | return defer.maybeDeferred(urlopen, url) |
|---|
| | 84 | |
|---|
| | 85 | def _parse_changes(self, query): |
|---|
| | 86 | dom = minidom.parseString(query.read()) |
|---|
| | 87 | items = dom.getElementsByTagName("item") |
|---|
| | 88 | changes = [] |
|---|
| | 89 | for i in items: |
|---|
| | 90 | d = dict() |
|---|
| | 91 | for k in ["description", "link", "author", "pubDate"]: |
|---|
| | 92 | d[k] = i.getElementsByTagName(k)[0].firstChild.wholeText |
|---|
| | 93 | # strip out HTML newlines |
|---|
| | 94 | d["description"] = d["description"].replace("<br/>","") |
|---|
| | 95 | # need to parse date with timezone, and turn into a UTC timestamp |
|---|
| | 96 | d["pubDate"] = rfc822.mktime_tz(rfc822.parsedate_tz(d["pubDate"]) ) |
|---|
| | 97 | changes.append(d) |
|---|
| | 98 | changes = [c for c in changes if c["pubDate"] > self.lastChange] |
|---|
| | 99 | changes.reverse() # want t hem in reverse chronological order |
|---|
| | 100 | return changes |
|---|
| | 101 | |
|---|
| | 102 | def _process_changes(self, query): |
|---|
| | 103 | change_list = self._parse_changes(query) |
|---|
| | 104 | for change in change_list: |
|---|
| | 105 | c = changes.Change(who = change["author"], |
|---|
| | 106 | files = [], # sucks |
|---|
| | 107 | comments = change["description"], |
|---|
| | 108 | when = change["pubDate"], |
|---|
| | 109 | branch = self.branch) |
|---|
| | 110 | self.parent.addChange(c) |
|---|
| | 111 | self.lastChange = max(self.lastPoll, *[c["pubDate"] for c in |
|---|
| | 112 | change_list]) |
|---|
| | 113 | |