Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build: Simplify and optimize git backend: New clone+fetch pattern #10430

Merged
merged 60 commits into from Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
f8f35ac
New git clone/fetch/checkout behavior first steps
benjaoming Jun 13, 2023
9d616f0
Use `identifier` argument for `.update()`, add lots of comments for f…
benjaoming Jun 13, 2023
614963d
Add missing str() cast
benjaoming Jun 13, 2023
82178a3
Remove the `identifier` keyword since we are forced to use verbose_na…
benjaoming Jun 13, 2023
a45b42b
Revert "Remove the `identifier` keyword since we are forced to use ve…
benjaoming Jun 14, 2023
746d94b
We need the identifier for branches, but not for tags and PRs
benjaoming Jun 14, 2023
b3af824
Update some comments after noticing how `Build.identifier/.verbose_na…
benjaoming Jun 14, 2023
1f538b6
Merge branch 'main' of github.com:readthedocs/readthedocs.org into fe…
benjaoming Jun 14, 2023
04f3caf
lint
benjaoming Jun 14, 2023
2f8d346
Revert "Revert "Remove the `identifier` keyword since we are forced t…
benjaoming Jun 15, 2023
c634ca4
Use version_identifier as an instance property
benjaoming Jun 15, 2023
6725dbb
Update readthedocs/vcs_support/backends/git.py
benjaoming Jun 15, 2023
e186e7f
Updates to return values, comments and logging
benjaoming Jun 15, 2023
7fcf937
Merge branch 'feature/git-clone-fetch-checkout' of github.com:readthe…
benjaoming Jun 15, 2023
8d13787
Restore common
benjaoming Jun 15, 2023
a41e352
Stop fetching --tags and fetch the direct remote reference
benjaoming Jun 15, 2023
0cb043a
Appease linter: Use raise-from pattern
benjaoming Jun 15, 2023
e9322df
Hello darker my old friend
benjaoming Jun 15, 2023
b6ba466
Restore error handling for clone operation
benjaoming Jun 15, 2023
230e04f
Update a few comments, add a bit more raise-from pattern (actually tr…
benjaoming Jun 15, 2023
e3fcbf5
WIP: refine some comments, log calls and small things while testing l…
benjaoming Jun 15, 2023
abf41b9
New test class with Feature flag defined
benjaoming Jun 19, 2023
c2b3d44
Adds a tearDown method after discovering that tests were wrongly pass…
benjaoming Jun 19, 2023
84492ad
Merge branch 'main' of github.com:readthedocs/readthedocs.org into fe…
benjaoming Jun 19, 2023
9587844
Log a warning if we aren't specifying the version type when using Git…
benjaoming Jun 19, 2023
9dbc8c3
Update test cases for new git clone+fetch pattern
benjaoming Jun 19, 2023
d8cbc50
Git fetch change: Fetch branches to :refs/remotes/origin/<branch-name…
benjaoming Jun 19, 2023
15eab05
Overwrite tests to call clone_ng and fetch_ng, and rewrite some entirely
benjaoming Jun 19, 2023
c5abe46
Update update() docstring
benjaoming Jun 19, 2023
577bf51
Always use lsremote for GIT_CLONE_FETCH_CHECKOUT_PATTERN
benjaoming Jun 20, 2023
b507d02
Builds need to know about Version.machine=True for tags "stable" so t…
benjaoming Jun 20, 2023
52f071c
Conditionally log a warning on stable versions
benjaoming Jun 20, 2023
693eef9
linting
benjaoming Jun 20, 2023
bd98bf6
add "machine" to version API test case
benjaoming Jun 20, 2023
21dd16e
Use "refspec" terminology more consistently
benjaoming Jun 20, 2023
b1cea44
Add test case for un-named default branches
benjaoming Jun 20, 2023
2eba2f5
Don't have "--no-checkout" when we *need* to checkout the remote HEAD
benjaoming Jun 20, 2023
176f802
Add test case to verify that we can clone+fetch special tag "stable" …
benjaoming Jun 20, 2023
4191e11
Update readthedocs/vcs_support/backends/git.py
benjaoming Jun 20, 2023
acb7dd9
Remove unused logging
benjaoming Jun 20, 2023
3fe7486
Use `git fetch <commit hash>` for special "stable" versions
benjaoming Jun 20, 2023
33fa7da
Building an unnamed default branch now skips `git fetch`
benjaoming Jun 20, 2023
6431c00
Remove unnecessary logging, add a lot of comments to tests
benjaoming Jun 20, 2023
e42f902
Apply suggestions from @humitos code review
benjaoming Jun 22, 2023
c573736
Update readthedocs/projects/models.py
benjaoming Jun 22, 2023
4343fcf
Always clean up the presumed repo working dir after a test case
benjaoming Jun 26, 2023
f02c8ff
Move `machine` to `VersionAdminSerializer`
benjaoming Jun 26, 2023
ae064b8
Add precision to comment
benjaoming Jun 26, 2023
2f3e6ca
Use identifier and verbose_name that looks more like actual data woul…
benjaoming Jun 26, 2023
f2fc2b0
remove unused local reference in git fetch for "stable" tag
benjaoming Jun 26, 2023
6628d4f
Don't use --no-checkout, just checkout whatever is at the FETCH_HEAD
benjaoming Jun 26, 2023
5d5a6f5
Add more logging
benjaoming Jun 26, 2023
85ac0e4
Update comment
benjaoming Jun 26, 2023
f1d426b
Fix logic condition: Always include `vcs_repository.supports_lsremote`
benjaoming Jun 27, 2023
19eb282
Log a warning when we try to generate a pr/mr refspec for an unsuppor…
benjaoming Jun 27, 2023
5b10400
improve doc strings
benjaoming Jun 27, 2023
b007df9
Merge branch 'main' of github.com:readthedocs/readthedocs.org into fe…
benjaoming Jun 27, 2023
a2d7223
Remove TestGitBackendTwiceInARow
benjaoming Jun 27, 2023
41e1f09
Use real commit hash in test case
benjaoming Jun 27, 2023
98597f2
Merge branch 'main' of github.com:readthedocs/readthedocs.org into fe…
humitos Jul 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 15 additions & 14 deletions readthedocs/api/v2/serializers.py
Expand Up @@ -130,20 +130,20 @@ class VersionSerializer(serializers.ModelSerializer):
class Meta:
model = Version
fields = [
'id',
'project',
'slug',
'identifier',
'verbose_name',
'privacy_level',
'active',
'built',
'downloads',
'type',
'has_pdf',
'has_epub',
'has_htmlzip',
'documentation_type',
"id",
"project",
"slug",
"identifier",
"verbose_name",
"privacy_level",
"active",
"built",
"downloads",
"type",
"has_pdf",
"has_epub",
"has_htmlzip",
"documentation_type",
]

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -192,6 +192,7 @@ class Meta(VersionSerializer.Meta):
"addons",
"build_data",
"canonical_url",
"machine",
]


Expand Down
32 changes: 14 additions & 18 deletions readthedocs/api/v2/views/integrations.py
Expand Up @@ -15,11 +15,7 @@
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.views import APIView

from readthedocs.builds.constants import (
EXTERNAL_VERSION_STATE_CLOSED,
EXTERNAL_VERSION_STATE_OPEN,
LATEST,
)
from readthedocs.builds.constants import LATEST
from readthedocs.core.signals import webhook_bitbucket, webhook_github, webhook_gitlab
from readthedocs.core.views.hooks import (
build_branches,
Expand Down Expand Up @@ -97,8 +93,8 @@ def post(self, request, project_slug):
if not Project.objects.is_active(self.project):
resp = {'detail': 'This project is currently disabled'}
return Response(resp, status=status.HTTP_406_NOT_ACCEPTABLE)
except Project.DoesNotExist:
raise NotFound('Project not found')
except Project.DoesNotExist as exc:
raise NotFound("Project not found") from exc
if not self.is_payload_valid():
log.warning('Invalid payload for project and integration.')
return Response(
Expand Down Expand Up @@ -190,7 +186,7 @@ def get_response_push(self, project, branches):

:param project: Project instance
:type project: Project
:param branches: List of branch names to build
:param branches: List of branch/tag names to build
:type branches: list(str)
"""
to_build, not_building = build_branches(project, branches)
Expand Down Expand Up @@ -365,7 +361,7 @@ def get_external_version_data(self):
return data
except KeyError as e:
key = e.args[0]
raise ParseError(f"Invalid payload. {key} is required.")
raise ParseError(f"Invalid payload. {key} is required.") from e

def is_payload_valid(self):
"""
Expand Down Expand Up @@ -500,8 +496,8 @@ def handle_webhook(self):
try:
branch = self._normalize_ref(self.data["ref"])
return self.get_response_push(self.project, [branch])
except KeyError:
raise ParseError('Parameter "ref" is required')
except KeyError as exc:
raise ParseError('Parameter "ref" is required') from exc

return None

Expand Down Expand Up @@ -581,7 +577,7 @@ def get_external_version_data(self):
return data
except KeyError as e:
key = e.args[0]
raise ParseError(f"Invalid payload. {key} is required.")
raise ParseError(f"Invalid payload. {key} is required.") from e

def handle_webhook(self):
"""
Expand Down Expand Up @@ -624,8 +620,8 @@ def handle_webhook(self):
try:
branch = self._normalize_ref(data["ref"])
return self.get_response_push(self.project, [branch])
except KeyError:
raise ParseError('Parameter "ref" is required')
except KeyError as exc:
raise ParseError('Parameter "ref" is required') from exc

if self.project.external_builds_enabled and event == GITLAB_MERGE_REQUEST:
if (
Expand Down Expand Up @@ -726,8 +722,8 @@ def handle_webhook(self):
)
log.debug("Triggered sync_versions.")
return self.sync_versions_response(self.project)
except KeyError:
raise ParseError('Invalid request')
except KeyError as exc:
raise ParseError("Invalid request") from exc
return None

def is_payload_valid(self):
Expand Down Expand Up @@ -809,8 +805,8 @@ def handle_webhook(self):
self.update_default_branch(default_branch)

return self.get_response_push(self.project, branches)
except TypeError:
raise ParseError('Invalid request')
except TypeError as exc:
raise ParseError("Invalid request") from exc

def is_payload_valid(self):
"""
Expand Down
12 changes: 10 additions & 2 deletions readthedocs/builds/tasks.py
Expand Up @@ -292,8 +292,16 @@ def sync_versions_task(project_pk, tags_data, branches_data, **kwargs):
Creates new Version objects for tags/branches that aren't tracked in the database,
and deletes Version objects for tags/branches that don't exists in the repository.

:param tags_data: List of dictionaries with ``verbose_name`` and ``identifier``.
:param branches_data: Same as ``tags_data`` but for branches.
:param tags_data: List of dictionaries with ``verbose_name`` and ``identifier``
Example: [
{"verbose_name": "v1.0.0",
"identifier": "67a9035990f44cb33091026d7453d51606350519"},
].
:param branches_data: Same as ``tags_data`` but for branches (branch name, branch identifier).
Example: [
{"verbose_name": "latest",
"identifier": "main"},
].
:returns: `True` or `False` if the task succeeded.
"""
project = Project.objects.get(pk=project_pk)
Expand Down
4 changes: 3 additions & 1 deletion readthedocs/doc_builder/director.py
Expand Up @@ -94,6 +94,8 @@ def setup_vcs(self):
environment=self.vcs_environment,
verbose_name=self.data.version.verbose_name,
version_type=self.data.version.type,
version_identifier=self.data.version.identifier,
version_machine=self.data.version.machine,
humitos marked this conversation as resolved.
Show resolved Hide resolved
)

# We can't do too much on ``pre_checkout`` because we haven't
Expand Down Expand Up @@ -222,7 +224,7 @@ def build(self):
def checkout(self):
"""Checkout Git repo and load build config file."""

log.info("Cloning repository.")
log.info("Cloning and fetching.")
self.vcs_repository.update()

identifier = self.data.build_commit or self.data.version.identifier
Expand Down
24 changes: 21 additions & 3 deletions readthedocs/projects/models.py
Expand Up @@ -995,7 +995,13 @@ def has_htmlzip(self, version_slug=LATEST, version_type=None):
)

def vcs_repo(
self, environment, version=LATEST, verbose_name=None, version_type=None
self,
environment,
version=LATEST,
verbose_name=None,
version_type=None,
version_identifier=None,
version_machine=None,
):
"""
Return a Backend object for this project able to handle VCS commands.
Expand All @@ -1014,8 +1020,13 @@ def vcs_repo(
repo = None
else:
repo = backend(
self, version, environment=environment,
verbose_name=verbose_name, version_type=version_type
self,
version,
environment=environment,
verbose_name=verbose_name,
version_type=version_type,
version_identifier=version_identifier,
version_machine=version_machine,
)
return repo

Expand Down Expand Up @@ -1937,6 +1948,7 @@ def add_features(sender, **kwargs):
INDEX_FROM_HTML_FILES = 'index_from_html_files'

# Build related features
GIT_CLONE_FETCH_CHECKOUT_PATTERN = "git_clone_fetch_checkout_pattern"
HOSTING_INTEGRATIONS = "hosting_integrations"
NO_CONFIG_FILE_DEPRECATED = "no_config_file"

Expand Down Expand Up @@ -2060,6 +2072,12 @@ def add_features(sender, **kwargs):
"sources"
),
),
(
GIT_CLONE_FETCH_CHECKOUT_PATTERN,
_(
"Build: Use simplified and optimized git clone + git fetch + git checkout patterns"
),
),
(
HOSTING_INTEGRATIONS,
_(
Expand Down
14 changes: 11 additions & 3 deletions readthedocs/projects/tasks/mixins.py
Expand Up @@ -45,9 +45,13 @@ def sync_versions(self, vcs_repository):
# Do not use ``ls-remote`` if the VCS does not support it or if we
# have already cloned the repository locally. The latter happens
# when triggering a normal build.
use_lsremote = (
vcs_repository.supports_lsremote
and not vcs_repository.repo_exists()
# Always use lsremote when we have GIT_CLONE_FETCH_CHECKOUT_PATTERN on
# and the repo supports lsremote.
# The new pattern does not fetch branch and tag data, so we always
# have to do ls-remote.
use_lsremote = vcs_repository.supports_lsremote and (
self.data.project.has_feature(Feature.GIT_CLONE_FETCH_CHECKOUT_PATTERN)
or not vcs_repository.repo_exists()
)
sync_tags = vcs_repository.supports_tags and not self.data.project.has_feature(
Feature.SKIP_SYNC_TAGS
Expand All @@ -63,6 +67,10 @@ def sync_versions(self, vcs_repository):
include_tags=sync_tags,
include_branches=sync_branches,
)

# GIT_CLONE_FETCH_CHECKOUT_PATTERN: When the feature flag becomes default, we
# can remove this segment since lsremote is always on.
# We can even factor out the dependency to gitpython.
else:
if sync_tags:
tags = vcs_repository.tags
Expand Down
17 changes: 9 additions & 8 deletions readthedocs/rtd_tests/tests/test_api.py
Expand Up @@ -3127,14 +3127,15 @@ def test_get_version_by_id(self):
"users": [1],
"urlconf": None,
},
'privacy_level': 'public',
'downloads': {},
'identifier': '2404a34eba4ee9c48cc8bc4055b99a48354f4950',
'slug': '0.8',
'has_epub': False,
'has_htmlzip': False,
'has_pdf': False,
'documentation_type': 'sphinx',
"privacy_level": "public",
"downloads": {},
"identifier": "2404a34eba4ee9c48cc8bc4055b99a48354f4950",
"slug": "0.8",
"has_epub": False,
"has_htmlzip": False,
"has_pdf": False,
"documentation_type": "sphinx",
"machine": False,
}

self.assertDictEqual(
Expand Down