diff --git a/Bugtracker/__init__.py b/Bugtracker/__init__.py index 0e18efb..f62b8b0 100644 --- a/Bugtracker/__init__.py +++ b/Bugtracker/__init__.py @@ -24,7 +24,7 @@ import supybot.world as world from imp import reload -__version__ = "2.8.0" +__version__ = "2.9.0" __author__ = supybot.Author("Krytarik Raido", "krytarik", "krytarik@tuxgarage.com") __contributors__ = { supybot.Author("Dennis Kaarsemaker", "Seveas", "dennis@kaarsemaker.net"): ['Original Author'], diff --git a/Bugtracker/plugin.py b/Bugtracker/plugin.py index 113eaaf..2f2b991 100644 --- a/Bugtracker/plugin.py +++ b/Bugtracker/plugin.py @@ -24,7 +24,7 @@ import supybot.conf as conf import supybot.registry as registry import supybot.log as supylog -import re, os, sys, time +import re, os, sys, time, json import xml.dom.minidom as minidom from email.parser import FeedParser if sys.version_info < (3,0): @@ -131,7 +131,7 @@ class Bugtracker(callbacks.PluginRegexp): registerBugtracker(name) group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False) if group.trackertype() in defined_bugtrackers: - self.db[name] = defined_bugtrackers[group.trackertype()](name, group.url(), group.description()) + self.db[name] = defined_bugtrackers[group.trackertype()](name, group.url(), group.description(), group.trackertype()) else: supylog.warning("Bugtracker: Unknown trackertype: %s (%s)" % (group.trackertype(), name)) self.shorthand = utils.abbrev(list(self.db.keys())) @@ -151,8 +151,8 @@ class Bugtracker(callbacks.PluginRegexp): def add(self, irc, msg, args, name, trackertype, url, description): """ [] - Add a bugtracker to the list of defined bugtrackers. Currently - supported types are Launchpad, Debbugs, Bugzilla, Mantis, and Trac. + Add a bugtracker to the list of defined bugtrackers. Currently supported + types are Launchpad, Debbugs, Bugzilla, SourceForge, Github, Mantis, and Trac. will be used to reference the bugtracker in all commands. Unambiguous abbreviations of it will also be accepted. will be used to reference the bugtracker in the @@ -165,7 +165,7 @@ class Bugtracker(callbacks.PluginRegexp): url = url[:-1] trackertype = trackertype.lower() if trackertype in defined_bugtrackers: - self.db[name] = defined_bugtrackers[trackertype](name, url, description) + self.db[name] = defined_bugtrackers[trackertype](name, url, description, trackertype) else: irc.error("Bugtrackers of type '%s' are not understood" % trackertype) return @@ -323,7 +323,7 @@ class Bugtracker(callbacks.PluginRegexp): irc.reply(r) def turlSnarfer(self, irc, msg, match): - r"(https?://)?((bugs\.debian\.org|pad\.lv)/|\S+/(show_bug\.cgi\?id=|bugreport\.cgi\?bug=|view\.php\?id=|bug=|bugs/|\+bug/|ticket/))(?P\d+)" + r"(https?://)?((bugs\.debian\.org|pad\.lv)/|\S+/(show_bug\.cgi\?id=|bugreport\.cgi\?bug=|view\.php\?id=|bug=|bugs/|\+bug/|ticket/|feature-requests/|patches/|todo/|issues/|pulls?/))(?P\d+)/?" channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None if checkAddressed(msg.args[1].strip(), channel): return @@ -396,6 +396,9 @@ class Bugtracker(callbacks.PluginRegexp): #TODO: As we will depend on launchpadlib, we should consider using lazr.uri.URI to do URL parsing def get_tracker(self, snarfurl): + # SourceForge short domain + snarfurl = snarfurl.replace('sf.net', 'sourceforge.net', 1) + # Launchpad URL shortening snarfurl = re.sub(r'pad\.lv/(bug=)?(?P[0-9]+)', r'launchpad.net/bugs/\g', snarfurl) @@ -411,10 +414,21 @@ class Bugtracker(callbacks.PluginRegexp): # No tracker found, bummer. Let's try and get one if 'show_bug.cgi' in snarfurl: tracker = Bugzilla().get_tracker(snarfurl) - if tracker: - self.db[tracker.name] = tracker - self.shorthand = utils.abbrev(list(self.db.keys())) - return tracker + elif 'sourceforge.net' in snarfurl: + tracker = SourceForge().get_tracker(snarfurl) + elif 'github.com' in snarfurl: + tracker = GitHub().get_tracker(snarfurl) + elif 'view.php' in snarfurl: + tracker = Mantis().get_tracker(snarfurl) + elif '/ticket/' in snarfurl: + tracker = Trac().get_tracker(snarfurl) + else: + return None + + if tracker: + self.db[tracker.name] = tracker + self.shorthand = utils.abbrev(list(self.db.keys())) + return tracker return None def get_bug(self, channel, tracker, id, do_assignee, do_extinfo, do_url=True, do_tracker=True): @@ -430,10 +444,20 @@ class Bugtracker(callbacks.PluginRegexp): if duplicate and not self.is_ok(channel, tracker, bid): continue - if do_tracker: - report = '%s bug %d' % (tracker.description, bid) + bugtype = re.match(r'.*/(feature-)?(?Prequest|patch|todo|issue|pull|ticket)(e?s)?/[0-9]+/?$', url) + if do_tracker and tracker.trackertype != 'github': + if re.match(r'.*/(bugs|feature-requests|patches|todo|issues|pulls?|ticket)/?$', tracker.description): + report = '%s %d' % (tracker.description, bid) + else: + if bugtype: + report = '%s %s %d' % (tracker.description, bugtype.group('type'), bid) + else: + report = '%s bug %d' % (tracker.description, bid) else: - report = 'Bug %d' % bid + if bugtype: + report = '%s %d' % (bugtype.group('type').title(), bid) + else: + report = 'Bug %d' % bid if product: report += ' in %s' % product @@ -472,10 +496,11 @@ class Bugtracker(callbacks.PluginRegexp): # Define all bugtrackers class IBugtracker: - def __init__(self, name=None, url=None, description=None): + def __init__(self, name=None, url=None, description=None, trackertype=None): self.name = name self.url = url self.description = description + self.trackertype = trackertype self.errget = 'Could not get data from %s: %s (%s)' self.errparse = 'Could not parse data from %s: %s (%s)' @@ -504,11 +529,36 @@ class Bugzilla(IBugtracker): name = desc = match.group('name') url = 'https://%s' % match.group('url') # registerBugtracker(name, url, desc, 'bugzilla') - return Bugzilla(name, url, desc) + return Bugzilla(name, url, desc, 'bugzilla') except: return None def get_bug(self, id): + url = "%s/rest/bug/%d" % (self.url, id) + try: + bugjson = utils.web.getUrl(url) + bug = json.loads(bugjson.decode('utf-8'))['bugs'][0] + except Exception as e: + # For old-stable Bugzilla + if 'HTTP Error 404' in str(e): + return self.get_bug_old(id) + raise BugtrackerError(self.errget % (self.description, e, url)) + try: + status = bug['status'] + if bug['resolution']: + status += ': %s' % bug['resolution'] + if bug['assigned_to_detail']: + assignee = bug['assigned_to_detail']['real_name'] + if not assignee: + assignee = bug['assigned_to_detail']['name'] + else: + assignee = '' + return [(id, bug['product'], bug['summary'], bug['severity'], status, assignee, + "%s/show_bug.cgi?id=%d" % (self.url, id), [], [])] + except Exception as e: + raise BugtrackerError(self.errparse % (self.description, e, url)) + + def get_bug_old(self, id): # Deprecated url = "%s/show_bug.cgi?id=%d&ctype=xml" % (self.url, id) try: bugxml = utils.web.getUrl(url) @@ -741,6 +791,71 @@ class Debbugs(IBugtracker): except Exception as e: raise BugtrackerError(self.errparse % (self.description, e, url)) +class SourceForge(IBugtracker): + def get_tracker(self, url): + try: + match = re.match(r'sourceforge\.net/p/[^\s/]+/(bugs|feature-requests|patches|todo)', url) + name = desc = match.group(0) + url = 'https://%s' % name +# registerBugtracker(name, url, desc, 'sourceforge') + return SourceForge(name, url, desc, 'sourceforge') + except: + return None + + def get_bug(self, id): + url = "%s/%d/" % (self.url.replace('sourceforge.net', 'sourceforge.net/rest'), id) + try: + bugjson = utils.web.getUrl(url) + bug = json.loads(bugjson.decode('utf-8'))['ticket'] + except Exception as e: + raise BugtrackerError(self.errget % (self.description, e, url)) + try: + product = severity = '' + if bug['labels']: + product = bug['labels'][0] + if '_priority' in bug['custom_fields']: + severity = 'Pri: %s' % bug['custom_fields']['_priority'] + return [(id, product, bug['summary'], severity, ': '.join(bug['status'].split('-')), + bug['assigned_to'], "%s/%d/" % (self.url, id), [], [])] + except Exception as e: + raise BugtrackerError(self.errparse % (self.description, e, url)) + +class GitHub(IBugtracker): + def get_tracker(self, url): + try: + match = re.match(r'github\.com/[^\s/]+/[^\s/]+/(issues|pulls?)', url) + name = desc = match.group(0) + url = 'https://%s' % name + # Pulls are inconsistent in main and single page URLs + name = desc = re.sub(r'/pull$', r'/pulls', name) +# registerBugtracker(name, url, desc, 'github') + return GitHub(name, url, desc, 'github') + except: + return None + + def get_bug(self, id): + url = "%s/%d" % (self.url.replace('github.com', 'api.github.com/repos'), id) + # Pulls are inconsistent in web and API URLs + url = url.replace('/pull/', '/pulls/') + try: + bugjson = utils.web.getUrl(url) + bug = json.loads(bugjson.decode('utf-8')) + except Exception as e: + raise BugtrackerError(self.errget % (self.description, e, url)) + try: + product = '/'.join(url.split('/')[-4:-2]) + if 'merged' in bug and bug['merged']: + status = 'Merged' + else: + status = bug['state'] + if bug['assignee']: + assignee = bug['assignee']['login'] + else: + assignee = '' + return [(id, product, bug['title'], '', status, assignee, bug['html_url'], [], [])] + except Exception as e: + raise BugtrackerError(self.errparse % (self.description, e, url)) + class Mantis(IBugtracker): def __init__(self, *args, **kwargs): if not sys.version_info < (3,0): @@ -749,11 +864,25 @@ class Mantis(IBugtracker): IBugtracker.__init__(self, *args, **kwargs) self.soap_proxy = SOAPProxy("%s/api/soap/mantisconnect.php" % self.url, namespace="http://futureware.biz/mantisconnect") + def get_tracker(self, url): + try: + match = re.match(r'(?P(?P[^\s/]+).*)/view\.php', url) + name = desc = match.group('name') + url = 'https://%s' % match.group('url') +# registerBugtracker(name, url, desc, 'mantis') + return Mantis(name, url, desc, 'mantis') + except: + return None + def get_bug(self, id): url = "%s/view.php?id=%d" % (self.url, id) try: raw = self.soap_proxy.mc_issue_get('', '', id) except Exception as e: + # Often SOAP is not enabled + if '.' in self.name: + supylog.exception(self.errget % (self.description, e, url)) + return raise BugtrackerError(self.errget % (self.description, e, url)) if not raw: raise BugNotFoundError @@ -767,11 +896,25 @@ class Mantis(IBugtracker): # has commas, things get tricky. # This should be more robust than the screen scraping done previously. class Trac(IBugtracker): + def get_tracker(self, url): + try: + match = re.match(r'(?P[^\s/]+).*/ticket', url) + name = desc = match.group('name') + url = 'https://%s' % match.group(0) +# registerBugtracker(name, url, desc, 'trac') + return Trac(name, url, desc, 'trac') + except: + return None + def get_bug(self, id): # This is still a little rough, but it works :) url = "%s/%d" % (self.url, id) try: raw = utils.web.getUrl("%s?format=tab" % url).decode('utf-8') except Exception as e: + # Due to unreliable matching + if '.' in self.name: + supylog.exception(self.errget % (self.description, e, url)) + return if 'HTTP Error 500' in str(e): raise BugNotFoundError raise BugtrackerError(self.errget % (self.description, e, url)) @@ -807,6 +950,7 @@ registerBugtracker('gnome', 'https://bugzilla.gnome.org', 'Gnome', 'bugzilla') registerBugtracker('gnome2', 'https://bugs.gnome.org', 'Gnome', 'bugzilla') registerBugtracker('kde', 'https://bugs.kde.org', 'KDE', 'bugzilla') registerBugtracker('xfce', 'https://bugzilla.xfce.org', 'Xfce', 'bugzilla') +registerBugtracker('lxde', 'https://sourceforge.net/p/lxde/bugs', 'LXDE', 'sourceforge') registerBugtracker('freedesktop', 'https://bugzilla.freedesktop.org', 'Freedesktop', 'bugzilla') registerBugtracker('freedesktop2', 'https://bugs.freedesktop.org', 'Freedesktop', 'bugzilla') registerBugtracker('openoffice', 'https://bz.apache.org/ooo', 'OpenOffice', 'bugzilla') @@ -815,7 +959,9 @@ registerBugtracker('ubottu', 'https://launchpad.net', 'Ubottu', 'launchpad') registerBugtracker('launchpad', 'https://launchpad.net', 'Launchpad', 'launchpad') registerBugtracker('lp', 'https://launchpad.net', 'Launchpad', 'launchpad') registerBugtracker('debian', 'https://bugs.debian.org', 'Debian', 'debbugs') +registerBugtracker('supybot', 'https://sourceforge.net/p/supybot/bugs', 'Supybot', 'sourceforge') +registerBugtracker('irssi', 'https://github.com/irssi/irssi/issues', 'irssi/irssi', 'github') registerBugtracker('mantis', 'https://www.mantisbt.org/bugs', 'Mantis', 'mantis') registerBugtracker('trac', 'https://trac.edgewall.org/ticket', 'Trac', 'trac') -registerBugtracker('django', 'https://code.djangoproject.com/ticket', 'Django', 'trac') +registerBugtracker('pidgin', 'https://developer.pidgin.im/ticket', 'Pidgin', 'trac') Class = Bugtracker