Source code for IGitt.GitHub.GitHubMergeRequest

"""
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']