예제 #1
0
class SourceManagement:
    """Abstract source code management services like GitHub and GitLab."""

    def __init__(self, service_type: ServiceType, service_url: str, token: str, slug: str):
        """Initialize source code management tools abstraction.

        Note that we are using IGitt for calls. IGitt keeps URL to services in its global context per GitHub/GitLab.
        This is global context is initialized in the manager with a hope to fix this behavior for our needs.
        """
        self.service_type = service_type
        self.slug = slug
        self.service_url = service_url
        self.token = token

        if self.service_type == ServiceType.GITHUB:
            self.repository = GitHubRepository(token=GitHubToken(token), repository=slug)
        elif self.service_type == ServiceType.GITLAB:
            self.repository = GitLabRepository(token=GitLabPrivateToken(token), repository=slug)
        else:
            raise NotImplementedError

    def get_issue(self, title: str) -> Issue:
        """Retrieve issue with the given title."""
        for issue in self.repository.issues:
            if issue.title == title:
                return issue

        return None

    def open_issue_if_not_exist(self, title: str, body: typing.Callable,
                                refresh_comment: typing.Callable = None, labels: list = None) -> Issue:
        """Open the given issue if does not exist already (as opened)."""
        _LOGGER.debug(f"Reporting issue {title!r}")
        issue = self.get_issue(title)
        if issue:
            _LOGGER.info(f"Issue already noted on upstream with id #{issue.number}")
            if not refresh_comment:
                return None

            comment_body = refresh_comment(issue)
            if comment_body:
                issue.add_comment(comment_body)
                _LOGGER.info(f"Added refresh comment to issue #{issue.number}")
            else:
                _LOGGER.debug(f"Refresh comment not added")
        else:
            issue = self.repository.create_issue(title, body())
            issue.labels = set(labels or [])
            _LOGGER.info(f"Reported issue {title!r} with id #{issue.number}")
            return issue

        return None

    def close_issue_if_exists(self, title: str, comment: str = None):
        """Close the given issue (referenced by its title) and close it with a comment."""
        issue = self.get_issue(title)
        if not issue:
            _LOGGER.debug(f"Issue {title!r} not found, not closing it")
            return

        issue.add_comment(comment)
        issue.close()

    def _github_open_merge_request(self, commit_msg, body, branch_name) -> GitHubMergeRequest:
        """Create a GitHub pull request with the given dependency update."""
        url = f'{IGitt.GitHub.BASE_URL}/repos/{self.slug}/pulls'
        response = requests.Session().post(
            url,
            headers={
                'Accept': 'application/vnd.github.v3+json',
                'Authorization': f'token {self.token}'
            },
            json={
                'title': commit_msg,
                'body': body,
                'head': branch_name,
                'base': 'master',
                'maintainer_can_modify': True
            }
        )
        try:
            response.raise_for_status()
        except Exception as exc:
            raise RuntimeError(f"Failed to create a pull request: {response.text}") from exc

        mr_number = response.json()['number']
        _LOGGER.info(f"Newly created pull request #{mr_number} available at {response.json()['html_url']}")
        return GitHubMergeRequest.from_data(
            response.json(), token=GitHubToken(self.token), repository=self.slug, number=mr_number
        )

    def _gitlab_open_merge_request(self, commit_msg, body, branch_name) -> GitLabMergeRequest:
        url = f'{IGitt.GitLab.BASE_URL}/projects/{quote_plus(self.slug)}/merge_requests'
        # Use Session as these calls are mocked based on tls_verify configuration.
        response = requests.Session().post(
            url,
            params={'private_token': self.token},
            json={
                'title': commit_msg,
                'description': body,
                'source_branch': branch_name,
                'target_branch': 'master',
                'allow_collaboration': True
            }
        )
        try:
            response.raise_for_status()
        except Exception as exc:
            raise RuntimeError(f"Failed to create a pull request: {response.text}") from exc

        mr_number = response.json()['iid']
        _LOGGER.info(f"Newly created pull request #{mr_number} available at {response.json()['web_url']}")
        return GitLabMergeRequest.from_data(
            response.json(), token=GitLabPrivateToken(self.token), repository=self.slug, number=mr_number
        )

    def assign(self, issue: Issue, assignees: typing.List[str]) -> None:
        """Assign users (by their accounts) to the given issue."""
        if self.service_type == ServiceType.GITHUB:
            users = (GitHubUser(GitHubToken(self.token), username) for username in assignees)
        elif self.service_type == ServiceType.GITLAB:
            users = (GitLabUser(GitLabPrivateToken(self.token), username) for username in assignees)
        else:
            raise NotImplementedError

        issue.assign(*users)

    def open_merge_request(self, commit_msg: str, branch_name: str, body: str, labels: list) -> MergeRequest:
        """Open a merge request for the given branch."""
        if self.service_type == ServiceType.GITHUB:
            merge_request = self._github_open_merge_request(commit_msg, body, branch_name)
        elif self.service_type == ServiceType.GITLAB:
            merge_request = self._gitlab_open_merge_request(commit_msg, body, branch_name)
        else:
            raise NotImplementedError

        merge_request.labels = set(labels or [])
        return merge_request

    def _github_delete_branch(self, branch: str) -> None:
        """Delete the given branch from remote repository."""
        response = requests.Session().delete(
            f'{IGitt.GitHub.BASE_URL}/repos/{self.slug}/git/refs/heads/{branch}',
            headers={f'Authorization': f'token {self.token}'},
        )

        response.raise_for_status()
        # GitHub returns an empty string, noting to return.

    def _gitlab_delete_branch(self, branch: str) -> None:
        """Delete the given branch from remote repository."""
        response = requests.Session().delete(
            f'{IGitt.GitLab.BASE_URL}/projects/{quote_plus(self.slug)}/repository/branches/{branch}',
            params={'private_token': self.token},
        )
        response.raise_for_status()

    def _github_list_branches(self) -> typing.Set[str]:
        """Get listing of all branches available on the remote GitHub repository."""
        response = requests.Session().get(
            f'{IGitt.GitHub.BASE_URL}/repos/{self.slug}/branches',
            headers={f'Authorization': f'token {self.token}'},
        )

        response.raise_for_status()
        # TODO: pagination?
        return response.json()

    def _gitlab_list_branches(self) -> typing.Set[str]:
        """Get listing of all branches available on the remote GitLab repository."""
        response = requests.Session().get(
            f"{IGitt.GitLab.BASE_URL}/projects/{quote_plus(self.slug)}/repository/branches",
            params={'private_token': self.token},
        )

        response.raise_for_status()
        # TODO: pagination?
        return response.json()

    def list_branches(self) -> set:
        """Get branches available on remote."""
        # TODO: remove this logic once IGitt will support branch operations
        if self.service_type == ServiceType.GITHUB:
            return self._github_list_branches()
        elif self.service_type == ServiceType.GITLAB:
            return self._gitlab_list_branches()
        else:
            raise NotImplementedError

    def delete_branch(self, branch_name: str) -> None:
        """Delete the given branch from remote."""
        # TODO: remove this logic once IGitt will support branch operations
        if self.service_type == ServiceType.GITHUB:
            return self._github_delete_branch(branch_name)
        elif self.service_type == ServiceType.GITLAB:
            return self._gitlab_delete_branch(branch_name)
        else:
            raise NotImplementedError
