# Copyright (C) 2026 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
from datetime import datetime, timezone
import re
from typing import Optional
from swh.model.model import Snapshot, SnapshotTargetType
from swh.model.swhids import CoreSWHID, ObjectType
[docs]
def utc_now():
return datetime.now(tz=timezone.utc)
[docs]
def find_release_from_version(
version: str, snapshot: Snapshot, branch_prefix=b"refs/tags/"
) -> Optional[CoreSWHID]:
"""Try to find branch in a snapshot corresponding to a specific software origin
version using a set of heuristics.
Args:
version: Version number for a release of a software origin
snapshot: Snapshot of a software origin
branch_prefix: Only consider branches whose name starts with that prefix
Returns:
the SWHID of a release or revision related to the version number if found,
:const:`None` otherwise
"""
version = version.lower()
branch_names = [
branch_prefix + branch_suffix.encode()
for branch_suffix in (
version,
version + ".0",
"v" + version + ".0",
"v" + version,
version.replace(".", "_"),
"v" + version.replace(".", "_"),
version.replace(".", "-"),
"v" + version.replace(".", "-"),
version.replace("-", "."),
"v" + version.replace("-", "."),
"v" + version.replace("_", "-"),
version.replace("-", "_").replace(".", "_"),
"v_" + version.replace("-", "_").replace(".", "_"),
)
]
if version.endswith(".0"):
branch_names += [
branch_prefix + version.encode()[:-2],
branch_prefix + b"v" + version.encode()[:-2],
]
if version.endswith("."):
branch_names += [
branch_prefix + version.encode()[:-1],
branch_prefix + b"v" + version.encode()[:-1],
]
if version.endswith("-na"):
branch_names += [
branch_prefix + version[:-3].encode(),
branch_prefix + b"v" + version[:-3].encode(),
]
if m := re.match(
r".*(?P<suffix>-(a|b|c|d|p|pre|sp|rc|alpha|beta|update|milestone))[0-9]*$",
version,
):
replacements = {
b"-a": [b"a", b"-alpha"],
b"-b": [b"b", b"-beta"],
b"-c": [b"c"],
b"-d": [b"d"],
b"-p": [b"p", b"_", b"-patch"],
b"-pre": [b"pre"],
b"-sp": [b"-preview", b".0-preview"],
b"-rc": [b"rc"],
b"-alpha": [b"-a", b"alpha"],
b"-beta": [b"-b", b"beta"],
b"-update": [b"+"],
b"-milestone": [b"m", b"-m", b"_m"],
}
old_suffix = m.group("suffix").encode()
branch_names.extend(
branch_name.replace(old_suffix, new_suffix)
for branch_name in list(branch_names)
for new_suffix in replacements[old_suffix]
)
branch_names += [branch_name + b"-release" for branch_name in branch_names]
branch_names += [branch_name + b"_release" for branch_name in branch_names]
filtered_branches = {
kb.lower(): vb
for kb, vb in snapshot.branches.items()
if kb.lower().endswith(
tuple(
branch_name.replace(branch_prefix, b"") for branch_name in branch_names
)
)
or version.encode() in kb.lower()
}
found_branch_name = None
for branch_name in branch_names:
if branch_name.lower() in filtered_branches:
found_branch_name = branch_name.lower()
break
else:
if len(filtered_branches):
found_branch_name = next(iter(sorted(filtered_branches)))
return (
CoreSWHID(
object_type=(
ObjectType.RELEASE
if filtered_branches[found_branch_name].target_type
== SnapshotTargetType.RELEASE
else ObjectType.REVISION
),
object_id=filtered_branches[found_branch_name].target,
)
if found_branch_name
else None
)