Skip to content

Sort versions taking into consideration the vcs type #6049

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

Merged
merged 4 commits into from
Aug 8, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions readthedocs/projects/templatetags/projects_tags.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

"""Project template tags and filters."""

from django import template
Expand All @@ -13,9 +11,12 @@
@register.filter
def sort_version_aware(versions):
"""Takes a list of versions objects and sort them using version schemes."""
repo_type = None
if versions:
repo_type = versions[0].project.repo_type
return sorted(
versions,
key=lambda version: comparable_version(version.verbose_name),
key=lambda version: comparable_version(version.verbose_name, repo_type=repo_type),
reverse=True,
)

Expand Down
26 changes: 19 additions & 7 deletions readthedocs/projects/version_handling.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

"""Project version handling."""
import unicodedata

Expand All @@ -10,6 +8,7 @@
STABLE_VERBOSE_NAME,
TAG,
)
from readthedocs.vcs_support.backends import backend_cls


def parse_version_failsafe(version_string):
Expand Down Expand Up @@ -49,27 +48,40 @@ def parse_version_failsafe(version_string):
return None


def comparable_version(version_string):
def comparable_version(version_string, repo_type=None):
"""
Can be used as ``key`` argument to ``sorted``.

The ``LATEST`` version shall always beat other versions in comparison.
``STABLE`` should be listed second. If we cannot figure out the version
number then we sort it to the bottom of the list.

If `repo_type` is given, it adds the default "master" version
from the VCS (master, default, trunk).
This version is highest than LATEST and STABLE.

:param version_string: version as string object (e.g. '3.10.1' or 'latest')
:type version_string: str or unicode

:param repo_type: Repository type from which the versions are generated.

:returns: a comparable version object (e.g. 'latest' -> Version('99999.0'))

:rtype: packaging.version.Version
"""
highest_versions = []
if repo_type:
backend = backend_cls.get(repo_type)
if backend and backend.fallback_branch:
highest_versions.append(backend.fallback_branch)
highest_versions.extend([LATEST_VERBOSE_NAME, STABLE_VERBOSE_NAME])

comparable = parse_version_failsafe(version_string)
if not comparable:
if version_string == LATEST_VERBOSE_NAME:
comparable = Version('99999.0')
elif version_string == STABLE_VERBOSE_NAME:
comparable = Version('9999.0')
if version_string in highest_versions:
position = highest_versions.index(version_string)
version_number = str(999999 - position)
comparable = Version(version_number)
else:
comparable = Version('0.01')
return comparable
Expand Down
126 changes: 125 additions & 1 deletion readthedocs/rtd_tests/tests/projects/test_version_sorting.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from django.test import TestCase
from django_dynamic_fixture import get

from readthedocs.builds.constants import BRANCH
from readthedocs.builds.constants import BRANCH, LATEST
from readthedocs.builds.models import Version
from readthedocs.projects.constants import (
REPO_TYPE_BZR,
REPO_TYPE_GIT,
REPO_TYPE_HG,
REPO_TYPE_SVN,
)
from readthedocs.projects.models import Project
from readthedocs.projects.templatetags.projects_tags import sort_version_aware

Expand Down Expand Up @@ -65,3 +71,121 @@ def test_sort_alpha(self):
['latest', 'carrot', 'banana', 'apple'],
[v.slug for v in sort_version_aware(versions)],
)

def test_sort_git_master(self):
identifiers = ['master', '1.0', '2.0', '1.1', '1.9', '1.10']
self.project.repo_type = REPO_TYPE_GIT
self.project.save()
self.project.versions.get(slug=LATEST).delete()

for identifier in identifiers:
get(
Version,
project=self.project,
type=BRANCH,
identifier=identifier,
verbose_name=identifier,
slug=identifier,
)

versions = list(Version.objects.filter(project=self.project))
self.assertEqual(
['master', '2.0', '1.10', '1.9', '1.1', '1.0'],
[v.slug for v in sort_version_aware(versions)],
)

def test_sort_git_master_and_latest(self):
"""
The branch named master should havea a higher priority
than latest, ideally users should only have one of the two activated.
"""
identifiers = ['latest', 'master', '1.0', '2.0', '1.1', '1.9', '1.10']
self.project.repo_type = REPO_TYPE_GIT
self.project.save()
self.project.versions.get(slug=LATEST).delete()

for identifier in identifiers:
get(
Version,
project=self.project,
type=BRANCH,
identifier=identifier,
verbose_name=identifier,
slug=identifier,
)

versions = list(Version.objects.filter(project=self.project))
self.assertEqual(
['master', 'latest', '2.0', '1.10', '1.9', '1.1', '1.0'],
[v.slug for v in sort_version_aware(versions)],
)

def test_sort_hg_default(self):
identifiers = ['default', '1.0', '2.0', '1.1', '1.9', '1.10']
self.project.repo_type = REPO_TYPE_HG
self.project.save()
self.project.versions.get(slug=LATEST).delete()

for identifier in identifiers:
get(
Version,
project=self.project,
type=BRANCH,
identifier=identifier,
verbose_name=identifier,
slug=identifier,
)

versions = list(Version.objects.filter(project=self.project))
self.assertEqual(
['default', '2.0', '1.10', '1.9', '1.1', '1.0'],
[v.slug for v in sort_version_aware(versions)],
)

def test_sort_bzr_latest(self):
"""
BZR doesn't have a name for "master",
so here master gets sorted by its ascii value.
"""
identifiers = ['master', '1.0', '2.0', '1.1', '1.9', '1.10']
self.project.repo_type = REPO_TYPE_BZR
self.project.save()
self.project.versions.get(slug=LATEST).delete()

for identifier in identifiers:
get(
Version,
project=self.project,
type=BRANCH,
identifier=identifier,
verbose_name=identifier,
slug=identifier,
)

versions = list(Version.objects.filter(project=self.project))
self.assertEqual(
['2.0', '1.10', '1.9', '1.1', '1.0', 'master'],
[v.slug for v in sort_version_aware(versions)],
)

def test_sort_svn_trunk(self):
identifiers = ['/trunk/', '1.0', '2.0', '1.1', '1.9', '1.10']
Copy link
Member Author

Choose a reason for hiding this comment

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

not really sure about this one, but it's as fallback_branch, so 🤷‍♂️

and not like we have many of these 👀

self.project.repo_type = REPO_TYPE_SVN
self.project.save()
self.project.versions.get(slug=LATEST).delete()

for identifier in identifiers:
get(
Version,
project=self.project,
type=BRANCH,
identifier=identifier,
verbose_name=identifier,
slug=identifier,
)

versions = list(Version.objects.filter(project=self.project))
self.assertEqual(
['/trunk/', '2.0', '1.10', '1.9', '1.1', '1.0'],
[v.slug for v in sort_version_aware(versions)],
)
2 changes: 0 additions & 2 deletions readthedocs/vcs_support/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

"""Listing of all the VCS backends."""
from __future__ import absolute_import
from . import bzr, hg, git, svn
Expand Down