From e0926a9a3c66ff6d7e1721670cb9c640902c2aba Mon Sep 17 00:00:00 2001 From: Krytarik Raido Date: Sun, 21 Jun 2020 09:04:04 +0200 Subject: [PATCH] Bugtracker: Add support for commits. --- Bugtracker/__init__.py | 2 +- Bugtracker/config.py | 9 ++- Bugtracker/plugin.py | 79 +++++++++++------- Bugtracker/trackers.py | 178 +++++++++++++++++++++++++++-------------- 4 files changed, 178 insertions(+), 90 deletions(-) diff --git a/Bugtracker/__init__.py b/Bugtracker/__init__.py index 7742af2..c5fcd5e 100644 --- a/Bugtracker/__init__.py +++ b/Bugtracker/__init__.py @@ -24,7 +24,7 @@ import supybot.world as world from imp import reload -__version__ = "4.2.0" +__version__ = "4.3.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/config.py b/Bugtracker/config.py index c666691..62f26dc 100644 --- a/Bugtracker/config.py +++ b/Bugtracker/config.py @@ -49,8 +49,9 @@ def configure(advanced): else: 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) + 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) oopsSnarfer = yn("Enable detecting Launchpad OOPS IDs in all channels?", default=Bugtracker.oopsSnarfer._default) if advanced: @@ -72,6 +73,7 @@ def configure(advanced): saveDiscoveredTrackers = yn("Save automatically discovered trackers to configuration?", default=Bugtracker.saveDiscoveredTrackers._default) Bugtracker.bugSnarfer.setValue(bugSnarfer) + Bugtracker.commitSnarfer.setValue(commitSnarfer) Bugtracker.cveSnarfer.setValue(cveSnarfer) Bugtracker.oopsSnarfer.setValue(oopsSnarfer) Bugtracker.replyNoBugtracker.setValue(replyNoBugtracker) @@ -90,6 +92,11 @@ conf.registerChannelValue(Bugtracker, 'bugSnarfer', enabled, such that any bugtracker URLs and bug ### seen in 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', registry.Boolean(False, """Determines whether the CVE snarfer will be enabled, such that any CVE URLs and CVE-????-???? seen in the channel diff --git a/Bugtracker/plugin.py b/Bugtracker/plugin.py index bfa4c29..35d5e61 100644 --- a/Bugtracker/plugin.py +++ b/Bugtracker/plugin.py @@ -71,7 +71,7 @@ class Bugtracker(callbacks.PluginRegexp): """Show a link to a bug report with a brief description""" threaded = True callBefore = ('URL') - regexps = ('turlSnarfer', 'bugSnarfer', 'cveSnarfer', 'oopsSnarfer') + regexps = ('bugSnarfer', 'bugUrlSnarfer', 'commitSnarfer', 'commitUrlSnarfer', 'cveSnarfer', 'oopsSnarfer') def __init__(self, irc): self.__parent = super(Bugtracker, self) @@ -97,14 +97,14 @@ class Bugtracker(callbacks.PluginRegexp): 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): + def is_ok(self, channel, tracker, bugid): """Flood/repeat protection""" now = time.time() for k in list(self.shown.keys()): 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 + if (channel, tracker, bugid) not in self.shown: + self.shown[(channel, tracker, bugid)] = now return True return False @@ -308,26 +308,39 @@ class Bugtracker(callbacks.PluginRegexp): def bugSnarfer(self, irc, msg, match): r"(?P[a-z][^\s:]*(\s+(bug|ticket|issue|pull|pr|merge|mr)('?s)?)?):*\s+#?(?P\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[a-z][^\s:]*(\s+(commit)('?s)?)?):*\s+#?(?P[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 if checkAddressed(msg.args[1].strip(), channel): return - if not self.registryValue('bugSnarfer', channel): + if not self.registryValue('{}Snarfer'.format(termtype), channel): return nbugs = msg.tagged('nbugs') or 0 if nbugs >= 5: 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 # 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)] # End HACK # Get tracker name bt = [x.lower() for x in match.group('bt').split()] - sure_bug = re.match(r"^(?Pbug|ticket|issue|pull|pr|merge|mr)('?s)?$", bt[-1]) + if termtype != 'commit': + sure_bug = re.match(r"^(?Pbug|ticket|issue|pull|pr|merge|mr)('?s)?$", bt[-1]) + else: + sure_bug = re.match(r"^(?Pcommit)('?s)?$", bt[-1]) # Get bug type if sure_bug: @@ -337,7 +350,7 @@ class Bugtracker(callbacks.PluginRegexp): 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] msg.tag('nbugs', nbugs + len(bugids)) @@ -376,13 +389,12 @@ class Bugtracker(callbacks.PluginRegexp): return for bugid in bugids: - bugid = int(bugid) try: report = self.get_bug(channel or msg.nick, tracker, bugtype, bugid, self.registryValue('showassignee', channel), self.registryValue('extended', channel), do_tracker=showTracker) except trackers.BugNotFoundError: 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: if self.registryValue('replyWhenError') and sure_bug: irc.error(str(e)) @@ -390,19 +402,26 @@ class Bugtracker(callbacks.PluginRegexp): if report: irc.reply(report) - 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/|feature-requests/|patches/|todo/|issues/|pulls?/|merge_requests/))(?P\d+)/?" + 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\d+)" + self.urlSnarfer(irc, msg, match, 'bug') + + def commitUrlSnarfer(self, irc, msg, match): + r"(https?://)?\S+/commits?/(?P[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 if checkAddressed(msg.args[1].strip(), channel): return - if not self.registryValue('bugSnarfer', channel): + if not self.registryValue('{}Snarfer'.format(urltype), channel): return nbugs = msg.tagged('nbugs') or 0 if nbugs >= 5: return msg.tag('nbugs', nbugs+1) url = match.group(0) - bugid = int(match.group('bug')) + bugid = match.group('bug') if '://' in url: url = url[url.rfind('://')+3:] try: @@ -413,7 +432,7 @@ class Bugtracker(callbacks.PluginRegexp): self.registryValue('extended', channel), do_url=False) except trackers.BugNotFoundError: 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: if self.registryValue('replyWhenError'): irc.error(str(e)) @@ -423,7 +442,7 @@ class Bugtracker(callbacks.PluginRegexp): # Only useful to Launchpad developers def oopsSnarfer(self, irc, msg, match): - r"(https?://\S+[=/])?OOPS-(?P[\dA-Za-z]{6,})" + r"(https?://\S+[=/])?OOPS-(?P[a-f0-9]{6,})" channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None if checkAddressed(msg.args[1].strip(), channel): return @@ -482,11 +501,11 @@ class Bugtracker(callbacks.PluginRegexp): tracker = trackers.SourceForge().get_tracker(snarfurl) elif 'github.com' in 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/]+){2,}/issues', snarfurl): + or re.match(r'[^\s/]+/[^\s/]+(/[^\s/]+){2,}/(issues|commits?)', snarfurl): 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) if not tracker: 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): return - bugtype = re.match(r'.*/(feature-)?(?Prequest|patch|todo|issue|pull|merge|ticket)(_requests)?(e?s)?/[0-9]+/?$', url) + bugtype = re.match(r'.*/(feature-)?(?Prequest|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 re.match(r'.*/(bugs|feature-requests|patches|todo|issues|pulls?|merge_requests|ticket)/?$', tracker.description): - report = '%s %d' % (tracker.description, bugid) + if re.match(r'.*/(bugs|feature-requests|patches|todo|issues|pulls?|merge_requests|ticket|commits?)$', tracker.description): + report = '%s %s' % (tracker.description, bugid) else: if bugtype: - report = '%s %s %d' % (tracker.description, bugtype.group('type'), bugid) + report = '%s %s %s' % (tracker.description, bugtype.group('type'), bugid) else: - report = '%s bug %d' % (tracker.description, bugid) + report = '%s bug %s' % (tracker.description, bugid) else: if bugtype: - report = '%s %d' % (bugtype.group('type').title(), bugid) + report = '%s %s' % (bugtype.group('type').title(), bugid) else: - report = 'Bug %d' % bugid + report = 'Bug %s' % bugid if product: report += ' in %s' % product @@ -548,8 +567,10 @@ class Bugtracker(callbacks.PluginRegexp): severity_status = [] 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 status.split())) - report += ' [%s]' % ', '.join(severity_status) + if 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: report += ' [duplicate: %s]' % duplicate[0] diff --git a/Bugtracker/trackers.py b/Bugtracker/trackers.py index 150227c..3af7e90 100644 --- a/Bugtracker/trackers.py +++ b/Bugtracker/trackers.py @@ -114,7 +114,7 @@ class Bugzilla(IBugtracker): pass def get_bug(self, bugtype, bugid): - url = "%s/rest/bug/%d" % (self.url, bugid) + url = "%s/rest/bug/%s" % (self.url, bugid) try: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8'))['bugs'][0] @@ -134,12 +134,12 @@ class Bugzilla(IBugtracker): else: 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: raise BugtrackerError(self.errparse % (self.description, e, url)) 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: bugxml = utils.web.getUrl(url) zilladom = minidom.parseString(bugxml) @@ -150,7 +150,7 @@ class Bugzilla(IBugtracker): errtxt = bug_n.getAttribute('error') if errtxt in ('NotFound', 'InvalidBugId'): 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) try: title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0]) @@ -170,7 +170,7 @@ class Bugzilla(IBugtracker): assignee = '' except Exception as e: 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): 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' try: - bugdata = self.lp.bugs[bugid] + bugdata = self.lp.bugs[int(bugid)] if bugdata.private: raise BugtrackerError("This bug is private") duplicate = [] @@ -287,27 +287,27 @@ class Launchpad(IBugtracker): if type(e).__name__ == 'HTTPError': # messy, but saves trying to import lazr.restfulclient.errors.HTPError if e.response.status == 404: 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 - 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 #%d is private or does not exist (%s/bugs/%d)" % (bugid, self.url, bugid)) # Could be private, could just not exist - raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%d' % (self.url, bugid))) + if bugNo != bugid: # A duplicate of a private bug, at least we know it exists + 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 #%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/%s' % (self.url, bugid))) elif isinstance(e, KeyError): 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, - 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 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: if 'HTTP Error 404' in str(e): 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: 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: # Split bug data into separate pieces (bug data, task data) @@ -324,16 +324,16 @@ class Launchpad(IBugtracker): else: assignee = '' 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 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']) return data return (bugid, taskdata['task'], bugdata['title'], taskdata['importance'], taskdata['status'], - assignee, "%s/bugs/%d" % (self.url, bugid), [], []) + assignee, "%s/bugs/%s" % (self.url, bugid), [], []) # # Debbugs sucks donkeyballs @@ -351,7 +351,7 @@ class Debbugs(IBugtracker): self.soap_client = SoapClient("%s/cgi-bin/soap.cgi" % self.url, namespace="Debbugs/SOAP") 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: raw = self.soap_client.get_status(bugs=bugid) except Exception as e: @@ -364,7 +364,7 @@ class Debbugs(IBugtracker): status = 'Fixed' else: 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: raise BugtrackerError(self.errparse % (self.description, e, url)) @@ -380,7 +380,7 @@ class SourceForge(IBugtracker): pass 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: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8'))['ticket'] @@ -393,31 +393,40 @@ class SourceForge(IBugtracker): if '_priority' in bug['custom_fields']: severity = 'Pri: %s' % bug['custom_fields']['_priority'] 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: 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) + match = re.match(r'github\.com/[^\s/]+/[^\s/]+/(issues|pulls?|commits?)', url) desc = match.group(0) url = 'https://%s' % desc # Pulls are inconsistent in main and single page URLs 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() return GitHub(name, url, desc, 'github') except: pass 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 url = url.replace('/pull/', '/pulls/') + # Commits are inconsistent in web and API URLs + url = url.replace('/commit/', '/commits/') if bugtype in ('issue', 'bug'): url = url.replace('/pulls/', '/issues/') + url = url.replace('/commits/', '/issues/') elif bugtype in ('pull', 'pr', 'merge', 'mr'): url = url.replace('/issues/', '/pulls/') + url = url.replace('/commits/', '/pulls/') + elif bugtype == 'commit': + url = url.replace('/issues/', '/commits/') + url = url.replace('/pulls/', '/commits/') try: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8')) @@ -425,27 +434,40 @@ class GitHub(IBugtracker): raise BugtrackerError(self.errget % (self.description, e, url)) try: product = '/'.join(self.url.split('/')[-3:-1]) - if 'merged' in bug and bug['merged']: - status = 'Merged' - else: - status = bug['state'] - if bug['assignee']: - assignee = bug['assignee']['login'] + if '/commits/' not in url: + title = bug['title'] + if 'merged' in bug and bug['merged']: + status = 'Merged' + else: + status = bug['state'] + if bug['assignee']: + assignee = bug['assignee']['login'] + else: + assignee = '' else: + bugid = bug['sha'][:7] + title = bug['commit']['message'].split('\n', 1)[0] + status = '' assignee = '' - return (bugid, product, bug['title'], '', status, assignee, bug['html_url'], [], []) + return (bugid, product, title, '', status, assignee, bug['html_url'], [], []) except Exception as e: raise BugtrackerError(self.errparse % (self.description, e, url)) class GitLab(IBugtracker): def get_tracker(self, url, bugid): try: - match = re.match(r'[^\s/]+/(?P[^\s/]+/[^\s/]+(/[^\s/]+)*?)/(-/)?(issues|merge_requests)', url) + match = re.match(r'[^\s/]+/(?P[^\s/]+/[^\s/]+(/[^\s/]+)*?)/(-/)?(issues|merge_requests|commits?)', url) desc = match.group(0) - name = desc.lower() 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) + # 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) bug = json.loads(bugjson.decode('utf-8')) return GitLab(name, url, desc, 'gitlab') @@ -453,13 +475,22 @@ class GitLab(IBugtracker): pass def get_bug(self, bugtype, bugid): - match = re.match(r'[^\s:]+://[^\s/]+/(?P[^\s/]+/[^\s/]+(/[^\s/]+)*?)/(-/)?(issues|merge_requests)', self.url) - url = "%s/%d" % (re.sub(r'(://[^\s/]+)/[^\s/]+(/[^\s/]+)+/(-/)?', + match = re.match(r'[^\s:]+://[^\s/]+/(?P[^\s/]+/[^\s/]+(/[^\s/]+)*?)/(-/)?(issues|merge_requests|commits?)', self.url) + url = "%s/%s" % (re.sub(r'(://[^\s/]+)/[^\s/]+(/[^\s/]+)+/(-/)?', 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'): url = url.replace('/merge_requests/', '/issues/') + url = url.replace('/commits/', '/issues/') elif bugtype in ('merge', 'mr', 'pull', 'pr'): 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: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8')) @@ -467,27 +498,40 @@ class GitLab(IBugtracker): raise BugtrackerError(self.errget % (self.description, e, url)) try: product = match.group('project') - status = bug['state'] - if bug['assignees']: - assino = len(bug['assignees']) - if assino == 1: - assignee = bug['assignees'][0]['name'] + if '/commits/' not in url: + title = bug['title'] + status = bug['state'] + if bug['assignees']: + assino = len(bug['assignees']) + if assino == 1: + assignee = bug['assignees'][0]['name'] + else: + assignee = '%d people' % assino else: - assignee = '%d people' % assino + assignee = '' else: + bugid = bug['id'][:7] + title = bug['message'].split('\n', 1)[0] + status = '' assignee = '' - return (bugid, product, bug['title'], '', status, assignee, bug['web_url'], [], []) + return (bugid, product, title, '', status, assignee, bug['web_url'], [], []) except Exception as e: raise BugtrackerError(self.errparse % (self.description, e, url)) class Gitea(IBugtracker): def get_tracker(self, url, bugid): 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) - name = desc.lower() 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) bug = json.loads(bugjson.decode('utf-8')) return Gitea(name, url, desc, 'gitea') @@ -495,11 +539,20 @@ class Gitea(IBugtracker): pass 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'): url = url.replace('/pulls/', '/issues/') + url = url.replace('/commits/', '/issues/') elif bugtype in ('pull', 'pr', 'merge', 'mr'): 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: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8')) @@ -507,20 +560,27 @@ class Gitea(IBugtracker): raise BugtrackerError(self.errget % (self.description, e, url)) try: product = '/'.join(self.url.split('/')[-3:-1]) - if 'merged' in bug and bug['merged']: - status = 'Merged' - else: - status = bug['state'] - if bug['assignee']: - assignee = bug['assignee']['username'] + if '/commits/' not in url: + title = bug['title'] + if 'merged' in bug and bug['merged']: + status = 'Merged' + else: + status = bug['state'] + if bug['assignee']: + assignee = bug['assignee']['username'] + else: + assignee = '' else: + bugid = bug['sha'][:7] + title = bug['commit']['message'].split('\n', 1)[0] + status = '' assignee = '' - # Issues have no 'html_url', but pulls do + # Issues have no 'html_url', but pulls and commits do if 'html_url' in bug: htmlurl = bug['html_url'] else: 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: raise BugtrackerError(self.errparse % (self.description, e, url)) @@ -540,7 +600,7 @@ class Mantis(IBugtracker): pass 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: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8'))['issues'][0] @@ -555,11 +615,11 @@ class Mantis(IBugtracker): raise BugtrackerError(self.errparse % (self.description, e, url)) 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: raw = self.soap_client.mc_issue_get(username='', password='', issue_id=bugid) except Exception as e: - if 'Issue #%d not found' % bugid in str(e): + if 'Issue #%s not found' % bugid in str(e): raise BugNotFoundError # Often SOAP is not enabled if '.' in self.name: @@ -589,7 +649,7 @@ class Trac(IBugtracker): pass 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: raw = utils.web.getUrl("%s?format=tab" % url).decode('utf-8') except Exception as e: