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
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)