Skip to content

Commit c5aac40

Browse files
committed
encoding: rename internal string helpers and drop public to_bytes
Rename internal string encoders for clarity: - maybe_string -> decode_string - to_bytes -> encode_string Keep the new filesystem-path helpers (decode_fs_path, encode_fs_path) internal; use utils.encode_fs_path / utils.encode_string in __init__.py. Remove pygit2.to_bytes from the public API; it was an undocumented accidental public helper. Update changelog. Assisted-by: Kimi K3
1 parent d65d61e commit c5aac40

13 files changed

Lines changed: 119 additions & 113 deletions

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
`Config.__init__()`, and improve `Config` documentation
88
[#1468](https://github.com/libgit2/pygit2/pull/1468)
99

10-
- Fix handling of non-UTF-8 file paths: add `decode_fs_path` and
11-
`encode_fs_path`, use the filesystem codec for paths in `Repository.status()`,
12-
`DiffFile.path`, index entries, checkout callbacks, and related APIs
10+
- Fix `UnicodeDecodeError` with non-UTF-8 file paths in `Repository.status()`,
11+
`DiffFile.path`, index paths, checkout callbacks, and related APIs
1312
[#1451](https://github.com/libgit2/pygit2/issues/1451)
1413
[#1469](https://github.com/libgit2/pygit2/pull/1469)
1514

@@ -42,6 +41,8 @@ Breaking changes:
4241
- Remove deprecated `Remote.ls_remotes(...)`, use `Remote.list_heads(...)`
4342
instead
4443

44+
- Remove `pygit2.to_bytes`; it was an undocumented accidental public API
45+
4546

4647
# 1.19.3 (2026-06-13)
4748

pygit2/__init__.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import typing
3232

3333
# High level API
34-
from . import enums
34+
from . import enums, utils
3535
from ._build import __version__
3636

3737
# Low level API
@@ -366,7 +366,7 @@
366366
from .settings import Settings
367367
from .submodules import Submodule
368368
from .transaction import ReferenceTransaction
369-
from .utils import decode_fs_path, encode_fs_path, to_bytes, to_str
369+
from .utils import to_str
370370

371371
# Features
372372
features = enums.Feature(C.git_libgit2_features())
@@ -431,28 +431,28 @@ def init_repository(
431431
options.mode = mode
432432

433433
if workdir_path:
434-
workdir_path_ref = ffi.new('char []', encode_fs_path(workdir_path))
434+
workdir_path_ref = ffi.new('char []', utils.encode_fs_path(workdir_path))
435435
options.workdir_path = workdir_path_ref
436436

437437
if description:
438-
description_ref = ffi.new('char []', to_bytes(description))
438+
description_ref = ffi.new('char []', utils.encode_string(description))
439439
options.description = description_ref
440440

441441
if template_path:
442-
template_path_ref = ffi.new('char []', encode_fs_path(template_path))
442+
template_path_ref = ffi.new('char []', utils.encode_fs_path(template_path))
443443
options.template_path = template_path_ref
444444

445445
if initial_head:
446-
initial_head_ref = ffi.new('char []', to_bytes(initial_head))
446+
initial_head_ref = ffi.new('char []', utils.encode_string(initial_head))
447447
options.initial_head = initial_head_ref
448448

449449
if origin_url:
450-
origin_url_ref = ffi.new('char []', to_bytes(origin_url))
450+
origin_url_ref = ffi.new('char []', utils.encode_string(origin_url))
451451
options.origin_url = origin_url_ref
452452

453453
# Call
454454
crepository = ffi.new('git_repository **')
455-
err = C.git_repository_init_ext(crepository, encode_fs_path(path), options)
455+
err = C.git_repository_init_ext(crepository, utils.encode_fs_path(path), options)
456456
check_error(err)
457457

458458
# Ok
@@ -531,13 +531,13 @@ def clone_repository(
531531
opts.fetch_opts.depth = depth
532532

533533
if checkout_branch:
534-
checkout_branch_ref = ffi.new('char []', to_bytes(checkout_branch))
534+
checkout_branch_ref = ffi.new('char []', utils.encode_string(checkout_branch))
535535
opts.checkout_branch = checkout_branch_ref
536536

537537
with git_fetch_options(payload, opts=opts.fetch_opts):
538538
with git_proxy_options(payload, opts.fetch_opts.proxy_opts, proxy):
539539
crepo = ffi.new('git_repository **')
540-
err = C.git_clone(crepo, to_bytes(url), encode_fs_path(path), opts)
540+
err = C.git_clone(crepo, utils.encode_string(url), utils.encode_fs_path(path), opts)
541541
payload.check_error(err)
542542

543543
# Ok
@@ -560,7 +560,7 @@ def filter_unregister(name: str) -> None:
560560
if FilterList._is_filter_in_use(name):
561561
raise RuntimeError(f"filter still in use: '{name}'")
562562

563-
c_name = to_bytes(name)
563+
c_name = utils.encode_string(name)
564564
err = C.git_filter_unregister(c_name)
565565
check_error(err)
566566

@@ -934,7 +934,6 @@ def filter_unregister(name: str) -> None:
934934
'transaction',
935935
'ReferenceTransaction',
936936
'utils',
937-
'to_bytes',
938937
'to_str',
939938
# __init__ module defined symbols
940939
'features',

pygit2/callbacks.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@
7777
from .utils import (
7878
StrArray,
7979
decode_fs_path,
80+
decode_string,
8081
encode_fs_path,
81-
maybe_string,
82+
encode_string,
8283
ptr_to_bytes,
83-
to_bytes,
8484
)
8585

8686
_Credentials = Username | UserPass | Keypair
@@ -460,7 +460,7 @@ def git_proxy_options(
460460
elif type(proxy) is str:
461461
opts.type = C.GIT_PROXY_SPECIFIED
462462
# Keep url in memory, otherwise memory is freed and bad things happen
463-
payload.__proxy_url = ffi.new('char[]', to_bytes(proxy)) # type: ignore[union-attr]
463+
payload.__proxy_url = ffi.new('char[]', encode_string(proxy)) # type: ignore[union-attr]
464464
opts.url = payload.__proxy_url # type: ignore[union-attr]
465465
else:
466466
raise TypeError('Proxy must be None, True, or a string')
@@ -644,8 +644,8 @@ def _push_update_reference_cb(ref, msg, data):
644644
if not push_update_reference:
645645
return 0
646646

647-
refname = maybe_string(ref)
648-
message = maybe_string(msg)
647+
refname = decode_string(ref)
648+
message = decode_string(msg)
649649
push_update_reference(refname, message)
650650
return 0
651651

@@ -713,7 +713,7 @@ def _update_tips_cb(refname, a, b, data):
713713
if not update_tips:
714714
return 0
715715

716-
s = maybe_string(refname)
716+
s = decode_string(refname)
717717
a = Oid(raw=bytes(ffi.buffer(a)[:]))
718718
b = Oid(raw=bytes(ffi.buffer(b)[:]))
719719
update_tips(s, a, b)
@@ -727,8 +727,8 @@ def _update_tips_cb(refname, a, b, data):
727727

728728
def get_credentials(fn, url, username, allowed):
729729
"""Call fn and return the credentials object."""
730-
url_str = maybe_string(url)
731-
username_str = maybe_string(username)
730+
url_str = decode_string(url)
731+
username_str = decode_string(username)
732732

733733
creds = fn(url_str, username_str, allowed)
734734

@@ -746,33 +746,33 @@ def get_credentials(fn, url, username, allowed):
746746
if cred_type == CredentialType.USERPASS_PLAINTEXT:
747747
name, passwd = credential_tuple
748748
err = C.git_credential_userpass_plaintext_new(
749-
ccred, to_bytes(name), to_bytes(passwd)
749+
ccred, encode_string(name), encode_string(passwd)
750750
)
751751

752752
elif cred_type == CredentialType.SSH_KEY:
753753
name, pubkey, privkey, passphrase = credential_tuple
754-
name = to_bytes(name)
754+
name = encode_string(name)
755755
if pubkey is None and privkey is None:
756756
err = C.git_credential_ssh_key_from_agent(ccred, name)
757757
else:
758758
err = C.git_credential_ssh_key_new(
759-
ccred, name, to_bytes(pubkey), to_bytes(privkey), to_bytes(passphrase)
759+
ccred, name, encode_string(pubkey), encode_string(privkey), encode_string(passphrase)
760760
)
761761

762762
elif cred_type == CredentialType.USERNAME:
763763
(name,) = credential_tuple
764-
err = C.git_credential_username_new(ccred, to_bytes(name))
764+
err = C.git_credential_username_new(ccred, encode_string(name))
765765

766766
elif cred_type == CredentialType.SSH_MEMORY:
767767
name, pubkey, privkey, passphrase = credential_tuple
768768
if pubkey is None and privkey is None:
769769
raise TypeError('SSH keys from memory are empty')
770770
err = C.git_credential_ssh_key_memory_new(
771771
ccred,
772-
to_bytes(name),
773-
to_bytes(pubkey),
774-
to_bytes(privkey),
775-
to_bytes(passphrase),
772+
encode_string(name),
773+
encode_string(pubkey),
774+
encode_string(privkey),
775+
encode_string(passphrase),
776776
)
777777
else:
778778
raise TypeError('unsupported credential type')

pygit2/config.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
# Import from pygit2
3636
from .errors import check_error
3737
from .ffi import C, ffi
38-
from .utils import encode_fs_path, to_bytes
38+
from .utils import encode_fs_path, encode_string
3939

4040
if TYPE_CHECKING:
4141
from ._libgit2.ffi import GitConfigC, GitConfigEntryC
@@ -46,7 +46,7 @@ def str_to_bytes(value: str | bytes, name: str) -> bytes:
4646
if not isinstance(value, str):
4747
raise TypeError(f'{name} must be a string')
4848

49-
return to_bytes(value)
49+
return encode_string(value)
5050

5151

5252
class ConfigIterator:
@@ -183,7 +183,7 @@ def __setitem__(self, key: str | bytes, value: bool | int | str | bytes) -> None
183183
elif isinstance(value, int):
184184
err = C.git_config_set_int64(self._config, key, value)
185185
else:
186-
err = C.git_config_set_string(self._config, key, to_bytes(value))
186+
err = C.git_config_set_string(self._config, key, encode_string(value))
187187

188188
check_error(err)
189189

@@ -215,7 +215,7 @@ def get_multivar(
215215
to filter the variables we're interested in.
216216
"""
217217
name = str_to_bytes(name, 'name')
218-
regex_bytes = to_bytes(regex or None)
218+
regex_bytes = encode_string(regex or None)
219219

220220
citer = ffi.new('git_config_iterator **')
221221
err = C.git_config_multivar_iterator_new(citer, self._config, name, regex_bytes)
@@ -305,15 +305,15 @@ def snapshot(self) -> 'Config':
305305
@staticmethod
306306
def parse_bool(text: str) -> bool:
307307
res = ffi.new('int *')
308-
err = C.git_config_parse_bool(res, to_bytes(text))
308+
err = C.git_config_parse_bool(res, encode_string(text))
309309
check_error(err)
310310

311311
return res[0] != 0
312312

313313
@staticmethod
314314
def parse_int(text: str) -> int:
315315
res = ffi.new('int64_t *')
316-
err = C.git_config_parse_int64(res, to_bytes(text))
316+
err = C.git_config_parse_int64(res, encode_string(text))
317317
check_error(err)
318318

319319
return res[0]

pygit2/filter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from ._pygit2 import Blob, FilterSource
3333
from .errors import check_error
3434
from .ffi import C, ffi
35-
from .utils import encode_fs_path, to_bytes
35+
from .utils import encode_fs_path, encode_string
3636

3737
if TYPE_CHECKING:
3838
from ._libgit2.ffi import GitFilterListC
@@ -152,7 +152,7 @@ def _is_filter_in_use(cls, name: str) -> bool:
152152
def __contains__(self, name: str) -> bool:
153153
if not isinstance(name, str):
154154
raise TypeError('argument must be str')
155-
c_name = to_bytes(name)
155+
c_name = encode_string(name)
156156
result = C.git_filter_list_contains(self._pointer, c_name)
157157
return bool(result)
158158

pygit2/options.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
from .errors import check_error
3636
from .ffi import C, ffi
37-
from .utils import decode_fs_path, encode_fs_path, to_bytes, to_str
37+
from .utils import decode_fs_path, encode_fs_path, encode_string, to_str
3838

3939
if TYPE_CHECKING:
4040
from ._libgit2.ffi import NULL_TYPE, ArrayC, char, char_pointer
@@ -520,7 +520,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED)
520520
check_args(option_type, arg1, arg2, 1)
521521

522522
agent = arg1
523-
agent_bytes = to_bytes(agent)
523+
agent_bytes = encode_string(agent)
524524
agent_cdata = ffi.new('char[]', agent_bytes)
525525

526526
err = C.git_libgit2_opts(option_type, agent_cdata)
@@ -531,7 +531,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED)
531531
check_args(option_type, arg1, arg2, 1)
532532

533533
ciphers = arg1
534-
ciphers_bytes = to_bytes(ciphers)
534+
ciphers_bytes = encode_string(ciphers)
535535
ciphers_cdata = ffi.new('char[]', ciphers_bytes)
536536

537537
err = C.git_libgit2_opts(option_type, ciphers_cdata)
@@ -669,7 +669,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED)
669669
ext_strings: list[ArrayC[char]] = [] # Keep references during the call
670670

671671
for i, ext in enumerate(extensions):
672-
ext_bytes = to_bytes(ext)
672+
ext_bytes = encode_string(ext)
673673
ext_string: ArrayC[char] = ffi.new('char[]', ext_bytes)
674674
ext_strings.append(ext_string)
675675
ext_array[i] = ffi.cast('char *', ext_string)
@@ -781,7 +781,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED)
781781
check_args(option_type, arg1, arg2, 1)
782782

783783
product = arg1
784-
product_bytes = to_bytes(product)
784+
product_bytes = encode_string(product)
785785
product_cdata = ffi.new('char[]', product_bytes)
786786

787787
err = C.git_libgit2_opts(option_type, product_cdata)

pygit2/rebase.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from .errors import check_error
3232
from .ffi import C, ffi
3333
from .index import Index
34-
from .utils import maybe_string
34+
from .utils import decode_string
3535

3636
if TYPE_CHECKING:
3737
from ._libgit2.ffi import GitRebaseC, GitRebaseOperationC
@@ -70,7 +70,7 @@ def __init__(self, type: RebaseOperationType, id: Oid, exec: 'str | None') -> No
7070
def _from_c(cls, coperation: 'GitRebaseOperationC') -> 'RebaseOperation':
7171
type = RebaseOperationType(coperation.type)
7272
id = Oid(raw=bytes(ffi.buffer(ffi.addressof(coperation, 'id'))[:]))
73-
exec = maybe_string(coperation.exec)
73+
exec = decode_string(coperation.exec)
7474
return cls(type, id, exec)
7575

7676
def __repr__(self) -> str:
@@ -229,7 +229,7 @@ def abort(self) -> None:
229229
@property
230230
def orig_head_name(self) -> 'str | None':
231231
"""The original HEAD ref name."""
232-
return maybe_string(C.git_rebase_orig_head_name(self._rebase))
232+
return decode_string(C.git_rebase_orig_head_name(self._rebase))
233233

234234
@property
235235
def orig_head_id(self) -> Oid:
@@ -240,7 +240,7 @@ def orig_head_id(self) -> Oid:
240240
@property
241241
def onto_name(self) -> 'str | None':
242242
"""The onto ref name."""
243-
return maybe_string(C.git_rebase_onto_name(self._rebase))
243+
return decode_string(C.git_rebase_onto_name(self._rebase))
244244

245245
@property
246246
def onto_id(self) -> Oid:

pygit2/refspec.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
# Import from pygit2
2929
from .errors import check_error
3030
from .ffi import C, ffi
31-
from .utils import to_bytes
31+
from .utils import encode_string
3232

3333

3434
class Refspec:
@@ -67,16 +67,16 @@ def src_matches(self, ref: str) -> bool:
6767
"""Return True if the given string matches the source of this refspec,
6868
False otherwise.
6969
"""
70-
return bool(C.git_refspec_src_matches(self._refspec, to_bytes(ref)))
70+
return bool(C.git_refspec_src_matches(self._refspec, encode_string(ref)))
7171

7272
def dst_matches(self, ref: str) -> bool:
7373
"""Return True if the given string matches the destination of this
7474
refspec, False otherwise."""
75-
return bool(C.git_refspec_dst_matches(self._refspec, to_bytes(ref)))
75+
return bool(C.git_refspec_dst_matches(self._refspec, encode_string(ref)))
7676

7777
def _transform(self, ref: str, fn: Callable) -> str:
7878
buf = ffi.new('git_buf *', (ffi.NULL, 0))
79-
err = fn(buf, self._refspec, to_bytes(ref))
79+
err = fn(buf, self._refspec, encode_string(ref))
8080
check_error(err)
8181

8282
try:

0 commit comments

Comments
 (0)