-
-
Notifications
You must be signed in to change notification settings - Fork 402
Description
Description
Iterating over repo.config raises a RuntimeError if the configuration (including any include.path-sourced files) contains a valueless key — a boolean key written without =, e.g.:
[some-section "identifier"]
booleanflagTraceback
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
File ".../pygit2/config.py", line 65, in __next__
return self._next_entry()
~~~~~~~~~~~~~~~~^^
File ".../pygit2/config.py", line 72, in _next_entry
return ConfigEntry._from_c(centry[0], self)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File ".../pygit2/config.py", line 349, in _from_c
entry.raw_value = entry.raw_value
^^^^^^^^^^^^^^^
File ".../functools.py", line 1126, in __get__
val = self.func(instance)
File ".../pygit2/config.py", line 369, in raw_value
return ffi.string(self.c_value)
~~~~~~~~~~^^^^^^^^^^^^^^
RuntimeError: cannot use string() on <cdata 'char *' NULL>
Root Cause
libgit2 deliberately sets git_config_entry.value = NULL for valueless keys. The entry struct is zero-initialized via git__calloc, and the value field is only populated when a value is actually present (src/libgit2/config_file.c):
entry = git__calloc(1, sizeof(git_config_list_entry));
/* ... */
if (var_value) {
entry->base.entry.value = git__strdup(var_value);
GIT_ERROR_CHECK_ALLOC(entry->base.entry.value);
}libgit2's own code handles the NULL case explicitly (same file):
else if ((!existing->base.entry.value && !value) ||
(existing->base.entry.value && value &&
!strcmp(existing->base.entry.value, value)))So NULL in git_config_entry.value is intentional and documented behavior.
pygit2's _from_c eagerly caches raw_value during iteration (the workaround introduced for #970):
if iterator is not None:
entry.raw_name = entry.raw_name
entry.raw_value = entry.raw_value # ← crashes when c_value is NULL
entry.level = entry.levelAnd raw_value has no NULL guard:
@cached_property
def raw_value(self) -> bytes:
return ffi.string(self.c_value) # RuntimeError if c_value is NULLExpected Behavior
Valueless keys should be represented with value = None (or raw_value = None) rather than raising an exception.
Suggested Fix
Add a NULL check in raw_value:
@cached_property
def raw_value(self) -> bytes | None:
return ffi.string(self.c_value) if self.c_value != ffi.NULL else NoneAnd propagate accordingly in value:
@property
def value(self) -> str | None:
return self.raw_value.decode('utf-8') if self.raw_value is not None else NoneReproduction
$ mkdir /tmp/testrepo && cd /tmp/testrepo && git init
Initialized empty Git repository in /tmp/testrepo/.git/
$ printf '[mysection]\n\tbooleanflag\n' >> .git/config
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[mysection]
booleanflag
$ uv run --with pygit2==1.19.1 python - <<EOF
import pygit2
repo = pygit2.Repository('/tmp/testrepo')
for entry in repo.config:
print(entry.name, entry.value)
EOF
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
File ".../pygit2/config.py", line 65, in __next__
return self._next_entry()
~~~~~~~~~~~~~~~~^^
File ".../pygit2/config.py", line 72, in _next_entry
return ConfigEntry._from_c(centry[0], self)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File ".../pygit2/config.py", line 349, in _from_c
entry.raw_value = entry.raw_value
^^^^^^^^^^^^^^^
File ".../functools.py", line 1126, in __get__
val = self.func(instance)
File ".../pygit2/config.py", line 369, in raw_value
return ffi.string(self.c_value)
~~~~~~~~~~^^^^^^^^^^^^^^
RuntimeError: cannot use string() on <cdata 'char *' NULL>