Skip to content

Commit 6aae0d9

Browse files
codexByron
authored andcommitted
fix(gix): preserve bytes in native submodule configuration values
The dedicated Gix submodule parser returns path, URL and branch values as bytes. Strict decoding forced valid non-UTF-8 values into the CLI fallback. Decode those values with the shared lossless helper while preserving existing checks for duplicate sections, implicit values and raw branch spelling. Record the remaining native API gap: non-UTF-8 subsection names require CLI because generic config keys require text, and the byte-capable branch getter interprets empty/implicit values instead of exposing their raw spelling. No Python configuration semantics or fabricated native success are introduced. The native regression fails before the fix and passes with Git.execute blocked. Scripted tests also verify byte subsection names through fallback and cached field reads against Git 2.54.0. Connected tests pass: nine Gix cases, two CLI cases and seven native-only skips. Ruff, mypy and whitespace checks pass.
1 parent 318a008 commit 6aae0d9

4 files changed

Lines changed: 57 additions & 2 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ and predate the current fallback; they do not establish native coverage.
369369
| Managed `symbolic_ref` | Nonrecursive target lookup | Mutation, command-level missing-reference diagnostics |
370370
| Reference enumeration, `for_each_ref` | Sorted reference names and literal prefixes, preserving symbolic aliases | Dangling symbolic refs, root refs, glob patterns, other formats/options; GIX-25 |
371371
| Managed `config --get KEY` | Merged repository config snapshot | Files/streams, enumeration, mutation; GIX-12 |
372-
| Submodule enumeration and cached fields | Dedicated native `.gitmodules` parser over the existing worktree/blob source, with raw path/URL/branch values | Other/duplicate sections, ambiguous brackets, missing/implicit fields and parser errors; GIX-12 |
372+
| Submodule enumeration and cached fields | Dedicated native `.gitmodules` parser over the existing worktree/blob source, with raw path/URL/branch values | Non-UTF-8 subsection names, other/duplicate sections, ambiguous brackets, missing/implicit fields and parser errors; GIX-12 |
373373
| Commit hooks | No bound native lookup/execution API | All hooks, including absent hooks; GIX-23 |
374374
| Worktree inventory, `worktree list` | Main and linked worktree metadata, including bare main repositories and locks | Prunable entries; GIX-14 |
375375
| `Repo.merge_base`, `Repo.is_ancestor` | Native graph queries for two revisions | Octopus/fork-point and other options |
@@ -1088,6 +1088,16 @@ runtime validation and timings remain attributed to their measured snapshots.
10881088

10891089
### Remaining configuration reads (GIX-12)
10901090

1091+
Native `.gitmodules` reads preserve arbitrary bytes in paths, URLs and branch
1092+
values through `surrogateescape`. Non-UTF-8 subsection names use the CLI fallback:
1093+
GixPython 0.3.0's `ConfigFile.string()` and `.boolean()` require text keys.
1094+
Although `ModulesFile.path()` and `.url()` accept byte names, `.branch()`
1095+
interprets the value: an empty value becomes `HEAD`, and missing and implicit
1096+
values both become `None`. A byte-key raw configuration accessor is needed to
1097+
retain GitPython's configuration contract natively. The scripted
1098+
`submodule-bytes.sh` regressions cover values, names and cached-field rereads;
1099+
the native value test prohibits CLI execution.
1100+
10911101
The October 7 inventory identified 951 remote-enumeration reads, 674 actor
10921102
lookups, 262 remote-property reads, 202 tracking-branch lookups and 111 fetch
10931103
refspec checks. These are historical call-site counts, not additive savings.

‎git/_backend.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ def submodule_config(source: Any) -> Any:
425425
if option != "branch" or config.boolean(key) is not None:
426426
raise _Unsupported("missing or implicit submodule fields")
427427
else:
428-
values[option] = value.decode(defenc)
428+
values[option] = safe_decode(value)
429429
result[name] = values
430430
except _Unsupported as exc:
431431
return _fallback("Submodule.config", str(exc))

‎test/test_submodule.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
33

44
import gc
5+
from io import BytesIO
56
import os
67
import os.path as osp
78
from pathlib import Path
@@ -32,6 +33,34 @@
3233
from test.lib.fixtures import fixture_repo, read_only_fixture, writable_fixture
3334

3435

36+
@pytest.mark.parametrize("scenario, name", [("ascii-name", "module"), ("byte-name", "module-\udcff")])
37+
def test_submodule_configuration_preserves_non_utf8_bytes(scenario, name):
38+
with fixture_repo("submodule-bytes.sh", scenario) as repo:
39+
data = repo.git.config("list", "--null", "--file", ".gitmodules", stdout_as_string=False)
40+
assert b".path\npath-\xfe\0" in data
41+
assert b".url\nurl-\xfd\0" in data
42+
assert b".branch\nrefs/heads/branch-\xfc\0" in data
43+
submodule = repo.submodules[0]
44+
assert submodule.name == name
45+
assert submodule.path == "path-\udcfe"
46+
assert submodule.url == "url-\udcfd"
47+
assert submodule.branch_path == "refs/heads/branch-\udcfc"
48+
submodule._clear_cache()
49+
assert (submodule.path, submodule.url, submodule.branch_path) == (
50+
"path-\udcfe",
51+
"url-\udcfd",
52+
"refs/heads/branch-\udcfc",
53+
)
54+
55+
56+
@pytest.mark.skipif(git._backend.name != "gix", reason="requires the native backend")
57+
def test_native_submodule_configuration_preserves_non_utf8_values():
58+
path = read_only_fixture("submodule-bytes.sh", "ascii-name") / "repo" / ".gitmodules"
59+
with mock.patch.object(Git, "execute", side_effect=AssertionError("Git must not run")):
60+
native = git._backend.submodule_config(BytesIO(path.read_bytes()))
61+
assert native == {"module": {"path": "path-\udcfe", "url": "url-\udcfd", "branch": "refs/heads/branch-\udcfc"}}
62+
63+
3564
@pytest.mark.parametrize("state", ["unborn", "untracked", "detached"])
3665
@pytest.mark.parametrize("remote", [None, ".", "origin"], ids=["working-tree", "local-origin", "origin"])
3766
@patch_git_config("protocol.file.allow", "always")

‎tests/fixtures/submodule-bytes.sh‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
git init -q repo
4+
cd repo
5+
git -c user.name=fixture -c user.email=fixture@example.invalid commit --allow-empty -qm initial
6+
case "$1" in
7+
ascii-name) name=module ;;
8+
byte-name) name=$'module-\377' ;;
9+
*) exit 1 ;;
10+
esac
11+
printf '[submodule "%s"]\npath = path-\376\nurl = url-\375\nbranch = refs/heads/branch-\374\n' "$name" > .gitmodules
12+
modules=$(git hash-object -w .gitmodules)
13+
commit=$(git rev-parse HEAD)
14+
tree=$(printf '100644 blob %s\t.gitmodules\000160000 commit %s\tpath-\376\000' "$modules" "$commit" | git mktree -z)
15+
commit=$(printf 'Add submodule\n' | git -c user.name=fixture -c user.email=fixture@example.invalid commit-tree "$tree" -p "$commit")
16+
git update-ref HEAD "$commit"

0 commit comments

Comments
 (0)