Skip to content

References.get and Branches.get return None for an invalid name - #1495

Merged
jdavid merged 1 commit into
libgit2:masterfrom
rawsun007:get-invalid-spec
Sep 18, 2026
Merged

jdavid merged 1 commit into
libgit2:masterfrom
rawsun007:get-invalid-spec

Conversation

@rawsun007

Copy link
Copy Markdown
Contributor

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 None

Both 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:

call before after
repo.references.get('master') InvalidSpecError None
'master' in repo.references InvalidSpecError False
repo.branches.get('my branch') InvalidSpecError None
'my branch' in repo.branches InvalidSpecError False
repo.references.get('refs/heads/nope') None None
repo.branches.get('nope') None None

'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.

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
@jdavid
jdavid merged commit 58d8134 into libgit2:master Sep 18, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

References.get() and Branches.get() raise InvalidSpecError where the docs promise None

2 participants