diff --git a/Bugtracker/__init__.py b/Bugtracker/__init__.py index 4d62e80..e7816b6 100644 --- a/Bugtracker/__init__.py +++ b/Bugtracker/__init__.py @@ -24,7 +24,7 @@ import supybot.world as world from imp import reload -__version__ = "3.8.0" +__version__ = "3.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 191086f..9293395 100644 --- a/Bugtracker/plugin.py +++ b/Bugtracker/plugin.py @@ -352,7 +352,7 @@ class Bugtracker(callbacks.PluginRegexp): return msg def bugSnarfer(self, irc, msg, match): - r"(?P[a-z][^\s:]*(\s+bugs?)?):*\s+#?(?P\d+(?!\d*[-.]\d+)(\s*([,\s]+|[,\s]*(and|und|en|et|ir|[&+]+))\s*#?\d+(?!\d*[-.]\d+))*)" + 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+))*)" channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None if checkAddressed(msg.args[1].strip(), channel): return @@ -372,7 +372,13 @@ class Bugtracker(callbacks.PluginRegexp): # Get tracker name bt = [x.lower() for x in match.group('bt').split()] - sure_bug = bt[-1] in ('bug', 'bugs') + sure_bug = re.match(r"(?Pbug|ticket|issue|pull|pr|merge|mr)('?s)?", bt[-1]) + + # Get bug type + if sure_bug: + bugtype = sure_bug.group('type') + else: + bugtype = '' bugids = list(set(bugids)) # remove dupes @@ -417,7 +423,7 @@ class Bugtracker(callbacks.PluginRegexp): for bugid in bugids: bugid = int(bugid) try: - report = self.get_bug(channel or msg.nick, tracker, 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) except BugNotFoundError: if self.registryValue('replyWhenNotFound'): @@ -449,7 +455,7 @@ class Bugtracker(callbacks.PluginRegexp): tracker = self.get_tracker(url, bugid) if not tracker: return - report = self.get_bug(channel or msg.nick, tracker, bugid, self.registryValue('showassignee', channel), + report = self.get_bug(channel or msg.nick, tracker, 'url', bugid, self.registryValue('showassignee', channel), self.registryValue('extended', channel), do_url=False) except BugtrackerError as e: irc.error(str(e)) @@ -547,33 +553,33 @@ class Bugtracker(callbacks.PluginRegexp): self.shorthand = utils.abbrev(list(self.db.keys())) return tracker - def get_bug(self, channel, tracker, id, do_assignee, do_extinfo, do_url=True, do_tracker=True): - if not self.is_ok(channel, tracker, id): + def get_bug(self, channel, tracker, bugtype, bugid, do_assignee, do_extinfo, do_url=True, do_tracker=True): + if not self.is_ok(channel, tracker, bugid): return - bugdata = tracker.get_bug(id) + bugdata = tracker.get_bug(bugtype, bugid) if not bugdata: return - (bid, product, title, severity, status, assignee, url, extinfo, duplicate) = bugdata + (bugid, product, title, severity, status, assignee, url, extinfo, duplicate) = bugdata - if duplicate and not self.is_ok(channel, tracker, bid): + 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) 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, bid) + report = '%s %d' % (tracker.description, bugid) else: if bugtype: - report = '%s %s %d' % (tracker.description, bugtype.group('type'), bid) + report = '%s %s %d' % (tracker.description, bugtype.group('type'), bugid) else: - report = '%s bug %d' % (tracker.description, bid) + report = '%s bug %d' % (tracker.description, bugid) else: if bugtype: - report = '%s %d' % (bugtype.group('type').title(), bid) + report = '%s %d' % (bugtype.group('type').title(), bugid) else: - report = 'Bug %d' % bid + report = 'Bug %d' % bugid if product: report += ' in %s' % product @@ -621,7 +627,7 @@ class IBugtracker: self.errget = 'Could not get data from %s: %s (%s)' self.errparse = 'Could not parse data from %s: %s (%s)' - def get_bug(self, id): + def get_bug(self, bugid): raise BugTrackerError("Bugtracker class does not implement get_bug") def get_tracker(self, url): @@ -650,15 +656,15 @@ class Bugzilla(IBugtracker): except: pass - def get_bug(self, id): - url = "%s/rest/bug/%d" % (self.url, id) + def get_bug(self, bugtype, bugid): + url = "%s/rest/bug/%d" % (self.url, bugid) 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) + return self.get_bug_old(bugtype, bugid) raise BugtrackerError(self.errget % (self.description, e, url)) try: status = bug['status'] @@ -670,13 +676,13 @@ class Bugzilla(IBugtracker): 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), [], []) + return (bugid, bug['product'], bug['summary'], bug['severity'], status, assignee, + "%s/show_bug.cgi?id=%d" % (self.url, bugid), [], []) 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) + def get_bug_old(self, bugtype, bugid): # Deprecated + url = "%s/show_bug.cgi?id=%d&ctype=xml" % (self.url, bugid) try: bugxml = utils.web.getUrl(url) zilladom = minidom.parseString(bugxml) @@ -687,7 +693,7 @@ class Bugzilla(IBugtracker): errtxt = bug_n.getAttribute('error') if errtxt == 'NotFound': raise BugNotFoundError - s = 'Error getting %s bug #%d: %s' % (self.description, id, errtxt) + s = 'Error getting %s bug #%d: %s' % (self.description, bugid, errtxt) raise BugtrackerError(s) try: title = _getnodetxt(bug_n.getElementsByTagName('short_desc')[0]) @@ -707,7 +713,7 @@ class Bugzilla(IBugtracker): assignee = '' except Exception as e: raise BugtrackerError(self.errparse % (self.description, e, url)) - return (id, product, title, severity, status, assignee, "%s/show_bug.cgi?id=%d" % (self.url, id), [], []) + return (bugid, product, title, severity, status, assignee, "%s/show_bug.cgi?id=%d" % (self.url, bugid), [], []) class Launchpad(IBugtracker): statuses = ("Unknown", "Invalid", "Opinion", "Won't Fix", "Fix Released", "Fix Committed", "New", @@ -789,14 +795,14 @@ class Launchpad(IBugtracker): return 0 return 0 - def get_bug(self, id): #TODO: Remove this method and rename 'get_bug_new' to 'get_bug' + def get_bug(self, bugtype, bugid): #TODO: Remove this method and rename 'get_bug_new' to 'get_bug' if self.lp: - return self.get_bug_new(id) - return self.get_bug_old(id) + return self.get_bug_new(bugtype, bugid) + return self.get_bug_old(bugtype, bugid) - def get_bug_new(self, id): #TODO: Rename this method to 'get_bug' + def get_bug_new(self, bugtype, bugid): #TODO: Rename this method to 'get_bug' try: - bugdata = self.lp.bugs[id] + bugdata = self.lp.bugs[bugid] if bugdata.private: raise BugtrackerError("This bug is private") duplicate = [] @@ -824,27 +830,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(id): # 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)' % (id, bugNo, self.url, bugNo)) - raise BugtrackerError("Bug #%d is private or does not exist (%s/bugs/%d)" % (id, self.url, id)) # Could be private, could just not exist - raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%d' % (self.url, id))) + 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))) elif isinstance(e, KeyError): raise BugNotFoundError - raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%d' % (self.url, id))) + raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%d' % (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) - def get_bug_old(self, id, duplicate=None): # Deprecated + def get_bug_old(self, bugtype, bugid, duplicate=None): # Deprecated try: - bugdata = utils.web.getUrl("%s/bugs/%d/+text" % (self.url, id)).decode('utf-8') + bugdata = utils.web.getUrl("%s/bugs/%d/+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, id, self.url, id)) + raise BugtrackerError('Bug #%d is a duplicate of bug #%d, but it is private (%s/bugs/%d)' % (duplicate, bugid, self.url, bugid)) else: raise BugNotFoundError - raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%d' % (self.url, id))) + raise BugtrackerError(self.errget % (self.description, e, '%s/bugs/%d' % (self.url, bugid))) try: # Split bug data into separate pieces (bug data, task data) @@ -861,16 +867,16 @@ class Launchpad(IBugtracker): else: assignee = '' except Exception as e: - raise BugtrackerError(self.errparse % (self.description, e, '%s/bugs/%d' % (self.url, id))) + raise BugtrackerError(self.errparse % (self.description, e, '%s/bugs/%d' % (self.url, bugid))) # Try and find duplicates if bugdata['duplicate-of']: - data = self.get_bug_old(int(bugdata['duplicate-of']), duplicate or id) + data = self.get_bug_old(bugtype, int(bugdata['duplicate-of']), duplicate or bugid) data[8].append(bugdata['bug']) return data - return (id, taskdata['task'], bugdata['title'], taskdata['importance'], taskdata['status'], - assignee, "%s/bugs/%d" % (self.url, id), [], []) + return (bugid, taskdata['task'], bugdata['title'], taskdata['importance'], taskdata['status'], + assignee, "%s/bugs/%d" % (self.url, bugid), [], []) # # Debbugs sucks donkeyballs @@ -887,10 +893,10 @@ class Debbugs(IBugtracker): IBugtracker.__init__(self, *args, **kwargs) self.soap_client = SoapClient("%s/cgi-bin/soap.cgi" % self.url, namespace="Debbugs/SOAP") - def get_bug(self, id): - url = "%s/cgi-bin/bugreport.cgi?bug=%d" % (self.url, id) + def get_bug(self, bugtype, bugid): + url = "%s/cgi-bin/bugreport.cgi?bug=%d" % (self.url, bugid) try: - raw = self.soap_client.get_status(bugs=id) + raw = self.soap_client.get_status(bugs=bugid) except Exception as e: raise BugtrackerError(self.errget % (self.description, e, url)) if not hasattr(raw, 'item'): @@ -901,7 +907,7 @@ class Debbugs(IBugtracker): status = 'Fixed' else: status = 'Open' - return (id, str(raw.package), str(raw.subject), str(raw.severity), status, '', "%s/%d" % (self.url, id), [], []) + return (bugid, str(raw.package), str(raw.subject), str(raw.severity), status, '', "%s/%d" % (self.url, bugid), [], []) except Exception as e: raise BugtrackerError(self.errparse % (self.description, e, url)) @@ -916,8 +922,8 @@ class SourceForge(IBugtracker): except: pass - def get_bug(self, id): - url = "%s/%d/" % (self.url.replace('sourceforge.net', 'sourceforge.net/rest'), id) + def get_bug(self, bugtype, bugid): + url = "%s/%d/" % (self.url.replace('sourceforge.net', 'sourceforge.net/rest'), bugid) try: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8'))['ticket'] @@ -929,8 +935,8 @@ class SourceForge(IBugtracker): 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), [], []) + return (bugid, product, bug['summary'], severity, ': '.join(bug['status'].split('-')), + bug['assigned_to'], "%s/%d/" % (self.url, bugid), [], []) except Exception as e: raise BugtrackerError(self.errparse % (self.description, e, url)) @@ -947,10 +953,14 @@ class GitHub(IBugtracker): except: pass - def get_bug(self, id): - url = "%s/%d" % (self.url.replace('github.com', 'api.github.com/repos'), id) + def get_bug(self, bugtype, bugid): + url = "%s/%d" % (self.url.replace('github.com', 'api.github.com/repos'), bugid) # Pulls are inconsistent in web and API URLs url = url.replace('/pull/', '/pulls/') + if bugtype in ('issue', 'bug'): + url = url.replace('/pulls/', '/issues/') + elif bugtype in ('pull', 'pr', 'merge', 'mr'): + url = url.replace('/issues/', '/pulls/') try: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8')) @@ -966,28 +976,32 @@ class GitHub(IBugtracker): assignee = bug['assignee']['login'] else: assignee = '' - return (id, product, bug['title'], '', status, assignee, bug['html_url'], [], []) + return (bugid, product, bug['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, id): + def get_tracker(self, url, bugid): try: match = re.match(r'[^\s/]+/[^\s/]+/[^\s/]+/(-/)?(issues|merge_requests)', url) desc = match.group(0) name = desc.lower() url = 'https://%s' % desc bugurl = "%s/%d" % (re.sub(r'(://[^\s/]+)/([^\s/]+)/([^\s/]+)/(-/)?', - r'\g<1>/api/v4/projects/\g<2>%2F\g<3>/', url), id) + r'\g<1>/api/v4/projects/\g<2>%2F\g<3>/', url), bugid) bugjson = utils.web.getUrl(bugurl) bug = json.loads(bugjson.decode('utf-8')) return GitLab(name, url, desc, 'gitlab') except: pass - def get_bug(self, id): + def get_bug(self, bugtype, bugid): url = "%s/%d" % (re.sub(r'(://[^\s/]+)/([^\s/]+)/([^\s/]+)/(-/)?', - r'\g<1>/api/v4/projects/\g<2>%2F\g<3>/', self.url), id) + r'\g<1>/api/v4/projects/\g<2>%2F\g<3>/', self.url), bugid) + if bugtype in ('issue', 'bug'): + url = url.replace('/merge_requests/', '/issues/') + elif bugtype in ('merge', 'mr', 'pull', 'pr'): + url = url.replace('/issues/', '/merge_requests/') try: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8')) @@ -1004,26 +1018,30 @@ class GitLab(IBugtracker): assignee = '%d people' % assino else: assignee = '' - return (id, product, bug['title'], '', status, assignee, bug['web_url'], [], []) + return (bugid, product, bug['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, id): + def get_tracker(self, url, bugid): try: match = re.match(r'[^\s/]+/[^\s/]+/[^\s/]+/(issues|pulls)', 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), id) + bugurl = '%s/%d' % (re.sub(r'(://[^\s/]+)/', r'\g<1>/api/v1/repos/', url), bugid) bugjson = utils.web.getUrl(bugurl) bug = json.loads(bugjson.decode('utf-8')) return Gitea(name, url, desc, 'gitea') except: pass - def get_bug(self, id): - url = "%s/%d" % (re.sub(r'(://[^\s/]+)/', r'\g<1>/api/v1/repos/', self.url), id) + def get_bug(self, bugtype, bugid): + url = "%s/%d" % (re.sub(r'(://[^\s/]+)/', r'\g<1>/api/v1/repos/', self.url), bugid) + if bugtype in ('issue', 'bug'): + url = url.replace('/pulls/', '/issues/') + elif bugtype in ('pull', 'pr', 'merge', 'mr'): + url = url.replace('/issues/', '/pulls/') try: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8')) @@ -1040,7 +1058,11 @@ class Gitea(IBugtracker): else: assignee = '' # Issues have no 'html_url', but pulls do - return (id, product, bug['title'], '', status, assignee, "%s/%d" % (self.url, id), [], []) + if 'html_url' in bug: + htmlurl = bug['html_url'] + else: + htmlurl = url.replace('/api/v1/repos/', '/') + return (bugid, product, bug['title'], '', status, assignee, htmlurl, [], []) except Exception as e: raise BugtrackerError(self.errparse % (self.description, e, url)) @@ -1059,27 +1081,27 @@ class Mantis(IBugtracker): except: pass - def get_bug(self, id): - url = "%s/api/rest/issues/%d" % (self.url, id) + def get_bug(self, bugtype, bugid): + url = "%s/api/rest/issues/%d" % (self.url, bugid) try: bugjson = utils.web.getUrl(url) bug = json.loads(bugjson.decode('utf-8'))['issues'][0] except Exception as e: # REST API may not be enabled yet if 'HTTP Error 404' in str(e): - return self.get_bug_old(id) + return self.get_bug_old(bugtype, bugid) raise BugtrackerError(self.errget % (self.description, e, url)) try: - return (id, bug['project']['name'], bug['summary'], bug['severity']['name'], bug['resolution']['name'], '', url, [], []) + return (bugid, bug['project']['name'], bug['summary'], bug['severity']['name'], bug['resolution']['name'], '', url, [], []) except Exception as e: raise BugtrackerError(self.errparse % (self.description, e, url)) - def get_bug_old(self, id): # Deprecated - url = "%s/view.php?id=%d" % (self.url, id) + def get_bug_old(self, bugtype, bugid): # Deprecated + url = "%s/view.php?id=%d" % (self.url, bugid) try: - raw = self.soap_client.mc_issue_get(username='', password='', issue_id=id) + raw = self.soap_client.mc_issue_get(username='', password='', issue_id=bugid) except Exception as e: - if 'Issue #%d not found' % id in str(e): + if 'Issue #%d not found' % bugid in str(e): raise BugNotFoundError # Often SOAP is not enabled if '.' in self.name: @@ -1089,7 +1111,7 @@ class Mantis(IBugtracker): if not hasattr(raw, 'id'): raise BugNotFoundError try: - return (id, str(raw.project.name), str(raw.summary), str(raw.severity.name), str(raw.resolution.name), '', url, [], []) + return (bugid, str(raw.project.name), str(raw.summary), str(raw.severity.name), str(raw.resolution.name), '', url, [], []) except Exception as e: raise BugtrackerError(self.errparse % (self.description, e, url)) @@ -1108,8 +1130,8 @@ class Trac(IBugtracker): except: pass - def get_bug(self, id): # This is still a little rough, but it works :) - url = "%s/%d" % (self.url, id) + def get_bug(self, bugtype, bugid): # This is still a little rough, but it works :) + url = "%s/%d" % (self.url, bugid) try: raw = utils.web.getUrl("%s?format=tab" % url).decode('utf-8') except Exception as e: @@ -1136,7 +1158,7 @@ class Trac(IBugtracker): severity = rest[headers.index("priority")] if "owner" in headers: assignee = rest[headers.index("owner")] - return (id, package, title, severity, status, assignee, url, [], []) + return (bugid, package, title, severity, status, assignee, url, [], []) except Exception as e: # Due to unreliable matching if '.' in self.name: