ubuntu-bots/Bugtracker/plugin.py

786 lines
33 KiB
Python
Raw Normal View History

2006-06-26 17:57:20 +00:00
###
# Copyright (c) 2005-2007 Dennis Kaarsemaker
2006-06-26 17:57:20 +00:00
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
###
from supybot.commands import *
import supybot.utils as utils
import supybot.ircmsgs as ircmsgs
2006-06-26 17:57:20 +00:00
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
import supybot.conf as conf
import supybot.registry as registry
import supybot.schedule as schedule
2006-06-26 17:57:20 +00:00
2006-12-27 14:33:59 +00:00
import re, os, time, imaplib, commands
2006-06-26 17:57:20 +00:00
import xml.dom.minidom as minidom
from htmlentitydefs import entitydefs as entities
import email.FeedParser
import SOAPpy
2006-06-26 17:57:20 +00:00
def registerBugtracker(name, url='', description='', trackertype=''):
conf.supybot.plugins.Bugtracker.bugtrackers().add(name)
group = conf.registerGroup(conf.supybot.plugins.Bugtracker.bugtrackers, name)
URL = conf.registerGlobalValue(group, 'url', registry.String(url, ''))
DESC = conf.registerGlobalValue(group, 'description', registry.String(description, ''))
TRACKERTYPE = conf.registerGlobalValue(group, 'trackertype', registry.String(trackertype, ''))
if url:
URL.setValue(url)
if description:
DESC.setValue(description)
if trackertype:
if defined_bugtrackers.has_key(trackertype.lower()):
TRACKERTYPE.setValue(trackertype.lower())
else:
raise BugtrackerError("Unknown trackertype: %s" % trackertype)
entre = re.compile('&(\S*?);')
def _getnodetxt(node):
L = []
for childnode in node.childNodes:
if childnode.nodeType == childnode.TEXT_NODE:
L.append(childnode.data)
val = ''.join(L)
if node.hasAttribute('encoding'):
encoding = node.getAttribute('encoding')
if encoding == 'base64':
try:
val = val.decode('base64')
except:
val = 'Cannot convert bug data from base64.'
while entre.search(val):
entity = entre.search(val).group(1)
if entity in entities:
val = entre.sub(entities[entity], val)
else:
val = entre.sub('?', val)
return val
class BugtrackerError(Exception):
"""A bugtracker error"""
pass
class BugNotFoundError(Exception):
"""Pity, bug isn't there"""
pass
cvere = re.compile(r'<th.*?Description.*?<td.*?>(.*?)\s*</td>', re.I | re.DOTALL)
2006-06-26 17:57:20 +00:00
class Bugtracker(callbacks.PluginRegexp):
"""Show a link to a bug report with a brief description"""
threaded = True
callBefore = ['URL']
regexps = ['turlSnarfer', 'bugSnarfer', 'oopsSnarfer', 'cveSnarfer']
2006-06-26 17:57:20 +00:00
def __init__(self, irc):
callbacks.PluginRegexp.__init__(self, irc)
self.db = ircutils.IrcDict()
for name in self.registryValue('bugtrackers'):
registerBugtracker(name)
group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False)
if group.trackertype() in defined_bugtrackers.keys():
self.db[name] = defined_bugtrackers[group.trackertype()](name, group.url(), group.description())
else:
raise BugtrackerError("Unknown trackertype: %s" % group.trackertype())
self.shorthand = utils.abbrev(self.db.keys())
# Schedule bug reporting
if self.registryValue('imap_server') and self.registryValue('reportercache'):
try:
schedule.removeEvent(self.name() + '.bugreporter')
except:
pass
schedule.addPeriodicEvent(lambda: self.reportnewbugs(irc), 60, name=self.name() + '.bugreporter')
self.shown = {}
2006-12-27 14:33:59 +00:00
def die(self):
try:
schedule.removeEvent(self.name())
except:
pass
def is_ok(self, channel, tracker, bug):
now = time.time()
for k in self.shown.keys():
2006-12-27 14:33:59 +00:00
if self.shown[k] < now - self.registryValue('repeatdelay', channel):
self.shown.pop(k)
if (channel, tracker, bug) not in self.shown:
self.shown[(channel, tracker, bug)] = now
return True
return False
def is_new(self, tracker, tag, id):
bugreporter_base = self.registryValue('reportercache')
if not os.path.exists(os.path.join(bugreporter_base,tag,tracker.name,str(int(id/1000)),str(id))):
try:
os.makedirs(os.path.join(bugreporter_base,tag,tracker.name,str(int(id/1000))))
except:
pass
fd = open(os.path.join(bugreporter_base,tag,tracker.name,str(int(id/1000)),str(id)),'w')
fd.close()
return True
return False
def reportnewbugs(self,irc):
# Compile list of bugs
self.log.info("Checking for new bugs")
bugs = {}
if self.registryValue('imap_ssl'):
sc = imaplib.IMAP4_SSL(self.registryValue('imap_server'))
else:
sc = imaplib.IMAP4(self.registryValue('imap_server'))
sc.login(self.registryValue('imap_user'), self.registryValue('imap_password'))
2006-12-27 14:33:59 +00:00
sc.select('INBOX')
new_mail = sc.search(None, '(UNSEEN)')[1][0].split()[:20]
# Read all new mail
2006-12-27 14:33:59 +00:00
for m in new_mail:
msg = sc.fetch(m, 'RFC822')[1][0][1]
fp = email.FeedParser.FeedParser()
fp.feed(msg)
bug = fp.close()
2006-12-27 14:33:59 +00:00
tag = bug['Delivered-To']
if '+' not in tag:
self.log.info('Ignoring e-mail with no detectable bug')
continue
2006-12-27 14:33:59 +00:00
tag = tag[tag.find('+')+1:tag.find('@')]
if tag not in bugs:
bugs[tag] = {}
# Determine bugtracker type (currently only Launchpad is supported anyway)
if bug['X-Launchpad-Bug']:
tracker = self.db['launchpad']
id = int(bug['Reply-To'].split()[1])
if self.is_new(tracker, tag, id):
component = bug['X-Launchpad-Bug']
if 'component' in component:
component = component[component.find('component=')+10:]
component = component[:component.find(';')].replace('None','')
else:
component = ''
2006-12-27 14:33:59 +00:00
try:
if component:
bugs[tag][id] = self.get_bug('',tracker, id, False)[0].replace('"','(%s) "' % component, 1)
else:
bugs[tag][id] = self.get_bug('',tracker, id, False)[0]
if '[apport]' in bugs[tag][id]:
bugs[tag].pop(id)
2006-12-27 14:33:59 +00:00
except:
self.log.info("Unable to get new bug %d" % id)
pass
else:
self.log.info('Ignoring e-mail with no detectable bug')
2006-12-27 14:33:59 +00:00
for c in irc.state.channels:
tag = self.registryValue('bugReporter', channel=c)
if not tag:
continue
if tag not in bugs.keys():
continue
for b in sorted(bugs[tag].keys()):
irc.queueMsg(ircmsgs.privmsg(c,'New bug: #%s' % bugs[tag][b][bugs[tag][b].find('bug ')+4:]))
print "Reported!"
2006-06-26 17:57:20 +00:00
def add(self, irc, msg, args, name, trackertype, url, description):
"""<name> <type> <url> [<description>]
Add a bugtracker <url> to the list of defined bugtrackers. <type> is the
type of the tracker (currently only Launchpad, Debbugs, Bugzilla,
2006-06-26 17:57:20 +00:00
Issuezilla and Trac are known). <name> is the name that will be used to
reference the bugzilla in all commands. Unambiguous abbreviations of
<name> will be accepted also. <description> is the common name for the
bugzilla and will be listed with the bugzilla query; if not given, it
defaults to <name>.
"""
name = name.lower()
if not description:
description = name
if url[-1] == '/':
url = url[:-1]
trackertype = trackertype.lower()
if trackertype in defined_bugtrackers:
self.db[name] = defined_bugtrackers[trackertype](name,url,description)
else:
irc.error("Bugtrackers of type '%s' are not understood" % trackertype)
return
registerBugtracker(name, url, description, trackertype)
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
add = wrap(add, ['something', 'something', 'url', additional('text')])
def remove(self, irc, msg, args, name):
"""<abbreviation>
Remove the bugtracker associated with <abbreviation> from the list of
defined bugtrackers.
"""
try:
name = self.shorthand[name.lower()]
del self.db[name]
self.registryValue('bugtrackers').remove(name)
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
except KeyError:
s = self.registryValue('replyNoBugtracker', msg.args[0])
irc.error(s % name)
remove = wrap(remove, ['text'])
def rename(self, irc, msg, args, oldname, newname, newdesc):
"""<oldname> <newname>
Rename the bugtracker associated with <oldname> to <newname>
"""
try:
name = self.shorthand[oldname.lower()]
group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False)
d = group.description()
if newdesc:
d = newdesc
self.db[newname] = defined_bugtrackers[group.trackertype()](name,group.url(),d)
registerBugtracker(newname, group.url(), d, group.trackertype())
del self.db[name]
self.registryValue('bugtrackers').remove(name)
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
except KeyError:
s = self.registryValue('replyNoBugtracker', msg.args[0])
irc.error(s % name)
rename = wrap(rename, ['something','something', additional('text')])
2006-06-26 17:57:20 +00:00
def list(self, irc, msg, args, name):
"""[abbreviation]
List defined bugtrackers. If [abbreviation] is specified, list the
information for that bugtracker.
"""
if name:
name = name.lower()
try:
name = self.shorthand[name]
(url, description, type) = (self.db[name].url, self.db[name].description,
self.db[name].__class__.__name__)
irc.reply('%s: %s, %s [%s]' % (name, description, url, type))
except KeyError:
s = self.registryValue('replyNoBugtracker', msg.args[0])
irc.error(s % name)
else:
if self.db:
L = self.db.keys()
L.sort()
irc.reply(utils.str.commaAndify(L))
else:
irc.reply('I have no defined bugtrackers.')
list = wrap(list, [additional('text')])
def bugSnarfer(self, irc, msg, match):
r"""\b(?P<bt>(([a-z0-9]+)?\s+bugs?|[a-z0-9]+))\s+#?(?P<bug>\d+(?!\d*\.\d+)((,|\s*(and|en|et|und|ir))\s*#?\d+(?!\d*\.\d+))*)"""
2006-10-20 14:15:36 +00:00
if msg.args[0][0] == '#' and not self.registryValue('bugSnarfer', msg.args[0]):
2006-06-26 17:57:20 +00:00
return
2007-02-04 22:10:10 +00:00
nbugs = msg.tagged('nbugs')
if not nbugs: nbugs = 0
if nbugs >= 5:
return
2006-06-26 17:57:20 +00:00
# Don't double on commands
s = str(msg).split(':')[2]
if s[0] in str(conf.supybot.reply.whenAddressedBy.chars):
return
sure_bug = match.group('bt').endswith('bug') or match.group('bt').endswith('bug')
2006-06-26 17:57:20 +00:00
# Get tracker name
bugids = match.group('bug')
reps = ((' ',''),('#',''),('and',','),('en',','),('et',','),('und',','),('ir',','))
2006-06-26 17:57:20 +00:00
for r in reps:
bugids = bugids.replace(r[0],r[1])
2007-02-04 22:10:10 +00:00
bugids = bugids.split(',')[:5-nbugs]
if not sure_bug:
bugids = [x for x in bugids if int(x) > 100]
2007-02-04 22:10:10 +00:00
msg.tag('nbugs', nbugs + len(bugids))
2006-06-26 17:57:20 +00:00
bt = map(lambda x: x.lower(), match.group('bt').split())
name = ''
if len(bt) == 1 and not (bt[0] in ['bug','bugs']):
try:
name = bt[0].lower()
tracker = self.db[name]
except:
return
elif len(bt) == 2:
try:
name = bt[0].lower()
tracker = self.db[name]
except:
name = ''
pass
if not name:
2006-12-27 14:33:59 +00:00
snarfTarget = self.registryValue('snarfTarget', msg.args[0]).lower()
2006-06-26 17:57:20 +00:00
if not snarfTarget:
return
try:
name = self.shorthand[snarfTarget.lower()]
except:
s = self.registryValue('replyNoBugtracker', name)
irc.error(s % name)
try:
tracker = self.db[name]
except KeyError:
s = self.registryValue('replyNoBugtracker', name)
irc.error(s % name)
else:
for bugid in bugids:
bugid = int(bugid)
try:
report = self.get_bug(msg.args[0],tracker,bugid,self.registryValue('showassignee', msg.args[0]))
except BugNotFoundError:
2007-02-04 22:10:10 +00:00
if self.registryValue('replyWhenNotFound'):
irc.error("%s bug %d could not be found" % (tracker.description, bugid))
2006-06-26 17:57:20 +00:00
except BugtrackerError, e:
2006-10-19 14:39:11 +00:00
if 'private' in str(e):
irc.reply("Bug %d on http://launchpad.net/bugs/%d is private" % (bugid, bugid))
return
2006-06-26 17:57:20 +00:00
if not sure_bug and bugid < 30:
return
irc.error(str(e))
else:
2006-12-27 14:33:59 +00:00
for r in report:
irc.reply(r, prefixNick=False)
2006-06-26 17:57:20 +00:00
def turlSnarfer(self, irc, msg, match):
2007-02-04 22:10:10 +00:00
r"(?P<tracker>https?://\S*?)/(Bugs/0*|str.php\?L|show_bug.cgi\?id=|bugreport.cgi\?bug=|(bugs|\+bug)/|ticket/|tracker/|\S*aid=)(?P<bug>\d+)(?P<sfurl>&group_id=\d+&at_id=\d+)?"
2006-10-20 14:15:36 +00:00
if msg.args[0][0] == '#' and not self.registryValue('bugSnarfer', msg.args[0]):
2006-06-26 17:57:20 +00:00
return
2007-02-04 22:10:10 +00:00
nbugs = msg.tagged('nbugs')
if not nbugs: nbugs = 0
if nbugs >= 5:
return
msg.tag('nbugs', nbugs+1)
2006-06-26 17:57:20 +00:00
try:
tracker = self.get_tracker(match.group(0),match.group('sfurl'))
if not tracker:
return
report = self.get_bug(msg.args[0],tracker,int(match.group('bug')),self.registryValue('showassignee', msg.args[0]), do_url = False)
2006-06-26 17:57:20 +00:00
except BugtrackerError, e:
irc.error(str(e))
else:
2006-12-27 14:33:59 +00:00
for r in report:
irc.reply(r, prefixNick=False)
2006-06-26 17:57:20 +00:00
turlSnarfer = urlSnarfer(turlSnarfer)
# Only useful for launchpad developers
def oopsSnarfer(self, irc, msg, match):
r"OOPS-(?P<oopsid>\d*[A-Z]\d+)"
if msg.args[0][0] == '#' and not self.registryValue('bugSnarfer', msg.args[0]):
return
2006-06-26 17:57:20 +00:00
oopsid = match.group(1)
irc.reply("https://devpad.canonical.com/~jamesh/oops.cgi/%s" % oopsid, prefixNick=False)
2006-06-26 17:57:20 +00:00
def cveSnarfer(self, irc, msg, match):
r"(cve[- ]\d{4}[- ]\d{4})"
if msg.args[0][0] == '#' and not self.registryValue('bugSnarfer', msg.args[0]):
return
cve = match.group(1).replace(' ','-').upper()
url = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s' % cve
cvedata = utils.web.getUrl(url)
m = cvere.search(cvedata)
if m:
cve = m.group(1).replace('\n', ' ')
irc.reply("%s (%s)" % (cve,url))
def cveUrlSnarfer(self, irc, msg, match):
pass
2006-06-26 17:57:20 +00:00
def get_tracker(self,snarfurl,sfdata):
2007-02-10 21:47:18 +00:00
snarfurl = snarfurl.replace('sf.net','sourceforge.net')
2006-10-19 14:39:11 +00:00
snarfhost = snarfurl.replace('http://','').replace('https://','')
if '/' in snarfurl:
snarfhost = snarfhost[:snarfhost.index('/')]
2006-06-26 17:57:20 +00:00
for t in self.db.keys():
tracker = self.db[t]
url = tracker.url.replace('http://','').replace('https://','')
if 'sourceforge.net' in url:
# Try to find the correct sf tracker
if str(sfdata) in tracker.url:
return tracker
if '/' in url:
url = url[:url.index('/')]
2006-10-19 14:39:11 +00:00
if url in snarfhost:
2006-06-26 17:57:20 +00:00
return tracker
if 'sourceforge.net' in snarfurl:
return self.db['sourceforge']
# No tracker found, bummer. Let's try and add one
if 'show_bug.cgi' in snarfurl:
tracker = Bugzilla().get_tracker(snarfurl)
if tracker:
self.db[tracker.name] = tracker
self.shorthand = utils.abbrev(self.db.keys())
return tracker
return None
def get_bug(self, channel, tracker, id, do_assignee, do_url = True):
2006-12-27 14:33:59 +00:00
reports = []
for r in tracker.get_bug(id):
if not self.is_ok(channel, tracker, r[0]):
continue
2006-12-27 14:33:59 +00:00
(bid, product, title, severity, status, assignee, url) = r
severity = severity[0].upper() + severity[1:].lower()
status = status[0].upper() + status[1:].lower()
if not do_url:
url = ''
if product:
reports.append("%s bug %s in %s \"%s\" [%s,%s] %s" % (tracker.description, bid, product,
title, severity, status, url))
else:
reports.append("%s bug %s \"%s\" [%s,%s] %s" % (tracker.description, bid, title, severity, status, url))
if do_assignee and assignee:
reports[-1] = reports[-1] + (" - Assigned to %s" % assignee)
return reports
2006-06-26 17:57:20 +00:00
# Define all bugtrackers
class IBugtracker:
def __init__(self, name=None, url=None, description=None):
self.name = name
self.url = url
self.description = description
def get_bug(self, id):
raise BugTrackerError("Bugtracker class does not implement get_bug")
def get_tracker(self, url):
raise BugTrackerError("Bugtracker class does not implement get_tracker")
class Bugzilla(IBugtracker):
def get_tracker(self, url):
url = url.replace('show_bug','xml')
try:
bugxml = utils.web.getUrl(url)
tree = minidom.parseString(bugxml)
url = str(tree.getElementsByTagName('bugzilla')[0].attributes['urlbase'].childNodes[0].data)
if url[-1] == '/':
url = url[:-1]
name = url[url.find('//') + 2:]
if '/' in name:
name = name[:name.find('/')]
desc = name
registerBugtracker(name, url, desc, 'bugzilla')
tracker = Bugzilla(name, url, desc)
return tracker
except:
return None
def get_bug(self, id):
url = "%s/xml.cgi?id=%d" % (self.url,id)
try:
bugxml = utils.web.getUrl(url)
zilladom = minidom.parseString(bugxml)
except Exception, e:
s = 'Could not parse XML returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
bug_n = zilladom.getElementsByTagName('bug')[0]
if bug_n.hasAttribute('error'):
errtxt = bug_n.getAttribute('error')
if errtxt == 'NotFound':
raise BugNotFoundError
2006-06-26 17:57:20 +00:00
s = 'Error getting %s bug #%s: %s' % (self.description, id, errtxt)
raise BugtrackerError, s
try:
title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0])
status = _getnodetxt(bug_n.getElementsByTagName('bug_status')[0])
try:
status += ": " + _getnodetxt(bug_n.getElementsByTagName('resolution')[0])
except:
pass
component = _getnodetxt(bug_n.getElementsByTagName('component')[0])
severity = _getnodetxt(bug_n.getElementsByTagName('bug_severity')[0])
assignee = '(unavailable)'
try:
assignee = _getnodetxt(bug_n.getElementsByTagName('assigned_to')[0])
except:
pass
2006-06-26 17:57:20 +00:00
except Exception, e:
s = 'Could not parse XML returned by %s bugzilla: %s' % (self.description, e)
raise BugtrackerError, s
2006-12-27 14:33:59 +00:00
return [(id, component, title, severity, status, assignee, "%s/show_bug.cgi?id=%d" % (self.url, id))]
2006-06-26 17:57:20 +00:00
class Issuezilla(IBugtracker):
def get_bug(self, id):
url = "%s/xml.cgi?id=%d" % (self.url,id)
try:
bugxml = utils.web.getUrl(url)
zilladom = minidom.parseString(bugxml)
except Exception, e:
s = 'Could not parse XML returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
bug_n = zilladom.getElementsByTagName('issue')[0]
if not (bug_n.getAttribute('status_code') == '200'):
if bug_n.getAttribute('status_message') == 'NotFound':
raise BugNotFoundError
2006-06-26 17:57:20 +00:00
s = 'Error getting %s bug #%s: %s' % (self.description, id, bug_n.getAttribute('status_message'))
raise BugtrackerError, s
try:
title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0])
status = _getnodetxt(bug_n.getElementsByTagName('issue_status')[0])
try:
status += ": " + _getnodetxt(bug_n.getElementsByTagName('resolution')[0])
except:
pass
component = _getnodetxt(bug_n.getElementsByTagName('component')[0])
severity = _getnodetxt(bug_n.getElementsByTagName('issue_type')[0])
2006-12-27 14:33:59 +00:00
assignee = _getnodetxt(bug_n.getElementsByTagName('assigned_to')[0])
2006-06-26 17:57:20 +00:00
except Exception, e:
s = 'Could not parse XML returned by %s bugzilla: %s' % (self.description, e)
raise BugtrackerError, s
2006-12-27 14:33:59 +00:00
return [(id, component, title, severity, status, assignee, "%s/show_bug.cgi?id=%d" % (self.url, id))]
2006-06-26 17:57:20 +00:00
class Launchpad(IBugtracker):
2006-06-26 17:57:20 +00:00
def _parse(self, task):
parser = email.FeedParser.FeedParser()
parser.feed(task)
return parser.close()
def _sort(self, task1, task2):
# Status sort:
try:
statuses = ['Rejected', 'Fix Released', 'Fix Committed', 'Unconfirmed','Needs Info','Confirmed','In Progress']
2006-10-19 14:39:11 +00:00
severities = ['Undecided', 'Wishlist', 'Minor', 'Low', 'Normal', 'Medium', 'Major', 'High', 'Critical']
2007-02-10 21:47:18 +00:00
if task1['status'] not in statuses and task2['status'] in statuses: return -1
if task1['status'] in statuses and task2['status'] not in statuses: return 1
if task1['importance'] not in severities and task2['importance'] in severities: return -1
if task1['importance'] in severities and task2['importance'] not in severities: return 1
2006-06-26 17:57:20 +00:00
if not (task1['status'] == task2['status']):
if statuses.index(task1['status']) < statuses.index(task2['status']):
return -1
return 1
if not (task1['importance'] == task2['importance']):
if severities.index(task1['importance']) < severities.index(task2['importance']):
2006-06-26 17:57:20 +00:00
return -1
return 1
except: # Launchpad changed again?
return 0
return 0
def get_bug(self, id):
try:
print("%s/bugs/%d/+text" % (self.url,id))
bugdata = utils.web.getUrl("%s/bugs/%d/+text" % (self.url,id))
2006-06-26 17:57:20 +00:00
except Exception, e:
if '404' in str(e):
raise BugNotFoundError
2006-06-26 17:57:20 +00:00
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
summary = {}
# Trap private bugs
if "<!-- 4a. didn't try to log in last time: -->" in bugdata:
raise BugtrackerError, "This bug is private"
try:
# Split bug data into separate pieces (bug data, task data)
data = bugdata.split('\n\n')
bugdata = data[0]
taskdata = data[1:]
parser = email.FeedParser.FeedParser()
parser.feed(bugdata)
bugdata = parser.close()
taskdata = map(self._parse, taskdata)
taskdata.sort(self._sort)
taskdata = taskdata[-1]
except Exception, e:
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
2006-12-27 14:33:59 +00:00
# Try and find duplicates
2006-06-26 17:57:20 +00:00
t = taskdata['task']
if '(' in t:
t = t[:t.rfind('(') -1]
2006-12-27 14:33:59 +00:00
if bugdata['duplicate-of']:
dupbug = self.get_bug(int(bugdata['duplicate-of']))
return [(id, t, bugdata['title'] + (' (dup-of: %d)' % dupbug[0][0]), taskdata['importance'],
taskdata['status'], taskdata['assignee'], "%s/bugs/%s" % (self.url, id))] + dupbug
2006-12-27 14:33:59 +00:00
return [(id, t, bugdata['title'], taskdata['importance'],
taskdata['status'], taskdata['assignee'], "%s/bugs/%s" % (self.url, id))]
2006-06-26 17:57:20 +00:00
# <rant>
# Debbugs sucks donkeyballs
# * HTML pages are inconsistent
# * Parsing mboxes gets incorrect with cloning perversions (eg with bug 330000)
# * No sane way of accessing bug reports in a machine readable way (bts2ldap
# has no search on bugid)
# * The damn thing allow incomplete bugs, eg bugs without severity set. WTF?!?
2006-06-26 17:57:20 +00:00
#
# Fortunately bugs.donarmstrong.com has a SOAP interface which we can use.
2006-06-26 17:57:20 +00:00
# </rant>
class Debbugs(IBugtracker):
def __init__(self, *args, **kwargs):
IBugtracker.__init__(self, *args, **kwargs)
self.soap_proxy = SOAPpy.SOAPProxy("bugs.donarmstrong.com/cgi-bin/soap.cgi", "Debbugs/SOAP/Status")
self.soap_proxy.soapaction = "Debbugs/SOAP/Status#get_status"
2006-06-26 17:57:20 +00:00
def get_bug(self, id):
try:
raw = self.soap_proxy.get_status(id)
2006-06-26 17:57:20 +00:00
except Exception, e:
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
if not raw:
raise BugNotFoundError
raw = raw['item']['value']
2006-06-26 17:57:20 +00:00
try:
if len(raw['fixed_versions']):
status = 'Fixed'
else:
status = 'Open'
return [(id, raw['package'], raw['subject'], raw['severity'], status, '', "%s/%s" % (self.url, id))]
2006-06-26 17:57:20 +00:00
except Exception, e:
s = 'Could not parse data returned by %s bugtracker: %s' % (self.description, e)
raise BugtrackerError, s
# For trac based trackers we also need to do some screenscraping - should be
# doable unless a certain track instance uses weird templates.
class Trac(IBugtracker):
def get_bug(self, id):
url = "%s/%d" % (self.url, id)
try:
bugdata = utils.web.getUrl(url)
except Exception, e:
# Hacketiehack
if 'HTTP Error 500' in str(e):
raise BugNotFoundError
2006-06-26 17:57:20 +00:00
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
for l in bugdata.split("\n"):
if 'class="summary"' in l:
title = l[l.find('>')+1:l.find('</')]
if 'class="status"' in l:
status = l[l.find('>(')+2:l.find(')')]
2006-06-26 17:57:20 +00:00
if 'headers="h_component"' in l:
package = l[l.find('>')+1:l.find('</')]
if 'headers="h_severity"' in l:
severity = l[l.find('>')+1:l.find('</')]
if 'headers="h_stage"' in l:
severity = l[l.find('>')+1:l.find('</')]
2006-12-27 14:33:59 +00:00
if 'headers="h_owner"' in l:
assignee = l[l.find('>')+1:l.find('</')]
return [(id, package, title, severity, status, assignee, "%s/%s" % (self.url, id))]
class WikiForms(IBugtracker):
def get_bug(self, id):
def strip_tags(s):
while '<' in s and '>' in s:
s = str(s[:s.find('<')]) + str(s[s.find('>')+1:])
return s
url = "%s/%05d" % (self.url, id)
try:
bugdata = utils.web.getUrl(url)
except Exception, e:
if 'HTTP Error 404' in str(e):
raise BugNotFoundError
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
for l in bugdata.split("\n"):
l2 = l.lower()
if '<dt>importance</dt>' in l2:
severity = 'Importance ' + strip_tags(l[l.find('<dd>')+4:])
if '<dt>summary</dt>' in l2:
title = strip_tags(l[l.find('<dd>')+4:])
if '<dt>status</dt>' in l2:
status = strip_tags(l[l.find('<dd>')+4:])
if '<dt>category</dt>' in l2:
package = strip_tags(l[l.find('<dd>')+4:])
2006-12-27 14:33:59 +00:00
return [(id, package, title, severity, status, '', "%s/%05d" % (self.url, id))]
2006-06-26 17:57:20 +00:00
class Str(IBugtracker):
def get_bug(self, id):
def strip_tags(s):
while '<' in s and '>' in s:
s = str(s[:s.find('<')]) + str(s[s.find('>')+1:])
return s
url = "%s?L%d" % (self.url, id)
try:
bugdata = utils.web.getUrl(url)
except Exception, e:
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
for l in bugdata.split("\n"):
l2 = l.lower()
if 'nowrap>priority:</th>' in l2:
severity = 'Priority ' + l[l.find(' - ')+3:min(l.find(','),l.find('</td>'))]
if '>application:</th>' in l2:
package = l[l.find('<td>')+4:l.find('</td>')]
if 'nowrap>status:</th>' in l2:
status = l[l.find(' - ')+3:l.find('</td>')]
if 'nowrap>summary:</th>' in l2:
title = l[l.find('<td>')+4:l.find('</td>')]
if 'nowrap>assigned to:</th>' in l2:
assignee = strip_tags(l[l.find('<td>')+4:l.find('</td>')])
if assignee == 'Unassigned':
assignee = 'nobody'
2007-02-04 17:35:40 +00:00
return [(id, package, title, severity, status, assignee, "%s?L%d" % (self.url, id))]
2006-06-26 17:57:20 +00:00
sfre = re.compile(r"""
.*?
<h2>\[.*?\]\s*(?P<title>.*?)</h2>
.*?
2007-02-04 22:10:10 +00:00
assigned.*?<br>\s+(?P<assignee>\S+)
2006-12-27 14:33:59 +00:00
.*?
2007-02-04 22:10:10 +00:00
priority.*?(?P<priority>\d+)
2006-06-26 17:57:20 +00:00
.*?
2007-02-04 22:10:10 +00:00
status.*?<br>\s+(?P<status>\S+)
2006-06-26 17:57:20 +00:00
.*?
2007-02-04 22:10:10 +00:00
resolution.*?<br>\s+(?P<resolution>\S+)
2006-06-26 17:57:20 +00:00
.*?
""", re.VERBOSE | re.DOTALL | re.I)
class Sourceforge(IBugtracker):
_sf_url = 'http://sf.net/support/tracker.php?aid=%d'
def get_bug(self, id):
url = self._sf_url % id
try:
bugdata = utils.web.getUrl(url)
except Exception, e:
s = 'Could not parse data returned by %s: %s' % (self.description, e)
raise BugtrackerError, s
try:
reo = sfre.search(bugdata)
status = reo.group('status')
resolution = reo.group('resolution')
if not (resolution.lower() == 'none'):
status += ' ' + resolution
2006-12-27 14:33:59 +00:00
return [(id, None, reo.group('title'), "Pri: %s" % reo.group('priority'), status, reo.group('assignee'),self._sf_url % id)]
2006-06-26 17:57:20 +00:00
except:
2007-02-04 22:10:10 +00:00
raise
raise BugNotFoundError
2006-06-26 17:57:20 +00:00
# Introspection is quite cool
defined_bugtrackers = {}
v = vars()
for k in v.keys():
if type(v[k]) == type(IBugtracker) and issubclass(v[k], IBugtracker) and not (v[k] == IBugtracker):
defined_bugtrackers[k.lower()] = v[k]
registerBugtracker('mozilla', 'http://bugzilla.mozilla.org', 'Mozilla', 'bugzilla')
registerBugtracker('ubuntu', 'http://bugzilla.ubuntu.com', 'Ubuntu', 'bugzilla')
registerBugtracker('gnome', 'http://bugzilla.gnome.org', 'Gnome', 'bugzilla')
registerBugtracker('gnome2', 'http://bugs.gnome.org', 'Gnome', 'bugzilla')
registerBugtracker('kde', 'http://bugs.kde.org', 'KDE', 'bugzilla')
registerBugtracker('ximian', 'http://bugzilla.ximian.com', 'Ximian', 'bugzilla')
registerBugtracker('freedesktop', 'http://bugzilla.freedesktop.org', 'Freedesktop', 'bugzilla')
registerBugtracker('freedesktop2', 'http://bugs.freedesktop.org', 'Freedesktop', 'bugzilla')
registerBugtracker('openoffice', 'http://openoffice.org/issues', 'OpenOffice.org', 'issuezilla')
registerBugtracker('launchpad', 'https://launchpad.net', 'Launchpad', 'launchpad')
registerBugtracker('lp', 'https://launchpad.net', 'Launchpad', 'launchpad')
registerBugtracker('malone', 'https://launchpad.net', 'Launchpad', 'launchpad')
2006-06-26 17:57:20 +00:00
registerBugtracker('debian', 'http://bugs.debian.org', 'Debian', 'debbugs')
2007-02-04 22:10:10 +00:00
registerBugtracker('trac', 'http://trac.edgewall.org/ticket', 'Trac', 'trac')
2006-06-26 17:57:20 +00:00
registerBugtracker('django', 'http://code.djangoproject.com/ticket', 'Django', 'trac')
registerBugtracker('cups', 'http://www.cups.org/str.php', 'CUPS', 'str')
registerBugtracker('gnewsense', 'http://bugs.gnewsense.org/Bugs', 'gNewSense', 'wikiforms')
2006-06-26 17:57:20 +00:00
registerBugtracker('supybot', 'http://sourceforge.net/tracker/?group_id=58965&atid=489447', 'Supybot', 'sourceforge')
# Don't delete this one
2006-06-26 17:57:20 +00:00
registerBugtracker('sourceforge', 'http://sourceforge.net/tracker/', 'Sourceforge', 'sourceforge')
Class = Bugtracker