dep_freshness: gittag looks up tags in the dependency's own repo
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run

A `pkg @ git+https://github.com/<owner>/<repo>@pkg-vX.Y.Z` pin outside
kuhyx/utils was asked of utils, which has no such tag, so
steam-backlog-enforcer's leisure-watch pin was undeterminable (exit 3).
Foreign pins are now named `<host>/<owner>/<repo>:<pkg>`; utils pins keep
their bare names (allowlist labels and cache keys unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N
This commit is contained in:
Krzysztof Rudnicki 2026-10-19 12:56:36 +02:00
parent 35f6f95956
commit 00085a7de6
3 changed files with 121 additions and 4 deletions

View File

@ -16,6 +16,7 @@ import tomllib
from dep_freshness._tables import GITTAG, PYPI, TOOLCHAIN
from dep_freshness.models import Dep
from dep_freshness.parsers._lines import index
from dep_freshness.registries import gittag
from dep_freshness.versions import exact_pin
_REQ = re.compile(
@ -35,7 +36,7 @@ _SKIP_PREFIXES = ("-", "#", "git+", "http://", "https://", ".", "/")
_DIRECT = re.compile(
r"^\s*(?P<name>[A-Za-z0-9][A-Za-z0-9._-]*)"
r"(?:\[[^\]]*\])?"
r"\s*@\s*git\+\S+?@(?P<ref>[A-Za-z0-9_.-]+-v[0-9][^\s#]*)"
r"\s*@\s*git\+(?P<url>\S+?)@(?P<ref>[A-Za-z0-9_.-]+-v[0-9][^\s#]*)"
)
@ -46,8 +47,9 @@ def _direct_reference(requirement: str, path: Path, line: int) -> Dep | None:
return None
ref = match.group("ref")
package, _, version = ref.rpartition("-v")
name = gittag.qualified_name(match.group("url"), package or match.group("name"))
return Dep(
ecosystem=GITTAG, name=package or match.group("name"), constraint=ref,
ecosystem=GITTAG, name=name, constraint=ref,
path=path, line=line or _ref_line(path, ref), pinned=version or None,
)

View File

@ -1,22 +1,53 @@
"""Shared libraries consumed by git tag out of `kuhyx/utils`.
"""Libraries consumed by git tag: `kuhyx/utils` by default, or their own repo.
"Newest" here is the newest tag matching `<package>-vX.Y.Z`, not a registry
answer. The transport is `git ls-remote` (see `_git.py`), never the REST API.
A dep pinned out of kuhyx/utils keeps its bare package name -- that is the
allowlist label and cache key every consumer already has. Any other repo is
named `<host>/<owner>/<repo>:<package>`, so the resolver (which only ever
sees the name) can still find the remote: asking utils for leisure-watch's
tags answered "undeterminable" (exit 3) in steam-backlog-enforcer.
"""
from __future__ import annotations
import re
from urllib.parse import urlsplit
from dep_freshness._tables import UTILS_TAG_REMOTE
from dep_freshness.registries._git import ls_remote
from dep_freshness.versions import newest_stable
_TAG = re.compile(r"refs/tags/(?P<name>[A-Za-z0-9_.-]+)-v(?P<version>[0-9][^\s]*)$")
_UTILS_SOURCE = urlsplit(UTILS_TAG_REMOTE).netloc + urlsplit(UTILS_TAG_REMOTE).path
def latest(package: str, remote: str = UTILS_TAG_REMOTE) -> str | None:
def _source(url: str) -> str | None:
"""`host/owner/repo` behind a git URL, or None when it cannot be read."""
parts = urlsplit(url.strip())
path = parts.path.strip("/").removesuffix(".git")
if not parts.hostname or path.count("/") < 1:
return None
return f"{parts.hostname}/{path}"
def qualified_name(url: str, package: str) -> str:
"""The dep name for `package` pinned out of `url` (see module docstring).
An unreadable URL falls back to the bare name -- the pre-existing
behaviour -- rather than a guessed remote.
"""
source = _source(url)
if source is None or source.casefold() == _UTILS_SOURCE.casefold():
return package
return f"{source}:{package}"
def latest(name: str) -> str | None:
"""Highest semver tag prefixed with `<package>-v`, or None if there is none."""
source, _, package = name.rpartition(":")
remote = f"https://{source}" if source else UTILS_TAG_REMOTE
found = []
for line in ls_remote(remote, "--tags", "--refs"):
match = _TAG.search(line)

View File

@ -0,0 +1,84 @@
"""Git-tag deps outside kuhyx/utils are looked up in THEIR repo, not utils.
`leisure-watch @ git+https://github.com/kuhyx/leisure-watch@leisure-watch-v0.1.0`
was asked of kuhyx/utils, which has no `leisure-watch-v*` tag, so the answer
came back undeterminable (exit 3) in steam-backlog-enforcer. The shared libs
in utils must keep their bare names: those are the allowlist labels and the
cache keys every consumer already has.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from dep_freshness._tables import UTILS_TAG_REMOTE
from dep_freshness.parsers import python
from dep_freshness.registries import gittag
LW_URL = "https://github.com/kuhyx/leisure-watch"
REQS = f"""\
leisure-watch @ git+{LW_URL}@leisure-watch-v0.1.0
earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.14.0#subdirectory=earned_time
gatelock @ git+https://github.com/kuhyx/utils.git@gatelock-v0.11.0#subdirectory=gatelock
"""
def _names(tmp_path: Path) -> dict[str, str]:
path = tmp_path / "requirements.txt"
path.write_text(REQS, encoding="utf-8")
return {d.constraint: d.name for d in python.parse_requirements(path)}
def test_a_foreign_repo_pin_is_qualified_by_its_source(tmp_path):
names = _names(tmp_path)
assert (
names["leisure-watch-v0.1.0"] == "github.com/kuhyx/leisure-watch:leisure-watch"
)
def test_utils_pins_keep_their_bare_names(tmp_path):
names = _names(tmp_path)
assert names["earned-time-v0.14.0"] == "earned-time"
assert names["gatelock-v0.11.0"] == "gatelock"
@pytest.mark.parametrize(
("url", "expected"),
[
("https://github.com/kuhyx/utils", "pkg"),
("https://GitHub.com/Kuhyx/Utils.git/", "pkg"),
("ssh://git@github.com/kuhyx/utils.git", "pkg"),
("https://gitlab.com/someone/thing.git", "gitlab.com/someone/thing:pkg"),
("not a url", "pkg"),
("https://github.com/lonely", "pkg"),
],
)
def test_qualified_name(url, expected):
assert gittag.qualified_name(url, "pkg") == expected
@pytest.fixture
def remotes(monkeypatch):
seen: list[str] = []
def fake_ls_remote(remote, *_args):
seen.append(remote)
return [
"aaa\trefs/tags/leisure-watch-v0.1.0",
"bbb\trefs/tags/leisure-watch-v0.1.1",
]
monkeypatch.setattr(gittag, "ls_remote", fake_ls_remote)
return seen
def test_a_qualified_name_asks_its_own_repo(remotes):
assert gittag.latest("github.com/kuhyx/leisure-watch:leisure-watch") == "0.1.1"
assert remotes == [LW_URL]
def test_a_bare_name_still_asks_utils(remotes):
assert gittag.latest("leisure-watch") == "0.1.1"
assert remotes == [UTILS_TAG_REMOTE]