| # SPDX-License-Identifier: Apache-2.0 |
| # |
| # Modifications by Apache Solr contributors; see git log for details. |
| # Licensed under the Apache License, Version 2.0. |
| # |
| # The OpenSearch Contributors require contributions made to |
| # this file be licensed under the Apache-2.0 license or a |
| # compatible open source license. |
| # Modifications Copyright OpenSearch Contributors. See |
| # GitHub history for details. |
| # Licensed to Elasticsearch B.V. under one or more contributor |
| # license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright |
| # ownership. Elasticsearch B.V. licenses this file to you under |
| # the Apache License, Version 2.0 (the "License"); you may |
| # not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| import re |
| import functools |
| |
| from solrorbit import exceptions |
| |
| VERSIONS = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$") |
| |
| VERSIONS_OPTIONAL = re.compile(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-(.+))?$") |
| |
| |
| def _versions_pattern(strict): |
| return VERSIONS if strict else VERSIONS_OPTIONAL |
| |
| |
| def is_version_identifier(text, strict=True): |
| return text is not None and _versions_pattern(strict).match(text) is not None |
| |
| |
| def major_version(version): |
| """ |
| Determines the major version of a given version string. |
| |
| :param version: A version string in the format major.minor.path-suffix (suffix is optional) |
| :return: The major version (as int). In case the version string is invalid, an ``exceptions.InvalidSyntax`` is raised. |
| """ |
| major, _, _, _ = components(version) |
| return major |
| |
| |
| def components(version, strict=True): |
| """ |
| Determines components of a version string. |
| |
| :param version: A version string in the format major.minor.path-suffix (suffix is optional) |
| :param strict: Determines whether versions need to have at least "major", "minor" and "patch" defined. Default: True |
| :return: A tuple with four components determining "major", "minor", "patch" and "suffix" (any part except "major" may be `None`) |
| """ |
| versions_pattern = _versions_pattern(strict) |
| matches = versions_pattern.match(version) |
| if matches: |
| if matches.start(4) > 0: |
| return int(matches.group(1)), int(matches.group(2)), int(matches.group(3)), matches.group(4) |
| elif matches.start(3) > 0: |
| return int(matches.group(1)), int(matches.group(2)), int(matches.group(3)), None |
| elif matches.start(2) > 0: |
| return int(matches.group(1)), int(matches.group(2)), None, None |
| elif matches.start(1) > 0: |
| return int(matches.group(1)), None, None, None |
| else: |
| return int(version), None, None, None |
| raise exceptions.InvalidSyntax("version string '%s' does not conform to pattern '%s'" % (version, versions_pattern.pattern)) |
| |
| |
| @functools.total_ordering |
| class Version: |
| """ |
| Represents a version with components major, minor, patch and suffix (suffix is optional). Suffixes are not |
| considered for version comparisons as its contents are opaque and a semantically correct order cannot be defined. |
| """ |
| def __init__(self, major, minor, patch, suffix=None): |
| self.major = major |
| self.minor = minor |
| self.patch = patch |
| self.suffix = suffix |
| |
| def __eq__(self, o: object) -> bool: |
| return isinstance(o, type(self)) and (self.major, self.minor, self.patch) == (o.major, o.minor, o.patch) |
| |
| def __lt__(self, o: object) -> bool: |
| return isinstance(o, type(self)) and (self.major, self.minor, self.patch) < (o.major, o.minor, o.patch) |
| |
| def __hash__(self) -> int: |
| return hash(self.major) ^ hash(self.minor) ^ hash(self.patch) ^ hash(self.suffix) |
| |
| def __repr__(self) -> str: |
| v = f"{self.major}.{self.minor}.{self.patch}" |
| return f"{v}-{self.suffix}" if self.suffix else v |
| |
| @classmethod |
| def from_string(cls, v): |
| return cls(*components(v)) |
| |
| |
| def variants_of(version): |
| for v, _ in VersionVariants(version).all_versions: |
| yield v |
| |
| |
| class VersionVariants: |
| """ |
| Build all possible variations of a version. |
| |
| e.g. if version "5.0.0-SNAPSHOT" is given, the possible variations are: |
| self.with_suffix: "5.0.0-SNAPSHOT", |
| self.with_patch: "5.0.0", |
| self.with_minor: "5.0", |
| self.with_major: "5" |
| """ |
| |
| def __init__(self, version): |
| """ |
| :param version: A version string in the format major.minor.path-suffix (suffix is optional) |
| """ |
| self.major, self.minor, self.patch, self.suffix = components(version) |
| |
| self.with_major = f"{int(self.major)}" |
| self.with_minor = f"{int(self.major)}.{int(self.minor)}" |
| self.with_patch = f"{int(self.major)}.{int(self.minor)}.{int(self.patch)}" |
| self.with_suffix = f"{int(self.major)}.{int(self.minor)}.{int(self.patch)}-{self.suffix}" if self.suffix \ |
| else None |
| |
| @property |
| def all_versions(self): |
| """ |
| :return: a list of tuples containing version variants and version type |
| ordered from most specific to most generic variation. |
| |
| Example: |
| [("5.0.0-SNAPSHOT", "with_suffix"), ("5.0.0", "with_patch"), ("5.0", "with_minor"), ("5", "with_major")] |
| """ |
| |
| versions = [(self.with_suffix, "with_suffix")] if self.suffix else [] |
| versions.extend([(self.with_patch, "with_patch"), |
| (self.with_minor, "with_minor"), |
| (self.with_major, "with_major")]) |
| |
| return versions |
| |
| |
| def best_match(available_alternatives, distribution_version): |
| """ |
| Finds the most specific branch for a given distribution version assuming that versions have the pattern: |
| |
| major.minor.patch-suffix |
| |
| and the provided alternatives reflect this pattern. |
| Best matches for distribution_version from available_alternatives may be: |
| 1. exact matches of major.minor |
| 2. nearest prior minor within the same major |
| 3. major version |
| 4. as a last resort, `main`. |
| |
| See test_find_best_match() for examples. |
| |
| :param available_alternatives: A list of possible distribution versions (or shortened versions). |
| :param distribution_version: An Solr distribution version. |
| :return: The most specific alternative that is available or None. |
| """ |
| if is_version_identifier(distribution_version): |
| versions = VersionVariants(distribution_version) |
| for version, version_type in versions.all_versions: |
| if version in available_alternatives: |
| return version |
| # match nearest prior minor |
| if version_type == "with_minor" and (latest_minor := latest_bounded_minor(available_alternatives, versions)): |
| if latest_minor: |
| return f"{versions.major}.{latest_minor}" |
| # not found in the available alternatives, it could still be a main version |
| major, _, _, _ = components(distribution_version) |
| if major > _latest_major(available_alternatives): |
| return "main" |
| elif not distribution_version: |
| return "main" |
| return None |
| |
| |
| def _latest_major(alternatives): |
| max_major = -1 |
| for a in alternatives: |
| if is_version_identifier(a, strict=False): |
| major, _, _, _ = components(a, strict=False) |
| max_major = max(major, max_major) |
| return max_major |
| |
| |
| def latest_bounded_minor(alternatives, target_version): |
| """ |
| Finds the closest minor version that is smaller or eq to target_version from a list of version alternatives. |
| Versions including patch or patch-suffix in alternatives are ignored. |
| See test_latest_bounded_minor() for examples. |
| |
| :param alternatives: list of alternative versions |
| :param target_version: a VersionVariants object presenting the distribution version |
| :return: the closest minor version (if available) from alternatives otherwise None |
| """ |
| eligible_minors = [] |
| for a in alternatives: |
| if is_version_identifier(a, strict=False): |
| major, minor, patch, suffix = components(a, strict=False) |
| if patch is not None or suffix is not None: |
| # branches containing patch or patch-suffix aren't supported |
| continue |
| if major == target_version.major and minor and minor <= target_version.minor: |
| eligible_minors.append(minor) |
| |
| # no matching minor version |
| if not eligible_minors: |
| return None |
| |
| eligible_minors.sort() |
| |
| return min(eligible_minors, key=lambda x: abs(x - target_version.minor)) |