Skip to content

Commit 976aac4

Browse files
authored
Build: better support for empty default branch (#12462)
The basic idea is to have the value of latest in sync with the value from the default branch, which we already do. But, in case the default branch is left empty, we were falling back to master, which is just a guess of the default, so instead of guessing the value, we just don't update it if we don't know its value. When cloning the repo, we have access to the default branch, so that's when we update latest for those projects. Note this is only relevant for projects that aren't linked to a remote repository, since for those we know the default branch. So, when we try to build the latest version of a project that doesn't have a default branch, we: - Omit the ref from the fetch command. - Use the default branch from the repository for the checkout (we could skip this step, but since we have a new feature that keeps the files from a previous clone, we need to do a checkout to make sure we are building from the given branch/commit). - Update the identifier from latest to the default branch we detected from the cloned repository. There's still the problem about latest's identifier being out of sync when the default branch changes, but that's fixed after a new build, and this isn't a new problem (now users aren't blocked at least), and changing the default branch of a repo isn't something users do that often, and this only affects projects that aren't linked to a repository, so I think we are okay with that for now. This replaces #10927 Closes #12231
1 parent 7021e8f commit 976aac4

File tree

10 files changed

+449
-72
lines changed

10 files changed

+449
-72
lines changed

readthedocs/api/v2/views/integrations.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from readthedocs.builds.constants import BRANCH
2323
from readthedocs.builds.constants import LATEST
24+
from readthedocs.builds.constants import LATEST_VERBOSE_NAME
2425
from readthedocs.builds.constants import TAG
2526
from readthedocs.core.signals import webhook_bitbucket
2627
from readthedocs.core.signals import webhook_github
@@ -334,7 +335,7 @@ def get_closed_external_version_response(self, project):
334335

335336
def update_default_branch(self, default_branch):
336337
"""
337-
Update the `Version.identifer` for `latest` with the VCS's `default_branch`.
338+
Update the `Version.identifier` for `latest` with the VCS's `default_branch`.
338339
339340
The VCS's `default_branch` is the branch cloned when there is no specific branch specified
340341
(e.g. `git clone <URL>`).
@@ -356,7 +357,9 @@ def update_default_branch(self, default_branch):
356357
# Always check for the machine attribute, since latest can be user created.
357358
# RTD doesn't manage those.
358359
self.project.versions.filter(slug=LATEST, machine=True).update(
359-
identifier=default_branch
360+
identifier=default_branch,
361+
verbose_name=LATEST_VERBOSE_NAME,
362+
type=BRANCH,
360363
)
361364

362365

readthedocs/builds/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,10 @@ def is_public(self):
222222
def is_external(self):
223223
return self.type == EXTERNAL
224224

225+
@property
226+
def is_machine_latest(self):
227+
return self.machine and self.slug == LATEST
228+
225229
@property
226230
def explicit_name(self):
227231
"""

readthedocs/doc_builder/director.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,26 @@ def checkout(self):
232232
dismissable=True,
233233
)
234234

235-
identifier = self.data.build_commit or self.data.version.identifier
236-
log.info("Checking out.", identifier=identifier)
237-
self.vcs_repository.checkout(identifier)
235+
# Get the default branch of the repository if the project doesn't
236+
# have an explicit default branch set and we are building latest.
237+
# The identifier from latest will be updated with this value
238+
# if the build succeeds.
239+
is_latest_without_default_branch = (
240+
self.data.version.is_machine_latest and not self.data.project.default_branch
241+
)
242+
if is_latest_without_default_branch:
243+
self.data.default_branch = self.data.build_director.vcs_repository.get_default_branch()
244+
log.info(
245+
"Default branch for the repository detected.",
246+
default_branch=self.data.default_branch,
247+
)
248+
249+
# We can skip the checkout step since we just cloned the repository,
250+
# and the default branch is already checked out.
251+
if not is_latest_without_default_branch:
252+
identifier = self.data.build_commit or self.data.version.identifier
253+
log.info("Checking out.", identifier=identifier)
254+
self.vcs_repository.checkout(identifier)
238255

239256
# The director is responsible for understanding which config file to use for a build.
240257
# In order to reproduce a build 1:1, we may use readthedocs_yaml_path defined by the build

