"""
Contains the abstraction for a commit in GitLab.
"""
from typing import Optional
from typing import Set
from typing import Union
from urllib.parse import quote_plus
from IGitt import ElementDoesntExistError
from IGitt.GitHub.GitHubCommit import get_diff_index
from IGitt.GitLab import GitLabMixin
from IGitt.GitLab import GitLabOAuthToken, GitLabPrivateToken
from IGitt.GitLab.GitLabComment import GitLabComment
from IGitt.GitLab.GitLabRepository import GitLabRepository
from IGitt.GitLab.GitLabIssue import GitLabIssue
from IGitt.Interfaces import get, post
from IGitt.Interfaces.Comment import CommentType
from IGitt.Interfaces.Commit import Commit
from IGitt.Interfaces.CommitStatus import Status, CommitStatus
GL_STATE_TRANSLATION = {
Status.RUNNING: 'running',
Status.CANCELED: 'canceled',
Status.ERROR: 'failed',
Status.FAILED: 'failed',
Status.PENDING: 'pending',
Status.SUCCESS: 'success',
Status.MANUAL: 'manual',
Status.CREATED: 'created',
Status.SKIPPED: 'skipped'
}
INV_GL_STATE_TRANSLATION = {val: key for key, val
in GL_STATE_TRANSLATION.items()}
GITLAB_KEYWORD_REGEX = {'fix': r'[Ff]ix(?:e[sd]|ing)?',
'close': r'[Cc]los(?:e[sd]?|ing)',
'resolve': r'[Rr]esolv(?:e[sd]?|ing)'}
[docs]class GitLabCommit(GitLabMixin, Commit):
"""
Represents a commit on GitLab.
"""
def __init__(self,
token: Union[GitLabOAuthToken, GitLabPrivateToken],
repository: str,
sha: Optional[str],
branch: Optional[str]=None):
"""
Creates a new GitLabCommit object.
:param token: A Token object to be used for authentication.
:param repository: The full repository name.
:param sha: The full commit SHA, if None given provide a branch.
:param branch: A branch name if SHA is unavailable. Note that lazy
loading won't work in that case.
"""
assert sha or branch, 'Either full SHA or branch name has to be given!'
self._token = token
self._repository = repository
self._sha = sha
self._branch = branch
self._url = '/projects/{id}/repository/commits/{sha}'.format(
id=quote_plus(repository), sha=sha if sha else branch)
@property
def message(self) -> str:
"""
Returns the commit message.
:return: Commit message as string.
"""
return self.data['message']
@property
def sha(self):
"""
Retrieves the SHA of the commit:
>>> from os import environ
>>> commit = GitLabCommit(
... GitLabOAuthToken(environ['GITLAB_TEST_TOKEN']),
... 'gitmate-test-user/test', '674498'
... )
>>> commit.sha
'674498'
:return: A string holding the SHA of the commit.
"""
return self._sha if self._sha else self.data['id']
@property
def repository(self):
"""
Retrieves the repository that holds this commit.
>>> from os import environ
>>> commit = GitLabCommit(
... GitLabOAuthToken(environ['GITLAB_TEST_TOKEN']),
... 'gitmate-test-user/test', '3fc4b86'
... )
>>> commit.repository.full_name
'gitmate-test-user/test'
:return: A usable Repository instance.
"""
return GitLabRepository(self._token, self._repository)
@property
def parent(self):
"""
Retrieves the parent commit. In case of a merge commit the first parent
will be returned.
>>> from os import environ
>>> commit = GitLabCommit(
... GitLabOAuthToken(environ['GITLAB_TEST_TOKEN']),
... 'gitmate-test-user/test', '3fc4b86'
... )
>>> commit.parent.sha
'674498fd415cfadc35c5eb28b8951e800f357c6f'
:return: A Commit object.
"""
return GitLabCommit(self._token, self._repository,
self.data['parent_ids'][0])
[docs] 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, self.absolute_url(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
@property
def combined_status(self) -> Status:
"""
Retrieves a combined status of all the commits.
:return:
Status.FAILED if any of the commits report as error or failure or
canceled
Status.PENDING if there are no statuses or a commit is pending or a
test is running
Status.SUCCESS if the latest status for all commits is success
:raises AssertionError:
If the status couldn't be matched with any of the possible outcomes
Status.SUCCESS, Status.FAILED and Status.PENDING.
"""
statuses = set(map(lambda status: status.status, self.get_statuses()))
if (
not len(statuses) or
Status.PENDING in statuses or
Status.RUNNING in statuses or
Status.CREATED in statuses):
return Status.PENDING
if (
Status.FAILED in statuses or
Status.ERROR in statuses or
Status.CANCELED in statuses):
return Status.FAILED
assert all(status in {Status.SUCCESS, Status.MANUAL}
for status in statuses)
return Status.SUCCESS
[docs] def set_status(self, status: CommitStatus):
"""
Adds the given status to the commit.
>>> from os import environ
>>> commit = GitLabCommit(
... GitLabOAuthToken(environ['GITLAB_TEST_TOKEN']),
... 'gitmate-test-user/test', '3fc4b86'
... )
>>> status = CommitStatus(Status.FAILED, 'Theres a problem',
... 'gitmate/test')
>>> commit.set_status(status)
>>> commit.get_statuses().pop().description
'Theres a problem'
If a status with the same context already exists, it will be bluntly
overridden:
>>> status.status = Status.SUCCESS
>>> status.description = "Theres no problem"
>>> commit.set_status(status)
>>> len(commit.get_statuses())
1
>>> commit.get_statuses().pop().description
'Theres no problem'
:param status: The CommitStatus to set to this commit.
:raises RuntimeError: If something goes wrong (network, auth...).
"""
data = {'state': GL_STATE_TRANSLATION[status.status],
'target_url': status.url, 'description': status.description,
'name': status.context}
status_url = '/projects/{repo}/statuses/{sha}'.format(
repo=quote_plus(self._repository), sha=self.sha)
post(self._token, self.absolute_url(status_url), data)
[docs] def get_patch_for_file(self, filename: str):
r"""
Retrieves the unified diff for the commit.
>>> from os import environ
>>> commit = GitLabCommit(
... GitLabOAuthToken(environ['GITLAB_TEST_TOKEN']),
... 'gitmate-test-user/test', '3fc4b86'
... )
>>> assert (commit.get_patch_for_file('README.md') ==
... '--- a/README.md\n+++ b/README.md\n@@ -1,2 +1,4 @@\n '
... '# test\n a test repo\n+\n+a tst pr\n')
But only if it exists!
>>> commit.get_patch_for_file('IDONTEXISTFILE')
Traceback (most recent call last):
...
IGitt.ElementDoesntExistError: The file does not exist.
:param filename: The file to retrieve patch for.
:return: A string containing the patch.
:raises ElementDoesntExistError: If the given filename does not exist.
"""
diff = get(self._token, self.url + '/diff')
for patch in diff:
if filename in (patch['new_path'], patch['old_path']):
return patch['diff']
raise ElementDoesntExistError('The file does not exist.')
@property
def unified_diff(self):
"""
Retrieves the unified diff for the commit excluding the diff index.
"""
return '\n'.join(patch['diff']
for patch in get(self._token, self.url + '/diff')
)
@property
def closes_issues(self) -> Set[GitLabIssue]:
"""
Returns a set of GitLabIssue objects which would be closed upon merging
this pull request.
"""
issues = self._get_closes_issues()
return {GitLabIssue(self._token, repo_name, number)
for number, repo_name in issues}
@property
def mentioned_issues(self) -> Set[GitLabIssue]:
"""
Returns a set of GitLabIssue objects which are related to the merge
request.
"""
issues = self._get_mentioned_issues()
return {GitLabIssue(self._token, repo_name, number)
for number, repo_name in issues}
@property
def will_fix_issues(self) -> Set[GitLabIssue]:
"""
Returns a set of GitLabIssue objects which would be fixed as stated in
this commit message.
"""
issues = self.get_keywords_issues(GITLAB_KEYWORD_REGEX['fix'],
[self.message])
return {GitLabIssue(self._token, repo_name, number)
for number, repo_name in issues}
@property
def will_close_issues(self) -> Set[GitLabIssue]:
"""
Returns a set of GitLabIssue objects which would be closed as stated in
this commit message.
"""
issues = self.get_keywords_issues(GITLAB_KEYWORD_REGEX['close'],
[self.message])
return {GitLabIssue(self._token, repo_name, number)
for number, repo_name in issues}
@property
def will_resolve_issues(self) -> Set[GitLabIssue]:
"""
Returns a set of GitLabIssue objects which would be resolved as stated
in this commit message.
"""
issues = self.get_keywords_issues(GITLAB_KEYWORD_REGEX['resolve'],
[self.message])
return {GitLabIssue(self._token, repo_name, number)
for number, repo_name in issues}