Skip to content
Open
7 changes: 5 additions & 2 deletions readthedocs/api/v2/views/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from readthedocs.builds.constants import BRANCH
from readthedocs.builds.constants import LATEST
from readthedocs.builds.constants import LATEST_VERBOSE_NAME
from readthedocs.builds.constants import TAG
from readthedocs.core.signals import webhook_bitbucket
from readthedocs.core.signals import webhook_github
Expand Down Expand Up @@ -313,7 +314,7 @@ def get_closed_external_version_response(self, project):

def update_default_branch(self, default_branch):
"""
Update the `Version.identifer` for `latest` with the VCS's `default_branch`.
Update the `Version.identifier` for `latest` with the VCS's `default_branch`.

The VCS's `default_branch` is the branch cloned when there is no specific branch specified
(e.g. `git clone <URL>`).
Expand All @@ -335,7 +336,9 @@ def update_default_branch(self, default_branch):
# Always check for the machine attribute, since latest can be user created.
# RTD doesn't manage those.
self.project.versions.filter(slug=LATEST, machine=True).update(
identifier=default_branch
identifier=default_branch,
verbose_name=LATEST_VERBOSE_NAME,
type=BRANCH,
)


Expand Down
4 changes: 4 additions & 0 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ def is_public(self):
def is_external(self):
return self.type == EXTERNAL

@property
def is_machine_latest(self):
return self.machine and self.slug == LATEST

@property
def explicit_name(self):
"""
Expand Down
18 changes: 17 additions & 1 deletion readthedocs/doc_builder/director.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,23 @@ def checkout(self):
dismissable=True,
)

identifier = self.data.build_commit or self.data.version.identifier
# Get the default branch of the repository if the project doesn't
# have an explicit default branch set and we are building latest.
# The identifier from latest will be updated with this value
# if the build succeeds.
if self.data.version.is_machine_latest and not self.data.project.default_branch:
self.data.default_branch = self.data.build_director.vcs_repository.get_default_branch()
log.info(
"Default branch for the repository detected.",
default_branch=self.data.default_branch,
)

# We can't skip the checkout step.
# If Feature.DONT_CLEAN_BUILD is enabled, we need to explicitly call checkout
# with the default branch, otherwise we could end up in the wrong branch.
identifier = (
self.data.build_commit or self.data.default_branch or self.data.version.identifier
)
log.info("Checking out.", identifier=identifier)
self.vcs_repository.checkout(identifier)

Expand Down
46 changes: 37 additions & 9 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1177,15 +1177,21 @@ def get_original_latest_version(self):
When latest is machine created, it's basically an alias
for the default branch/tag (like main/master),

Returns None if the current default version doesn't point to a valid version.
Returns None if latest doesn't point to a valid version,
or if isn't managed by RTD (machine=False).
"""
default_version_name = self.get_default_branch()
# For latest, the identifier is the name of the branch/tag.
latest_version_identifier = (
self.versions.filter(slug=LATEST, machine=True)
.values_list("identifier", flat=True)
.first()
)
Comment on lines +1182 to +1186
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is introducing an extra query for each time latest is returned in a response, since we now rely on latest having the correct value of the default branch (similar to get_original_stable_version).

if not latest_version_identifier:
return None
return (
self.versions(manager=INTERNAL)
.exclude(slug=LATEST)
.filter(
verbose_name=default_version_name,
)
.filter(verbose_name=latest_version_identifier)
.first()
)

Expand All @@ -1203,8 +1209,22 @@ def update_latest_version(self):
return

# default_branch can be a tag or a branch name!
default_version_name = self.get_default_branch()
original_latest = self.get_original_latest_version()
default_version_name = self.get_default_branch(fallback_to_vcs=False)
# If the default_branch is not set, it means that the user
# wants to use the default branch of the respository, but
# we don't know what that is here, `latest` will be updated
# on the next build.
if not default_version_name:
return

# Search for a branch or tag with the name of the default branch,
# so we can sync latest with it.
original_latest = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems we updated get_original_latest_version, but are also not using it here. Should we be using it here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, they are slightly different. One gets the version matching the value of the current LATEST, while the other tries to get the version matching the value of default_branch (to update LATEST).

self.versions(manager=INTERNAL)
.exclude(slug=LATEST)
.filter(verbose_name=default_version_name)
.first()
)
latest.verbose_name = LATEST_VERBOSE_NAME
latest.type = original_latest.type if original_latest else BRANCH
# For latest, the identifier is the name of the branch/tag.
Expand Down Expand Up @@ -1304,14 +1324,22 @@ def get_default_version(self):
return self.default_version
return LATEST

def get_default_branch(self):
"""Get the version representing 'latest'."""
def get_default_branch(self, fallback_to_vcs=True):
"""
Get the name of the branch or tag that the user wants to use as 'latest'.

In case the user explicitly set a default branch, we use that,
otherwise we try to get it from the remote repository.
"""
if self.default_branch:
return self.default_branch

if self.remote_repository and self.remote_repository.default_branch:
return self.remote_repository.default_branch

if not fallback_to_vcs:
return None

vcs_class = self.vcs_class()
if vcs_class:
return vcs_class.fallback_branch
Expand Down
31 changes: 20 additions & 11 deletions readthedocs/projects/tasks/builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from readthedocs.builds import tasks as build_tasks
from readthedocs.builds.constants import ARTIFACT_TYPES
from readthedocs.builds.constants import ARTIFACT_TYPES_WITHOUT_MULTIPLE_FILES_SUPPORT
from readthedocs.builds.constants import BRANCH
from readthedocs.builds.constants import BUILD_FINAL_STATES
from readthedocs.builds.constants import BUILD_STATE_BUILDING
from readthedocs.builds.constants import BUILD_STATE_CANCELLED
Expand Down Expand Up @@ -115,6 +116,10 @@ class TaskData:
config: BuildConfigV2 = None
project: APIProject = None
version: APIVersion = None
# Default branch for the repository.
# Only set when building the latest version, and the project
# doesn't have an explicit default branch.
default_branch: str | None = None

# Dictionary returned from the API.
build: dict = field(default_factory=dict)
Expand Down Expand Up @@ -644,18 +649,22 @@ def on_success(self, retval, task_id, args, kwargs):
# NOTE: we are updating the db version instance *only* when
# TODO: remove this condition and *always* update the DB Version instance
if "html" in valid_artifacts:
data = {
"built": True,
"documentation_type": self.data.version.documentation_type,
"has_pdf": "pdf" in valid_artifacts,
"has_epub": "epub" in valid_artifacts,
"has_htmlzip": "htmlzip" in valid_artifacts,
"build_data": self.data.version.build_data,
"addons": self.data.version.addons,
}
# Update the latest version to point to the current VCS default branch
# if the project doesn't have an explicit default branch set.
if self.data.default_branch:
data["identifier"] = self.data.default_branch
data["type"] = BRANCH
try:
self.data.api_client.version(self.data.version.pk).patch(
{
"built": True,
"documentation_type": self.data.version.documentation_type,
"has_pdf": "pdf" in valid_artifacts,
"has_epub": "epub" in valid_artifacts,
"has_htmlzip": "htmlzip" in valid_artifacts,
"build_data": self.data.version.build_data,
"addons": self.data.version.addons,
}
)
self.data.api_client.version(self.data.version.pk).patch(data)
except HttpClientError:
# NOTE: I think we should fail the build if we cannot update
# the version at this point. Otherwise, we will have inconsistent data
Expand Down
Loading