readthedocs/projects/models.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,15 +1201,21 @@ def get_original_latest_version(self):
12011201
When latest is machine created, it's basically an alias
12021202
for the default branch/tag (like main/master),
12031203
1204-
Returns None if the current default version doesn't point to a valid version.
1204+
Returns None if latest doesn't point to a valid version,
1205+
or if isn't managed by RTD (machine=False).
12051206
"""
1206-
default_version_name = self.get_default_branch()
1207+
# For latest, the identifier is the name of the branch/tag.
1208+
latest_version_identifier = (
1209+
self.versions.filter(slug=LATEST, machine=True)
1210+
.values_list("identifier", flat=True)
1211+
.first()
1212+
)
1213+
if not latest_version_identifier:
1214+
return None
12071215
return (
12081216
self.versions(manager=INTERNAL)
12091217
.exclude(slug=LATEST)
1210-
.filter(
1211-
verbose_name=default_version_name,
1212-
)
1218+
.filter(verbose_name=latest_version_identifier)
12131219
.first()
12141220
)
12151221

@@ -1227,8 +1233,22 @@ def update_latest_version(self):
12271233
return
12281234

12291235
# default_branch can be a tag or a branch name!
1230-
default_version_name = self.get_default_branch()
1231-
original_latest = self.get_original_latest_version()
1236+
default_version_name = self.get_default_branch(fallback_to_vcs=False)
1237+
# If the default_branch is not set, it means that the user
1238+
# wants to use the default branch of the respository, but
1239+
# we don't know what that is here, `latest` will be updated
1240+
# on the next build.
1241+
if not default_version_name:
1242+
return
1243+
1244+
# Search for a branch or tag with the name of the default branch,
1245+
# so we can sync latest with it.
1246+
original_latest = (
1247+
self.versions(manager=INTERNAL)
1248+
.exclude(slug=LATEST)
1249+
.filter(verbose_name=default_version_name)
1250+
.first()
1251+
)
12321252
latest.verbose_name = LATEST_VERBOSE_NAME
12331253
latest.type = original_latest.type if original_latest else BRANCH
12341254
# For latest, the identifier is the name of the branch/tag.
@@ -1328,14 +1348,22 @@ def get_default_version(self):
13281348
return self.default_version
13291349
return LATEST
13301350

1331-
def get_default_branch(self):
1332-
"""Get the version representing 'latest'."""
1351+
def get_default_branch(self, fallback_to_vcs=True):
1352+
"""
1353+
Get the name of the branch or tag that the user wants to use as 'latest'.
1354+
1355+
In case the user explicitly set a default branch, we use that,
1356+
otherwise we try to get it from the remote repository.
1357+
"""
13331358
if self.default_branch:
13341359
return self.default_branch
13351360

13361361
if self.remote_repository and self.remote_repository.default_branch:
13371362
return self.remote_repository.default_branch
13381363

1364+
if not fallback_to_vcs:
1365+
return None
1366+
13391367
vcs_class = self.vcs_class()
13401368
if vcs_class:
13411369
return vcs_class.fallback_branch

readthedocs/projects/tasks/builds.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from readthedocs.builds import tasks as build_tasks
2828
from readthedocs.builds.constants import ARTIFACT_TYPES
2929
from readthedocs.builds.constants import ARTIFACT_TYPES_WITHOUT_MULTIPLE_FILES_SUPPORT
30+
from readthedocs.builds.constants import BRANCH
3031
from readthedocs.builds.constants import BUILD_FINAL_STATES
3132
from readthedocs.builds.constants import BUILD_STATE_BUILDING
3233
from readthedocs.builds.constants import BUILD_STATE_CANCELLED
@@ -117,6 +118,10 @@ class TaskData:
117118
config: BuildConfigV2 = None
118119
project: APIProject = None
119120
version: APIVersion = None
121+
# Default branch for the repository.
122+
# Only set when building the latest version, and the project
123+
# doesn't have an explicit default branch.
124+
default_branch: str | None = None
120125

121126
# Dictionary returned from the API.
122127
build: dict = field(default_factory=dict)
@@ -665,18 +670,22 @@ def on_success(self, retval, task_id, args, kwargs):
665670
# NOTE: we are updating the db version instance *only* when
666671
# TODO: remove this condition and *always* update the DB Version instance
667672
if "html" in valid_artifacts:
673+
data = {
674+
"built": True,
675+
"documentation_type": self.data.version.documentation_type,
676+
"has_pdf": "pdf" in valid_artifacts,
677+
"has_epub": "epub" in valid_artifacts,
678+
"has_htmlzip": "htmlzip" in valid_artifacts,
679+
"build_data": self.data.version.build_data,
680+
"addons": self.data.version.addons,
681+
}
682+
# Update the latest version to point to the current VCS default branch
683+
# if the project doesn't have an explicit default branch set.
684+
if self.data.default_branch:
685+
data["identifier"] = self.data.default_branch
686+
data["type"] = BRANCH
668687
try:
669-
self.data.api_client.version(self.data.version.pk).patch(
670-
{
671-
"built": True,
672-
"documentation_type": self.data.version.documentation_type,
673-
"has_pdf": "pdf" in valid_artifacts,
674-
"has_epub": "epub" in valid_artifacts,
675-
"has_htmlzip": "htmlzip" in valid_artifacts,
676-
"build_data": self.data.version.build_data,
677-
"addons": self.data.version.addons,
678-
}
679-
)
688+
self.data.api_client.version(self.data.version.pk).patch(data)
680689
except HttpClientError:
681690
# NOTE: I think we should fail the build if we cannot update
682691
# the version at this point. Otherwise, we will have inconsistent data

0 commit comments

Comments
 (0)