|
| 1 | +"""Git output carries paths and reference names with arbitrary bytes.""" |
| 2 | + |
| 3 | +import os |
| 4 | +import sys |
| 5 | + |
| 6 | +import pytest |
| 7 | + |
| 8 | +from git import Git, GitCommandError, Repo, RemoteProgress |
| 9 | +from git.cmd import handle_process_output |
| 10 | +from git.compat import safe_decode, safe_encode |
| 11 | +from test.lib.fixtures import writable_fixture |
| 12 | + |
| 13 | + |
| 14 | +@pytest.mark.parametrize("as_process", [False, True]) |
| 15 | +def test_text_process_output_preserves_bytes(as_process): |
| 16 | + proc = Git().execute( |
| 17 | + [sys.executable, "-c", "import sys; sys.stdout.buffer.write(b'name-\\xff\\n')"], |
| 18 | + universal_newlines=True, |
| 19 | + as_process=as_process, |
| 20 | + ) |
| 21 | + if as_process: |
| 22 | + lines = [] |
| 23 | + handle_process_output(proc, lines.append, None, decode_streams=False) |
| 24 | + proc.wait() |
| 25 | + assert safe_encode("".join(lines)) == b"name-\xff\n" |
| 26 | + else: |
| 27 | + assert safe_encode(proc) == b"name-\xff" |
| 28 | + |
| 29 | + |
| 30 | +def test_binary_process_output_handler_preserves_bytes(): |
| 31 | + proc = Git().execute( |
| 32 | + [sys.executable, "-c", "import sys; sys.stdout.buffer.write(b'name-\\xff\\n')"], as_process=True |
| 33 | + ) |
| 34 | + lines = [] |
| 35 | + handle_process_output(proc, lines.append, None) |
| 36 | + proc.wait() |
| 37 | + assert safe_encode("".join(lines)) == b"name-\xff\n" |
| 38 | + |
| 39 | + |
| 40 | +def test_process_error_preserves_previously_read_bytes(): |
| 41 | + proc = Git().execute([sys.executable, "-c", "raise SystemExit(1)"], as_process=True) |
| 42 | + with pytest.raises(GitCommandError) as error: |
| 43 | + proc.wait(stderr=safe_decode(b"fatal: name-\xff")) |
| 44 | + assert "name-\udcff" in str(error.value) |
| 45 | + |
| 46 | + |
| 47 | +def test_progress_preserves_byte_diagnostics(): |
| 48 | + progress = RemoteProgress() |
| 49 | + progress._parse_progress_line(b"fatal: name-\xff") |
| 50 | + assert safe_encode(progress.error_lines[0]) == b"fatal: name-\xff" |
| 51 | + |
| 52 | + |
| 53 | +@pytest.mark.skipif(os.name == "nt", reason="POSIX timeout and byte-argument handling") |
| 54 | +def test_timeout_preserves_byte_arguments(): |
| 55 | + with pytest.raises(GitCommandError) as error: |
| 56 | + Git().execute( |
| 57 | + [sys.executable, "-c", "import time; time.sleep(5)", safe_decode(b"name-\xff")], |
| 58 | + kill_after_timeout=0.1, |
| 59 | + ) |
| 60 | + assert "name-\udcff" in str(error.value) |
| 61 | + |
| 62 | + |
| 63 | +@pytest.mark.skipif(os.name == "nt", reason="Windows arguments cannot express arbitrary path bytes") |
| 64 | +def test_clone_preserves_byte_path_in_failure(): |
| 65 | + with writable_fixture("remote-byte-output.sh", "sha1") as temporary: |
| 66 | + root = temporary.path() |
| 67 | + with pytest.raises(GitCommandError) as error: |
| 68 | + Repo.clone_from(root / safe_decode(b"missing-\xff"), root / "clone") |
| 69 | + assert "missing-\udcff" in str(error.value) |
| 70 | + |
| 71 | + |
| 72 | +@pytest.mark.parametrize("object_format", ["sha1", "sha256"]) |
| 73 | +def test_fetch_and_push_preserve_byte_reference_results(object_format): |
| 74 | + with writable_fixture("remote-byte-output.sh", object_format) as temporary: |
| 75 | + root = temporary.path() |
| 76 | + with Repo(root / "repo") as repo: |
| 77 | + origin = repo.create_remote("origin", str(root / "source")) |
| 78 | + fetched = origin.fetch() |
| 79 | + assert {safe_encode(info.ref.path) for info in fetched} == { |
| 80 | + b"refs/remotes/origin/main", |
| 81 | + b"refs/remotes/origin/branch-\xff", |
| 82 | + } |
| 83 | + with Repo(root / "source") as repo: |
| 84 | + target = repo.create_remote("target", str(root / "target")) |
| 85 | + pushed = target.push("refs/heads/*:refs/heads/*") |
| 86 | + assert not pushed.error |
| 87 | + assert {safe_encode(info.remote_ref_string) for info in pushed} == { |
| 88 | + b"refs/heads/main", |
| 89 | + b"refs/heads/branch-\xff", |
| 90 | + } |
0 commit comments