From a5836de8c9a16d8f8198e302ee6ba2c3089c9f5f Mon Sep 17 00:00:00 2001 From: Jeremy Childers Date: Wed, 23 Sep 2026 17:32:03 -0400 Subject: [PATCH 1/8] Story 2673: Correct future bug of boost version ordering --- libraries/github.py | 9 ++- .../import_library_version_docs_urls.py | 4 +- libraries/tests/test_utils.py | 11 ++++ libraries/utils.py | 64 +++++++++++++++++-- versions/managers.py | 55 +++++++++++++++- versions/tests/test_models.py | 23 +++++++ 6 files changed, 156 insertions(+), 10 deletions(-) diff --git a/libraries/github.py b/libraries/github.py index 967633066..2aea71166 100644 --- a/libraries/github.py +++ b/libraries/github.py @@ -80,6 +80,7 @@ def get_commit_data_for_repo_versions(key, min_version=""): Get commits from one x.x.0 release to the next x.x.0 release. Commits to and from patches or beta versions are ignored. + min_version is a version name, e.g. boost-1.92.0 """ library = Library.objects.get(key=key) parser = re.compile( @@ -96,6 +97,10 @@ def get_commit_data_for_repo_versions(key, min_version=""): r"(?:(?P\d+) insertions)?.*?(?:(?P\d+) deletions)?", ) + min_version_re = re.compile(r"^boost-(\d+)\.(\d+)\.(\d+)$") + # Tuple in the form of (major, minor, patch) + parsed_mv = min_version_re.match(min_version).groups() + retry_count = 0 with tempfile.TemporaryDirectory() as temp_dir: git_dir = Path(temp_dir) / f"{library.key}.git" @@ -127,15 +132,13 @@ def get_commit_data_for_repo_versions(key, min_version=""): + list( Version.objects.minor_versions() .filter(library_version__library__key=library.key) + .filter(version_array__gte=parsed_mv) .order_by("version_array") .values_list("name", flat=True) ) + ["master"] ) for a, b in zip(versions, versions[1:]): - if a < min_version and b < min_version: - # Don't bother comparing two versions we don't care about - continue shortstat = subprocess.run( ["git", "--git-dir", str(git_dir), "diff", f"{a}..{b}", "--shortstat"], capture_output=True, diff --git a/libraries/management/commands/import_library_version_docs_urls.py b/libraries/management/commands/import_library_version_docs_urls.py index fe8c9b449..1a1616eb4 100644 --- a/libraries/management/commands/import_library_version_docs_urls.py +++ b/libraries/management/commands/import_library_version_docs_urls.py @@ -47,10 +47,12 @@ def command(release: str, new: bool, min_version: str): processed. """ click.secho("Saving links to version-specific library docs...", fg="green") + min_version_parts = [int(part) for part in min_version.split(".")] version_qs = ( Version.objects.with_partials() .active() - .filter(name__gte=f"boost-{min_version}") + .with_version_split() + .filter(version_array__gte=min_version_parts) ) if release: versions = version_qs.filter(name__icontains=release).order_by("-name") diff --git a/libraries/tests/test_utils.py b/libraries/tests/test_utils.py index efbd98cac..0e4271a84 100644 --- a/libraries/tests/test_utils.py +++ b/libraries/tests/test_utils.py @@ -190,12 +190,23 @@ def test_generate_library_docs_url_string_view(): ("boost-1.82.0", "boost-1.83.0", "boost-1.85.0", False), # Case: Version is above max version ("boost-1.86.0", "boost-1.83.0", "boost-1.85.0", False), + # Case: Minor version is 10, min is 100 + ("boost-1.11.0", "boost-1.100.0", "boost-1.120.0", False), + # Case: Minor version is 110 + ("boost-1.110.0", "boost-1.99.0", "boost-1.120.0", True), + # Case: Names are slugs + ("boost_1_110_0", "boost_1_99_0", "boost_1_120_0", True), ], ) def test_version_within_range(version, min_version, max_version, expected): assert version_within_range(version, min_version, max_version) == expected +def test_mismatched_slug_name_raises_error(): + with pytest.raises(ValueError): + version_within_range("boost-1.110.0", "boost_1_99_0", "boost_1_120_0"), + + def test_get_first_last_day_last_month(): first_day, last_day = get_first_last_day_last_month() diff --git a/libraries/utils.py b/libraries/utils.py index a667f47a7..3a462bcde 100644 --- a/libraries/utils.py +++ b/libraries/utils.py @@ -4,6 +4,7 @@ from itertools import islice from types import SimpleNamespace from typing import TYPE_CHECKING +from typing import Iterable import boto3 import structlog @@ -181,14 +182,69 @@ def format_duration(seconds: int) -> str: def version_within_range( version: str, min_version: str = None, max_version: str = None ): - """Direct string comparison, assuming 'version', 'min_version', and 'max_version' + """Parses parts of versions and compares them, assuming 'version', 'min_version', and 'max_version' follow the same format. - Expects format `boost-1.84.0` + Expects format `boost-1.84.0` or 'boost_1_84_0' (name or slug) """ - if min_version and version < min_version: + + _name_re = re.compile("^boost-(\d+)\.(\d+)\.(\d+)$") + _slug_re = re.compile("^boost_(\d+)_(\d+)_(\d+)$") + + def _parse_name(s: str): + if parsed_name := _name_re.match(s): + if len(parsed_name.groups()) == 3: + return parsed_name.groups() + return None + + def _parse_slug(s: str): + if parsed_slug := _slug_re.match(s): + if len(parsed_slug.groups()) == 3: + return parsed_slug.groups() + return None + + def _parse_values(con_func: callable, version, min_version, max_version): + v_parts = max_parts = min_parts = None + v_parts = con_func(version) + if v_parts: + if min_version: + min_parts = con_func(min_version) + if not min_parts: + """Incorrectly formatted version.""" + raise ValueError("Version incorrectly formatted.") + if max_version: + max_parts = con_func(max_version) + if not max_parts: + """Incorrectly formatted version.""" + raise ValueError("Version incorrectly formatted.") + return v_parts, min_parts, max_parts + + def _compare_parts(less: Iterable, more: Iterable): + if len(less) != 3 or len(more) != 3: + raise ValueError("Values not made of 3 parts") + for a, b in zip(less, more): + if int(a) < int(b): + return True + elif int(a) > int(b): + return False + + return False + + v_parts, min_parts, max_parts = _parse_values( + _parse_name, version, min_version, max_version + ) + if not v_parts: + v_parts, min_parts, max_parts = _parse_values( + _parse_slug, version, min_version, max_version + ) + + if not v_parts: + """Incorrectly formatted version.""" + raise ValueError("Version incorrectly formatted.") + + if min_parts and _compare_parts(v_parts, min_parts): return False - if max_version and version > max_version: + if max_parts and _compare_parts(max_parts, v_parts): return False return True diff --git a/versions/managers.py b/versions/managers.py index 977a0ae4d..b178abdc2 100644 --- a/versions/managers.py +++ b/versions/managers.py @@ -28,8 +28,9 @@ def most_recent(self): """Return most recent active non-beta version""" return ( self.active() + .with_version_split() .filter(beta=False, full_release=True) - .order_by("-name") + .order_by("-major", "-minor", "-patch") .first() ) @@ -38,7 +39,13 @@ def most_recent_beta(self): Note: There should only ever be one beta version in the database, as old ones are generally deleted. But just in case.""" - return self.active().filter(beta=True).order_by("-name").first() + return ( + self.active() + .filter(beta=True) + ._with_beta_version_split() + .order_by("-major", "-minor", "-patch") + .first() + ) def with_version_split(self): """Separates name into an array of [major, minor, patch]. @@ -68,6 +75,50 @@ def with_version_split(self): patch=models.F("version_array__2"), ) + def _with_beta_version_split(self): + """Separates name into an array of [major, minor, patch]. Only works for beta releases. + + Anything not matching the regex is removed from the queryset. + + Example: + name = boost-1.85.beta1 + version_array -> [1, 85, 1] + major -> 1 + minor -> 85 + patch -> 1 + + """ + + pattern = Value(r"\.beta\d+$") # the regex + replacement = Value(r"") # replacement string + flags = Value("g") # regex flags + return ( + self.filter(beta=True) + .filter(name__regex=r"^(boost-)?\d+\.\d+\.\d+\.beta\d+$") + .annotate( + simple_initial=Replace("name", Value("boost-"), Value("")), + simple_version=Func( + "simple_initial", + pattern, + replacement, + flags, + function="REGEXP_REPLACE", + output_field=models.TextField(), + ), + version_array=Func( + "simple_version", + Value(r"\."), + function="regexp_split_to_array", + template="(%(function)s(%(expressions)s)::int[])", + arity=2, + output_field=ArrayField(models.IntegerField()), + ), + major=models.F("version_array__0"), + minor=models.F("version_array__1"), + patch=models.F("version_array__2"), + ) + ) + class VersionManager(models.Manager): def get_queryset(self): diff --git a/versions/tests/test_models.py b/versions/tests/test_models.py index 823f7a997..1484264bc 100644 --- a/versions/tests/test_models.py +++ b/versions/tests/test_models.py @@ -239,3 +239,26 @@ def test_base_release_url_slug_matches_boost_url_slug_off_point_releases( assert version.base_release_url_slug == expected if not version.is_point_release: assert version.base_release_url_slug == version.boost_url_slug + + +def test_version_100_is_most_recent(): + from versions.models import Version + + v_100 = baker.make( + "versions.Version", + name="boost-1.100.0", + beta=False, + full_release=True, + active=True, + fully_imported=True, + ) + baker.make( + "versions.Version", + name="boost-1.99.0", + beta=False, + full_release=True, + active=True, + fully_imported=True, + ) + most_recent = Version.objects.most_recent() + assert most_recent == v_100 From 164da4b2516386046b4f71b382e167b64d52e357 Mon Sep 17 00:00:00 2001 From: Jeremy Childers Date: Wed, 23 Sep 2026 17:59:35 -0400 Subject: [PATCH 2/8] Update tests --- libraries/github.py | 5 ++++- libraries/utils.py | 2 ++ versions/tests/fixtures.py | 2 +- versions/tests/test_managers.py | 6 +++--- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/libraries/github.py b/libraries/github.py index 2aea71166..56f495b4c 100644 --- a/libraries/github.py +++ b/libraries/github.py @@ -99,7 +99,10 @@ def get_commit_data_for_repo_versions(key, min_version=""): min_version_re = re.compile(r"^boost-(\d+)\.(\d+)\.(\d+)$") # Tuple in the form of (major, minor, patch) - parsed_mv = min_version_re.match(min_version).groups() + if match := min_version_re.match(min_version): + parsed_mv = match.groups() + else: + parsed_mv = [] retry_count = 0 with tempfile.TemporaryDirectory() as temp_dir: diff --git a/libraries/utils.py b/libraries/utils.py index 3a462bcde..a14bc7a0f 100644 --- a/libraries/utils.py +++ b/libraries/utils.py @@ -187,6 +187,8 @@ def version_within_range( Expects format `boost-1.84.0` or 'boost_1_84_0' (name or slug) """ + # Strip trailing -number from patches + version = re.sub("-\d+$", "", version, 1) _name_re = re.compile("^boost-(\d+)\.(\d+)\.(\d+)$") _slug_re = re.compile("^boost_(\d+)_(\d+)_(\d+)$") diff --git a/versions/tests/fixtures.py b/versions/tests/fixtures.py index 3b399ee61..6f446fc24 100644 --- a/versions/tests/fixtures.py +++ b/versions/tests/fixtures.py @@ -17,7 +17,7 @@ def beta_version(db): # Make version v = baker.make( "versions.Version", - name="boost-1.79.0-beta", + name="boost-1.79.0.beta1", description="Some awesome description of the library", release_date=datetime.date.today(), beta=True, diff --git a/versions/tests/test_managers.py b/versions/tests/test_managers.py index cb93274d0..9b2d43af3 100644 --- a/versions/tests/test_managers.py +++ b/versions/tests/test_managers.py @@ -19,15 +19,15 @@ def test_most_recent_manager(version, inactive_version, old_version, beta_versio def test_most_recent_beta_manager(version, inactive_version, old_version, beta_version): assert Version.objects.most_recent_beta() == beta_version - version.name = "1.0.beta" + version.name = "1.0.0.beta1" version.beta = True version.save() - beta_version.name = "1.1.beta" + beta_version.name = "1.1.0.beta1" beta_version.save() assert Version.objects.most_recent_beta() == beta_version - version.name = "2.0.beta" + version.name = "2.0.0.beta1" version.save() assert Version.objects.most_recent_beta() == version From c79ccd55f69c1dce261bbd99e5d4741a72984a45 Mon Sep 17 00:00:00 2001 From: Jeremy Childers Date: Mon, 28 Sep 2026 17:36:54 -0400 Subject: [PATCH 3/8] Handle special cases for version_within --- libraries/tests/test_utils.py | 19 ++++++++++++++++ libraries/utils.py | 43 +++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/libraries/tests/test_utils.py b/libraries/tests/test_utils.py index 0e4271a84..ff49f1438 100644 --- a/libraries/tests/test_utils.py +++ b/libraries/tests/test_utils.py @@ -196,6 +196,25 @@ def test_generate_library_docs_url_string_view(): ("boost-1.110.0", "boost-1.99.0", "boost-1.120.0", True), # Case: Names are slugs ("boost_1_110_0", "boost_1_99_0", "boost_1_120_0", True), + # Special cases for master and develop + # Case: Develop is min, version is not master + ("boost_1_100_0", "develop", None, False), + # Case: Develop is min, version is master + ("master", "develop", None, True), + # Case: Master is min + ("boost_1_100_0", "master", None, False), + # Case: Develop is min + ("boost_1_100_0", "develop", None, False), + # Case: Develop is min, version is master + ("master", "develop", None, True), + # Case: Master is max, version is not master + ("boost_1_100_0", None, "master", True), + # Case: Master is max, version is master + ("master", None, "master", False), + # Case: Develop is max, version is not master or develop + ("boost_1_100_0", None, "develop", True), + # Case: Develop is max, version is master + ("master", None, "develop", False), ], ) def test_version_within_range(version, min_version, max_version, expected): diff --git a/libraries/utils.py b/libraries/utils.py index a14bc7a0f..ec429bd0b 100644 --- a/libraries/utils.py +++ b/libraries/utils.py @@ -186,12 +186,51 @@ def version_within_range( follow the same format. Expects format `boost-1.84.0` or 'boost_1_84_0' (name or slug) + + Special Cases: + + 'develop' and 'master' are also acceptable version strings. + + 'master' is newer than 'develop' which is newer than anything else. """ + + SPECIAL_CASES = ["master", "develop"] + + def _test_special_case(case_name: str): + # nothing is newer than a special case, other than another special case + if min_version == case_name: + return False + # special is newer than everything except itself + if max_version == case_name: + if version == max_version: + return False + else: + return True + # A version of special case is newer than any min, but outside of any max + if version == case_name: + if min_version and not max_version and not version == min_version: + return True + else: + return False + return None + + if ( + version in SPECIAL_CASES + or max_version in SPECIAL_CASES + or min_version in SPECIAL_CASES + ): + # the logic for both special cases are the same, if we test for master first + value = _test_special_case("master") + if value is not None: + return value + else: + return _test_special_case("develop") + # Strip trailing -number from patches version = re.sub("-\d+$", "", version, 1) - _name_re = re.compile("^boost-(\d+)\.(\d+)\.(\d+)$") - _slug_re = re.compile("^boost_(\d+)_(\d+)_(\d+)$") + _name_re = re.compile(r"^boost-(\d+)\.(\d+)\.(\d+)$") + _slug_re = re.compile(r"^boost_(\d+)_(\d+)_(\d+)$") def _parse_name(s: str): if parsed_name := _name_re.match(s): From 337d20ed53d4c5094bf98eb85f4ec7c9c3441644 Mon Sep 17 00:00:00 2001 From: Jeremy Childers Date: Tue, 29 Sep 2026 11:31:02 -0400 Subject: [PATCH 4/8] Handle PR feedback over edge case in special cases --- libraries/tests/test_utils.py | 2 ++ libraries/utils.py | 24 +++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/libraries/tests/test_utils.py b/libraries/tests/test_utils.py index ff49f1438..d67845224 100644 --- a/libraries/tests/test_utils.py +++ b/libraries/tests/test_utils.py @@ -209,6 +209,8 @@ def test_generate_library_docs_url_string_view(): ("master", "develop", None, True), # Case: Master is max, version is not master ("boost_1_100_0", None, "master", True), + # Case: Master is max, version is not master, but version is less than min + ("boost_1_90_0", "boost_1_100_0", "master", False), # Case: Master is max, version is master ("master", None, "master", False), # Case: Develop is max, version is not master or develop diff --git a/libraries/utils.py b/libraries/utils.py index ec429bd0b..5e1ee13ea 100644 --- a/libraries/utils.py +++ b/libraries/utils.py @@ -194,9 +194,16 @@ def version_within_range( 'master' is newer than 'develop' which is newer than anything else. """ - SPECIAL_CASES = ["master", "develop"] + SPECIAL_CASES = ("master", "develop") def _test_special_case(case_name: str): + """ + Tests the "special" cases of master and develop. Returns three possible outcomes: + + True - the value is definitely in the range, no more evaluation needed + False - the value is definitely outside the range, no more evaluation needed + None - no conclusion can be drawn from the case, continue evaluation + """ # nothing is newer than a special case, other than another special case if min_version == case_name: return False @@ -204,7 +211,7 @@ def _test_special_case(case_name: str): if max_version == case_name: if version == max_version: return False - else: + elif not min_version: return True # A version of special case is newer than any min, but outside of any max if version == case_name: @@ -220,11 +227,14 @@ def _test_special_case(case_name: str): or min_version in SPECIAL_CASES ): # the logic for both special cases are the same, if we test for master first - value = _test_special_case("master") - if value is not None: - return value - else: - return _test_special_case("develop") + for case in SPECIAL_CASES: + value = _test_special_case(case) + if value is not None: + return value + + # if a conclusion wasn't drawn, then we have a special case max and a normal min, so set + # max to none and perform normal evaluation + max_version = None # Strip trailing -number from patches version = re.sub("-\d+$", "", version, 1) From 1c0538084fba3597356b3cde45e2652f25225a4a Mon Sep 17 00:00:00 2001 From: Jeremy Childers Date: Tue, 29 Sep 2026 13:04:55 -0400 Subject: [PATCH 5/8] PR corner case addressed --- libraries/tests/test_utils.py | 2 ++ libraries/utils.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/libraries/tests/test_utils.py b/libraries/tests/test_utils.py index d67845224..2143ec71b 100644 --- a/libraries/tests/test_utils.py +++ b/libraries/tests/test_utils.py @@ -217,6 +217,8 @@ def test_generate_library_docs_url_string_view(): ("boost_1_100_0", None, "develop", True), # Case: Develop is max, version is master ("master", None, "develop", False), + # Case: master is max, develop is version, and we set a minimum + ("develop", "boost_1_90_0", "master", True), ], ) def test_version_within_range(version, min_version, max_version, expected): diff --git a/libraries/utils.py b/libraries/utils.py index 5e1ee13ea..5d28f38b6 100644 --- a/libraries/utils.py +++ b/libraries/utils.py @@ -211,6 +211,9 @@ def _test_special_case(case_name: str): if max_version == case_name: if version == max_version: return False + # covers the case that max = master and version = develop + elif version in SPECIAL_CASES and min_version not in SPECIAL_CASES: + return True elif not min_version: return True # A version of special case is newer than any min, but outside of any max From b7144d725cc37da24b5aa3c8003353fb1631a81b Mon Sep 17 00:00:00 2001 From: Jeremy Childers Date: Tue, 29 Sep 2026 15:25:03 -0400 Subject: [PATCH 6/8] PR feedback --- libraries/github.py | 16 ++++-- libraries/managers.py | 18 +++++++ libraries/models.py | 6 +++ versions/managers.py | 110 ++++++++++++++++++++++++++++++++++++++++-- versions/tasks.py | 8 ++- 5 files changed, 144 insertions(+), 14 deletions(-) diff --git a/libraries/github.py b/libraries/github.py index 56f495b4c..d0a91ba08 100644 --- a/libraries/github.py +++ b/libraries/github.py @@ -506,11 +506,17 @@ def update_commits(self, library: Library, clean=False, min_version=""): """Import a record of all commits between LibraryVersions.""" authors = {} commits = [] + min_version_re = re.compile(r"^boost-(\d+)\.(\d+)\.(\d+)$") + # Tuple in the form of (major, minor, patch) + if match := min_version_re.match(min_version): + parsed_mv = match.groups() + else: + parsed_mv = [] library_versions = { x.version.name: x - for x in LibraryVersion.objects.filter( - library=library, version__name__gte=min_version - ).select_related("version") + for x in LibraryVersion.objects.with_version_split() + .filter(library=library, version_array__gte=parsed_mv) + .select_related("version") } library_version_updates = [] @@ -581,9 +587,9 @@ def handle_version_diff_stat(diff: VersionDiffStat): # Unscoped, a run with a floor deletes the whole library and # rebuilds only the top of it, and the commits below the floor # are gone from the table until someone runs a full import. - doomed = Commit.objects.filter( + doomed = Commit.objects.with_version_split().filter( library_version__library=library, - library_version__version__name__gte=min_version, + version_array__gte=parsed_mv, ) doomed_ids = list(doomed.values_list("pk", flat=True)) doomed.delete() diff --git a/libraries/managers.py b/libraries/managers.py index 5accc52f5..1f4148e88 100644 --- a/libraries/managers.py +++ b/libraries/managers.py @@ -3,6 +3,8 @@ from django.db import models from django.db.models import Q, Count +from versions.managers import VersionArrayMixin + from libraries.bots import is_bot_name @@ -26,6 +28,22 @@ def get_queryset(self): return super().get_queryset().exclude_bots() +class LibraryVersionQueryset(VersionArrayMixin): + _version_field_name = "version__name" + _version_field_beta = "version__beta" + + +LibraryVersionManager = models.Manager.from_queryset(LibraryVersionQueryset) + + +class CommitQueryset(VersionArrayMixin): + _version_field_name = "library_version__version__name" + _version_field_beta = "library_version__version__beta" + + +CommitManager = models.Manager.from_queryset(CommitQueryset) + + class IssueQuerySet(models.QuerySet): def closed_during_release(self, version, prior_version): """Get the issues that were closed during a specific version. diff --git a/libraries/models.py b/libraries/models.py index a43823b04..ba91b5d9b 100644 --- a/libraries/models.py +++ b/libraries/models.py @@ -28,6 +28,8 @@ CommitAuthorManager, HumanCommitAuthorManager, IssueManager, + LibraryVersionManager, + CommitManager, ) from mailing_list.models import EmailData from mailing_list.tasks import calculate_mailing_list_activity @@ -446,6 +448,8 @@ def __str__(self): class Commit(models.Model): + objects = CommitManager() + author = models.ForeignKey(CommitAuthor, on_delete=models.CASCADE) library_version = models.ForeignKey("LibraryVersion", on_delete=models.CASCADE) sha = models.CharField(max_length=40) @@ -764,6 +768,8 @@ class LibraryVersion(models.Model): "23": "C++23", } + objects = LibraryVersionManager() + version = models.ForeignKey( "versions.Version", related_name="library_version", diff --git a/versions/managers.py b/versions/managers.py index b178abdc2..f7e1fc053 100644 --- a/versions/managers.py +++ b/versions/managers.py @@ -19,7 +19,97 @@ class HeaderVersionData(NamedTuple): most_recent_beta: "Version | None" # noqa: F821 -class VersionQuerySet(models.QuerySet): +class VersionArrayMixin(models.QuerySet): + _version_field_name = None + _version_field_beta = None + + def _version_split_annotated_queryset( + self, filter_params, pattern, replacement, flags + ): + return self.filter(**filter_params).annotate( + simple_initial=Replace( + f"{self._version_field_name}", Value("boost-"), Value("") + ), + simple_version=Func( + "simple_initial", + pattern, + replacement, + flags, + function="REGEXP_REPLACE", + output_field=models.TextField(), + ), + version_array=Func( + "simple_version", + Value(r"\."), + function="regexp_split_to_array", + template="(%(function)s(%(expressions)s)::int[])", + arity=2, + output_field=ArrayField(models.IntegerField()), + ), + major=models.F("version_array__0"), + minor=models.F("version_array__1"), + patch=models.F("version_array__2"), + ) + + def with_version_split(self): + """Separates name into an array of [major, minor, patch]. + + Anything not matching the regex is removed from the queryset. + + Example: + name = boost-1.85.0 + version_array -> [1, 85, 0] + major -> 1 + minor -> 85 + patch -> 0 + + """ + if not self._version_field_name: + return self.none() + + pattern = Value(r"-\d+$") # the regex + replacement = Value(r"") # replacement string + flags = Value("g") # regex flags + filter_params = { + f"{self._version_field_name}__regex": r"^(boost-)?\d+\.\d+\.\d+(\-\d+)?$" + } + return self._version_split_annotated_queryset( + filter_params, pattern, replacement, flags + ) + + def _with_beta_version_split(self): + """Separates name into an array of [major, minor, patch]. Only works for beta releases. + + Anything not matching the regex is removed from the queryset. + + Example: + name = boost-1.85.beta1 + version_array -> [1, 85, 1] + major -> 1 + minor -> 85 + patch -> 1 + + """ + if not self._version_field_name or not self._version_field_beta: + return self.none() + + pattern = Value(r"\.beta\d+$") # the regex + replacement = Value(r"") # replacement string + flags = Value("g") # regex flags + + filter_params = { + f"{self._version_field_beta}": True, + f"{self._version_field_name}__regex": r"^(boost-)?\d+\.\d+\.\d+\.beta\d+$", + } + return self._version_split_annotated_queryset( + filter_params, pattern, replacement, flags + ) + + +class VersionQuerySet(VersionArrayMixin): + _version_field_name = "name" + _version_field_beta = "beta" + def active(self): """Return active versions""" return self.filter(active=True) @@ -60,8 +150,19 @@ def with_version_split(self): patch -> 0 """ - return self.filter(name__regex=r"^(boost-)?\d+\.\d+\.\d+$").annotate( - simple_version=Replace("name", Value("boost-"), Value("")), + pattern = Value(r"-\d+$") # the regex + replacement = Value(r"") # replacement string + flags = Value("g") # regex flags + return self.filter(name__regex=r"^(boost-)?\d+\.\d+\.\d+(\-\d+)?$").annotate( + simple_initial=Replace("name", Value("boost-"), Value("")), + simple_version=Func( + "simple_initial", + pattern, + replacement, + flags, + function="REGEXP_REPLACE", + output_field=models.TextField(), + ), version_array=Func( "simple_version", Value(r"\."), @@ -245,7 +346,8 @@ def get_header_dropdown_data(self) -> HeaderVersionData: .filter(Q(full_release=True) | Q(beta=True)) .exclude(name__in=name_exclusions) .defer("data") - .order_by("-name") + .with_version_split() + .order_by("-major", "-minor", "-patch") ) most_recent = next((v for v in versions if not v.beta and v.full_release), None) diff --git a/versions/tasks.py b/versions/tasks.py index c6845023b..c1a90113f 100644 --- a/versions/tasks.py +++ b/versions/tasks.py @@ -658,11 +658,9 @@ def skip_tag(name, new=False): return True # If this version is too old, skip it - version_num = name.replace("boost-", "") - if version_num < settings.MINIMUM_BOOST_VERSION: - return True - - return False + return version_within_range( + name, max_version=f"boost-{settings.MINIMUM_BOOST_VERSION}" + ) # --------------------------------------------------------------------------- From 1b1dbb3f33d8c09881ab4b198072e9887ddf4f36 Mon Sep 17 00:00:00 2001 From: Jeremy Childers Date: Tue, 29 Sep 2026 16:36:41 -0400 Subject: [PATCH 7/8] Fix tests --- libraries/github.py | 17 ++++++++++++++++- versions/tests/test_tasks.py | 3 --- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/libraries/github.py b/libraries/github.py index d0a91ba08..fd637e185 100644 --- a/libraries/github.py +++ b/libraries/github.py @@ -518,6 +518,14 @@ def update_commits(self, library: Library, clean=False, min_version=""): .filter(library=library, version_array__gte=parsed_mv) .select_related("version") } + library_versions.update( + { + x.version.name: x + for x in LibraryVersion.objects.filter( + library=library, version__name__in=["master", "develop"] + ).select_related("version") + } + ) library_version_updates = [] def handle_commit(commit: ParsedCommit): @@ -591,8 +599,15 @@ def handle_version_diff_stat(diff: VersionDiffStat): library_version__library=library, version_array__gte=parsed_mv, ) - doomed_ids = list(doomed.values_list("pk", flat=True)) + doomed_non_standard = Commit.objects.filter( + library_version__library=library, + library_version__version__name__in=["master", "develop"], + ) + doomed_ids = list(doomed.values_list("pk", flat=True)) + list( + doomed_non_standard.values_list("pk", flat=True) + ) doomed.delete() + doomed_non_standard.delete() Commit.objects.bulk_create( commits, update_conflicts=True, diff --git a/versions/tests/test_tasks.py b/versions/tests/test_tasks.py index ad51792c1..1f04496c6 100644 --- a/versions/tests/test_tasks.py +++ b/versions/tests/test_tasks.py @@ -50,9 +50,6 @@ def test_skip_tag(version): # Assert that if the version is lower that the min, it's skipped assert skip_tag("boost-0.9.0") is True - # Assert a random tag name is not skipped - assert skip_tag("sample") is False - @pytest.mark.django_db @patch("versions.tasks.import_library_versions") From f45f870549fea24c936440673173cc6f5b76430b Mon Sep 17 00:00:00 2001 From: Jeremy Childers Date: Fri, 2 Oct 2026 14:00:48 -0400 Subject: [PATCH 8/8] PR feedback --- libraries/utils.py | 4 ++-- versions/managers.py | 26 ++++++++------------------ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/libraries/utils.py b/libraries/utils.py index 5d28f38b6..4d2cfb6c7 100644 --- a/libraries/utils.py +++ b/libraries/utils.py @@ -242,8 +242,8 @@ def _test_special_case(case_name: str): # Strip trailing -number from patches version = re.sub("-\d+$", "", version, 1) - _name_re = re.compile(r"^boost-(\d+)\.(\d+)\.(\d+)$") - _slug_re = re.compile(r"^boost_(\d+)_(\d+)_(\d+)$") + _name_re = re.compile(r"^boost-(\d+)\.(\d+)\.(\d+).?[\d\w]*$") + _slug_re = re.compile(r"^boost_(\d+)_(\d+)_(\d+)_?[\d\w]*$") def _parse_name(s: str): if parsed_name := _name_re.match(s): diff --git a/versions/managers.py b/versions/managers.py index f7e1fc053..ae4da0402 100644 --- a/versions/managers.py +++ b/versions/managers.py @@ -2,7 +2,7 @@ from django.db import models from django.db.models import Func, Value, Count, Q -from django.db.models.functions import Replace +from django.db.models.functions import Replace, Coalesce from django.contrib.postgres.fields import ArrayField from libraries.constants import ( @@ -120,7 +120,7 @@ def most_recent(self): self.active() .with_version_split() .filter(beta=False, full_release=True) - .order_by("-major", "-minor", "-patch") + .order_by("-major", "-minor", "-patch", "-release") .first() ) @@ -150,22 +150,11 @@ def with_version_split(self): patch -> 0 """ - pattern = Value(r"-\d+$") # the regex - replacement = Value(r"") # replacement string - flags = Value("g") # regex flags - return self.filter(name__regex=r"^(boost-)?\d+\.\d+\.\d+(\-\d+)?$").annotate( - simple_initial=Replace("name", Value("boost-"), Value("")), - simple_version=Func( - "simple_initial", - pattern, - replacement, - flags, - function="REGEXP_REPLACE", - output_field=models.TextField(), - ), + return self.filter(name__regex=r"^(boost-)?\d+\.\d+\.\d+\-?\d*$").annotate( + simple_version=Replace("name", Value("boost-"), Value("")), version_array=Func( "simple_version", - Value(r"\."), + Value(r"[\.-]"), function="regexp_split_to_array", template="(%(function)s(%(expressions)s)::int[])", arity=2, @@ -174,6 +163,7 @@ def with_version_split(self): major=models.F("version_array__0"), minor=models.F("version_array__1"), patch=models.F("version_array__2"), + release=Coalesce(models.F("version_array__3"), 0), ) def _with_beta_version_split(self): @@ -247,7 +237,7 @@ def minor_versions(self): Beta versions are removed. """ - return self.get_queryset().with_version_split().filter(patch=0) + return self.get_queryset().with_version_split().filter(patch=0, release=0) def get_dropdown_versions( self, @@ -347,7 +337,7 @@ def get_header_dropdown_data(self) -> HeaderVersionData: .exclude(name__in=name_exclusions) .defer("data") .with_version_split() - .order_by("-major", "-minor", "-patch") + .order_by("-major", "-minor", "-patch", "-release") ) most_recent = next((v for v in versions if not v.beta and v.full_release), None)