예제 #2
0
class GitLabRepositoryTest(IGittTestCase):

    def setUp(self):
        self.token = GitLabOAuthToken(os.environ.get('GITLAB_TEST_TOKEN', ''))
        self.repo = GitLabRepository(self.token,
                                     'gitmate-test-user/test')

        self.fork_token = GitLabPrivateToken(os.environ.get('GITLAB_TEST_TOKEN_2', ''))
        self.fork_repo = GitLabRepository(self.fork_token,
                                          'gitmate-test-user/test')

    def test_id(self):
        self.assertEqual(self.repo.identifier, 3439658)

    def test_id_init(self):
        repo = GitLabRepository(self.token, 3439658)
        self.assertEqual(repo.full_name, 'gitmate-test-user/test')

    def test_top_level_org(self):
        self.assertEqual(self.repo.top_level_org.name, 'gitmate-test-user')

    def test_hoster(self):
        self.assertEqual(self.repo.hoster, 'gitlab')

    def test_full_name(self):
        self.assertEqual(self.repo.full_name, 'gitmate-test-user/test')

    def test_clone_url(self):
        self.assertEqual(self.repo.clone_url,
                         'https://{}@gitlab.com/gitmate-test-user/test.git'.format(
                             'oauth2:' + os.environ.get('GITLAB_TEST_TOKEN', ''))
                        )

    def test_get_labels(self):
        self.assertEqual(sorted(self.repo.get_labels()), ['a', 'b', 'c', 'dem'])

    def test_labels(self):
        with self.assertRaises(ElementAlreadyExistsError):
            self.repo.create_label('a', '#000000')

        with self.assertRaises(ElementDoesntExistError):
            self.repo.delete_label('f')

        self.repo.create_label('bug', '#000000')
        self.assertEqual(sorted(self.repo.get_labels()),
                         ['a', 'b', 'bug', 'c', 'dem'])
        self.repo.delete_label('bug')
        self.assertEqual(sorted(self.repo.get_labels()), ['a', 'b', 'c', 'dem'])

    def test_get_issue(self):
        self.assertEqual(self.repo.get_issue(1).title, 'new title')

    def test_get_mr(self):
        self.assertEqual(self.repo.get_mr(2).title, 'Sils/severalcommits')

    def test_create_issue(self):
        self.assertEqual(self.repo.create_issue(
            'title', 'body').title, 'title')

    def test_hooks(self):
        self.repo.register_hook('http://some.url/in/the/world', 'secret',
                                events={WebhookEvents.MERGE_REQUEST})
        self.assertIn('http://some.url/in/the/world', self.repo.hooks)
        # won't register again
        self.repo.register_hook('http://some.url/in/the/world')
        self.repo.delete_hook('http://some.url/in/the/world')
        self.assertNotIn('http://some.url/in/the/world', self.repo.hooks)
        # events not specified, register all
        self.repo.register_hook('http://some.url/in/the/world')
        self.assertIn('http://some.url/in/the/world', self.repo.hooks)
        self.repo.delete_hook('http://some.url/in/the/world')

    def test_merge_requests(self):
        self.assertEqual(len(self.repo.merge_requests), 32)

    def test_issues(self):
        self.assertEqual(len(self.repo.issues), 14)

    def test_create_fork(self):
        try:
            fork = self.fork_repo.create_fork(namespace='gitmate-test-user-2')
        except RuntimeError:
            fork = GitLabRepository(self.fork_token, 'gitmate-test-user-2/test')
            fork.delete()
            fork = self.fork_repo.create_fork(namespace='gitmate-test-user-2')

        self.assertIsInstance(fork, GitLabRepository)

    def test_delete_repo(self):
        try:
            fork = self.fork_repo.create_fork(namespace='gitmate-test-user-2')
        except RuntimeError:
            fork = GitLabRepository(self.fork_token, 'gitmate-test-user-2/test')
            fork.delete()
            fork = self.fork_repo.create_fork(namespace='gitmate-test-user-2')


        self.assertIsNone(fork.delete())

    def test_create_mr(self):
        try:
            fork = self.fork_repo.create_fork(namespace='gitmate-test-user-2')
        except RuntimeError:
            fork = GitLabRepository(self.fork_token, 'gitmate-test-user-2/test')
            fork.delete()
            fork = self.fork_repo.create_fork(namespace='gitmate-test-user-2')

        fork.create_file(path='.coafile', message='hello', content='hello', branch='master')
        mr = fork.create_merge_request(title='coafile', head='master', base='master',
                                       target_project_id=self.repo.data['id'],
                                       target_project=self.repo.data['path_with_namespace'])

        self.assertIsInstance(mr, GitLabMergeRequest)

    def test_create_file(self):
        try:
            fork = self.fork_repo.create_fork(namespace='gitmate-test-user-2')
        except RuntimeError:
            fork = GitLabRepository(self.fork_token, 'gitmate-test-user-2/test')
            fork.delete()
            fork = self.fork_repo.create_fork(namespace='gitmate-test-user-2')
        author = {
            'name': 'gitmate-test-user-2',
            'email': '*****@*****.**'
        }

        self.assertIsInstance(fork.create_file(path='.coafile', message='hello',
                                               content='hello', branch='master', author=author),
                              GitLabContent)

    def test_search_issues(self):
        created_after = datetime(2017, 6, 18).date()
        created_before = datetime(2017, 7, 15).date()
        issues = list(self.repo.search_issues(created_after=created_after,
                                              created_before=created_before,
                                              state=IssueStates.OPEN))
        self.assertEqual(len(issues), 2)
        issues = list(self.repo.search_issues(created_after=created_after,
                                              created_before=created_before,
                                              state=IssueStates.CLOSED))
        self.assertEqual(len(issues), 0)

    def test_search_mrs(self):
        updated_after = datetime(2017, 6, 18).date()
        updated_before = datetime(2017, 7, 2).date()
        merge_requests = list(self.repo.search_mrs(
            updated_after=updated_after,
            updated_before=updated_before,
            state=MergeRequestStates.OPEN))
        self.assertEqual(len(merge_requests), 1)
        merge_requests = list(self.repo.search_mrs(
            updated_after=updated_after,
            updated_before=updated_before,
            state=MergeRequestStates.CLOSED))
        self.assertEqual(len(merge_requests), 2)
        merge_requests = list(self.repo.search_mrs(
            updated_after=updated_after,
            updated_before=updated_before))
        self.assertEqual(len(merge_requests), 3)

    def test_commits(self):
        self.assertEqual({commit.sha for commit in self.repo.commits},
                         {'69e17e536092754e98aafbe5da0ee2be5fea81fb',
                          'dd52e331780d30b58da030f9341abd07ba4ce31e',
                          '198dd16f8249ea98ed41876efe27d068b69fa215',
                          '674498fd415cfadc35c5eb28b8951e800f357c6f',
                          '71a61579cb3aa493e8eadc9f183ff4377be4d1be',
                          'd6fe46331fcc32ac73c9308f94350d5822ce717d',
                          '6371fe50a92fa2147dcde0ce011db726b35b2646',
                          '7747ee49b7d322e7d82520126ca275115aa67447',
                          'cb9aa1c8964b3bcb0c542b281e06b339ddcc015f',
                          '645961c0841a84c1dd2a58535aa70ad45be48c46',
                          'e3d12312ee8e4ba8e60ed009cf64fb3a1007b2c3',
                          '515280bfe8488e1b403e0dd95c41a404355ca184',
                          '05a9faff56fd9bdb25d18a554bb2f3320de3fc6f',
                          'ed5fb0a1cc38a078a6f72b3523b6bce8c14be9b8'})
        repo = GitLabRepository(self.token, 'gitmate-test-user/empty')
        self.assertEqual(repo.commits, set())

    def test_get_permission_level(self):
        sils = GitLabUser(self.token, 104269)
        user = GitLabUser(self.token)
        meetmangukiya = GitLabUser(self.token, 707601)
        noman = GitLabUser(self.token, 1)

        self.assertEqual(self.repo.get_permission_level(sils),
                         AccessLevel.ADMIN)
        self.assertEqual(self.repo.get_permission_level(user),
                         AccessLevel.ADMIN)
        self.assertEqual(self.repo.get_permission_level(meetmangukiya),
                         AccessLevel.CAN_WRITE)
        self.assertEqual(self.repo.get_permission_level(noman),
                         AccessLevel.CAN_VIEW)

    def test_parent(self):
        repo = GitLabRepository(self.token, 'nkprince007/test')
        self.assertEqual(repo.parent.full_name, 'gitmate-test-user/test')
        self.assertEqual(repo.parent.parent, None)