diff --git a/readthedocs/api/v2/views/integrations.py b/readthedocs/api/v2/views/integrations.py index 4424238a78b..1567e71b18e 100644 --- a/readthedocs/api/v2/views/integrations.py +++ b/readthedocs/api/v2/views/integrations.py @@ -19,7 +19,11 @@ webhook_github, webhook_gitlab, ) -from readthedocs.core.views.hooks import build_branches, sync_versions +from readthedocs.core.views.hooks import ( + build_branches, + sync_versions, + get_or_create_external_version, +) from readthedocs.integrations.models import HttpExchange, Integration from readthedocs.projects.models import Project @@ -29,6 +33,9 @@ GITHUB_EVENT_HEADER = 'HTTP_X_GITHUB_EVENT' GITHUB_SIGNATURE_HEADER = 'HTTP_X_HUB_SIGNATURE' GITHUB_PUSH = 'push' +GITHUB_PULL_REQUEST = 'pull_request' +GITHUB_PULL_REQUEST_OPEN = 'opened' +GITHUB_PULL_REQUEST_SYNC = 'synchronize' GITHUB_CREATE = 'create' GITHUB_DELETE = 'delete' GITLAB_TOKEN_HEADER = 'HTTP_X_GITLAB_TOKEN' @@ -110,6 +117,10 @@ def handle_webhook(self): """Handle webhook payload.""" raise NotImplementedError + def get_external_version_data(self): + """Get External Version data from payload.""" + raise NotImplementedError + def is_payload_valid(self): """Validates the webhook's payload using the integration's secret.""" return False @@ -218,6 +229,13 @@ def get_data(self): pass return super().get_data() + def get_external_version_data(self): + """Get Commit Sha and pull request number from payload""" + identifier = self.data['pull_request']['head']['sha'] + verbose_name = str(self.data['number']) + + return identifier, verbose_name + def is_payload_valid(self): """ GitHub use a HMAC hexdigest hash to sign the payload. @@ -271,6 +289,21 @@ def handle_webhook(self): raise ParseError('Parameter "ref" is required') if event in (GITHUB_CREATE, GITHUB_DELETE): return self.sync_versions(self.project) + + if ( + event == GITHUB_PULL_REQUEST and + self.data['action'] in [GITHUB_PULL_REQUEST_OPEN, GITHUB_PULL_REQUEST_SYNC] + ): + try: + identifier, verbose_name = self.get_external_version_data() + external_version = get_or_create_external_version( + self.project, identifier, verbose_name + ) + return self.get_response_push(self.project, [external_version.verbose_name]) + + except KeyError: + raise ParseError('Parameters "sha" and "number" are required') + return None def _normalize_ref(self, ref): diff --git a/readthedocs/core/views/hooks.py b/readthedocs/core/views/hooks.py index a3273a88871..bd5b8c40cb0 100644 --- a/readthedocs/core/views/hooks.py +++ b/readthedocs/core/views/hooks.py @@ -2,6 +2,8 @@ import logging +from readthedocs.builds.constants import EXTERNAL +from readthedocs.builds.models import Version from readthedocs.core.utils import trigger_build from readthedocs.projects.tasks import sync_repository_task @@ -88,3 +90,21 @@ def sync_versions(project): except Exception: log.exception('Unknown sync versions exception') return None + + +def get_or_create_external_version(project, identifier, verbose_name): + external_version = project.versions(manager=EXTERNAL).filter(verbose_name=verbose_name).first() + if external_version: + if external_version.identifier != identifier: + external_version.identifier = identifier + external_version.save() + else: + created_external_version = Version.objects.create( + project=project, + type=EXTERNAL, + identifier=identifier, + verbose_name=verbose_name, + active=True + ) + return created_external_version + return external_version diff --git a/readthedocs/projects/constants.py b/readthedocs/projects/constants.py index 7c83cffd7ea..8b770830f79 100644 --- a/readthedocs/projects/constants.py +++ b/readthedocs/projects/constants.py @@ -333,3 +333,5 @@ 'https://gitlab.com/{user}/{repo}/' '{action}/{version}{docroot}{path}{source_suffix}' ) + +GITHUB_GIT_PATTERN = 'pull/{id}/head:external-{id}' diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index 7efcd5a762a..2d0e087b2a2 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -138,7 +138,7 @@ def sync_repo(self): } ) version_repo = self.get_vcs_repo() - version_repo.update() + version_repo.update(version=self.version) self.sync_versions(version_repo) version_repo.checkout(self.version.identifier) finally: diff --git a/readthedocs/vcs_support/backends/bzr.py b/readthedocs/vcs_support/backends/bzr.py index e228ac720d3..99029d6dc82 100644 --- a/readthedocs/vcs_support/backends/bzr.py +++ b/readthedocs/vcs_support/backends/bzr.py @@ -17,7 +17,7 @@ class Backend(BaseVCS): supports_tags = True fallback_branch = '' - def update(self): + def update(self, version=None): # pylint: disable=arguments-differ super().update() retcode = self.run('bzr', 'status', record=False)[0] if retcode == 0: diff --git a/readthedocs/vcs_support/backends/git.py b/readthedocs/vcs_support/backends/git.py index aa3adc6c2bc..506143132ed 100644 --- a/readthedocs/vcs_support/backends/git.py +++ b/readthedocs/vcs_support/backends/git.py @@ -10,7 +10,9 @@ from django.core.exceptions import ValidationError from git.exc import BadName, InvalidGitRepositoryError +from readthedocs.builds.constants import EXTERNAL from readthedocs.config import ALL +from readthedocs.projects.constants import GITHUB_GIT_PATTERN from readthedocs.projects.exceptions import RepositoryError from readthedocs.projects.validators import validate_submodule_url from readthedocs.vcs_support.base import BaseVCS, VCSVersion @@ -25,6 +27,7 @@ class Backend(BaseVCS): supports_tags = True supports_branches = True + supports_external_branches = True supports_submodules = True fallback_branch = 'master' # default branch repo_depth = 50 @@ -50,13 +53,20 @@ def _get_clone_url(self): def set_remote_url(self, url): return self.run('git', 'remote', 'set-url', 'origin', url) - def update(self): + def update(self, version=None): # pylint: disable=arguments-differ """Clone or update the repository.""" super().update() if self.repo_exists(): self.set_remote_url(self.repo_url) + # A fetch is always required to get external versions properly + if version and version.type == EXTERNAL: + return self.fetch(version.verbose_name) return self.fetch() self.make_clean_working_dir() + # A fetch is always required to get external versions properly + if version and version.type == EXTERNAL: + self.clone() + return self.fetch(version.verbose_name) return self.clone() def repo_exists(self): @@ -143,8 +153,14 @@ def use_shallow_clone(self): from readthedocs.projects.models import Feature return not self.project.has_feature(Feature.DONT_SHALLOW_CLONE) - def fetch(self): - cmd = ['git', 'fetch', '--tags', '--prune', '--prune-tags'] + def fetch(self, verbose_name=None): + cmd = ['git', 'fetch', 'origin', + '--tags', '--prune', '--prune-tags'] + + if verbose_name and 'github.com' in self.repo_url: + cmd.append( + GITHUB_GIT_PATTERN.format(id=verbose_name) + ) if self.use_shallow_clone(): cmd.extend(['--depth', str(self.repo_depth)]) diff --git a/readthedocs/vcs_support/backends/hg.py b/readthedocs/vcs_support/backends/hg.py index 0361bfa462c..9b44faeed2e 100644 --- a/readthedocs/vcs_support/backends/hg.py +++ b/readthedocs/vcs_support/backends/hg.py @@ -13,7 +13,7 @@ class Backend(BaseVCS): supports_branches = True fallback_branch = 'default' - def update(self): + def update(self, version=None): # pylint: disable=arguments-differ super().update() retcode = self.run('hg', 'status', record=False)[0] if retcode == 0: diff --git a/readthedocs/vcs_support/backends/svn.py b/readthedocs/vcs_support/backends/svn.py index b1a945aec2c..50bab45a512 100644 --- a/readthedocs/vcs_support/backends/svn.py +++ b/readthedocs/vcs_support/backends/svn.py @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): else: self.base_url = self.repo_url - def update(self): + def update(self, version=None): # pylint: disable=arguments-differ super().update() # For some reason `svn status` gives me retcode 0 in non-svn # directories that's why I use `svn info` here.