Both are documented to return None when the lookup fails, and both catch
only KeyError. libgit2 reports two codes for a failed lookup:
GIT_ENOTFOUND, which errors.py maps to NotFoundError (a KeyError), and
GIT_EINVALIDSPEC, which maps to InvalidSpecError (a ValueError). Only the
first reaches the handler.
repo.references.get('master') InvalidSpecError
'master' in repo.references InvalidSpecError
repo.branches.get('my branch') InvalidSpecError
'my branch' in repo.branches InvalidSpecError
__contains__ is built on get() in both collections, so it raises too.
Branches.get is now typed Branch | None, dropping a "# type:ignore #
next commit". That makes mypy see what the signature always meant, so
the tests that call it and then use the result assert it is not None.
Same shape as the SubmoduleCollection.get fix in libgit2#1487.
Closes libgit2#1489
References.get() and Branches.get() are documented to return None when the lookup fails:
>>> head = repo.references.get('refs/heads/master') # Returns None if not found >>> other_branch = repo.branches.get('does-not-exist') # Returns NoneBoth catch only KeyError. libgit2 reports two codes for a failed lookup, and errors.py maps them to different Python hierarchies — GIT_ENOTFOUND to NotFoundError (a KeyError), GIT_EINVALIDSPEC to InvalidSpecError (a ValueError). Only the first reaches the handler.
On 1.20.1 with libgit2 1.9.7, in a fresh repository with one commit:
'master' is the interesting one: it is an entirely ordinary thing to pass, and it fails only because it is not a full reference name, which libgit2 classifies as an invalid spec rather than a miss. __contains__ is built on get() in both collections, so the membership test raises as well.
This is the same shape as SubmoduleCollection.get, fixed in #1487, and it is what I described in #1489 eleven days ago while offering to send the patch.
__getitem__ still raises for both kinds of failure; only the get()/in contract changes. Branches.get's return type also becomes Branch | None, which drops a # type:ignore that was marked "next commit".
Verification: pytest test/ is 665 passed, 12 skipped, 1 xpassed against a local build (LIBGIT2=$(brew --prefix libgit2) pip install -e ., libgit2 1.9.7). Reverting only the two source files fails exactly the two new tests. stubtest clean, ruff check and ruff format --check clean.
Closes #1489
Assisted-by: Claude Opus 5 (Claude Code), written and verified under my account.