You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/08/24 12:37:10 UTC
(tooling-trusted-releases) branch arm updated (44d3bba8 -> d5f5dfa5)
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a change to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
discard 44d3bba8 Fix version sort in catalog
new d5f5dfa5 Fix version sort in catalog
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (44d3bba8)
\
N -- N -- N refs/heads/arm (d5f5dfa5)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
tests/unit/test_catalog.py | 32 +++++++++++++++++++-------------
1 file changed, 19 insertions(+), 13 deletions(-)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org
(tooling-trusted-releases) 01/01: Fix version sort in catalog
Posted by ar...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit d5f5dfa5109f55309b4ae1effef90459818cb838
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Mon Aug 24 12:12:29 2026 +0100
Fix version sort in catalog
---
atr/shared/catalog.py | 68 ++++++++++++++++++++++++++++++++++------------
tests/unit/test_catalog.py | 32 +++++++++++++---------
2 files changed, 69 insertions(+), 31 deletions(-)
diff --git a/atr/shared/catalog.py b/atr/shared/catalog.py
index b699f227..bc476dab 100644
--- a/atr/shared/catalog.py
+++ b/atr/shared/catalog.py
@@ -18,9 +18,11 @@
import collections
import dataclasses
import datetime
-from collections.abc import Sequence
+from collections.abc import Iterable, Sequence
from typing import Final, Literal
+import packaging.version as version
+
import atr.cycles as cycles
import atr.models as models
import atr.models.safe as safe
@@ -60,7 +62,7 @@ def assemble(
# `atr_host` is only supplied by the API, which needs absolute CLE links. The
# page leaves it None and renders its own relative links via as_url instead.
cycles_by_key = {cycle.cycle_key: cycle for cycle in project_cycles}
- versions = _versions(artifacts, cycles_by_key, atr_host)
+ versions = _versions(artifacts, cycles_by_key, version_method, atr_host)
grouped = _grouped_layout(version_method)
if grouped:
# Cycle labels are unique within a project, so they group and look up safely.
@@ -143,27 +145,25 @@ def _artifact(row: sql.Artifact, downloadable: bool, archived: bool) -> models.a
)
-def _cycle_sort_key(cycle: sql.ProjectCycle) -> datetime.datetime:
- # Most recently active cycle first.
- return cycle.latest or datetime.datetime.min.replace(tzinfo=datetime.UTC)
-
-
def _cycles(
versions: Sequence[models.api.CatalogVersion],
cycles_by_label: dict[str, sql.ProjectCycle],
now: datetime.datetime,
) -> list[models.api.CatalogCycle]:
by_label: dict[str, list[models.api.CatalogVersion]] = collections.defaultdict(list)
- for version in versions:
- if version.cycle is None:
+ for entry in versions:
+ if entry.cycle is None:
continue
- by_label[version.cycle].append(version)
+ by_label[entry.cycle].append(entry)
catalog = [
models.api.CatalogCycle(cycle=label, lifecycle=_lifecycle_badge(cycles_by_label[label], now), versions=grouped)
for label, grouped in by_label.items()
]
- catalog.sort(key=lambda c: _cycle_sort_key(cycles_by_label[c.cycle]), reverse=True)
+ # A cycle label is a version prefix (2.26, 3.0), so order the cycles by that, newest first - only
+ # grouped (scheme) projects reach here, and a scheme means the version, not the date, is the order
+ ranks = _version_ranks(by_label.keys())
+ catalog.sort(key=lambda c: ranks[c.cycle], reverse=True)
return catalog
@@ -191,15 +191,40 @@ def _status(release: sql.Release | None) -> Literal["released", "archived"]:
return "archived"
-def _version_sort_key(version: models.api.CatalogVersion) -> tuple[datetime.datetime, int]:
+def _loose_version_key(text: str) -> tuple[tuple[int, int | str], ...]:
+ # A version split into numeric-then-string parts, for ordering strings PEP 440 rejects. Numbers
+ # sort ahead of strings so 1.0 leads 1.0-rc1, matching the fallback releases_by_project uses
+ parts: list[tuple[int, int | str]] = []
+ for part in text.replace("+", ".").replace("-", ".").split("."):
+ try:
+ parts.append((0, int(part)))
+ except ValueError:
+ parts.append((1, part))
+ return tuple(parts)
+
+
+def _version_ranks(labels: Iterable[str]) -> dict[str, int]:
+ # Rank distinct version strings (or cycle labels, which are version prefixes) low to high, so a
+ # caller sorts newest first by reversing. PEP 440 first; one string it rejects drops the whole set
+ # to the loose split, so an odd tag can't throw the order or raise
+ distinct = list(set(labels))
+ try:
+ ordered = sorted(distinct, key=version.Version)
+ except version.InvalidVersion:
+ ordered = sorted(distinct, key=_loose_version_key)
+ return {label: index for index, label in enumerate(ordered)}
+
+
+def _version_sort_key(entry: models.api.CatalogVersion) -> tuple[datetime.datetime, int]:
# Newest first: released date leads, svn revision breaks ties for older rows.
- released = version.released or datetime.datetime.min.replace(tzinfo=datetime.UTC)
- return (released, version.svn_revision or 0)
+ released = entry.released or datetime.datetime.min.replace(tzinfo=datetime.UTC)
+ return (released, entry.svn_revision or 0)
def _versions(
artifacts: Sequence[sql.Artifact],
cycles_by_key: dict[str, sql.ProjectCycle],
+ version_method: sql.VersionMethod,
atr_host: str | None = None,
) -> list[models.api.CatalogVersion]:
by_version: dict[safe.VersionKey, list[sql.Artifact]] = collections.defaultdict(list)
@@ -207,7 +232,7 @@ def _versions(
by_version[artifact.safe_version_key].append(artifact)
versions: list[models.api.CatalogVersion] = []
- for version, rows in by_version.items():
+ for version_key, rows in by_version.items():
release = next((row.release for row in rows if row.release is not None), None)
status = _status(release)
# Released files come off the live download route, archived ones off archive.apache.org;
@@ -220,13 +245,13 @@ def _versions(
cle_eligible = (release is not None) and (release.phase == sql.ReleasePhase.RELEASE)
versions.append(
models.api.CatalogVersion(
- version=version,
+ version=version_key,
status=status,
released=(release.released or release.created) if (release is not None) else None,
svn_revision=max(svn_revisions) if svn_revisions else None,
managed=any(row.managed for row in rows),
cycle=cycle.cycle if (cycle is not None) else None,
- cle_url=cle_release_url(atr_host, release.project_key, str(version))
+ cle_url=cle_release_url(atr_host, release.project_key, str(version_key))
if (atr_host is not None) and cle_eligible and (release is not None)
else None,
vote_thread_url=release.vote_thread_url if (release is not None) else None,
@@ -234,5 +259,12 @@ def _versions(
)
)
- versions.sort(key=_version_sort_key, reverse=True)
+ # A project with a version scheme (semver/calver) orders by the version itself, so a later patch
+ # of an older line can't jump a newer line the way a date sort lets it. A simple project has no
+ # scheme, so it keeps the by-date order its lone default cycle wants
+ if _grouped_layout(version_method):
+ ranks = _version_ranks(str(entry.version) for entry in versions)
+ versions.sort(key=lambda entry: ranks[str(entry.version)], reverse=True)
+ else:
+ versions.sort(key=_version_sort_key, reverse=True)
return versions
diff --git a/tests/unit/test_catalog.py b/tests/unit/test_catalog.py
index 96f3f733..98b02a6b 100644
--- a/tests/unit/test_catalog.py
+++ b/tests/unit/test_catalog.py
@@ -45,7 +45,7 @@ def test_archived_status_holds_without_an_archive_date() -> None:
undated = _release(project, "3.11.0", cycle_key="cassandra-default", released=_NOW, is_archived=True)
artifacts = [_artifact(project, "3.11.0", "a-3.11.0.tar.gz", release=undated)]
- versions = {str(v.version): v for v in catalog._versions(artifacts, {})}
+ versions = {str(v.version): v for v in catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE)}
assert versions["3.11.0"].status == "archived"
assert versions["3.11.0"].artifacts[0].downloadable is True
@@ -55,7 +55,9 @@ def test_cle_url_is_omitted_for_versions_without_a_backing_release() -> None:
project = _project()
artifacts = [_artifact(project, "3.0.0", "a-3.0.0.tar.gz", svn_revision=100)]
- versions = {str(v.version): v for v in catalog._versions(artifacts, {}, "atr.example.org")}
+ versions = {
+ str(v.version): v for v in catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE, "atr.example.org")
+ }
assert versions["3.0.0"].cle_url is None
@@ -66,7 +68,7 @@ def test_cle_url_is_omitted_when_no_host_is_supplied() -> None:
released = _release(project, "5.0.2", cycle_key="cassandra-default", released=_NOW)
artifacts = [_artifact(project, "5.0.2", "a-5.0.2.tar.gz", release=released)]
- versions = {str(v.version): v for v in catalog._versions(artifacts, {})}
+ versions = {str(v.version): v for v in catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE)}
assert versions["5.0.2"].cle_url is None
@@ -76,14 +78,18 @@ def test_cle_url_links_versions_backed_by_a_release_when_a_host_is_supplied() ->
released = _release(project, "5.0.2", cycle_key="cassandra-default", released=_NOW)
artifacts = [_artifact(project, "5.0.2", "a-5.0.2.tar.gz", release=released)]
- versions = {str(v.version): v for v in catalog._versions(artifacts, {}, "atr.example.org")}
+ versions = {
+ str(v.version): v for v in catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE, "atr.example.org")
+ }
assert versions["5.0.2"].cle_url == "https://atr.example.org/api/cle/release/cassandra/5.0.2"
-def test_cycles_are_ordered_by_latest_activity() -> None:
- five = _cycle("5.0", latest=datetime.datetime(2025, 10, 1, tzinfo=datetime.UTC))
- four = _cycle("4.1", latest=datetime.datetime(2025, 9, 1, tzinfo=datetime.UTC))
+def test_cycles_are_ordered_by_version_not_activity() -> None:
+ # Cycles order by the version their label carries, newest first, not by activity date: a late
+ # patch on the older 4.1 line post-dates 5.0 here, but 5.0 is the newer line, so it still leads
+ five = _cycle("5.0", latest=datetime.datetime(2025, 9, 1, tzinfo=datetime.UTC))
+ four = _cycle("4.1", latest=datetime.datetime(2025, 10, 1, tzinfo=datetime.UTC))
versions = [
_CatalogVersionStub("4.1.7", cycle=four).value,
_CatalogVersionStub("5.0.2", cycle=five).value,
@@ -109,7 +115,7 @@ def test_paired_sbom_shown_on_catalog_artifact() -> None:
_artifact(project, "5.0.2", "a-5.0.2.tar.gz", release=released, sbom_path="a-5.0.2.tar.gz.cdx.json"),
]
- versions = {str(v.version): v for v in catalog._versions(artifacts, {})}
+ versions = {str(v.version): v for v in catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE)}
assert versions["5.0.2"].artifacts[0].sbom_path == "a-5.0.2.tar.gz.cdx.json"
@@ -123,7 +129,7 @@ def test_released_and_archived_versions_are_downloadable() -> None:
_artifact(project, "4.1.7", "a-4.1.7.tar.gz", release=archived, svn_revision=28114),
]
- versions = {str(v.version): v for v in catalog._versions(artifacts, {})}
+ versions = {str(v.version): v for v in catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE)}
# Released off the live route, archived off archive.apache.org - both downloadable
assert versions["5.0.2"].artifacts[0].downloadable is True
@@ -166,7 +172,7 @@ def test_status_reflects_release_and_archive_state() -> None:
_artifact(project, "4.1.7", "a-4.1.7.tar.gz", release=archived),
]
- versions = {str(v.version): v for v in catalog._versions(artifacts, {})}
+ versions = {str(v.version): v for v in catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE)}
assert versions["5.0.2"].status == "released"
assert versions["4.1.7"].status == "archived"
@@ -176,7 +182,7 @@ def test_svn_revision_alone_does_not_make_a_version_managed() -> None:
project = _project()
artifacts = [_artifact(project, "2.0.0", "a-2.0.0.tar.gz", svn_revision=999, managed=False)]
- versions = {str(v.version): v for v in catalog._versions(artifacts, {})}
+ versions = {str(v.version): v for v in catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE)}
assert versions["2.0.0"].svn_revision == 999
assert versions["2.0.0"].managed is False
@@ -190,7 +196,7 @@ def test_version_is_managed_when_any_artifact_is_managed() -> None:
_artifact(project, "1.0.0", "a-1.0.0.jar", release=release, managed=True),
]
- versions = {str(v.version): v for v in catalog._versions(artifacts, {})}
+ versions = {str(v.version): v for v in catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE)}
assert versions["1.0.0"].managed is True
@@ -213,7 +219,7 @@ def test_versions_are_newest_first() -> None:
_artifact(project, "5.0.2", "a-5.0.2.tar.gz", release=newer),
]
- versions = catalog._versions(artifacts, {})
+ versions = catalog._versions(artifacts, {}, sql.VersionMethod.SIMPLE)
assert [str(v.version) for v in versions] == ["5.0.2", "5.0.1"]
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org