Bugtracker: Various improvements.
* Add option to set aliases on trackers. * Complete removal and renaming of trackers. * Only load default trackers if none set yet. * Improve handling of auto-discovered trackers. * Add function to reset trackers to defaults. * Minor fix to Encyclopedia along the way.
This commit is contained in:
@ -29,16 +29,18 @@ import xml.dom.minidom as minidom
|
||||
from email.parser import FeedParser
|
||||
from pysimplesoap.client import SoapClient
|
||||
|
||||
def registerBugtracker(name, url='', description='', trackertype=''):
|
||||
conf.supybot.plugins.Bugtracker.bugtrackers().add(name)
|
||||
def registerBugtracker(name, url='', description='', trackertype='', aliases=[]):
|
||||
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, ''))
|
||||
ALIASES = conf.registerGlobalValue(group, 'aliases', registry.SpaceSeparatedSetOfStrings(aliases, ''))
|
||||
if url:
|
||||
URL.setValue(url)
|
||||
if description:
|
||||
DESC.setValue(description)
|
||||
if aliases:
|
||||
ALIASES.setValue(aliases)
|
||||
if trackertype:
|
||||
if trackertype in defined_bugtrackers:
|
||||
TRACKERTYPE.setValue(trackertype)
|
||||
@ -125,17 +127,26 @@ class Bugtracker(callbacks.PluginRegexp):
|
||||
def __init__(self, irc):
|
||||
self.__parent = super(Bugtracker, self)
|
||||
self.__parent.__init__(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:
|
||||
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()))
|
||||
self.set_trackers()
|
||||
self.shown = {}
|
||||
|
||||
def set_trackers(self):
|
||||
self.db = ircutils.IrcDict()
|
||||
self.aliases = {}
|
||||
trackers = self.registryValue('bugtrackers', value=False)._children
|
||||
if not trackers:
|
||||
for (name, (url, description, trackertype, aliases)) in list(default_bugtrackers.items()):
|
||||
registerBugtracker(name, url, description, trackertype, aliases)
|
||||
for name in list(trackers.keys()):
|
||||
if trackers[name].trackertype() in defined_bugtrackers:
|
||||
self.db[name] = defined_bugtrackers[trackers[name].trackertype()](name, trackers[name].url(),
|
||||
trackers[name].description(), trackers[name].trackertype(), trackers[name].aliases())
|
||||
for a in trackers[name].aliases():
|
||||
self.aliases[a] = name
|
||||
else:
|
||||
supylog.warning("Bugtracker: Unknown trackertype: %s (%s)" % (trackers[name].trackertype(), name))
|
||||
self.shorthand = utils.abbrev(list(self.db.keys()))
|
||||
|
||||
def is_ok(self, channel, tracker, bug):
|
||||
"""Flood/repeat protection"""
|
||||
now = time.time()
|
||||
@ -151,7 +162,7 @@ class Bugtracker(callbacks.PluginRegexp):
|
||||
"""<name> <type> <url> [<description>]
|
||||
|
||||
Add a bugtracker to the list of defined bugtrackers. Currently supported types are
|
||||
Launchpad, Debbugs, Bugzilla, SourceForge, Github, GitLab, Gitea, Mantis, and Trac.
|
||||
Launchpad, Debbugs, Bugzilla, SourceForge, GitHub, GitLab, Gitea, Mantis, and Trac.
|
||||
<name> will be used to reference the bugtracker in all commands.
|
||||
Unambiguous abbreviations of it will also be accepted.
|
||||
<description> will be used to reference the bugtracker in the
|
||||
@ -181,8 +192,12 @@ class Bugtracker(callbacks.PluginRegexp):
|
||||
"""
|
||||
try:
|
||||
name = self.shorthand[name.lower()]
|
||||
for a in self.db[name].aliases:
|
||||
del self.aliases[a]
|
||||
del self.db[name]
|
||||
self.registryValue('bugtrackers').remove(name)
|
||||
group = self.registryValue('bugtrackers', value=False)
|
||||
if name in group._children:
|
||||
group.unregister(name)
|
||||
self.shorthand = utils.abbrev(list(self.db.keys()))
|
||||
irc.replySuccess()
|
||||
except KeyError:
|
||||
@ -191,52 +206,140 @@ class Bugtracker(callbacks.PluginRegexp):
|
||||
remove = wrap(remove, [('checkCapability', 'admin'), 'text'])
|
||||
|
||||
def rename(self, irc, msg, args, oldname, newname, newdesc):
|
||||
"""<oldname> <newname>
|
||||
"""<oldname> <newname> [<newdescription>]
|
||||
|
||||
Rename the bugtracker associated with <oldname> to <newname>.
|
||||
Rename the bugtracker associated with <oldname> to <newname>,
|
||||
optionally with <newdescription>.
|
||||
"""
|
||||
try:
|
||||
name = self.shorthand[oldname.lower()]
|
||||
group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False)
|
||||
d = group.description()
|
||||
oldname = self.shorthand[oldname.lower()]
|
||||
newname = newname.lower()
|
||||
tracker = self.db[oldname]
|
||||
d = tracker.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.db[newname] = defined_bugtrackers[tracker.trackertype](newname, tracker.url, d, tracker.aliases)
|
||||
registerBugtracker(newname, tracker.url, d, tracker.trackertype, tracker.aliases)
|
||||
del self.db[oldname]
|
||||
group = self.registryValue('bugtrackers', value=False)
|
||||
if oldname in group._children:
|
||||
group.unregister(oldname)
|
||||
self.shorthand = utils.abbrev(list(self.db.keys()))
|
||||
for a in tracker.aliases:
|
||||
self.aliases[a] = newname
|
||||
irc.replySuccess()
|
||||
except KeyError:
|
||||
s = self.registryValue('replyNoBugtracker', msg.args[0] if ircutils.isChannel(msg.args[0]) else None)
|
||||
irc.error(s % oldname)
|
||||
rename = wrap(rename, [('checkCapability', 'admin'), 'something', 'something', additional('text')])
|
||||
|
||||
def alias(self, irc, msg, args, name, alias):
|
||||
"""<name> [<alias>]
|
||||
|
||||
Add an alias to a defined bugtracker, or list any set ones.
|
||||
"""
|
||||
try:
|
||||
name = self.shorthand[name.lower()]
|
||||
if not alias:
|
||||
a = self.db[name].aliases
|
||||
irc.reply('%s: %s' % (name, utils.str.commaAndify(sorted(a)) if a else 'No aliases set'))
|
||||
return
|
||||
alias = alias.lower()
|
||||
self.db[name].aliases.add(alias)
|
||||
trackers = self.registryValue('bugtrackers', value=False)._children
|
||||
if name not in trackers:
|
||||
tracker = self.db[name]
|
||||
registerBugtracker(tracker.name, tracker.url, tracker.description, tracker.trackertype, tracker.aliases)
|
||||
trackers[name].aliases().add(alias)
|
||||
self.aliases[alias] = name
|
||||
irc.replySuccess()
|
||||
except KeyError:
|
||||
s = self.registryValue('replyNoBugtracker', msg.args[0] if ircutils.isChannel(msg.args[0]) else None)
|
||||
irc.error(s % name)
|
||||
rename = wrap(rename, [('checkCapability', 'admin'), 'something', 'something', additional('text')])
|
||||
alias = wrap(alias, [('checkCapability', 'admin'), 'something', additional('text')])
|
||||
|
||||
def unalias(self, irc, msg, args, name, alias):
|
||||
"""<name> <alias>
|
||||
|
||||
Remove an alias from a defined bugtracker.
|
||||
"""
|
||||
try:
|
||||
name = self.shorthand[name.lower()]
|
||||
c = 'bugtrackers.%s.aliases' % name.replace('.','\\.')
|
||||
alias = alias.lower()
|
||||
try:
|
||||
self.db[name].aliases.remove(alias)
|
||||
trackers = self.registryValue('bugtrackers', value=False)._children
|
||||
if name in trackers:
|
||||
trackers[name].aliases().remove(alias)
|
||||
del self.aliases[alias]
|
||||
irc.replySuccess()
|
||||
except ValueError:
|
||||
irc.error("Bugtracker '%s' has no alias '%s' set" % (name, alias))
|
||||
return
|
||||
except KeyError:
|
||||
s = self.registryValue('replyNoBugtracker', msg.args[0] if ircutils.isChannel(msg.args[0]) else None)
|
||||
irc.error(s % name)
|
||||
unalias = wrap(unalias, [('checkCapability', 'admin'), 'something', 'something'])
|
||||
|
||||
def list(self, irc, msg, args, name):
|
||||
"""[abbreviation]
|
||||
"""[<abbreviation>]
|
||||
|
||||
List defined bugtrackers. If [abbreviation] is specified, list the
|
||||
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, trackertype) = (self.db[name].url, self.db[name].description,
|
||||
self.db[name].__class__.__name__)
|
||||
irc.reply('%s: %s, %s [%s]' % (name, description, url, trackertype))
|
||||
name = self.shorthand[name.lower()]
|
||||
tracker = self.db[name]
|
||||
irc.reply('%s%s: %s, %s [%s]' % (name, ' (%s)' % ', '.join(sorted(tracker.aliases)) if tracker.aliases else '',
|
||||
tracker.description, tracker.url, tracker.__class__.__name__))
|
||||
except KeyError:
|
||||
s = self.registryValue('replyNoBugtracker', msg.args[0] if ircutils.isChannel(msg.args[0]) else None)
|
||||
irc.error(s % name)
|
||||
else:
|
||||
if self.db:
|
||||
L = list(self.db.keys())
|
||||
L.sort()
|
||||
irc.reply(utils.str.commaAndify(L))
|
||||
irc.reply(utils.str.commaAndify(sorted(list(self.db.keys()))))
|
||||
else:
|
||||
irc.reply('I have no defined bugtrackers.')
|
||||
list = wrap(list, [additional('text')])
|
||||
|
||||
def reset(self, irc, msg, args, name):
|
||||
"""[<abbreviation>]
|
||||
|
||||
Reset defined bugtrackers to defaults. If <abbreviation> is specified,
|
||||
reset only that bugtracker.
|
||||
"""
|
||||
group = self.registryValue('bugtrackers', value=False)
|
||||
if name:
|
||||
try:
|
||||
name = self.shorthand[name.lower()]
|
||||
for a in self.db[name].aliases:
|
||||
del self.aliases[a]
|
||||
del self.db[name]
|
||||
if name in group._children:
|
||||
group.unregister(name)
|
||||
if name in default_bugtrackers:
|
||||
(url, description, trackertype, aliases) = default_bugtrackers[name]
|
||||
if trackertype in defined_bugtrackers:
|
||||
self.db[name] = defined_bugtrackers[trackertype](name, url, description, trackertype, aliases)
|
||||
for a in aliases:
|
||||
self.aliases[a] = name
|
||||
registerBugtracker(name, url, description, trackertype, aliases)
|
||||
else:
|
||||
supylog.warning("Bugtracker: Unknown trackertype: %s (%s)" % (trackers[name].trackertype(), name))
|
||||
self.shorthand = utils.abbrev(list(self.db.keys()))
|
||||
except KeyError:
|
||||
s = self.registryValue('replyNoBugtracker', msg.args[0] if ircutils.isChannel(msg.args[0]) else None)
|
||||
irc.error(s % name)
|
||||
return
|
||||
else:
|
||||
for name in list(group._children.keys())[:]:
|
||||
group.unregister(name)
|
||||
self.set_trackers()
|
||||
irc.replySuccess()
|
||||
reset = wrap(reset, [('checkCapability', 'admin'), additional('text')])
|
||||
|
||||
def inFilter(self, irc, msg):
|
||||
if not (msg.prefix and msg.args):
|
||||
return msg
|
||||
@ -283,12 +386,16 @@ class Bugtracker(callbacks.PluginRegexp):
|
||||
if len(bt) == 1 and not sure_bug:
|
||||
try:
|
||||
name = bt[0]
|
||||
if name in list(self.aliases.keys()):
|
||||
name = self.aliases[name]
|
||||
tracker = self.db[name]
|
||||
except:
|
||||
return
|
||||
elif len(bt) == 2:
|
||||
try:
|
||||
name = bt[0]
|
||||
if name in list(self.aliases.keys()):
|
||||
name = self.aliases[name]
|
||||
tracker = self.db[name]
|
||||
except:
|
||||
name = ''
|
||||
@ -405,10 +512,7 @@ class Bugtracker(callbacks.PluginRegexp):
|
||||
snarfurl = re.sub(r'pad\.lv/(bug=)?(?P<bug>[0-9]+)', r'launchpad.net/bugs/\g<bug>', snarfurl)
|
||||
|
||||
for t in list(self.db.keys()):
|
||||
tracker = self.db.get(t, None)
|
||||
if not tracker:
|
||||
supylog.error("No tracker for key '%s'" % t)
|
||||
continue
|
||||
tracker = self.db[t]
|
||||
url = tracker.url[tracker.url.rfind('://')+3:]
|
||||
if url in snarfurl:
|
||||
return tracker
|
||||
@ -433,6 +537,8 @@ class Bugtracker(callbacks.PluginRegexp):
|
||||
|
||||
if tracker:
|
||||
self.db[tracker.name] = tracker
|
||||
if self.registryValue('saveDiscoveredTrackers'):
|
||||
registerBugtracker(tracker.name, tracker.url, tracker.description, tracker.trackertype, tracker.aliases)
|
||||
self.shorthand = utils.abbrev(list(self.db.keys()))
|
||||
return tracker
|
||||
|
||||
@ -501,11 +607,12 @@ class Bugtracker(callbacks.PluginRegexp):
|
||||
|
||||
# Define all bugtrackers
|
||||
class IBugtracker:
|
||||
def __init__(self, name=None, url=None, description=None, trackertype=None):
|
||||
def __init__(self, name=None, url=None, description=None, trackertype=None, aliases=[]):
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.description = description
|
||||
self.trackertype = trackertype
|
||||
self.aliases = set(aliases)
|
||||
self.errget = 'Could not get data from %s: %s (%s)'
|
||||
self.errparse = 'Could not parse data from %s: %s (%s)'
|
||||
|
||||
@ -530,10 +637,10 @@ class IBugtracker:
|
||||
class Bugzilla(IBugtracker):
|
||||
def get_tracker(self, url):
|
||||
try:
|
||||
match = re.match(r'(?P<url>(?P<name>[^\s/]+).*)/show_bug\.cgi', url)
|
||||
name = desc = match.group('name')
|
||||
match = re.match(r'(?P<url>(?P<desc>[^\s/]+).*)/show_bug\.cgi', url)
|
||||
desc = match.group('desc')
|
||||
name = desc.lower()
|
||||
url = 'https://%s' % match.group('url')
|
||||
# registerBugtracker(name, url, desc, 'bugzilla')
|
||||
return Bugzilla(name, url, desc, 'bugzilla')
|
||||
except:
|
||||
pass
|
||||
@ -797,9 +904,9 @@ 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')
|
||||
desc = match.group(0)
|
||||
name = desc.lower()
|
||||
url = 'https://%s' % desc
|
||||
return SourceForge(name, url, desc, 'sourceforge')
|
||||
except:
|
||||
pass
|
||||
@ -826,11 +933,11 @@ 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
|
||||
desc = match.group(0)
|
||||
url = 'https://%s' % desc
|
||||
# Pulls are inconsistent in main and single page URLs
|
||||
name = desc = re.sub(r'/pull$', r'/pulls', name)
|
||||
# registerBugtracker(name, url, desc, 'github')
|
||||
desc = re.sub(r'/pull$', r'/pulls', desc)
|
||||
name = desc.lower()
|
||||
return GitHub(name, url, desc, 'github')
|
||||
except:
|
||||
pass
|
||||
@ -862,12 +969,12 @@ class GitLab(IBugtracker):
|
||||
def get_tracker(self, url, id):
|
||||
try:
|
||||
match = re.match(r'[^\s/]+/[^\s/]+/[^\s/]+/(issues|merge_requests)', url)
|
||||
name = desc = match.group(0)
|
||||
url = 'https://%s' % name
|
||||
desc = match.group(0)
|
||||
name = desc.lower()
|
||||
url = 'https://%s' % desc
|
||||
bugurl = '%s/%d.json' % (url, id)
|
||||
bugjson = utils.web.getUrl(bugurl)
|
||||
bug = json.loads(bugjson.decode('utf-8'))
|
||||
# registerBugtracker(name, url, desc, 'gitlab')
|
||||
return GitLab(name, url, desc, 'gitlab')
|
||||
except:
|
||||
pass
|
||||
@ -894,12 +1001,12 @@ class Gitea(IBugtracker):
|
||||
def get_tracker(self, url, id):
|
||||
try:
|
||||
match = re.match(r'[^\s/]+/[^\s/]+/[^\s/]+/(issues|pulls)', url)
|
||||
name = desc = match.group(0)
|
||||
url = 'https://%s' % name
|
||||
desc = match.group(0)
|
||||
name = desc.lower()
|
||||
url = 'https://%s' % desc
|
||||
bugurl = '%s/%d' % (re.sub(r'://[^\s/]+/', r'\g<0>api/v1/repos/', url), id)
|
||||
bugjson = utils.web.getUrl(bugurl)
|
||||
bug = json.loads(bugjson.decode('utf-8'))
|
||||
# registerBugtracker(name, url, desc, 'gitea')
|
||||
return Gitea(name, url, desc, 'gitea')
|
||||
except:
|
||||
pass
|
||||
@ -933,10 +1040,10 @@ class Mantis(IBugtracker):
|
||||
|
||||
def get_tracker(self, url):
|
||||
try:
|
||||
match = re.match(r'(?P<url>(?P<name>[^\s/]+).*)/view\.php', url)
|
||||
name = desc = match.group('name')
|
||||
match = re.match(r'(?P<url>(?P<desc>[^\s/]+).*)/view\.php', url)
|
||||
desc = match.group('desc')
|
||||
name = desc.lower()
|
||||
url = 'https://%s' % match.group('url')
|
||||
# registerBugtracker(name, url, desc, 'mantis')
|
||||
return Mantis(name, url, desc, 'mantis')
|
||||
except:
|
||||
pass
|
||||
@ -982,10 +1089,10 @@ class Mantis(IBugtracker):
|
||||
class Trac(IBugtracker):
|
||||
def get_tracker(self, url):
|
||||
try:
|
||||
match = re.match(r'(?P<name>[^\s/]+).*/ticket', url)
|
||||
name = desc = match.group('name')
|
||||
match = re.match(r'(?P<desc>[^\s/]+).*/ticket', url)
|
||||
desc = match.group('desc')
|
||||
name = desc.lower()
|
||||
url = 'https://%s' % match.group(0)
|
||||
# registerBugtracker(name, url, desc, 'trac')
|
||||
return Trac(name, url, desc, 'trac')
|
||||
except:
|
||||
pass
|
||||
@ -1033,23 +1140,22 @@ for k in list(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', 'https://bugzilla.mozilla.org', 'Mozilla', 'bugzilla')
|
||||
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')
|
||||
registerBugtracker('ubuntu', 'https://launchpad.net', 'Ubuntu', 'launchpad')
|
||||
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('pidgin', 'https://developer.pidgin.im/ticket', 'Pidgin', 'trac')
|
||||
default_bugtrackers = {
|
||||
'mozilla': ('https://bugzilla.mozilla.org', 'Mozilla', 'bugzilla', []),
|
||||
'gtk': ('https://gitlab.gnome.org/GNOME/gtk/issues', 'GTK', 'gitlab', []),
|
||||
'kde': ('https://bugs.kde.org', 'KDE', 'bugzilla', []),
|
||||
'xfce': ('https://bugzilla.xfce.org', 'Xfce', 'bugzilla', []),
|
||||
'lxde': ('https://sourceforge.net/p/lxde/bugs', 'LXDE', 'sourceforge', []),
|
||||
'freedesktop': ('https://bugzilla.freedesktop.org', 'Freedesktop', 'bugzilla', []),
|
||||
'freedesktop2': ('https://bugs.freedesktop.org', 'Freedesktop', 'bugzilla', []),
|
||||
'openoffice': ('https://bz.apache.org/ooo', 'OpenOffice', 'bugzilla', []),
|
||||
'launchpad': ('https://launchpad.net', 'Launchpad', 'launchpad', ['lp', 'ubuntu', 'ubottu']),
|
||||
'debian': ('https://bugs.debian.org', 'Debian', 'debbugs', []),
|
||||
'supybot': ('https://sourceforge.net/p/supybot/bugs', 'Supybot', 'sourceforge', []),
|
||||
'irssi': ('https://github.com/irssi/irssi/issues', 'Irssi', 'github', []),
|
||||
'mantis': ('https://www.mantisbt.org/bugs', 'Mantis', 'mantis', []),
|
||||
'trac': ('https://trac.edgewall.org/ticket', 'Trac', 'trac', []),
|
||||
'pidgin': ('https://developer.pidgin.im/ticket', 'Pidgin', 'trac', [])
|
||||
}
|
||||
|
||||
Class = Bugtracker
|
||||
|
Reference in New Issue
Block a user