Skip to content

Commit a948fb4

Browse files
committed
fix: handle non-UTF-8 packed refs
1 parent f44c1fb commit a948fb4

2 files changed

Lines changed: 46 additions & 3 deletions

File tree

git/refs/symbolic.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,18 @@ def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]:
149149
The packed refs file will be kept open as long as we iterate.
150150
"""
151151
try:
152-
with open(cls._get_packed_refs_path(repo), "rt", encoding="UTF-8") as fp:
153-
for line in fp:
154-
line = line.strip()
152+
# Read in binary mode and decode leniently: ref names packed by `git
153+
# pack-refs` are arbitrary byte strings and are not guaranteed to be valid
154+
# UTF-8 (see e.g. core.precomposeUnicode-unaffected filesystems, or refs
155+
# created on a system with a different locale). Decoding strictly as UTF-8
156+
# would raise UnicodeDecodeError and make the entire packed-refs file
157+
# unreadable because of a single such ref. Use the same lenient
158+
# byte<->str roundtrip ("surrogateescape") already used elsewhere in
159+
# GitPython (see :func:`git.compat.safe_decode`) so that such refs are
160+
# preserved rather than crashing iteration.
161+
with open(cls._get_packed_refs_path(repo), "rb") as fp:
162+
for line_bytes in fp:
163+
line = line_bytes.decode(defenc, "surrogateescape").strip()
155164
if not line:
156165
continue
157166
if line.startswith("#"):

test/test_refs.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,3 +893,37 @@ def test_validity_ref_names(self):
893893

894894
# Valid reference name should not raise.
895895
check_ref("valid/ref/name")
896+
897+
def test_packed_refs_with_non_utf8_ref_name_does_not_raise(self):
898+
# See: https://github.com/gitpython-developers/GitPython/issues/2064
899+
#
900+
# Tag (and other) ref names stored in .git/packed-refs are arbitrary byte
901+
# strings as far as Git is concerned - they are not guaranteed to be valid
902+
# UTF-8. Iterating packed refs must not raise UnicodeDecodeError just because
903+
# one of the packed ref names happens to contain non-UTF-8 bytes.
904+
with tempfile.TemporaryDirectory() as tmp_dir:
905+
base_dir = Path(tmp_dir)
906+
with self._repo_with_initial_commit(base_dir) as repo:
907+
sha = repo.head.commit.hexsha
908+
909+
# A normal, valid-UTF-8 tag, packed alongside the problematic one.
910+
good_tag_ref = b"refs/tags/good-tag"
911+
# A tag name containing a byte sequence that is not valid UTF-8
912+
# (0xE9 here is not a valid standalone/leading UTF-8 byte in this
913+
# position), similar to what `git pack-refs` can legitimately
914+
# produce for a non-UTF-8 ref name.
915+
bad_tag_ref = b"refs/tags/release-\xe9tage"
916+
917+
packed_refs_path = Path(repo.common_dir) / "packed-refs"
918+
with open(packed_refs_path, "wb") as f:
919+
f.write(b"# pack-refs with: peeled fully-peeled sorted\n")
920+
f.write(sha.encode("ascii") + b" " + good_tag_ref + b"\n")
921+
f.write(sha.encode("ascii") + b" " + bad_tag_ref + b"\n")
922+
923+
# Must not raise UnicodeDecodeError.
924+
tags = repo.tags
925+
926+
tag_names = {t.name.encode("utf-8", "surrogateescape") for t in tags}
927+
assert b"good-tag" in tag_names
928+
assert b"release-\xe9tage" in tag_names
929+
assert len(tags) == 2

0 commit comments

Comments
 (0)