diff --git a/readthedocs/api/v2/utils.py b/readthedocs/api/v2/utils.py index 0c51b2f9c42..98d83f9de60 100644 --- a/readthedocs/api/v2/utils.py +++ b/readthedocs/api/v2/utils.py @@ -115,10 +115,6 @@ def sync_versions_to_db(project, versions, type): if latest_version: # Put back the RTD's latest version latest_version.machine = True - latest_version.identifier = project.get_default_branch() - latest_version.verbose_name = LATEST_VERBOSE_NAME - # The machine created latest version always points to a branch. - latest_version.type = BRANCH latest_version.save() if added: log.info( diff --git a/readthedocs/builds/tasks.py b/readthedocs/builds/tasks.py index 2baef786a2b..008086738e7 100644 --- a/readthedocs/builds/tasks.py +++ b/readthedocs/builds/tasks.py @@ -359,6 +359,8 @@ def sync_versions_task(project_pk, tags_data, branches_data, **kwargs): versions=added_versions, ) + # Sync latest and stable to match the correct type and identifier. + project.update_latest_version() # TODO: move this to an automation rule promoted_version = project.update_stable_version() new_stable = project.get_stable_version() diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index ac5c5d359ee..50945e58fb4 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -23,7 +23,14 @@ from django_extensions.db.models import TimeStampedModel from taggit.managers import TaggableManager -from readthedocs.builds.constants import EXTERNAL, INTERNAL, LATEST, STABLE +from readthedocs.builds.constants import ( + BRANCH, + EXTERNAL, + INTERNAL, + LATEST, + LATEST_VERBOSE_NAME, + STABLE, +) from readthedocs.core.history import ExtraHistoricalRecords from readthedocs.core.resolver import resolve, resolve_domain from readthedocs.core.utils import extract_valid_attributes_for_model, slugify @@ -52,10 +59,7 @@ from readthedocs.storage import build_media_storage from readthedocs.vcs_support.backends import backend_cls -from .constants import ( - DOWNLOADABLE_MEDIA_TYPES, - MEDIA_TYPES, -) +from .constants import DOWNLOADABLE_MEDIA_TYPES, MEDIA_TYPES log = structlog.get_logger(__name__) @@ -1106,6 +1110,51 @@ def get_original_stable_version(self): ) return original_stable + def get_latest_version(self): + return self.versions.filter(slug=LATEST).first() + + def get_original_latest_version(self): + """ + Get the original version that latest points to. + + 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. + """ + default_version_name = self.get_default_branch() + return ( + self.versions(manager=INTERNAL) + .exclude(slug=LATEST) + .filter( + verbose_name=default_version_name, + ) + .first() + ) + + def update_latest_version(self): + """ + If the current latest version is machine created, update it. + + A machine created LATEST version is an alias for the default branch/tag, + so we need to update it to match the type and identifier of the default branch/tag. + """ + latest = self.get_latest_version() + if not latest: + latest = self.versions.create_latest() + if not latest.machine: + 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() + 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. + latest.identifier = default_version_name + latest.save() + return latest + def update_stable_version(self): """ Returns the version that was promoted to be the new stable version. diff --git a/readthedocs/rtd_tests/tests/test_sync_versions.py b/readthedocs/rtd_tests/tests/test_sync_versions.py index 1a8273440e6..a7934726fac 100644 --- a/readthedocs/rtd_tests/tests/test_sync_versions.py +++ b/readthedocs/rtd_tests/tests/test_sync_versions.py @@ -307,6 +307,53 @@ def test_update_latest_version_type(self): self.assertEqual(latest_version.verbose_name, "latest") self.assertEqual(latest_version.machine, True) + # Latest points to the default branch/tag. + self.pip.default_branch = "2.6" + self.pip.save() + + sync_versions_task( + self.pip.pk, + branches_data=[ + { + "identifier": "master", + "verbose_name": "master", + }, + { + "identifier": "2.6", + "verbose_name": "2.6", + }, + ], + tags_data=[], + ) + + latest_version = self.pip.versions.get(slug=LATEST) + self.assertEqual(latest_version.type, BRANCH) + self.assertEqual(latest_version.identifier, "2.6") + self.assertEqual(latest_version.verbose_name, "latest") + self.assertEqual(latest_version.machine, True) + + sync_versions_task( + self.pip.pk, + branches_data=[ + { + "identifier": "master", + "verbose_name": "master", + }, + ], + tags_data=[ + { + "identifier": "abc123", + "verbose_name": "2.6", + } + ], + ) + + latest_version = self.pip.versions.get(slug=LATEST) + self.assertEqual(latest_version.type, TAG) + self.assertEqual(latest_version.identifier, "2.6") + self.assertEqual(latest_version.verbose_name, "latest") + self.assertEqual(latest_version.machine, True) + def test_machine_attr_when_user_define_stable_tag_and_delete_it(self): """ The user creates a tag named ``stable`` on an existing repo, diff --git a/readthedocs/vcs_support/backends/git.py b/readthedocs/vcs_support/backends/git.py index 9f98b00eae3..2cd16baa50b 100644 --- a/readthedocs/vcs_support/backends/git.py +++ b/readthedocs/vcs_support/backends/git.py @@ -5,7 +5,13 @@ import structlog -from readthedocs.builds.constants import BRANCH, EXTERNAL, TAG +from readthedocs.builds.constants import ( + BRANCH, + EXTERNAL, + LATEST_VERBOSE_NAME, + STABLE_VERBOSE_NAME, + TAG, +) from readthedocs.config import ALL from readthedocs.projects.constants import ( GITHUB_BRAND, @@ -73,13 +79,27 @@ def get_remote_fetch_refspec(self): This method sits on top of a lot of legacy design. It decides how to treat the incoming ``Version.identifier`` from knowledge of how the caller (the build process) uses build data. + Thi is: + + For branches: + + - Version.identifier is the branch name. + - Version.verbose_name is also the branch name, + except for latest and stable (machine created), + where this is the alias name. + + For tags: - Version.identifier = a branch name (branches) - Version.identifier = commit (tags) - Version.identifier = commit (external versions) - Version.verbose_name = branch alias, e.g. latest (branches) - Version.verbose_name = tag name (tags) - Version.verbose_name = PR number (external versions) + - Version.identifier is the commit hash, + except for latest, where this is the tag name. + - Version.verbose_name is the tag name, + except for latest and stable (machine created), + where this is the alias name. + + For external versions: + + - Version.identifier is the commit hash. + - Version.verbose_name is the PR number. :return: A refspec valid for fetch operation """ @@ -115,12 +135,19 @@ def get_remote_fetch_refspec(self): # denoting that it's not a branch/tag that really exists. # Because we don't know if it originates from the default branch or some # other tagged release, we will fetch the exact commit it points to. - if self.version_machine and self.verbose_name == "stable": + if self.version_machine and self.verbose_name == STABLE_VERBOSE_NAME: if self.version_identifier: return f"{self.version_identifier}" log.error("'stable' version without a commit hash.") return None - return f"refs/tags/{self.verbose_name}:refs/tags/{self.verbose_name}" + + tag_name = self.verbose_name + # For a machine created "latest" tag, the name of the tag is set + # in the `Version.identifier` field, note that it isn't a commit + # hash, but the name of the tag. + if self.version_machine and self.verbose_name == LATEST_VERBOSE_NAME: + tag_name = self.version_identifier + return f"refs/tags/{tag_name}:refs/tags/{tag_name}" if self.version_type == EXTERNAL: # TODO: We should be able to resolve this without looking up in oauth registry