Bugtracker: Add support for commits.

This commit is contained in:
Krytarik Raido 2020-06-21 09:04:04 +02:00
parent 7473bf6fa7
commit e0926a9a3c
4 changed files with 178 additions and 90 deletions

View File

@ -24,7 +24,7 @@ import supybot.world as world
from imp import reload from imp import reload
__version__ = "4.2.0" __version__ = "4.3.0"
__author__ = supybot.Author("Krytarik Raido", "krytarik", "krytarik@tuxgarage.com") __author__ = supybot.Author("Krytarik Raido", "krytarik", "krytarik@tuxgarage.com")
__contributors__ = { __contributors__ = {
supybot.Author("Dennis Kaarsemaker", "Seveas", "dennis@kaarsemaker.net"): ['Original Author'], supybot.Author("Dennis Kaarsemaker", "Seveas", "dennis@kaarsemaker.net"): ['Original Author'],

View File

@ -49,8 +49,9 @@ def configure(advanced):
else: else:
return repeatdelay return repeatdelay
output("Each of the next 3 questions can be set per-channel with the '@config channel' command.") output("Each of the next 4 questions can be set per-channel with the '@config channel' command.")
bugSnarfer = yn("Enable detecting bug numbers and URLs in all channels?", default=Bugtracker.bugSnarfer._default) bugSnarfer = yn("Enable detecting bug numbers and URLs in all channels?", default=Bugtracker.bugSnarfer._default)
commitSnarfer = yn("Enable detecting commit hashes and URLs in all channels?", default=Bugtracker.commitSnarfer._default)
cveSnarfer = yn("Enable detecting CVE numbers and URLs in all channels?", default=Bugtracker.cveSnarfer._default) cveSnarfer = yn("Enable detecting CVE numbers and URLs in all channels?", default=Bugtracker.cveSnarfer._default)
oopsSnarfer = yn("Enable detecting Launchpad OOPS IDs in all channels?", default=Bugtracker.oopsSnarfer._default) oopsSnarfer = yn("Enable detecting Launchpad OOPS IDs in all channels?", default=Bugtracker.oopsSnarfer._default)
if advanced: if advanced:
@ -72,6 +73,7 @@ def configure(advanced):
saveDiscoveredTrackers = yn("Save automatically discovered trackers to configuration?", default=Bugtracker.saveDiscoveredTrackers._default) saveDiscoveredTrackers = yn("Save automatically discovered trackers to configuration?", default=Bugtracker.saveDiscoveredTrackers._default)
Bugtracker.bugSnarfer.setValue(bugSnarfer) Bugtracker.bugSnarfer.setValue(bugSnarfer)
Bugtracker.commitSnarfer.setValue(commitSnarfer)
Bugtracker.cveSnarfer.setValue(cveSnarfer) Bugtracker.cveSnarfer.setValue(cveSnarfer)
Bugtracker.oopsSnarfer.setValue(oopsSnarfer) Bugtracker.oopsSnarfer.setValue(oopsSnarfer)
Bugtracker.replyNoBugtracker.setValue(replyNoBugtracker) Bugtracker.replyNoBugtracker.setValue(replyNoBugtracker)
@ -90,6 +92,11 @@ conf.registerChannelValue(Bugtracker, 'bugSnarfer',
enabled, such that any bugtracker URLs and bug ### seen in the channel enabled, such that any bugtracker URLs and bug ### seen in the channel
will have their information reported into the channel.""")) will have their information reported into the channel."""))
conf.registerChannelValue(Bugtracker, 'commitSnarfer',
registry.Boolean(False, """Determines whether the commit snarfer will be
enabled, such that any commit URLs and commit ### seen in the channel
will have their information reported into the channel."""))
conf.registerChannelValue(Bugtracker, 'cveSnarfer', conf.registerChannelValue(Bugtracker, 'cveSnarfer',
registry.Boolean(False, """Determines whether the CVE snarfer will be registry.Boolean(False, """Determines whether the CVE snarfer will be
enabled, such that any CVE URLs and CVE-????-???? seen in the channel enabled, such that any CVE URLs and CVE-????-???? seen in the channel

View File

@ -71,7 +71,7 @@ class Bugtracker(callbacks.PluginRegexp):
"""Show a link to a bug report with a brief description""" """Show a link to a bug report with a brief description"""
threaded = True threaded = True
callBefore = ('URL') callBefore = ('URL')
regexps = ('turlSnarfer', 'bugSnarfer', 'cveSnarfer', 'oopsSnarfer') regexps = ('bugSnarfer', 'bugUrlSnarfer', 'commitSnarfer', 'commitUrlSnarfer', 'cveSnarfer', 'oopsSnarfer')
def __init__(self, irc): def __init__(self, irc):
self.__parent = super(Bugtracker, self) self.__parent = super(Bugtracker, self)
@ -97,14 +97,14 @@ class Bugtracker(callbacks.PluginRegexp):
supylog.warning("Bugtracker: Unknown trackertype '%s' (%s)" % (trackers[name].trackertype(), name)) supylog.warning("Bugtracker: Unknown trackertype '%s' (%s)" % (trackers[name].trackertype(), name))
self.shorthand = utils.abbrev(list(self.db.keys())) self.shorthand = utils.abbrev(list(self.db.keys()))
def is_ok(self, channel, tracker, bug): def is_ok(self, channel, tracker, bugid):
"""Flood/repeat protection""" """Flood/repeat protection"""
now = time.time() now = time.time()
for k in list(self.shown.keys()): for k in list(self.shown.keys()):
if self.shown[k] < now - self.registryValue('repeatdelay', channel): if self.shown[k] < now - self.registryValue('repeatdelay', channel):
self.shown.pop(k) self.shown.pop(k)
if (channel, tracker, bug) not in self.shown: if (channel, tracker, bugid) not in self.shown:
self.shown[(channel, tracker, bug)] = now self.shown[(channel, tracker, bugid)] = now
return True return True
return False return False
@ -308,26 +308,39 @@ class Bugtracker(callbacks.PluginRegexp):
def bugSnarfer(self, irc, msg, match): def bugSnarfer(self, irc, msg, match):
r"(?P<bt>[a-z][^\s:]*(\s+(bug|ticket|issue|pull|pr|merge|mr)('?s)?)?):*\s+#?(?P<bug>\d+(?!\d*[-.]\d+)(\s*([,\s]+|[,\s]*(and|und|en|et|ir|[&+]+))\s*#?\d+(?!\d*[-.]\d+))*)" r"(?P<bt>[a-z][^\s:]*(\s+(bug|ticket|issue|pull|pr|merge|mr)('?s)?)?):*\s+#?(?P<bug>\d+(?!\d*[-.]\d+)(\s*([,\s]+|[,\s]*(and|und|en|et|ir|[&+]+))\s*#?\d+(?!\d*[-.]\d+))*)"
self.termSnarfer(irc, msg, match, 'bug')
def commitSnarfer(self, irc, msg, match):
r"(?P<bt>[a-z][^\s:]*(\s+(commit)('?s)?)?):*\s+#?(?P<bug>[a-f0-9]{7,}(?![a-f0-9]*[-.][a-f0-9]{7,})(\s*([,\s]+|[,\s]*(and|und|en|et|ir|[&+]+))\s*#?[a-f0-9]{7,}(?![a-f0-9]*[-.][a-f0-9]{7,}))*)"
self.termSnarfer(irc, msg, match, 'commit')
def termSnarfer(self, irc, msg, match, termtype):
channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None
if checkAddressed(msg.args[1].strip(), channel): if checkAddressed(msg.args[1].strip(), channel):
return return
if not self.registryValue('bugSnarfer', channel): if not self.registryValue('{}Snarfer'.format(termtype), channel):
return return
nbugs = msg.tagged('nbugs') or 0 nbugs = msg.tagged('nbugs') or 0
if nbugs >= 5: if nbugs >= 5:
return return
bugids = re.split(r'[^\d]+', match.group('bug'))[:5-nbugs] if termtype != 'commit':
bugids = re.findall(r'[0-9]+', match.group('bug'))[:5-nbugs]
else:
bugids = re.findall(r'[a-f0-9]{7,}', match.group('bug'))[:5-nbugs]
# Begin HACK # Begin HACK
# Strings like "Ubuntu 1004" and "Ubuntu 1610" are false triggers for us # Strings like "Ubuntu 1004" and "Ubuntu 1610" are false triggers for us
if match.group('bt').lower() == 'ubuntu': if match.group('bt').lower() == 'ubuntu' and termtype == 'bug':
bugids = [x for x in bugids if not re.match(r'^([4-9]|[12][0-9])(04|10)$', x)] bugids = [x for x in bugids if not re.match(r'^([4-9]|[12][0-9])(04|10)$', x)]
# End HACK # End HACK
# Get tracker name # Get tracker name
bt = [x.lower() for x in match.group('bt').split()] bt = [x.lower() for x in match.group('bt').split()]
sure_bug = re.match(r"^(?P<type>bug|ticket|issue|pull|pr|merge|mr)('?s)?$", bt[-1]) if termtype != 'commit':
sure_bug = re.match(r"^(?P<type>bug|ticket|issue|pull|pr|merge|mr)('?s)?$", bt[-1])
else:
sure_bug = re.match(r"^(?P<type>commit)('?s)?$", bt[-1])
# Get bug type # Get bug type
if sure_bug: if sure_bug:
@ -337,7 +350,7 @@ class Bugtracker(callbacks.PluginRegexp):
bugids = list(set(bugids)) # remove dupes bugids = list(set(bugids)) # remove dupes
if not sure_bug: if not sure_bug and termtype == 'bug':
bugids = [x for x in bugids if int(x) > 100] bugids = [x for x in bugids if int(x) > 100]
msg.tag('nbugs', nbugs + len(bugids)) msg.tag('nbugs', nbugs + len(bugids))
@ -376,13 +389,12 @@ class Bugtracker(callbacks.PluginRegexp):
return return
for bugid in bugids: for bugid in bugids:
bugid = int(bugid)
try: try:
report = self.get_bug(channel or msg.nick, tracker, bugtype, bugid, self.registryValue('showassignee', channel), report = self.get_bug(channel or msg.nick, tracker, bugtype, bugid, self.registryValue('showassignee', channel),
self.registryValue('extended', channel), do_tracker=showTracker) self.registryValue('extended', channel), do_tracker=showTracker)
except trackers.BugNotFoundError: except trackers.BugNotFoundError:
if self.registryValue('replyWhenNotFound'): if self.registryValue('replyWhenNotFound'):
irc.error("Could not find %s bug %d" % (tracker.description, bugid)) irc.error("Could not find %s %s %s" % (tracker.description, termtype, bugid))
except trackers.BugtrackerError as e: except trackers.BugtrackerError as e:
if self.registryValue('replyWhenError') and sure_bug: if self.registryValue('replyWhenError') and sure_bug:
irc.error(str(e)) irc.error(str(e))
@ -390,19 +402,26 @@ class Bugtracker(callbacks.PluginRegexp):
if report: if report:
irc.reply(report) irc.reply(report)
def turlSnarfer(self, irc, msg, match): def bugUrlSnarfer(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/|feature-requests/|patches/|todo/|issues/|pulls?/|merge_requests/))(?P<bug>\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?/|merge_requests/))(?P<bug>\d+)"
self.urlSnarfer(irc, msg, match, 'bug')
def commitUrlSnarfer(self, irc, msg, match):
r"(https?://)?\S+/commits?/(?P<bug>[a-f0-9]{7,})"
self.urlSnarfer(irc, msg, match, 'commit')
def urlSnarfer(self, irc, msg, match, urltype):
channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None
if checkAddressed(msg.args[1].strip(), channel): if checkAddressed(msg.args[1].strip(), channel):
return return
if not self.registryValue('bugSnarfer', channel): if not self.registryValue('{}Snarfer'.format(urltype), channel):
return return
nbugs = msg.tagged('nbugs') or 0 nbugs = msg.tagged('nbugs') or 0
if nbugs >= 5: if nbugs >= 5:
return return
msg.tag('nbugs', nbugs+1) msg.tag('nbugs', nbugs+1)
url = match.group(0) url = match.group(0)
bugid = int(match.group('bug')) bugid = match.group('bug')
if '://' in url: if '://' in url:
url = url[url.rfind('://')+3:] url = url[url.rfind('://')+3:]
try: try:
@ -413,7 +432,7 @@ class Bugtracker(callbacks.PluginRegexp):
self.registryValue('extended', channel), do_url=False) self.registryValue('extended', channel), do_url=False)
except trackers.BugNotFoundError: except trackers.BugNotFoundError:
if self.registryValue('replyWhenNotFound'): if self.registryValue('replyWhenNotFound'):
irc.error("Could not find %s bug %s" % (tracker.description, match.group('bug'))) irc.error("Could not find %s %s %s" % (tracker.description, urltype, match.group('bug')))
except trackers.BugtrackerError as e: except trackers.BugtrackerError as e:
if self.registryValue('replyWhenError'): if self.registryValue('replyWhenError'):
irc.error(str(e)) irc.error(str(e))
@ -423,7 +442,7 @@ class Bugtracker(callbacks.PluginRegexp):
# Only useful to Launchpad developers # Only useful to Launchpad developers
def oopsSnarfer(self, irc, msg, match): def oopsSnarfer(self, irc, msg, match):
r"(https?://\S+[=/])?OOPS-(?P<oopsid>[\dA-Za-z]{6,})" r"(https?://\S+[=/])?OOPS-(?P<oopsid>[a-f0-9]{6,})"
channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None
if checkAddressed(msg.args[1].strip(), channel): if checkAddressed(msg.args[1].strip(), channel):
return return
@ -482,11 +501,11 @@ class Bugtracker(callbacks.PluginRegexp):
tracker = trackers.SourceForge().get_tracker(snarfurl) tracker = trackers.SourceForge().get_tracker(snarfurl)
elif 'github.com' in snarfurl: elif 'github.com' in snarfurl:
tracker = trackers.GitHub().get_tracker(snarfurl) tracker = trackers.GitHub().get_tracker(snarfurl)
elif re.match(r'[^\s/]+/[^\s/]+(/[^\s/]+)+/-/(issues|merge_requests)', snarfurl) \ elif re.match(r'[^\s/]+/[^\s/]+(/[^\s/]+)+/-/(issues|merge_requests|commits?)', snarfurl) \
or re.match(r'[^\s/]+/[^\s/]+(/[^\s/]+)+/merge_requests', snarfurl) \ or re.match(r'[^\s/]+/[^\s/]+(/[^\s/]+)+/merge_requests', snarfurl) \
or re.match(r'[^\s/]+/[^\s/]+(/[^\s/]+){2,}/issues', snarfurl): or re.match(r'[^\s/]+/[^\s/]+(/[^\s/]+){2,}/(issues|commits?)', snarfurl):
tracker = trackers.GitLab().get_tracker(snarfurl, bugid) tracker = trackers.GitLab().get_tracker(snarfurl, bugid)
elif re.match(r'[^\s/]+/[^\s/]+/[^\s/]+/issues', snarfurl): elif re.match(r'[^\s/]+/[^\s/]+/[^\s/]+/(issues|commits?)', snarfurl):
tracker = trackers.GitLab().get_tracker(snarfurl, bugid) tracker = trackers.GitLab().get_tracker(snarfurl, bugid)
if not tracker: if not tracker:
tracker = trackers.Gitea().get_tracker(snarfurl, bugid) tracker = trackers.Gitea().get_tracker(snarfurl, bugid)
@ -519,20 +538,20 @@ class Bugtracker(callbacks.PluginRegexp):
if duplicate and not self.is_ok(channel, tracker, bugid): if duplicate and not self.is_ok(channel, tracker, bugid):
return return
bugtype = re.match(r'.*/(feature-)?(?P<type>request|patch|todo|issue|pull|merge|ticket)(_requests)?(e?s)?/[0-9]+/?$', url) bugtype = re.match(r'.*/(feature-)?(?P<type>request|patch|todo|issue|pull|merge|ticket|commit)(_requests)?(e?s)?/[a-f0-9]+$', url)
if do_tracker and tracker.trackertype not in ('github', 'gitlab', 'gitea'): if do_tracker and tracker.trackertype not in ('github', 'gitlab', 'gitea'):
if re.match(r'.*/(bugs|feature-requests|patches|todo|issues|pulls?|merge_requests|ticket)/?$', tracker.description): if re.match(r'.*/(bugs|feature-requests|patches|todo|issues|pulls?|merge_requests|ticket|commits?)$', tracker.description):
report = '%s %d' % (tracker.description, bugid) report = '%s %s' % (tracker.description, bugid)
else: else:
if bugtype: if bugtype:
report = '%s %s %d' % (tracker.description, bugtype.group('type'), bugid) report = '%s %s %s' % (tracker.description, bugtype.group('type'), bugid)
else: else:
report = '%s bug %d' % (tracker.description, bugid) report = '%s bug %s' % (tracker.description, bugid)
else: else:
if bugtype: if bugtype:
report = '%s %d' % (bugtype.group('type').title(), bugid) report = '%s %s' % (bugtype.group('type').title(), bugid)
else: else:
report = 'Bug %d' % bugid report = 'Bug %s' % bugid
if product: if product:
report += ' in %s' % product report += ' in %s' % product
@ -548,8 +567,10 @@ class Bugtracker(callbacks.PluginRegexp):
severity_status = [] severity_status = []
if severity: if severity:
severity_status.append(' '.join(word[0].upper() + word[1:].lower() for word in severity.split())) severity_status.append(' '.join(word[0].upper() + word[1:].lower() for word in severity.split()))
severity_status.append(' '.join(word[0].upper() + word[1:].lower() for word in status.split())) if status:
report += ' [%s]' % ', '.join(severity_status) severity_status.append(' '.join(word[0].upper() + word[1:].lower() for word in status.split()))
if severity_status:
report += ' [%s]' % ', '.join(severity_status)
if duplicate: if duplicate:
report += ' [duplicate: %s]' % duplicate[0] report += ' [duplicate: %s]' % duplicate[0]

