"""
Contains a class representing the GitHub pull request.
Labels, Milestones, Assignees and Comments are an integral part of GitHubIssue
class. They have a rich unified way of handling issues and pull requests.
Reference
- https://developer.github.com/v3/pulls/#labels-assignees-and-milestones
The methods being used from GitHubIssue are:
- number
- assignee
- add_comment
- comments
- labels
- labels.setter
- available_labels
"""
from functools import lru_cache
from typing import Set
from IGitt.GitHub import GitHubToken
from IGitt.GitHub.GitHubCommit import GitHubCommit
from IGitt.GitHub.GitHubIssue import GitHubIssue
from IGitt.GitHub.GitHubUser import GitHubUser
from IGitt.Interfaces.MergeRequest import MergeRequest
from IGitt.Interfaces import get, put, MergeRequestStates, patch
# Issue is used as a Mixin, super() is never called by design!
from IGitt.Utils import PossiblyIncompleteDict
[docs]class GitHubMergeRequest(GitHubIssue, MergeRequest):
"""
A Pull Request on GitHub.
"""
def __init__(self, token: GitHubToken, repository: str, number: int):
"""
Creates a new Pull Request.
:param token: A GitHubToken object to authenticate with.
:param repository: The repository containing the PR.
:param number: The PR number.
"""
self._token = token
self._number = number
self._repository = repository
self._mr_url = self.absolute_url(
'/repos/' + repository + '/pulls/' + str(number))
self._url = '/repos/'+repository+'/issues/'+str(number)
def _get_data(self):
issue_data = get(self._token, self.url)
def get_full_data():
"""
Updates the incomplete issue data with the PR data to make it
complete.
"""
# Ignore PyLintBear (E1101), its type inference is too stupid
issue_data.update(get(self._token, self._mr_url))
return issue_data
# If issue data is sufficient, don't even get MR data
return PossiblyIncompleteDict(issue_data, get_full_data)
@property
def base(self):
"""
Retrieves the base commit as a commit object.
>>> from os import environ
>>> pr = GitHubMergeRequest(GitHubToken(environ['GITHUB_TEST_TOKEN']),
... 'gitmate-test-user/test', 7)
>>> pr.base.sha
'674498fd415cfadc35c5eb28b8951e800f357c6f'
:return: A Commit object.
"""
return GitHubCommit.from_data(self.data['base'], self._token,
self._repository,
self.data['base']['sha'])
@property
def head(self):
"""
Retrieves the head commit as a commit object.
>>> from os import environ
>>> pr = GitHubMergeRequest(GitHubToken(environ['GITHUB_TEST_TOKEN']),
... 'gitmate-test-user/test', 7)
>>> pr.head.sha
'f6d2b7c66372236a090a2a74df2e47f42a54456b'
:return: A Commit object.
"""
return GitHubCommit.from_data(self.data['head'], self._token,
self.repository.full_name,
self.data['head']['sha'])
@property
def base_branch_name(self) -> str:
"""
Retrieves the base branch name of the merge request, i.e. the one it
should be merged into.
>>> from os import environ
>>> pr = GitHubMergeRequest(GitHubToken(environ['GITHUB_TEST_TOKEN']),
... 'gitmate-test-user/test', 7)
>>> pr.base_branch_name
'master'
:return: A string.
"""
return self.data['base']['ref']
@property
def head_branch_name(self) -> str:
"""
Retrieves the head branch name of the merge request, i.e. the one that
will be merged.
>>> from os import environ
>>> pr = GitHubMergeRequest(GitHubToken(environ['GITHUB_TEST_TOKEN']),
... 'gitmate-test-user/test', 7)
>>> pr.head_branch_name
'gitmate-test-user-patch-2'
:return: A string.
"""
return self.data['head']['ref']
@property
@lru_cache(None)
def commits(self):
"""
Retrieves a tuple of commit objects that are included in the PR.
>>> from os import environ
>>> pr = GitHubMergeRequest(GitHubToken(environ['GITHUB_TEST_TOKEN']),
... 'gitmate-test-user/test', 7)
>>> [commit.sha for commit in pr.commits]
['f6d2b7c66372236a090a2a74df2e47f42a54456b']
:return: A tuple of commit objects.
"""
commits = get(self._token, self._mr_url + '/commits')
return tuple(GitHubCommit.from_data(commit, self._token,
self._repository, commit['sha'])
for commit in commits)
@property
def repository(self):
"""
Retrieves the repository where this comes from.
>>> from os import environ
>>> pr = GitHubMergeRequest(GitHubToken(environ['GITHUB_TEST_TOKEN']),
... 'gitmate-test-user/test', 7)
>>> pr.repository.full_name
'gitmate-test-user/test'
:return: The repository object.
"""
from .GitHubRepository import GitHubRepository
return GitHubRepository(self._token, self._repository)
@property
def source_repository(self):
"""
Retrieves the repository where this PR's head branch is located at.
>>> from os import environ
>>> pr = GitHubMergeRequest(GitHubToken(environ['GITHUB_TEST_TOKEN']),
... 'gitmate-test-user/test', 7)
>>> pr.source_repository.full_name
'gitmate-test-user/test'
:return: The repository object.
"""
from .GitHubRepository import GitHubRepository
return GitHubRepository(self._token,
self.data['head']['repo']['full_name'])
@property
def affected_files(self):
"""
Retrieves affected files from a GitHub pull request.
>>> from os import environ
>>> pr = GitHubMergeRequest(GitHubToken(environ['GITHUB_TEST_TOKEN']),
... 'gitmate-test-user/test', 7)
>>> pr.affected_files
{'README.md'}
:return: A set of filenames.
"""
files = get(self._token, self._mr_url + '/files')
return {file['filename'] for file in files}
@property
def diffstat(self):
"""
Gets additions and deletions of a merge request.
>>> from os import environ
>>> pr = GitHubMergeRequest(GitHubToken(environ['GITHUB_TEST_TOKEN']),
... 'gitmate-test-user/test', 7)
>>> pr.diffstat
(2, 0)
:return: An (additions, deletions) tuple.
"""
return self.data['additions'], self.data['deletions']
[docs] def delete(self):
"""
GitHub doesn't allow deleting issues or pull requests.
"""
raise NotImplementedError
@property
def closes_issues(self) -> Set[GitHubIssue]:
"""
Returns a set of GitHubIssue objects which would be closed upon merging
this pull request.
"""
return {issue for commit in self.commits
for issue in commit.closes_issues}
@property
def will_fix_issues(self) -> Set[GitHubIssue]:
"""
Returns a set of GitHubIssue objects which would be fixed as stated in
this pull request.
"""
return {issue for commit in self.commits
for issue in commit.will_fix_issues}
@property
def will_close_issues(self) -> Set[GitHubIssue]:
"""
Returns a set of GitHubIssue objects which would be closed as stated in
this pull request.
"""
return {issue for commit in self.commits
for issue in commit.will_close_issues}
@property
def will_resolve_issues(self) -> Set[GitHubIssue]:
"""
Returns a set of GitHubIssue objects which would be resolved as stated
in this pull request.
"""
return {issue for commit in self.commits
for issue in commit.will_resolve_issues}
@property
def mentioned_issues(self) -> Set[GitHubIssue]:
"""
Returns a set of GitHubIssue objects which are related to the pull
request.
"""
commit_bodies = [commit.message for commit in self.commits]
comment_bodies = [comment.body for comment in self.comments]
commit = next(iter(self.commits))
issues = commit.get_keywords_issues(r'',
commit_bodies + comment_bodies)
return {GitHubIssue(self._token, repo_name, number)
for number, repo_name in issues}
@property
def author(self) -> GitHubUser:
"""
Retrieves the author of the merge request.
:return: A GitHubUser object.
"""
return GitHubUser.from_data(self.data['user'],
self._token,
self.data['user']['login'])
@property
def state(self) -> MergeRequestStates:
"""
Retrieves the state of the Pull Request on GitHub.
:return: A MergeRequestStates object.
"""
state = {
'open': MergeRequestStates.OPEN,
'closed': MergeRequestStates.CLOSED,
}[self.data['state']]
if self.data['merged_at'] and self.data['state'] == 'closed':
return MergeRequestStates.MERGED
return state
[docs] def merge(self, message: str=None, sha: str=None,
should_remove_source_branch: bool=False,
_github_merge_method: str=None,
_gitlab_merge_when_pipeline_succeeds: bool=False):
"""
Merges the merge request.
:param message: The commit message.
:param sha: The commit sha that the HEAD must
match in order to merge.
:param should_remove_source_branch: Whether the source branch should be
removed upon a successful merge.
:param _github_merge_method: On GitHub, the merge method to use
when merging the MR. Can be one of
`merge`, `squash` or `rebase`.
:param _gitlab_wait_for_pipeline: On GitLab, whether the MR should be
merged immediately after the
pipeline succeeds.
:raises RuntimeError: If something goes wrong (network, auth...).
:raises NotImplementedError: If an unused parameter is passed.
"""
if should_remove_source_branch or _gitlab_merge_when_pipeline_succeeds:
raise NotImplementedError
merge_options = {}
if message:
lines = message.splitlines()
merge_options['commit_title'] = lines.pop(0)
merge_options['commit_message'] = '\n'.join(lines).strip()
if sha:
merge_options['sha'] = sha
if _github_merge_method:
merge_options['merge_method'] = _github_merge_method
put(self._token, self._mr_url + '/merge', merge_options)
self.refresh()
@property
def milestone(self):
"""
Retrieves the milestone.
"""
from IGitt.GitHub.GitHubMilestone import GitHubMilestone
return GitHubMilestone.from_data(
self.data['milestone'], self._token, self._repository,
self.data['milestone']['number']
) if self.data['milestone'] else None
@milestone.setter
def milestone(self, new_milestone):
"""
Setter for the Milestone.
Delete the Milestone with passing a 'None'
"""
self.data = patch(
self._token, self.url,
{'milestone': new_milestone.number if new_milestone else ''})
@property
def mergeable(self) -> bool:
"""
Returns true if there is no merge conflict.
"""
return self.data['mergeable']