Ticket #76: hgPoller-pushlog-v2.diff

File hgPoller-pushlog-v2.diff, 9.2 kB (added by bhearsum, 8 months ago)

hgpoller using pushlog

  • /dev/null

    old new  
     1import time 
     2from calendar import timegm 
     3from urllib2 import urlopen 
     4from xml.dom import minidom, Node 
     5 
     6from twisted.python import log, failure 
     7from twisted.internet import defer, reactor 
     8from twisted.internet.task import LoopingCall 
     9 
     10from buildbot.changes import base, changes 
     11 
     12 
     13# From pyiso8601 module, 
     14#  http://code.google.com/p/pyiso8601/source/browse/trunk/iso8601/iso8601.py 
     15#   Revision 22 
     16 
     17# Required license header: 
     18 
     19# Copyright (c) 2007 Michael Twomey 
     20#  
     21# Permission is hereby granted, free of charge, to any person obtaining a 
     22# copy of this software and associated documentation files (the 
     23# "Software"), to deal in the Software without restriction, including 
     24# without limitation the rights to use, copy, modify, merge, publish, 
     25# distribute, sublicense, and/or sell copies of the Software, and to 
     26# permit persons to whom the Software is furnished to do so, subject to 
     27# the following conditions: 
     28#  
     29# The above copyright notice and this permission notice shall be included 
     30# in all copies or substantial portions of the Software. 
     31#  
     32# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
     33# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
     34# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
     35# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
     36# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
     37# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
     38# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
     39 
     40"""ISO 8601 date time string parsing 
     41 
     42Basic usage: 
     43>>> import iso8601 
     44>>> iso8601.parse_date("2007-01-25T12:00:00Z") 
     45datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>) 
     46>>> 
     47 
     48""" 
     49 
     50from datetime import datetime, timedelta, tzinfo 
     51import re 
     52 
     53__all__ = ["parse_date", "ParseError"] 
     54 
     55# Adapted from http://delete.me.uk/2005/03/iso8601.html 
     56ISO8601_REGEX = re.compile(r"(?P<year>[0-9]{4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})" 
     57    r"((?P<separator>.)(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2})(:(?P<second>[0-9]{2})(\.(?P<fraction>[0-9]+))?)?" 
     58    r"(?P<timezone>Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?" 
     59) 
     60TIMEZONE_REGEX = re.compile("(?P<prefix>[+-])(?P<hours>[0-9]{2}).(?P<minutes>[0-9]{2})") 
     61 
     62class ParseError(Exception): 
     63    """Raised when there is a problem parsing a date string""" 
     64 
     65# Yoinked from python docs 
     66ZERO = timedelta(0) 
     67class Utc(tzinfo): 
     68    """UTC 
     69     
     70    """ 
     71    def utcoffset(self, dt): 
     72        return ZERO 
     73 
     74    def tzname(self, dt): 
     75        return "UTC" 
     76 
     77    def dst(self, dt): 
     78        return ZERO 
     79UTC = Utc() 
     80 
     81class FixedOffset(tzinfo): 
     82    """Fixed offset in hours and minutes from UTC 
     83     
     84    """ 
     85    def __init__(self, offset_hours, offset_minutes, name): 
     86        self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes) 
     87        self.__name = name 
     88 
     89    def utcoffset(self, dt): 
     90        return self.__offset 
     91 
     92    def tzname(self, dt): 
     93        return self.__name 
     94 
     95    def dst(self, dt): 
     96        return ZERO 
     97     
     98    def __repr__(self): 
     99        return "<FixedOffset %r>" % self.__name 
     100 
     101def parse_timezone(tzstring, default_timezone=UTC): 
     102    """Parses ISO 8601 time zone specs into tzinfo offsets 
     103     
     104    """ 
     105    if tzstring == "Z": 
     106        return default_timezone 
     107    # This isn't strictly correct, but it's common to encounter dates without 
     108    # timezones so I'll assume the default (which defaults to UTC). 
     109    # Addresses issue 4. 
     110    if tzstring is None: 
     111        return default_timezone 
     112    m = TIMEZONE_REGEX.match(tzstring) 
     113    prefix, hours, minutes = m.groups() 
     114    hours, minutes = int(hours), int(minutes) 
     115    if prefix == "-": 
     116        hours = -hours 
     117        minutes = -minutes 
     118    return FixedOffset(hours, minutes, tzstring) 
     119 
     120def parse_date(datestring, default_timezone=UTC): 
     121    """Parses ISO 8601 dates into datetime objects 
     122     
     123    The timezone is parsed from the date string. However it is quite common to 
     124    have dates without a timezone (not strictly correct). In this case the 
     125    default timezone specified in default_timezone is used. This is UTC by 
     126    default. 
     127    """ 
     128    if not isinstance(datestring, basestring): 
     129        raise ParseError("Expecting a string %r" % datestring) 
     130    m = ISO8601_REGEX.match(datestring) 
     131    if not m: 
     132        raise ParseError("Unable to parse date string %r" % datestring) 
     133    groups = m.groupdict() 
     134    tz = parse_timezone(groups["timezone"], default_timezone=default_timezone) 
     135    if groups["fraction"] is None: 
     136        groups["fraction"] = 0 
     137    else: 
     138        groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6) 
     139    return datetime(int(groups["year"]), int(groups["month"]), int(groups["day"]), 
     140        int(groups["hour"]), int(groups["minute"]), int(groups["second"]), 
     141        int(groups["fraction"]), tz) 
     142 
     143# End of iso8601.py 
     144 
     145 
     146class HgPoller(base.ChangeSource): 
     147    """This source will poll a Mercurial server over HTTP using 
     148    the built-in RSS feed for changes and submit them to the 
     149    change master.""" 
     150 
     151    compare_attrs = ['hgURL', 'branch', 'pollInterval'] 
     152    parent = None 
     153    loop = None 
     154    volatile = ['loop'] 
     155    working = False 
     156     
     157    def __init__(self, hgURL, branch, pushlogUrlOverride=None, pollInterval=30): 
     158        """ 
     159        @type   hgURL:          string 
     160        @param  hgURL:          The base URL of the Hg repo 
     161                                (e.g. http://hg.mozilla.org/) 
     162        @type   branch:         string 
     163        @param  branch:         The branch to check (e.g. mozilla-central) 
     164        @type   pollInterval:   int 
     165        @param  pollInterval:   The time (in seconds) between queries for 
     166                                changes 
     167        """ 
     168         
     169        self.hgURL = hgURL 
     170        self.branch = branch 
     171        self.pushlogUrlOverride = pushlogUrlOverride 
     172        self.pollInterval = pollInterval 
     173        self.lastChange = time.time() 
     174 
     175    def startService(self): 
     176        self.loop = LoopingCall(self.poll) 
     177        base.ChangeSource.startService(self) 
     178        reactor.callLater(0, self.loop.start, self.pollInterval) 
     179 
     180    def stopService(self): 
     181        self.loop.stop() 
     182        return base.ChangeSource.stopService(self) 
     183     
     184    def describe(self): 
     185        return "Getting changes from the Mercurial repo at %s%s" % \ 
     186               (self.hgURL, self.branch) 
     187     
     188    def poll(self): 
     189        if self.working: 
     190            log.msg("Not polling because last poll is still working") 
     191        else: 
     192            self.working = True 
     193            d = self._get_changes() 
     194            d.addCallback(self._process_changes) 
     195            d.addCallbacks(self._finished_ok, self._finished_failure) 
     196 
     197    def _finished_ok(self, res): 
     198        assert self.working 
     199        self.working = False 
     200 
     201        return res 
     202 
     203    def _finished_failure(self, res): 
     204        log.msg("Hg poll failed: %s" % res) 
     205        assert self.working 
     206        self.working = False 
     207        return None 
     208 
     209    def _make_url(self): 
     210        if self.pushlogUrlOverride: 
     211            return self.pushlogUrlOverride 
     212        else: 
     213            return "%s%s/pushlog" % (self.hgURL, self.branch) 
     214     
     215    def _get_changes(self): 
     216        url = self._make_url() 
     217        log.msg("Polling Hg server at %s" % url) 
     218         
     219        return defer.maybeDeferred(urlopen, url) 
     220 
     221    def _parse_date_string(self, dateString): 
     222        return timegm((parse_date(dateString).utctimetuple())) 
     223 
     224    def _parse_changes(self, query): 
     225        dom = minidom.parseString(query.read()) 
     226 
     227        items = dom.getElementsByTagName("entry") 
     228        changes = [] 
     229        for i in items: 
     230            d = {} 
     231            for k in ["title", "updated"]: 
     232                d[k] = i.getElementsByTagName(k)[0].firstChild.wholeText 
     233            d["updated"] = self._parse_date_string(d["updated"]) 
     234            d["changeset"] = d["title"].split(" ")[1] 
     235            nameNode = i.getElementsByTagName("author")[0].childNodes[1] 
     236            d["author"] = nameNode.firstChild.wholeText 
     237            d["link"] = i.getElementsByTagName("link")[0].getAttribute("href") 
     238            changes.append(d) 
     239        changes = [c for c in changes if c["updated"] > self.lastChange] 
     240        changes.reverse() # want them in chronological order 
     241        return changes 
     242     
     243    def _process_changes(self, query): 
     244        change_list = self._parse_changes(query) 
     245        for change in change_list: 
     246            adjustedChangeTime = change["updated"] 
     247            c = changes.Change(who = change["author"], 
     248                               files = [], # sucks 
     249                               revision = change["changeset"], 
     250                               comments = change["link"], 
     251                               when = adjustedChangeTime, 
     252                               branch = self.branch) 
     253            self.parent.addChange(c) 
     254        if len(change_list) > 0: 
     255            self.lastChange = max(self.lastChange, *[c["updated"] for c in 
     256                                                       change_list]) 
     257