View File

@ -114,7 +114,7 @@ class Bugzilla(IBugtracker):
pass pass
def get_bug(self, bugtype, bugid): def get_bug(self, bugtype, bugid):
url = "%s/rest/bug/%d" % (self.url, bugid) url = "%s/rest/bug/%s" % (self.url, bugid)
try: try:
bugjson = utils.web.getUrl(url) bugjson = utils.web.getUrl(url)
bug = json.loads(bugjson.decode('utf-8'))['bugs'][0] bug = json.loads(bugjson.decode('utf-8'))['bugs'][0]
@ -134,12 +134,12 @@ class Bugzilla(IBugtracker):
else: else:
assignee = '' assignee = ''
return (bugid, bug['product'], bug['summary'], bug['severity'], status, assignee, return (bugid, bug['product'], bug['summary'], bug['severity'], status, assignee,
"%s/show_bug.cgi?id=%d" % (self.url, bugid), [], []) "%s/show_bug.cgi?id=%s" % (self.url, bugid), [], [])
except Exception as e: except Exception as e:
raise BugtrackerError(self.errparse % (self.description, e, url)) raise BugtrackerError(self.errparse % (self.description, e, url))
def get_bug_old(self, bugtype, bugid): # Deprecated def get_bug_old(self, bugtype, bugid): # Deprecated
url = "%s/show_bug.cgi?id=%d&ctype=xml" % (self.url, bugid) url = "%s/show_bug.cgi?id=%s&ctype=xml" % (self.url, bugid)
try: try:
bugxml = utils.web.getUrl(url) bugxml = utils.web.getUrl(url)
zilladom = minidom.parseString(bugxml) zilladom = minidom.parseString(bugxml)
@ -150,7 +150,7 @@ class Bugzilla(IBugtracker):
errtxt = bug_n.getAttribute('error') errtxt = bug_n.getAttribute('error')
if errtxt in ('NotFound', 'InvalidBugId'): if errtxt in ('NotFound', 'InvalidBugId'):
raise BugNotFoundError raise BugNotFoundError
s = 'Could not get %s bug #%d: %s' % (self.description, bugid, errtxt) s = 'Could not get %s bug #%s: %s' % (self.description, bugid, errtxt)
raise BugtrackerError(s) raise BugtrackerError(s)
try: try:
title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0]) title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0])
@ -170,7 +170,7 @@ class Bugzilla(IBugtracker):
assignee = '' assignee = ''
except Exception as e: except Exception as e:
raise BugtrackerError(self.errparse % (self.description, e, url)) raise BugtrackerError(self.errparse % (self.description, e, url))
return (bugid, product, title, severity, status, assignee, "%s/show_bug.cgi?id=%d" % (self.url, bugid), [], []) return (bugid, product, title, severity, status, assignee, "%s/show_bug.cgi?id=%s" % (self.url, bugid), [], [])
class Launchpad(IBugtracker): class Launchpad(IBugtracker):
statuses = ("Unknown", "Invalid", "Opinion", "Won't Fix", "Fix Released", "Fix Committed", "New", statuses = ("Unknown", "Invalid", "Opinion", "Won't Fix", "Fix Released", "Fix Committed", "New",
@ -259,7 +259,7 @@ class Launchpad(IBugtracker):
def get_bug_new(self, bugtype, bugid): #TODO: Rename this method to 'get_bug' def get_bug_new(self, bugtype, bugid): #TODO: Rename this method to 'get_bug'
try: try:
bugdata = self.lp.bugs[bugid] bugdata = self.lp.bugs[int(bugid)]
if bugdata.private: if bugdata.private:
raise BugtrackerError("This bug is private") raise BugtrackerError("This bug is private")
duplicate = [] duplicate = []
@ -287,27 +287,27 @@ class Launchpad(IBugtracker):
if type(e).__name__ == 'HTTPError': # messy, but saves trying to import lazr.restfulclient.errors.HTPError if type(e).__name__ == 'HTTPError': # messy, but saves trying to import lazr.restfulclient.errors.HTPError
if e.response.status == 404: if e.response.status == 404:
bugNo = e.content.split()[-1][2:-1] # extract the real bug number bugNo = e.content.split()[-1][2:-1] # extract the real bug number
if bugNo != str(bugid): # A duplicate of a private bug, at least we know it exists if bugNo != bugid: # A duplicate of a private bug, at least we know it exists
raise BugtrackerError('Bug #%d is a duplicate of bug #%s, but it is private (%s/bugs/%s)' % (bugid, bugNo, self.url, bugNo)) raise BugtrackerError('Bug #%s is a duplicate of bug #%s, but it is private (%s/bugs/%s)' % (bugid, bugNo, self.url, bugNo))
raise BugtrackerError("Bug #%d is private or does not exist (%s/bugs/%d)" % (bugid, self.url, bugid)) # Could be private, could just not exist raise BugtrackerError("Bug #%s is private or does not exist (%s/bugs/%s)" % (bugid, self.url, bugid)) # Could be private, could just not exist
raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%d' % (self.url, bugid))) raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%s' % (self.url, bugid)))
elif isinstance(e, KeyError): elif isinstance(e, KeyError):
raise BugNotFoundError raise BugNotFoundError
raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%d' % (self.url, bugid))) raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%s' % (self.url, bugid)))
return (bugdata.id, taskdata.bug_target_display_name, bugdata.title, taskdata.importance, taskdata.status, return (bugdata.id, taskdata.bug_target_display_name, bugdata.title, taskdata.importance, taskdata.status,
assignee, "%s/bugs/%d" % (self.url, bugdata.id), extinfo, duplicate) assignee, "%s/bugs/%s" % (self.url, bugdata.id), extinfo, duplicate)
def get_bug_old(self, bugtype, bugid, duplicate=None): # Deprecated def get_bug_old(self, bugtype, bugid, duplicate=None): # Deprecated
try: try:
bugdata = utils.web.getUrl("%s/bugs/%d/+text" % (self.url, bugid)).decode('utf-8') bugdata = utils.web.getUrl("%s/bugs/%s/+text" % (self.url, bugid)).decode('utf-8')
except Exception as e: except Exception as e:
if 'HTTP Error 404' in str(e): if 'HTTP Error 404' in str(e):
if duplicate: if duplicate:
raise BugtrackerError('Bug #%d is a duplicate of bug #%d, but it is private (%s/bugs/%d)' % (duplicate, bugid, self.url, bugid)) raise BugtrackerError('Bug #%s is a duplicate of bug #%s, but it is private (%s/bugs/%s)' % (duplicate, bugid, self.url, bugid))
else: else:
raise BugNotFoundError raise BugNotFoundError
raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%d' % (self.url, bugid))) raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%s' % (self.url, bugid)))
try: try:
# Split bug data into separate pieces (bug data, task data) # Split bug data into separate pieces (bug data, task data)
@ -324,16 +324,16 @@ class Launchpad(IBugtracker):
else: else:
assignee = '' assignee = ''
except Exception as e: except Exception as e:
raise BugtrackerError(self.errparse % (self.description, e, '%s/bugs/%d' % (self.url, bugid))) raise BugtrackerError(self.errparse % (self.description, e, '%s/bugs/%s' % (self.url, bugid)))
# Try and find duplicates # Try and find duplicates
if bugdata['duplicate-of']: if bugdata['duplicate-of']:
data = self.get_bug_old(bugtype, int(bugdata['duplicate-of']), duplicate or bugid) data = self.get_bug_old(bugtype, bugdata['duplicate-of'], duplicate or bugid)
data[8].append(bugdata['bug']) data[8].append(bugdata['bug'])
return data return data
return (bugid, taskdata['task'], bugdata['title'], taskdata['importance'], taskdata['status'], return (bugid, taskdata['task'], bugdata['title'], taskdata['importance'], taskdata['status'],
assignee, "%s/bugs/%d" % (self.url, bugid), [], []) assignee, "%s/bugs/%s" % (self.url, bugid), [], [])
# <rant> # <rant>
# Debbugs sucks donkeyballs # Debbugs sucks donkeyballs
@ -351,7 +351,7 @@ class Debbugs(IBugtracker):
self.soap_client = SoapClient("%s/cgi-bin/soap.cgi" % self.url, namespace="Debbugs/SOAP") self.soap_client = SoapClient("%s/cgi-bin/soap.cgi" % self.url, namespace="Debbugs/SOAP")
def get_bug(self, bugtype, bugid): def get_bug(self, bugtype, bugid):
url = "%s/cgi-bin/bugreport.cgi?bug=%d" % (self.url, bugid) url = "%s/cgi-bin/bugreport.cgi?bug=%s" % (self.url, bugid)
try: try:
raw = self.soap_client.get_status(bugs=bugid) raw = self.soap_client.get_status(bugs=bugid)
except Exception as e: except Exception as e:
@ -364,7 +364,7 @@ class Debbugs(IBugtracker):
status = 'Fixed' status = 'Fixed'
else: else:
status = 'Open' status = 'Open'
return (bugid, str(raw.package), str(raw.subject), str(raw.severity), status, '', "%s/%d" % (self.url, bugid), [], []) return (bugid, str(raw.package), str(raw.subject), str(raw.severity), status, '', "%s/%s" % (self.url, bugid), [], [])
except Exception as e: except Exception as e:
raise BugtrackerError(self.errparse % (self.description, e, url)) raise BugtrackerError(self.errparse % (self.description, e, url))
@ -380,7 +380,7 @@ class SourceForge(IBugtracker):
pass pass
def get_bug(self, bugtype, bugid): def get_bug(self, bugtype, bugid):
url = "%s/%d/" % (self.url.replace('sourceforge.net', 'sourceforge.net/rest'), bugid) url = "%s/%s/" % (self.url.replace('sourceforge.net', 'sourceforge.net/rest'), bugid)
try: try:
bugjson = utils.web.getUrl(url) bugjson = utils.web.getUrl(url)
bug = json.loads(bugjson.decode('utf-8'))['ticket'] bug = json.loads(bugjson.decode('utf-8'))['ticket']
@ -393,31 +393,40 @@ class SourceForge(IBugtracker):
if '_priority' in bug['custom_fields']: if '_priority' in bug['custom_fields']:
severity = 'Pri: %s' % bug['custom_fields']['_priority'] severity = 'Pri: %s' % bug['custom_fields']['_priority']
return (bugid, product, bug['summary'], severity, ': '.join(bug['status'].split('-')), return (bugid, product, bug['summary'], severity, ': '.join(bug['status'].split('-')),
bug['assigned_to'], "%s/%d/" % (self.url, bugid), [], []) bug['assigned_to'], "%s/%s/" % (self.url, bugid), [], [])
except Exception as e: except Exception as e:
raise BugtrackerError(self.errparse % (self.description, e, url)) raise BugtrackerError(self.errparse % (self.description, e, url))
class GitHub(IBugtracker): class GitHub(IBugtracker):
def get_tracker(self, url): def get_tracker(self, url):
try: try:
match = re.match(r'github\.com/[^\s/]+/[^\s/]+/(issues|pulls?)', url) match = re.match(r'github\.com/[^\s/]+/[^\s/]+/(issues|pulls?|commits?)', url)
desc = match.group(0) desc = match.group(0)
url = 'https://%s' % desc url = 'https://%s' % desc
# Pulls are inconsistent in main and single page URLs # Pulls are inconsistent in main and single page URLs
desc = re.sub(r'/pull$', r'/pulls', desc) desc = re.sub(r'/pull$', r'/pulls', desc)
# Commits are inconsistent in main and single page URLs
desc = re.sub(r'/commit$', r'/commits', desc)
name = desc.lower() name = desc.lower()
return GitHub(name, url, desc, 'github') return GitHub(name, url, desc, 'github')
except: except:
pass pass
def get_bug(self, bugtype, bugid): def get_bug(self, bugtype, bugid):
url = "%s/%d" % (self.url.replace('github.com', 'api.github.com/repos'), bugid) url = "%s/%s" % (self.url.replace('github.com', 'api.github.com/repos'), bugid)
# Pulls are inconsistent in web and API URLs # Pulls are inconsistent in web and API URLs
url = url.replace('/pull/', '/pulls/') url = url.replace('/pull/', '/pulls/')
# Commits are inconsistent in web and API URLs
url = url.replace('/commit/', '/commits/')
if bugtype in ('issue', 'bug'): if bugtype in ('issue', 'bug'):
url = url.replace('/pulls/', '/issues/') url = url.replace('/pulls/', '/issues/')
url = url.replace('/commits/', '/issues/')
elif bugtype in ('pull', 'pr', 'merge', 'mr'): elif bugtype in ('pull', 'pr', 'merge', 'mr'):
url = url.replace('/issues/', '/pulls/') url = url.replace('/issues/', '/pulls/')
url = url.replace('/commits/', '/pulls/')
elif bugtype == 'commit':
url = url.replace('/issues/', '/commits/')
url = url.replace('/pulls/', '/commits/')
try: try:
bugjson = utils.web.getUrl(url) bugjson = utils.web.getUrl(url)
bug = json.loads(bugjson.decode('utf-8')) bug = json.loads(bugjson.decode('utf-8'))
@ -425,27 +434,40 @@ class GitHub(IBugtracker):
raise BugtrackerError(self.errget % (self.description, e, url)) raise BugtrackerError(self.errget % (self.description, e, url))
try: try:
product = '/'.join(self.url.split('/')[-3:-1]) product = '/'.join(self.url.split('/')[-3:-1])
if 'merged' in bug and bug['merged']: if '/commits/' not in url:
status = 'Merged' title = bug['title']
else: if 'merged' in bug and bug['merged']:
status = bug['state'] status = 'Merged'
if bug['assignee']: else:
assignee = bug['assignee']['login'] status = bug['state']
if bug['assignee']:
assignee = bug['assignee']['login']
else:
assignee = ''
else: else:
bugid = bug['sha'][:7]
title = bug['commit']['message'].split('\n', 1)[0]
status = ''
assignee = '' assignee = ''
return (bugid, product, bug['title'], '', status, assignee, bug['html_url'], [], []) return (bugid, product, title, '', status, assignee, bug['html_url'], [], [])
except Exception as e: except Exception as e:
raise BugtrackerError(self.errparse % (self.description, e, url)) raise BugtrackerError(self.errparse % (self.description, e, url))
class GitLab(IBugtracker): class GitLab(IBugtracker):
def get_tracker(self, url, bugid): def get_tracker(self, url, bugid):
try: try:
match = re.match(r'[^\s/]+/(?P<project>[^\s/]+/[^\s/]+(/[^\s/]+)*?)/(-/)?(issues|merge_requests)', url) match = re.match(r'[^\s/]+/(?P<project>[^\s/]+/[^\s/]+(/[^\s/]+)*?)/(-/)?(issues|merge_requests|commits?)', url)
desc = match.group(0) desc = match.group(0)
name = desc.lower()
url = 'https://%s' % desc url = 'https://%s' % desc
bugurl = "%s/%d" % (re.sub(r'(://[^\s/]+)/[^\s/]+(/[^\s/]+)+/(-/)?', # Commits are inconsistent in main and single page URLs
desc = re.sub(r'/commit$', r'/commits', desc)
name = desc.lower()
bugurl = "%s/%s" % (re.sub(r'(://[^\s/]+)/[^\s/]+(/[^\s/]+)+/(-/)?',
r'\g<1>/api/v4/projects/%s/' % match.group('project').replace('/', '%2F'), url), bugid) r'\g<1>/api/v4/projects/%s/' % match.group('project').replace('/', '%2F'), url), bugid)
# Commits are inconsistent in web and API URLs
bugurl = bugurl.replace('/commit/', '/commits/')
# Commits need an extra bit on API URLs
bugurl = bugurl.replace('/commits/', '/repository/commits/')
bugjson = utils.web.getUrl(bugurl) bugjson = utils.web.getUrl(bugurl)
bug = json.loads(bugjson.decode('utf-8')) bug = json.loads(bugjson.decode('utf-8'))
return GitLab(name, url, desc, 'gitlab') return GitLab(name, url, desc, 'gitlab')
@ -453,13 +475,22 @@ class GitLab(IBugtracker):
pass pass
def get_bug(self, bugtype, bugid): def get_bug(self, bugtype, bugid):
match = re.match(r'[^\s:]+://[^\s/]+/(?P<project>[^\s/]+/[^\s/]+(/[^\s/]+)*?)/(-/)?(issues|merge_requests)', self.url) match = re.match(r'[^\s:]+://[^\s/]+/(?P<project>[^\s/]+/[^\s/]+(/[^\s/]+)*?)/(-/)?(issues|merge_requests|commits?)', self.url)
url = "%s/%d" % (re.sub(r'(://[^\s/]+)/[^\s/]+(/[^\s/]+)+/(-/)?', url = "%s/%s" % (re.sub(r'(://[^\s/]+)/[^\s/]+(/[^\s/]+)+/(-/)?',
r'\g<1>/api/v4/projects/%s/' % match.group('project').replace('/', '%2F'), self.url), bugid) r'\g<1>/api/v4/projects/%s/' % match.group('project').replace('/', '%2F'), self.url), bugid)
# Commits are inconsistent in web and API URLs
url = url.replace('/commit/', '/commits/')
if bugtype in ('issue', 'bug'): if bugtype in ('issue', 'bug'):
url = url.replace('/merge_requests/', '/issues/') url = url.replace('/merge_requests/', '/issues/')
url = url.replace('/commits/', '/issues/')
elif bugtype in ('merge', 'mr', 'pull', 'pr'): elif bugtype in ('merge', 'mr', 'pull', 'pr'):
url = url.replace('/issues/', '/merge_requests/') url = url.replace('/issues/', '/merge_requests/')
url = url.replace('/commits/', '/merge_requests/')
elif bugtype == 'commit':
url = url.replace('/issues/', '/commits/')
url = url.replace('/merge_requests/', '/commits/')
# Commits need an extra bit on API URLs
url = url.replace('/commits/', '/repository/commits/')
try: try:
bugjson = utils.web.getUrl(url) bugjson = utils.web.getUrl(url)
bug = json.loads(bugjson.decode('utf-8')) bug = json.loads(bugjson.decode('utf-8'))
@ -467,27 +498,40 @@ class GitLab(IBugtracker):
raise BugtrackerError(self.errget % (self.description, e, url)) raise BugtrackerError(self.errget % (self.description, e, url))
try: try:
product = match.group('project') product = match.group('project')
status = bug['state'] if '/commits/' not in url:
if bug['assignees']: title = bug['title']
assino = len(bug['assignees']) status = bug['state']
if assino == 1: if bug['assignees']:
assignee = bug['assignees'][0]['name'] assino = len(bug['assignees'])
if assino == 1:
assignee = bug['assignees'][0]['name']
else:
assignee = '%d people' % assino
else: else:
assignee = '%d people' % assino assignee = ''
else: else:
bugid = bug['id'][:7]
title = bug['message'].split('\n', 1)[0]
status = ''
assignee = '' assignee = ''
return (bugid, product, bug['title'], '', status, assignee, bug['web_url'], [], []) return (bugid, product, title, '', status, assignee, bug['web_url'], [], [])
except Exception as e: except Exception as e:
raise BugtrackerError(self.errparse % (self.description, e, url)) raise BugtrackerError(self.errparse % (self.description, e, url))
class Gitea(IBugtracker): class Gitea(IBugtracker):
def get_tracker(self, url, bugid): def get_tracker(self, url, bugid):
try: try:
match = re.match(r'[^\s/]+/[^\s/]+/[^\s/]+/(issues|pulls)', url) match = re.match(r'[^\s/]+/[^\s/]+/[^\s/]+/(issues|pulls|commits?)', url)
desc = match.group(0) desc = match.group(0)
name = desc.lower()
url = 'https://%s' % desc url = 'https://%s' % desc
bugurl = '%s/%d' % (re.sub(r'(://[^\s/]+)/', r'\g<1>/api/v1/repos/', url), bugid) # Commits are inconsistent in main and single page URLs
desc = re.sub(r'/commit$', r'/commits', desc)
name = desc.lower()
bugurl = '%s/%s' % (re.sub(r'(://[^\s/]+)/', r'\g<1>/api/v1/repos/', url), bugid)
# Commits are inconsistent in web and API URLs
bugurl = bugurl.replace('/commit/', '/commits/')
# Commits need an extra bit on API URLs
bugurl = bugurl.replace('/commits/', '/git/commits/')
bugjson = utils.web.getUrl(bugurl) bugjson = utils.web.getUrl(bugurl)
bug = json.loads(bugjson.decode('utf-8')) bug = json.loads(bugjson.decode('utf-8'))
return Gitea(name, url, desc, 'gitea') return Gitea(name, url, desc, 'gitea')
@ -495,11 +539,20 @@ class Gitea(IBugtracker):
pass pass
def get_bug(self, bugtype, bugid): def get_bug(self, bugtype, bugid):
url = "%s/%d" % (re.sub(r'(://[^\s/]+)/', r'\g<1>/api/v1/repos/', self.url), bugid) url = "%s/%s" % (re.sub(r'(://[^\s/]+)/', r'\g<1>/api/v1/repos/', self.url), bugid)
# Commits are inconsistent in web and API URLs
url = url.replace('/commit/', '/commits/')
if bugtype in ('issue', 'bug'): if bugtype in ('issue', 'bug'):
url = url.replace('/pulls/', '/issues/') url = url.replace('/pulls/', '/issues/')
url = url.replace('/commits/', '/issues/')
elif bugtype in ('pull', 'pr', 'merge', 'mr'): elif bugtype in ('pull', 'pr', 'merge', 'mr'):
url = url.replace('/issues/', '/pulls/') url = url.replace('/issues/', '/pulls/')
url = url.replace('/commits/', '/pulls/')
elif bugtype == 'commit':
url = url.replace('/issues/', '/commits/')
url = url.replace('/pulls/', '/commits/')
# Commits need an extra bit on API URLs
url = url.replace('/commits/', '/git/commits/')
try: try:
bugjson = utils.web.getUrl(url) bugjson = utils.web.getUrl(url)
bug = json.loads(bugjson.decode('utf-8')) bug = json.loads(bugjson.decode('utf-8'))
@ -507,20 +560,27 @@ class Gitea(IBugtracker):
raise BugtrackerError(self.errget % (self.description, e, url)) raise BugtrackerError(self.errget % (self.description, e, url))
try: try:
product = '/'.join(self.url.split('/')[-3:-1]) product = '/'.join(self.url.split('/')[-3:-1])
if 'merged' in bug and bug['merged']: if '/commits/' not in url:
status = 'Merged' title = bug['title']
else: if 'merged' in bug and bug['merged']:
status = bug['state'] status = 'Merged'
if bug['assignee']: else:
assignee = bug['assignee']['username'] status = bug['state']
if bug['assignee']:
assignee = bug['assignee']['username']
else:
assignee = ''
else: else:
bugid = bug['sha'][:7]
title = bug['commit']['message'].split('\n', 1)[0]
status = ''
assignee = '' assignee = ''
# Issues have no 'html_url', but pulls do # Issues have no 'html_url', but pulls and commits do
if 'html_url' in bug: if 'html_url' in bug:
htmlurl = bug['html_url'] htmlurl = bug['html_url']
else: else:
htmlurl = url.replace('/api/v1/repos/', '/') htmlurl = url.replace('/api/v1/repos/', '/')
return (bugid, product, bug['title'], '', status, assignee, htmlurl, [], []) return (bugid, product, title, '', status, assignee, htmlurl, [], [])
except Exception as e: except Exception as e:
raise BugtrackerError(self.errparse % (self.description, e, url)) raise BugtrackerError(self.errparse % (self.description, e, url))
@ -540,7 +600,7 @@ class Mantis(IBugtracker):
pass pass
def get_bug(self, bugtype, bugid): def get_bug(self, bugtype, bugid):
url = "%s/api/rest/issues/%d" % (self.url, bugid) url = "%s/api/rest/issues/%s" % (self.url, bugid)
try: try:
bugjson = utils.web.getUrl(url) bugjson = utils.web.getUrl(url)
bug = json.loads(bugjson.decode('utf-8'))['issues'][0] bug = json.loads(bugjson.decode('utf-8'))['issues'][0]
@ -555,11 +615,11 @@ class Mantis(IBugtracker):
raise BugtrackerError(self.errparse % (self.description, e, url)) raise BugtrackerError(self.errparse % (self.description, e, url))
def get_bug_old(self, bugtype, bugid): # Deprecated def get_bug_old(self, bugtype, bugid): # Deprecated
url = "%s/view.php?id=%d" % (self.url, bugid) url = "%s/view.php?id=%s" % (self.url, bugid)
try: try:
raw = self.soap_client.mc_issue_get(username='', password='', issue_id=bugid) raw = self.soap_client.mc_issue_get(username='', password='', issue_id=bugid)
except Exception as e: except Exception as e:
if 'Issue #%d not found' % bugid in str(e): if 'Issue #%s not found' % bugid in str(e):
raise BugNotFoundError raise BugNotFoundError
# Often SOAP is not enabled # Often SOAP is not enabled
if '.' in self.name: if '.' in self.name:
@ -589,7 +649,7 @@ class Trac(IBugtracker):
pass pass
def get_bug(self, bugtype, bugid): # This is still a little rough, but it works :) def get_bug(self, bugtype, bugid): # This is still a little rough, but it works :)
url = "%s/%d" % (self.url, bugid) url = "%s/%s" % (self.url, bugid)
try: try:
raw = utils.web.getUrl("%s?format=tab" % url).decode('utf-8') raw = utils.web.getUrl("%s?format=tab" % url).decode('utf-8')
except Exception as e: except Exception as e: