From 6b8f1540a101c8cd4f3399ea1bb4a116ad814739 Mon Sep 17 00:00:00 2001 From: random-access7 Date: Tue, 3 Apr 2018 01:05:55 +0530 Subject: [PATCH] labhub.py: add migrate_issue plugin Includes a migrate issue plugin that migrates an issue from one repo to another subject to conditions on the command like only maintainers can perform migration, issue must exist and issue must not be closed already. The plugin copies the issue title, issue description but appends the URL of the old issue and handle of the user that migrated the issue, to the description of the new issue. All comments are copied and written along with other details like author, date/time and URL of the old comment. Also includes tests to check functionality. Closes https://github.com/coala/corobo/issues/518 --- plugins/labhub.py | 100 +++++++++++++++++++++++++++++++++++++++ tests/labhub_test.py | 108 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) 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..b6d64926 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,110 @@ 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=set()) + 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.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') + 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()