diff --git a/plugins/labhub.py b/plugins/labhub.py index 7616ba06..1801eb1a 100644 --- a/plugins/labhub.py +++ b/plugins/labhub.py @@ -381,3 +381,87 @@ def pr_stats(self, msg, match): state=type(self).community_state(pr_count) ) yield reply + + @re_botcmd(pattern=r'^migrate\s+https://(github|gitlab)\.com/([^/]+)/([^/]+)/+issues/(\d+)\s+https://(github|gitlab)\.com/([^/]+)/([^/]+)/*$', # Ignore LineLengthBear, PyCodeStyleBear + # Ignore LineLengthBear, PyCodeStyleBear + re_cmd_name_help='migrate ', + flags=re.IGNORECASE) + def migrate_issue(self, msg, match): + """ + Migrate an issue from source repo + to target repo owned by the org + """ + source_host = match.group(1) + source_org = match.group(2) + source_repo = match.group(3) + issue_number = match.group(4) + target_host = match.group(5) + target_org = match.group(6) + target_repo = match.group(7) + + user = msg.frm.nick + + if source_org != self.GH_ORG_NAME and source_org != self.GL_ORG_NAME: + return 'Source repository not owned by our org.' + + if target_org != self.GH_ORG_NAME and target_org != self.GL_ORG_NAME: + return 'Target repository not owned by our org.' + + if source_repo not in self.REPOS: + return 'Source repository does not exist.' + + if target_repo not in self.REPOS: + return 'Target repository does not exist.' + + if not self.TEAMS[self.GH_ORG_NAME + ' maintainers'].is_member(user): + return tenv().get_template( + 'labhub/errors/not-maintainer.jinja2.md' + ).render( + action='migrate issues', + target=user, + ) + + try: + source_issue = self.REPOS[source_repo].get_issue(int(issue_number)) + source_labels = source_issue.labels + + except RuntimeError as err: + sterr, errno = err.args + if errno == 404: + return 'Issue does not exist!' + else: + raise RuntimeError(sterr, errno) + + if str(source_issue.state) != 'open': + return 'Issue must be open in order to be migrated!' + + source_url = 'https://{}.com/{}/{}/issues/{}'.format( + source_host, source_org, source_repo, issue_number) + + ext_msg = ('\n\nThis is a migrated issue originally opened by @{} as {}' + ' and was migrated by @{}') + target_issue_desc = source_issue.description.rstrip() + ext_msg.format( + source_issue.author.username, source_url, str(user)) + target_issue = self.REPOS[target_repo].create_issue( + source_issue.title, target_issue_desc) + target_issue.labels = source_labels + + comment_ext = '\n\nOriginally commented by @{} on {} UTC' + + for comment in source_issue.comments: + target_comm = comment.body.rstrip() + comment_ext.format( + comment.author.username, str(comment.updated)) + target_issue.add_comment(target_comm) + + target_url = 'https://{}.com/{}/{}/issues/{}'.format( + target_host, target_org, target_repo, target_issue.number) + + migrate_comm = 'Issue has been migrated to this [repository]({}) by @{}' + source_issue.add_comment(migrate_comm.format( + target_url, str(user))) + + source_labels.add('Invalid') + source_issue.labels = source_labels + source_issue.close() + + return 'Issue has been successfully migrated: {}'.format(target_url) diff --git a/tests/labhub_test.py b/tests/labhub_test.py index 86fb6bcb..2f6a7c3e 100644 --- a/tests/labhub_test.py +++ b/tests/labhub_test.py @@ -344,3 +344,111 @@ def test_invite_me(self): 'Command \"hey\" / \"hey there\" not found.') with self.assertRaises(queue.Empty): testbot.pop_message() + + def test_migrate_issue(self): + plugins.labhub.GitHub = create_autospec(IGitt.GitHub.GitHub.GitHub) + plugins.labhub.GitLab = create_autospec(IGitt.GitLab.GitLab.GitLab) + labhub, testbot = plugin_testbot(plugins.labhub.LabHub, logging.ERROR) + labhub.activate() + + labhub.REPOS = { + 'a': self.mock_repo, + 'b': self.mock_repo + } + + mock_maint_team = create_autospec(github3.orgs.Team) + mock_maint_team.is_member.return_value = False + + labhub.TEAMS = { + 'coala maintainers': mock_maint_team, + 'coala developers': self.mock_team, + 'coala newcomers': self.mock_team + } + cmd = '!migrate https://github.com/{}/{}/issues/{} https://github.com/{}/{}/' + issue_check = 'Issue desc\n\nThis is a migrated issue originally opened by @{} as {} and was migrated by @{}' + comment_check = 'Comment body\n\nOriginally commented by @{} on {} UTC' + + # Not a maintainer + testbot.assertCommand(cmd.format('coala', 'a', '21', 'coala', 'b'), + 'you are not a maintainer!') + # Unknown first org + testbot.assertCommand(cmd.format('coa', 'a', '23', 'coala', 'b'), + 'Source repository not owned by our org') + # Unknown second org + testbot.assertCommand(cmd.format('coala', 'a', '23', 'coa', 'b'), + 'Target repository not owned by our org') + # Repo does not exist + testbot.assertCommand(cmd.format('coala', 'c', '23', 'coala', 'b'), + 'Source repository does not exist') + # Repo does not exist + testbot.assertCommand(cmd.format('coala', 'a', '23', 'coala', 'e'), + 'Target repository does not exist') + # No issue exists + mock_maint_team.is_member.return_value = True + self.mock_repo.get_issue = Mock(side_effect=RuntimeError('Error message', 404)) + testbot.assertCommand(cmd.format('coala', 'a', '21', 'coala', 'b'), + 'Issue does not exist!') + # Runtime error + mock_maint_team.is_member.return_value = True + self.mock_repo.get_issue = Mock(side_effect=RuntimeError('Error message', 403)) + testbot.assertCommand(cmd.format('coala', 'a', '21', 'coala', 'b'), + 'Computer says') + # Issue closed + mock_maint_team.is_member.return_value = True + mock_issue = create_autospec(IGitt.GitHub.GitHub.GitHubIssue) + self.mock_repo.get_issue = Mock(return_value=mock_issue) + mock_issue.labels = PropertyMock() + mock_issue.state = PropertyMock() + mock_issue.state = 'closed' + testbot.assertCommand(cmd.format('coala', 'a', '21', 'coala', 'b'), + 'Issue must be open') + # Migrate issue + mock_maint_team.is_member.return_value = True + mock_issue = create_autospec(IGitt.GitHub.GitHub.GitHubIssue) + mock_issue2 = create_autospec(IGitt.GitHub.GitHub.GitHubIssue) + + self.mock_repo.get_issue = Mock(return_value=mock_issue) + label_prop = PropertyMock(return_value=set()) + type(mock_issue).labels = label_prop + mock_issue.title = PropertyMock() + mock_issue.title = 'Issue title' + mock_issue.description = PropertyMock() + mock_issue.description = 'Issue desc' + mock_issue.state = PropertyMock() + mock_issue.state = 'open' + mock_issue.author.username = PropertyMock() + mock_issue.author.username = 'random-access7' + + self.mock_repo.create_issue = Mock(return_value=mock_issue2) + mock_issue2.labels = PropertyMock() + mock_issue2.number = PropertyMock() + mock_issue2.number = 45 + + mock_comment = create_autospec(IGitt.GitHub.GitHub.GitHubComment) + mock_comment2 = create_autospec(IGitt.GitHub.GitHub.GitHubComment) + + mock_issue.comments = PropertyMock() + mock_issue.comments = list() + mock_issue.comments.append(mock_comment) + mock_comment.author.username = PropertyMock() + mock_comment.author.username = 'random-access7' + mock_comment.body = PropertyMock() + mock_comment.body = 'Comment body' + mock_comment.updated = PropertyMock() + mock_comment.updated = '07/04/2018' + + testbot.assertCommand(cmd.format('coala', 'a', '21', 'coala', 'b'), + 'successfully migrated:') + + self.mock_repo.get_issue.assert_called_with(21) + + self.mock_repo.create_issue.assert_called_with('Issue title', + issue_check.format('random-access7', 'https://github.com/coala/a/issues/21', 'None')) + + mock_issue2.add_comment.assert_called_with(comment_check.format('random-access7', + '07/04/2018')) + + mock_issue.add_comment.assert_called_with( + 'Issue has been migrated to this [repository](https://github.com/coala/b/issues/45) by @None') + + mock_issue.close.assert_called_with()