project.py (248 lines of code) (raw):

import os from collections import defaultdict from html.entities import name2codepoint from dateutil.parser import parse from datetime import datetime import re from utils import fetch_labels_mapping, fetch_allowed_labels, convert_label class Project: def __init__(self, name, doneStatusCategoryId, jiraBaseUrl): self.name = name self.doneStatusCategoryId = doneStatusCategoryId self.jiraBaseUrl = jiraBaseUrl self._project = {'Milestones': defaultdict(int), 'Components': defaultdict( int), 'Labels': defaultdict(int), 'Types': defaultdict(int), 'Issues': []} self.labels_mapping = fetch_labels_mapping() self.approved_labels = fetch_allowed_labels() def get_milestones(self): return self._project['Milestones'] def get_components(self): return self._project['Components'] def get_issues(self): return self._project['Issues'] def get_types(self): return self._project['Types'] def get_all_labels(self): merge = self._project['Components'].copy() merge.update(self._project['Labels']) merge.update(self._project['Types']) merge.update({'imported-jira-issue': 0}) return merge def get_labels(self): merge = self._project['Labels'].copy() merge.update({'imported-jira-issue': 0}) return merge def add_item(self, item): itemProject = self._projectFor(item) if itemProject != self.name: print('Skipping item ' + item.key.text + ' for project ' + itemProject + ' current project: ' + self.name) return self._append_item_to_project(item) self._add_milestone(item) self._add_labels(item) self._add_subtasks(item) self._add_parenttask(item) self._add_comments(item) self._add_relationships(item) def prettify(self): def hist(h): for key in h.keys(): print(('%30s (%5d): ' + h[key] * '#') % (key, h[key])) print print(self.name + ':\n Milestones:') hist(self._project['Milestones']) print(' Types:') hist(self._project['Types']) print(' Components:') hist(self._project['Components']) print(' Labels:') hist(self._project['Labels']) print print('Total Issues to Import: %d' % len(self._project['Issues'])) def _projectFor(self, item): try: result = item.project.get('key') except AttributeError: result = item.key.text.split('-')[0] return result def _append_item_to_project(self, item): # todo assignee closed = str(item.statusCategory.get('id')) == self.doneStatusCategoryId closed_at = '' if closed: try: closed_at = self._convert_to_iso(item.resolved.text) except AttributeError: pass # TODO: ensure item.assignee/reporter.get('username') to avoid "JENKINSUSER12345" # TODO: fixit in gh issues # check if issue description is missing or empty and set a default if not hasattr(item, 'description') or not item.description: item.description = 'No Description' body = self._htmlentitydecode(item.description.text) # metadata: original author & link body = body + '\n\n---\n<details><summary><i>Originally reported by <a title="' + str(item.reporter) + '" href="' + self.jiraBaseUrl + '/secure/ViewProfile.jspa?name=' + item.reporter.get('username') + '">' + item.reporter.get('username') + '</a>, imported from: <a href="' + self.jiraBaseUrl + '/browse/' + item.key.text + '" target="_blank">' + item.title.text[item.title.text.index("]") + 2:len(item.title.text)] + '</a></i></summary>' # metadata: assignee body = body + '\n<i><ul>' if item.assignee != 'Unassigned': body = body + '\n<li><b>assignee</b>: <a title="' + str(item.assignee) + '" href="' + self.jiraBaseUrl + '/secure/ViewProfile.jspa?name=' + item.assignee.get('username') + '">' + item.assignee.get('username') + '</a>' try: body = body + '\n<li><b>status</b>: ' + item.status except AttributeError: pass try: body = body + '\n<li><b>priority</b>: ' + item.priority except AttributeError: pass try: body = body + '\n<li><b>resolution</b>: ' + item.resolution except AttributeError: pass try: body = body + '\n<li><b>resolved</b>: ' + self._convert_to_iso(item.resolved.text) except AttributeError: pass body = body + '\n<li><b>imported</b>: ' + datetime.today().strftime('%Y-%m-%d') body = body + '\n</ul></i>\n</details>' # retrieve jira components and labels as github labels labels = [] # set a default component if empty or missing if not hasattr(item, 'component') or not item.component: item.component = 'miscellaneous' for component in item.component: if os.getenv('JIRA_MIGRATION_INCLUDE_COMPONENT_IN_LABELS', 'true') == 'true': labels.append('jira-component:' + component.text.lower()) labels.append(component.text.lower()) labels.append(self._jira_type_mapping(item.type.text.lower())) for label in item.labels.findall('label'): converted_label = convert_label(label.text.strip().lower(), self.labels_mapping, self.approved_labels) if converted_label is not None: labels.append(converted_label) labels.append('imported-jira-issue') unique_labels = list(set(labels)) self._project['Issues'].append({'title': item.title.text, 'key': item.key.text, 'body': body, 'created_at': self._convert_to_iso(item.created.text), 'closed_at': closed_at, 'updated_at': self._convert_to_iso(item.updated.text), 'closed': closed, 'labels': unique_labels, 'comments': [], 'duplicates': [], 'is-duplicated-by': [], 'is-related-to': [], 'depends-on': [], 'blocks': [] }) if not self._project['Issues'][-1]['closed_at']: del self._project['Issues'][-1]['closed_at'] def _jira_type_mapping(self, issue_type): if issue_type == 'bug': return 'bug' if issue_type == 'improvement': return 'rfe' if issue_type == 'new feature': return 'rfe' if issue_type == 'task': return 'rfe' if issue_type == 'story': return 'rfe' if issue_type == 'patch': return 'rfe' if issue_type == 'epic': return 'epic' def _convert_to_iso(self, timestamp): dt = parse(timestamp) return dt.isoformat() def _add_milestone(self, item): try: self._project['Milestones'][item.fixVersion.text] += 1 # this prop will be deleted later: self._project['Issues'][-1]['milestone_name'] = item.fixVersion.text.trim() except AttributeError: pass def _add_labels(self, item): try: self._project['Components'][item.component.text] += 1 tmp_l = item.component.text.trim() if tmp_l == 'Bug': tmp_l = 'bug' self._project['Issues'][-1]['labels'].append(tmp_l) except AttributeError: pass try: for label in item.labels.label: self._project['Labels'][label.text] += 1 tmp_l = label.text.trim() if tmp_l == 'Bug': tmp_l = 'bug' self._project['Issues'][-1]['labels'].append(tmp_l) except AttributeError: pass try: self._project['Types'][item.type.text] += 1 tmp_l = item.type.text.trim() if tmp_l == 'Bug': tmp_l = 'bug' self._project['Issues'][-1]['labels'].append(tmp_l) except AttributeError: pass def _add_subtasks(self, item): try: subtaskList = '' for subtask in item.subtasks.subtask: subtaskList = subtaskList + '- ' + subtask + '\n' if subtaskList != '': print('-> subtaskList: ' + subtaskList) self._project['Issues'][-1]['comments'].append( {"created_at": self._convert_to_iso(item.created.text), "body": 'Subtasks:\n\n' + subtaskList}) except AttributeError: pass def _add_parenttask(self, item): try: parentTask = item.parent.text if parentTask != '': print('-> parentTask: ' + parentTask) self._project['Issues'][-1]['comments'].append( {"created_at": self._convert_to_iso(item.created.text), "body": 'Subtask of parent task ' + parentTask}) except AttributeError: pass def _add_comments(self, item): try: for comment in item.comments.comment: self._project['Issues'][-1]['comments'].append( {"created_at": self._convert_to_iso(comment.get('created')), "body": '<i><a href="' + self.jiraBaseUrl + '/secure/ViewProfile.jspa?name=' + comment.get('author') + '">' + comment.get('author') + '</a>:</i>\n' + self._htmlentitydecode(comment.text) }) except AttributeError: pass def _add_relationships(self, item): try: for issuelinktype in item.issuelinks.issuelinktype: for outwardlink in issuelinktype.outwardlinks: for issuelink in outwardlink.issuelink: for issuekey in issuelink.issuekey: tmp_outward = outwardlink.get("description").replace(' ', '-') if tmp_outward in self._project['Issues'][-1]: self._project['Issues'][-1][tmp_outward].append(issuekey.text) except AttributeError: pass except KeyError: print('1. KeyError at ' + item.key.text) try: for issuelinktype in item.issuelinks.issuelinktype: for inwardlink in issuelinktype.inwardlinks: for issuelink in inwardlink.issuelink: for issuekey in issuelink.issuekey: tmp_inward = inwardlink.get("description").replace(' ', '-') if tmp_inward in self._project['Issues'][-1]: self._project['Issues'][-1][tmp_inward].append(issuekey.text) except AttributeError: pass except KeyError: print('2. KeyError at ' + item.key.text) for customfield in item.customfields.findall('customfield'): if customfield.get('key') == 'com.pyxis.greenhopper.jira:gh-epic-link': epic_key = customfield.customfieldvalues.customfieldvalue self._project['Issues'][-1]['epic-link'] = epic_key def _htmlentitydecode(self, s): if s is None: return '' s = s.replace(' ' * 8, '') return re.sub('&(%s);' % '|'.join(name2codepoint), lambda m: chr(name2codepoint[m.group(1)]), s)