diff --git a/dep_freshness/evaluate.py b/dep_freshness/evaluate.py index a2200dd..1a03ae7 100644 --- a/dep_freshness/evaluate.py +++ b/dep_freshness/evaluate.py @@ -25,19 +25,24 @@ from dep_freshness.versions import behind _SHORT_SHA = 10 -def _commit(dep: Dep, head: str) -> Finding | None: - """A hash pin is current when the default branch's HEAD still starts with it.""" - short = head[:_SHORT_SHA] +def _commit(dep: Dep, heads: str) -> Finding | None: + """A hash pin is current while SOME branch head still starts with it. + + `heads` is the registry's space-joined list, default branch first; the + default head is what a stale finding proposes. + """ + short = heads.split()[0][:_SHORT_SHA] if not dep.pinned: return Finding(dep, Severity.UNPINNED, short, detail=f"pin a commit ({short})") - if head.lower().startswith(dep.pinned.lower()): + pin = dep.pinned.lower() + if any(head.lower().startswith(pin) for head in heads.split()): return None return Finding( dep, Severity.STALE, short, - detail="the source repository's default branch has moved on; a commit " - "pin has no release to wait for", + detail="no branch of the source repository points at this commit any " + "more; a commit pin has no release to wait for", ) diff --git a/dep_freshness/registries/gitcommit.py b/dep_freshness/registries/gitcommit.py index 47a0175..c3994d9 100644 --- a/dep_freshness/registries/gitcommit.py +++ b/dep_freshness/registries/gitcommit.py @@ -6,9 +6,10 @@ version, so the gate's question becomes the only one a commit pin admits: is the pinned commit still what the source repository's default branch points at? (kuhy, 2026-09-12: chosen over never-expiring allowlist entries.) -The answer is the full HEAD sha; `evaluate` compares by prefix so a 7-, 10- -or 40-character pin all work, and reports the first ten characters, which -is what JitPack accepts as a version. +The answer is every branch head's full sha, default branch first; `evaluate` +compares by prefix so a 7-, 10- or 40-character pin all work, and reports +the default head's first ten characters, which is what JitPack accepts as +a version. """ from __future__ import annotations @@ -36,12 +37,27 @@ def source_repo(name: str) -> str | None: def latest(name: str) -> str | None: - """The default branch's HEAD sha, or None when `name` is not a JitPack pin.""" + """Every branch head of the source repo, default branch first, space-joined. + + A pin is current when it is SOME branch's head, not only the default one: + Mihon pins `deadlock-fix` of sqldelight-androidx-driver and Tachiyomi + pins `fix-build2` of FlexibleAdapter, both side branches whose heads are + exactly the pinned commits. Comparing those against `master` alone + called two current pins stale. None when `name` is not a JitPack pin. + """ repo = source_repo(name) if repo is None: return None - for line in ls_remote(GITHUB_REMOTE.format(repo=repo), "HEAD"): + default: str | None = None + heads: list[str] = [] + # No `--heads`: that filter drops the symbolic HEAD line, which is the + # only way to learn WHICH head is the default branch's. + for line in ls_remote(GITHUB_REMOTE.format(repo=repo)): sha, _, ref = line.partition("\t") - if ref.strip() == "HEAD" and sha: - return sha.strip() - return None + sha, ref = sha.strip(), ref.strip() + if ref == "HEAD": + default = sha + elif ref.startswith("refs/heads/"): + heads.append(sha) + ordered = ([default] if default else []) + [h for h in heads if h != default] + return " ".join(ordered) or None diff --git a/dep_freshness/tests/test_gitcommit.py b/dep_freshness/tests/test_gitcommit.py index 70608b1..90945b3 100644 --- a/dep_freshness/tests/test_gitcommit.py +++ b/dep_freshness/tests/test_gitcommit.py @@ -22,6 +22,7 @@ from dep_freshness.resolve import _LOOKUP, Answer from dep_freshness.tests.conftest import write HEAD = "844a07002c3d2f2b68a6b26a25e8e401777c73e7" +SIDE = "c80135339b6f4c2d8e1a7b3f9d0e5c6a2b4d8f10" @pytest.fixture @@ -60,19 +61,25 @@ def test_the_github_repo_behind_a_jitpack_coordinate(name, expected): assert gitcommit.source_repo(name) == expected -def test_latest_is_the_default_branch_head(ls_remote): - calls = ls_remote(f"{HEAD}\tHEAD\n{HEAD}\trefs/heads/master\n") - assert gitcommit.latest("com.github.arkon.FlexibleAdapter:flexible-adapter") == HEAD - assert calls[-1] == [ - "git", - "ls-remote", - "https://github.com/arkon/FlexibleAdapter", - "HEAD", - ] +def test_latest_lists_every_branch_head_default_first(ls_remote): + calls = ls_remote( + f"{SIDE}\trefs/heads/fix-build2\n{HEAD}\tHEAD\n{HEAD}\trefs/heads/master\n" + f"{SIDE}\trefs/pull/1/head\n" + ) + assert ( + gitcommit.latest("com.github.arkon.FlexibleAdapter:flexible-adapter") + == f"{HEAD} {SIDE}" + ) + assert calls[-1] == ["git", "ls-remote", "https://github.com/arkon/FlexibleAdapter"] + + +def test_a_repo_with_no_default_head_still_lists_its_branches(ls_remote): + ls_remote(f"{SIDE}\trefs/heads/dev\n") + assert gitcommit.latest("com.github.mihonapp:image-decoder") == SIDE def test_no_head_line_means_no_answer(ls_remote): - ls_remote("\n \nabc\trefs/heads/dev\n") + ls_remote("\n \nabc\trefs/tags/v1\n") assert gitcommit.latest("com.github.mihonapp:image-decoder") is None @@ -144,17 +151,19 @@ def dep(pinned: str | None = "844a07002c") -> Dep: ) -def test_a_pin_at_head_passes_whatever_its_length_or_case(): +def test_a_pin_at_any_branch_head_passes_whatever_its_length_or_case(): assert judge(dep("844a07002c"), Answer(HEAD)) is None assert judge(dep("844A070"), Answer(HEAD)) is None assert judge(dep(HEAD), Answer(HEAD)) is None + # fix-build2's head, not master's: still current. + assert judge(dep("c8013533"), Answer(f"{HEAD} {SIDE}")) is None -def test_a_pin_behind_head_is_stale_and_reports_the_short_sha(): - finding = judge(dep("c8013533"), Answer(HEAD)) +def test_a_pin_on_no_branch_head_is_stale_and_proposes_the_default_head(): + finding = judge(dep("0123abc"), Answer(f"{HEAD} {SIDE}")) assert finding.severity is Severity.STALE assert finding.latest == "844a07002c" - assert "default branch has moved" in finding.detail + assert "no branch" in finding.detail def test_a_missing_pin_is_reported_unpinned_with_head_to_use():