Skip to content

Commit 5cbdf76

Browse files
codexByron
authored andcommitted
fix(gix): pass filename byte patterns to status and directory walks
Status and untracked-file filtering passed surrogateescaped strings into GixPython. Converting those strings to Rust text raised `UnicodeEncodeError`, while equivalent Git CLI queries correctly matched the original byte names. Encode patterns with the shared lossless helper at the two native boundaries. `StatusPlatform.into_iter()` and `Repository.dirwalk_iter()` already accept byte patterns, so no new API, fallback or path matching implementation is needed. The scripted SHA-1/SHA-256 regressions fail before the fix and pass on both backends. They check matching deleted paths, nonmatching paths and a byte-bearing character class matching an untracked file. Gix checks prohibit CLI execution. Mypy and Ruff pass. Windows operand tests remain excluded because equivalent arbitrary-byte CLI arguments cannot be represented there.
1 parent 35fa001 commit 5cbdf76

4 files changed

Lines changed: 43 additions & 2 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,10 @@ and predate the current fallback; they do not establish native coverage.
395395
Reference names and revisions are encoded with `surrogateescape` before calling
396396
GixPython's byte-capable `Target.Symbolic`, `Repository.try_find_reference`,
397397
`Repository.rev_parse_single`, `Repository.rev_parse` and `RefEdit` APIs.
398+
Status and untracked-file filters likewise pass byte patterns to
399+
`StatusPlatform.into_iter()` and `Repository.dirwalk_iter()`. Their regression
400+
checks matching and nonmatching patterns containing undecodable bytes, and
401+
prohibits CLI fallback in Gix mode.
398402
This preserves non-UTF-8 packed tag names without interpreting reference storage
399403
in Python. `test_non_utf8_tag_names_preserve_bytes` covers lightweight and annotated
400404
tag enumeration in SHA-1 and SHA-256 repositories on both backends and all platforms.

‎git/_backend.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ def is_dirty(command: Any, index: bool, working_tree: bool, untracked: bool, sub
515515
.index_worktree_submodules("configured" if submodules else "all", check_dirty=submodules)
516516
)
517517
dirty = False
518-
with status.into_iter(patterns) as entries:
518+
with status.into_iter([safe_encode(pattern) for pattern in patterns]) as entries:
519519
for item in entries:
520520
if item.summary() is None:
521521
continue
@@ -556,7 +556,7 @@ def untracked_files(command: Any, args: Tuple[Any, ...], options: Dict[str, Any]
556556
raise _Unsupported("invalid pathspec")
557557
walk_options = repo.dirwalk_options().emit_untracked("matching").emit_tracked(False)
558558
result = []
559-
with repo.dirwalk_iter(index, patterns, walk_options) as entries:
559+
with repo.dirwalk_iter(index, [safe_encode(pattern) for pattern in patterns], walk_options) as entries:
560560
for item in entries:
561561
entry = item.entry
562562
if entry.status == "Untracked":

‎test/test_byte_pathspecs.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Path filters preserve Git's filename bytes at the native boundary."""
2+
3+
import contextlib
4+
import os
5+
from unittest.mock import patch
6+
7+
import pytest
8+
9+
from git import Git, _backend
10+
from git.compat import safe_decode
11+
from test.lib.fixtures import fixture_repo
12+
13+
14+
@pytest.mark.skipif(os.name == "nt", reason="Windows CLI arguments cannot express arbitrary filename bytes")
15+
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
16+
def test_byte_pathspecs_preserve_matches(object_format):
17+
with fixture_repo("pathspec-bytes.sh", object_format) as repo:
18+
guard = (
19+
patch.object(Git, "execute", side_effect=AssertionError("unexpected pathspec CLI fallback"))
20+
if _backend.name == "gix"
21+
else contextlib.nullcontext()
22+
)
23+
with guard:
24+
# The tracked path is absent in the worktree, so only its matching filter is dirty.
25+
assert repo.is_dirty(path=safe_decode(b"tracked-\xff"))
26+
assert not repo.is_dirty(path=safe_decode(b"absent-\xff"))
27+
assert repo._get_untracked_files(safe_decode(b"[u\xff]*")) == ["untracked"]
28+
assert repo._get_untracked_files(safe_decode(b"absent-\xff")) == []

‎tests/fixtures/pathspec-bytes.sh‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
git init --initial-branch=main --object-format="$1" repo
4+
cd repo
5+
blob=$(printf 'contents\n' | git hash-object -w --stdin)
6+
printf '100644 %s\ttracked-\377\0' "$blob" | git update-index -z --index-info
7+
git commit -m initial
8+
printf 'untracked\n' >untracked
9+
printf 'other\n' >other

0 commit comments

Comments
 (0)