diff --git a/plugins/labhub.py b/plugins/labhub.py index 7616ba06..72e28a7f 100644 --- a/plugins/labhub.py +++ b/plugins/labhub.py @@ -381,3 +381,103 @@ 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 one repo + to another repo of coala + """ + orig_host = match.group(1) + org = match.group(2) + repo_name_orig = match.group(3) + issue_number = match.group(4) + user = msg.frm.nick + final_host = match.group(5) + org2 = match.group(6) + repo_name_final = match.group(7) + + try: + assert org == self.GH_ORG_NAME or org == self.GL_ORG_NAME + except AssertionError: + yield 'First repository not owned by our org.' + return + + try: + assert org2 == self.GH_ORG_NAME or org2 == self.GL_ORG_NAME + except AssertionError: + yield 'Second repository not owned by our org.' + return + + if repo_name_orig not in self.REPOS: + yield 'First repository does not exist!' + return + + if repo_name_final not in self.REPOS: + yield 'Second repository does not exist!' + return + + if not self.TEAMS[self.GH_ORG_NAME + ' maintainers'].is_member(user): + yield tenv().get_template( + 'labhub/errors/not-maintainer.jinja2.md' + ).render( + action='migrate issues', + target=user, + ) + return + + try: + old_issue = self.REPOS[repo_name_orig].get_issue(int(issue_number)) + old_labels = old_issue.labels + + except RuntimeError as err: + sterr, errno = err.args + if errno == 404: + yield 'Issue does not exist!' + return + else: + raise RuntimeError(sterr, errno) + + if str(old_issue.state) == 'closed': + yield 'Issue cannot be migrated as it has been closed already.' + return + + url1 = 'https://{}.com/{}/{}/issues/{}' + new_issue_title = old_issue.title + new_issue_description = old_issue.description.rstrip() + '\n\n' + issue_author = old_issue.author.username + ext_msg = 'Migrated issue originally opened by @' + issue_author + \ + ' from ' + url1.format( + orig_host, org, repo_name_orig, issue_number) + \ + ' Migration done by @' + str(user) + new_issue_description += ext_msg + new_issue = self.REPOS[repo_name_final].create_issue( + new_issue_title, new_issue_description) + new_issue.labels = old_labels + + for comment in old_issue.comments: + comm_text = comment.body.rstrip() + comm_url = url1.format( + orig_host, org, repo_name_orig, issue_number) + \ + '#issuecomment-' + str(comment.number) + new_body = comm_text + '\n\nOriginally written by @' + \ + comment.author.username + ' on ' + \ + str(comment.updated) + ' UTC' + \ + ' and you can view it [here!](' + comm_url + ')' + new_issue.add_comment(new_body) + + url2 = 'https://{}.com/{}/{}/issues/{}'.format( + final_host, org, repo_name_final, new_issue.number) + + mig_comm = 'Issue has been migrated to another [repository](' + \ + url2 + ') by @' + str(user) + old_issue.add_comment(mig_comm) + old_labels.add('Invalid') + old_issue.labels = old_labels + old_issue.close() + + yield 'New issue created: {}'.format(url2) + return diff --git a/tests/labhub_test.py b/tests/labhub_test.py index 86de6d34..0204c8a4 100644 --- a/tests/labhub_test.py +++ b/tests/labhub_test.py @@ -25,6 +25,7 @@ def setUp(self): self.mock_org = create_autospec(github3.orgs.Organization) self.mock_gh = create_autospec(github3.GitHub) + self.mock_team = create_autospec(github3.orgs.Team) self.mock_team.name = PropertyMock() self.mock_team.name = 'mocked team' @@ -343,3 +344,112 @@ 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/{}/{}/' + + # 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'), + 'First repository not owned by our org') + # Unknown second org + testbot.assertCommand(cmd.format('coala', 'a', '23','coa','b'), + 'Second repository not owned by our org') + # Repo does not exist + testbot.assertCommand(cmd.format('coala', 'c', '23','coala','b'), + 'First repository does not exist') + # Repo does not exist + testbot.assertCommand(cmd.format('coala', 'a', '23','coala','e'), + 'Second 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_iss = create_autospec(IGitt.GitHub.GitHub.GitHubIssue) + self.mock_repo.get_issue = Mock(return_value=mock_iss) + mock_iss.labels = PropertyMock() + mock_iss.state = PropertyMock() + mock_iss.state = 'closed' + testbot.assertCommand(cmd.format('coala', 'a', '21','coala','b'), + 'has been closed already') + # Migrate issue + mock_maint_team.is_member.return_value = True + mock_iss = create_autospec(IGitt.GitHub.GitHub.GitHubIssue) + issue2 = create_autospec(IGitt.GitHub.GitHub.GitHubIssue) + + self.mock_repo.get_issue = Mock(return_value=mock_iss) + label_prop = PropertyMock(return_value={'enhancement','bug'}) + type(mock_iss).labels = label_prop + mock_iss.title = PropertyMock() + mock_iss.title = 'Issue title' + mock_iss.description = PropertyMock() + mock_iss.description = 'Issue description' + mock_iss.state = PropertyMock() + mock_iss.state = 'open' + mock_iss.author.username = PropertyMock() + mock_iss.author.username = 'random-access7' + + self.mock_repo.create_issue = Mock(return_value=issue2) + issue2.labels = PropertyMock() + issue2.labels = set() + issue2.number = PropertyMock() + issue2.number = 45 + + mock_comment = create_autospec(IGitt.GitHub.GitHub.GitHubComment) + mock_comment2 = create_autospec(IGitt.GitHub.GitHub.GitHubComment) + + mock_iss.comments = PropertyMock() + mock_iss.comments = list() + mock_iss.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.number = PropertyMock() + mock_comment.number = 172 + mock_comment.updated = PropertyMock() + mock_comment.updated = '07/04/2018' + + testbot.assertCommand(cmd.format('coala', 'a', '21','coala','b'), + 'issue created:') + + self.mock_repo.get_issue.assert_called_with(21) + self.mock_repo.create_issue.assert_called_with('Issue title', + 'Issue description\n\nMigrated issue originally opened by @random-access7 ' + \ + 'from https://github.com/coala/a/issues/21 Migration done by @None') + assert sorted(list(issue2.labels)) == ['bug','enhancement', 'Invalid'] + issue2.add_comment.assert_called_with( + 'Comment body\n\nOriginally written by @random-access7 on 07/04/2018 UTC and ' + \ + 'you can view it [here!](https://github.com/coala/a/issues/21#issuecomment-172)') + mock_iss.add_comment.assert_called_with( + 'Issue has been migrated to another [repository](https://github.com/coala/b/issues/45)' + \ + ' by @None') + mock_iss.close.assert_called_with()