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
Fixes #1405
The bug, and one symptom the issue does not mention
SubmoduleCollection.get() promises:
It only catches KeyError. But git_submodule_lookup reports GIT_EEXISTS, not GIT_ENOTFOUND, when a repository exists at the path yet was never registered as a submodule — libgit2's message is submodule 'x' has not been added yet. check_error turns that into AlreadyExistsError, which escapes get().
Because __contains__ is implemented as self.get(name) is not None, the same input makes a containment test raise:
Reproduced on 1.20.0 from PyPI and on this branch's parent built against libgit2 1.9.7.
The reporter later wondered whether their confusion about nested repositories was the real issue. It was not the whole of it: whatever one thinks libgit2 should report for a nested repository, get() and in are documented to answer "absent" rather than raise, and today they raise.
The change
get() also catches AlreadyExistsError. __getitem__ is untouched, so callers who need to distinguish "a repository is there but unregistered" from "nothing is there" still can — the new test pins that too.
I considered instead translating GIT_EEXISTS to KeyError inside __getitem__, which would arguably make its own docstring more accurate. I did not, because it changes an exception type users may already catch, and it is your call rather than mine. Happy to switch if you prefer it.
Verification
Per CONTRIBUTING.md, the commit carries Assisted-by: Claude Code (Claude Opus 5). I can explain the change and stand behind it.