def test_github_comment_after_sync_no_data_in_db( self, m_set_status, m_get_statuses, m_sha, m_get_perms, m_author, m_body, m_head, m_commits ): m_get_statuses.return_value = ( CommitStatus(Status.FAILED, 'Terrible issues', 'review/gitmate/manual', 'https://gitmate.io'), CommitStatus(Status.SUCCESS, 'No issues', 'review/somewhere/else', 'https://some/url')) m_sha.return_value = 'f6d2b7c66372236a090a2a74df2e47f42a54456b' m_get_perms.return_value = AccessLevel.CAN_WRITE m_author.return_value = GitHubUser(self.gh_token, self.user.username) m_body.return_value = 'unack f6d2b7c' m_head.return_value = self.gh_commit m_commits.return_value = tuple([self.gh_commit]) response = self.simulate_github_webhook_call('issue_comment', self.gh_comment_data) self.assertEqual(response.status_code, status.HTTP_200_OK) args = sum([list(args) for args, _ in m_set_status.call_args_list], []) # 3 calls to be made as follows # Status.FAILED review/gitmate/manual/pr # Status.FAILED review/gitmate/manual # Status.FAILED review/gitmate/manual/pr self.assertEqual(m_set_status.call_count, 3) self.assertEqual([(arg.status, arg.context) for arg in args], [(Status.FAILED, 'review/gitmate/manual/pr'), (Status.FAILED, 'review/gitmate/manual'), (Status.FAILED, 'review/gitmate/manual/pr')])
def test_gitlab_ack( self, m_set_status, m_get_statuses, m_sha, m_get_perms, m_author, m_body, m_commits ): m_get_statuses.return_value = ( CommitStatus(Status.SUCCESS, 'No issues', 'review/gitmate/manual', 'https://gitmate.io'), CommitStatus(Status.SUCCESS, 'No issues', 'review/somewhere/else', 'https://some/url')) m_sha.return_value = 'f6d2b7c66372236a090a2a74df2e47f42a54456b' m_body.return_value = 'ack f6d2b7c' m_get_perms.return_value = AccessLevel.CAN_WRITE m_author.return_value = GitLabUser(self.gl_token, 0) m_commits.return_value = tuple([self.gl_commit]) response = self.simulate_gitlab_webhook_call('Merge Request Hook', self.gl_pr_data) response = self.simulate_gitlab_webhook_call('Note Hook', self.gl_comment_data) self.assertEqual(response.status_code, status.HTTP_200_OK) args = sum([list(args) for args, _ in m_set_status.call_args_list], []) # 3 calls to be made as follows # Status.SUCCESS review/gitmate/manual/pr # Status.SUCCESS review/gitmate/manual # Status.SUCCESS review/gitmate/manual/pr self.assertEqual(m_set_status.call_count, 3) self.assertEqual([(arg.status, arg.context) for arg in args], [(Status.SUCCESS, 'review/gitmate/manual/pr'), (Status.SUCCESS, 'review/gitmate/manual'), (Status.SUCCESS, 'review/gitmate/manual/pr')])
def test_set_status(self): commit = GitLabCommit(self.token, 'gitmate-test-user/test', '3fc4b860e0a2c17819934d678decacd914271e5c') status = CommitStatus(Status.FAILED, 'Theres a problem', 'gitmate/test') self.commit.set_status(status) self.assertEqual(commit.get_statuses().pop().description, 'Theres a problem')
def ack_state(self): state = CommitStatus(Status.SUCCESS, 'This PR is reviewed. :)', 'review/gitmate/manual/pr', 'https://gitmate.io') for acked in dict(self.acks).values(): if acked['status'] in [ Status.FAILED.value, Status.ERROR.value, Status.CANCELED.value ]: return CommitStatus(Status.FAILED, 'This PR needs work. :(', 'review/gitmate/manual/pr', 'https://gitmate.io') if acked['status'] == Status.PENDING.value: state = CommitStatus(Status.PENDING, 'This PR needs review.', 'review/gitmate/manual/pr', 'https://gitmate.io') return state
def pending(commit: Commit): for status in commit.get_statuses(): if status.context == 'review/gitmate/manual': return status state = CommitStatus(Status.PENDING, 'This commit needs review.', 'review/gitmate/manual', 'https://gitmate.io') commit.set_status(state) return state
def test_gitlab_ack_without_minimum_access_level( self, _, m_get_statuses, m_get_perms, m_username, m_body, m_author, m_sha, m_add_comment, m_commits ): m_get_statuses.return_value = ( CommitStatus(Status.SUCCESS, 'No issues', 'review/gitmate/manual', 'https://gitmate.io'), CommitStatus(Status.SUCCESS, 'No issues', 'review/somewhere/else', 'https://some/url')) m_sha.return_value = 'f6d2b7c66372236a090a2a74df2e47f42a54456b' m_get_perms.return_value = AccessLevel.CAN_VIEW m_username.return_value = self.user.username m_body.return_value = 'unack f6d2b7c' m_author.return_value = GitLabUser(self.gl_token, 0) m_commits.return_value = tuple([self.gl_commit]) _ = self.simulate_gitlab_webhook_call('Merge Request Hook', self.gl_pr_data) response = self.simulate_gitlab_webhook_call('Note Hook', self.gl_comment_data) self.assertEqual(response.status_code, status.HTTP_200_OK) m_add_comment.assert_called_once_with( 'Sorry @{}, you do not have the necessary permission levels to ' 'perform the action.'.format(self.user.username))
def test_github_ack_with_special_chars( self, m_set_status, m_get_statuses, m_sha, m_get_perms, m_author, m_body, m_head, m_commits ): self.repo.settings = [{ 'name': 'ack', 'settings': { 'ack_strs': r'bot\ack, bot\accept' } }] m_get_statuses.return_value = ( CommitStatus(Status.SUCCESS, 'No issues', 'review/gitmate/manual', 'https://gitmate.io'), CommitStatus(Status.SUCCESS, 'No issues', 'review/somewhere/else', 'https://some/url')) m_sha.return_value = 'f6d2b7c66372236a090a2a74df2e47f42a54456b' m_body.return_value = r'bot\accept f6d2b7c' m_get_perms.return_value = AccessLevel.CAN_WRITE m_author.return_value = GitHubUser(self.gh_token, self.user.username) m_head.return_value = self.gh_commit m_commits.return_value = tuple([self.gh_commit]) response = self.simulate_github_webhook_call('pull_request', self.gh_pr_data) response = self.simulate_github_webhook_call('issue_comment', self.gh_comment_data) self.assertEqual(response.status_code, status.HTTP_200_OK) args = sum([list(args) for args, _ in m_set_status.call_args_list], []) # 3 calls to be made as follows # Status.SUCCESS review/gitmate/manual/pr # Status.SUCCESS review/gitmate/manual # Status.SUCCESS review/gitmate/manual/pr self.assertEqual(m_set_status.call_count, 3) self.assertEqual([(arg.status, arg.context) for arg in args], [(Status.SUCCESS, 'review/gitmate/manual/pr'), (Status.SUCCESS, 'review/gitmate/manual'), (Status.SUCCESS, 'review/gitmate/manual/pr')])
def ack(self): """ Acknowledges the commit by setting the manual review GitMate status to success. >>> CommitMock = type('CommitMock', (Commit,), ... {'set_status': lambda self, s: print(s.status)}) >>> CommitMock().ack() Status.SUCCESS :raises RuntimeError: If something goes wrong (network, auth...). """ status = CommitStatus(Status.SUCCESS, 'This commit was acknowledged.', 'review/gitmate/manual', 'http://gitmate.io/') self.set_status(status)
def unack(self): """ Unacknowledges the commit by setting the manual review GitMate status to failed. >>> CommitMock = type('CommitMock', (Commit,), ... {'set_status': lambda self, s: print(s.status)}) >>> CommitMock().unack() Status.FAILED :raises RuntimeError: If something goes wrong (network, auth...). """ status = CommitStatus(Status.FAILED, 'This commit needs work.', 'review/gitmate/manual', 'http://gitmate.io/') self.set_status(status)
def pending(self): """ Sets the commit to a pending manual review state if there is no manual review state yet. Given a commit with an unrelated status: >>> CommitMock = type( ... 'CommitMock', (Commit,), ... {'set_status': lambda self, s: self.statuses.append(s), ... 'get_statuses': lambda self: self.statuses, ... 'statuses': []}) >>> commit = CommitMock() >>> commit.set_status(CommitStatus(Status.FAILED, context='unrelated')) >>> len(commit.get_statuses()) 1 The invocation of pending will now add a pending status: >>> commit.pending() >>> len(commit.get_statuses()) 2 >>> commit.get_statuses()[1].context 'review/gitmate/manual' However, if there is already a manual review state, the invocation of pending won't affect the status: >>> commit.get_statuses().clear() >>> commit.ack() >>> commit.pending() # Won't do anything >>> len(commit.get_statuses()) 1 >>> commit.get_statuses()[0].status <Status.SUCCESS: 1> :raises RuntimeError: If something goes wrong (network, auth...). """ for status in self.get_statuses(): if status.context == 'review/gitmate/manual': return status = CommitStatus(Status.PENDING, 'This commit needs review.', 'review/gitmate/manual', 'http://gitmate.io') self.set_status(status)
def test_github_pending_pr_open_event( self, m_set_status, m_get_statuses, m_sha, m_head, m_commits ): m_get_statuses.return_value = ( CommitStatus(Status.FAILED, 'Terrible issues', 'some/other/review', 'https://some/other/ci'),) m_sha.return_value = 'f6d2b7c66372236a090a2a74df2e47f42a54456b' m_head.return_value = self.gh_commit m_commits.return_value = tuple([self.gh_commit]) response = self.simulate_github_webhook_call('pull_request', self.gh_pr_data) self.assertEqual(response.status_code, status.HTTP_200_OK) args = sum([list(args) for args, _ in m_set_status.call_args_list], []) # 2 calls to be made as follows # Status.PENDING review/gitmate/manual # Status.PENDING review/gitmate/manual/pr self.assertEqual(m_set_status.call_count, 2) self.assertEqual([(arg.status, arg.context) for arg in args], [(Status.PENDING, 'review/gitmate/manual'), (Status.PENDING, 'review/gitmate/manual/pr')])
def get_statuses(self) -> Set[CommitStatus]: """ Retrieves the all commit statuses. :return: A (frozen)set of CommitStatus objects. :raises RuntimeError: If something goes wrong (network, auth...). """ url = self._url + '/statuses' statuses = get(self._token, url) # Only the first of each context is the one we want result = set() contexts = set() for status in statuses: if status['context'] not in contexts: result.add( CommitStatus(INV_GH_STATE_TRANSLATION[status['state']], status['description'], status['context'], status['target_url'])) contexts.add(status['context']) return result
def add_review_status(pr: MergeRequest): """ A responder to add pending/acknowledged/rejected status on commits on MergeRequest SYNCHRONIZED and OPENED events based on their generated commit hashes (generated from the unified diff and commit message). """ db_pr, _ = MergeRequestModel.objects.get_or_create( repo=Repository.from_igitt_repo(pr.repository), number=pr.number) head = pr.head hashes = [] for commit in pr.commits: commit_hash = _get_commit_hash(commit) hashes.append(commit_hash) # This commit was head of the PR before, deletion of the PR state is # not possible so we make it green to clean it up. if commit.sha == db_pr.last_head != head.sha: commit.set_status( CommitStatus(Status.SUCCESS, 'Outdated. Check ' + head.sha[:7] + ' instead.', 'review/gitmate/manual/pr', 'https://gitmate.io')) # copying status from unmodified commits in the same merge request if commit_hash in db_pr.acks: commit.set_status(_dict_to_status(db_pr.acks[commit_hash])) else: db_pr.acks[commit_hash] = _status_to_dict(pending(commit)) for chash in dict(db_pr.acks).keys(): if not chash in hashes: del db_pr.acks[chash] db_pr.last_head = head.sha db_pr.save() pr.head.set_status(db_pr.ack_state)
def test_gitlab_unmodified_commit( self, m_set_status, m_get_statuses, m_sha, m_message, m_diff, m_commits ): m_get_statuses.return_value = ( CommitStatus(Status.SUCCESS, 'Good to go!', 'review/gitmate/manual', 'https://gitmate.io'),) m_diff.return_value = ('--- a/README.md\n' '+++ b/README.md\n' '@@ -1,2 +1,4 @@\n' ' # test\n' ' a test repo\n' '+\n' '+a commiit that can one acknowledge') m_message.return_value = 'Update README.md' m_commits.return_value = tuple([self.gl_commit]) m_sha.return_value = 'f6d2b7c66372236a090a2a74df2e47f42a54456b' response = self.simulate_gitlab_webhook_call('Merge Request Hook', self.gl_pr_data) self.assertEqual(response.status_code, status.HTTP_200_OK) # resyncing merge request with a new unmodified commit m_sha.return_value = '9ba5b704f5866e468ec2e639fa893ae4c129f2ad' m_commits.return_value = tuple([GitLabCommit( self.gl_token, self.gl_repo.full_name, m_sha.return_value)]) response = self.simulate_gitlab_webhook_call('Merge Request Hook', self.gl_pr_data) self.assertEqual(response.status_code, status.HTTP_200_OK) args = sum([list(args) for args, _ in m_set_status.call_args_list], []) # 3 calls to be made as follows # Status.SUCCESS review/gitmate/manual/pr # Status.SUCCESS review/gitmate/manual # Status.SUCCESS review/gitmate/manual/pr self.assertEqual([(arg.status, arg.context) for arg in args], [(Status.SUCCESS, 'review/gitmate/manual/pr'), (Status.SUCCESS, 'review/gitmate/manual'), (Status.SUCCESS, 'review/gitmate/manual/pr')])
def get_statuses(self) -> Set[CommitStatus]: """ Retrieves the all commit statuses. :return: A (frozen)set of CommitStatus objects. :raises RuntimeError: If something goes wrong (network, auth...). """ # rebuild the url with full sha because gitlab doesn't work that way url = '/projects/{repo}/repository/commits/{sha}/statuses'.format( repo=quote_plus(self._repository), sha=self.sha) statuses = get(self._token, url) # Only the first of each context is the one we want result = set() contexts = set() for status in statuses: if status['name'] not in contexts: result.add(CommitStatus( INV_GL_STATE_TRANSLATION[status['status']], status['description'], status['name'], status['target_url'])) contexts.add(status['name']) return result
def ack(commit: Commit): state = CommitStatus(Status.SUCCESS, 'This commit was acknowledged. :)', 'review/gitmate/manual', 'https://gitmate.io') commit.set_status(state) return state
def unack(commit: Commit): state = CommitStatus(Status.FAILED, 'This commit needs work. :(', 'review/gitmate/manual', 'https://gitmate.io') commit.set_status(state) return state
def _dict_to_status(state: dict): return CommitStatus(**{**state, 'status': Status(state['status'])})