Skip to content

Commit 804bc80

Browse files
authored
Merge pull request #2279 from Keerthana-64/stdin-record-framing
fix: send one object name or path per git stdin record
2 parents f9e74ab + 3a35778 commit 804bc80

6 files changed

Lines changed: 58 additions & 9 deletions

File tree

‎AGENTS.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
Before starting work, read and follow [CONTRIBUTING.md](CONTRIBUTING.md),
44
including the [Prevent agent impersonation](CONTRIBUTING.md#prevent-agent-impersonation)
5-
section governing identification when communicating through a person's account.
5+
section governing agent identification and separation of unaltered user statements
6+
when communicating through a person's account.
67

78
# Commit messages
89

‎CONTRIBUTING.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ example in issue or PR descriptions and comments. AI assistance that does not re
2727
the person as the speaker, such as proofreading or wording polish, does not require
2828
identification.
2929

30+
Even when identifying themselves, agents must not assert on a person's behalf that
31+
that person performed an action, such as reviewing or approving a PR. The person
32+
must make any such statement themselves. If it is included alongside agent-authored
33+
content, it must be supplied by the person and preserved verbatim in a clearly
34+
labeled, separate user-authored section. Agents must not draft, paraphrase, or embed
35+
such statements in their own narration.
36+
3037
Attributing AI assistance in commit metadata, for example with a `Co-authored-by`
3138
trailer, is welcome but not required. Code is reviewed the same way regardless of its
3239
origin.

‎git/cmd.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,9 +1940,15 @@ def _prepare_ref(self, ref: object) -> bytes:
19401940
else:
19411941
refstr = ref
19421942

1943-
if not refstr.endswith("\n"):
1944-
refstr += "\n"
1945-
return refstr.encode(defenc)
1943+
# A line feed terminates a request, so one object name must be one line. An
1944+
# embedded one would queue a second request on the persistent command while
1945+
# only one response line is read back, leaving every later call one response
1946+
# behind, answered with the header of an object it did not ask for.
1947+
if refstr.endswith("\n"):
1948+
refstr = refstr[:-1]
1949+
if "\n" in refstr:
1950+
raise ValueError("Object name %r contains a line feed" % refstr)
1951+
return (refstr + "\n").encode(defenc)
19461952

19471953
def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any) -> "Git.AutoInterrupt":
19481954
cur_val = getattr(self, attr_name)

‎git/index/base.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -525,17 +525,17 @@ def _write_path_to_stdin(
525525
the piped-in files are processed anyway and just in time.
526526
527527
:note:
528-
Newlines are essential here, git's behaviour is somewhat inconsistent on
529-
this depending on the version, hence we try our best to deal with newlines
530-
carefully. Usually the last newline will not be sent, instead we will close
531-
stdin to break the pipe.
528+
Paths are NUL-terminated, so the command has to run with ``-z``. A path can
529+
contain a line feed, and with line-feed separation git would read such a
530+
path as two paths and act on files that were never passed. git also unquotes
531+
a line-feed separated path that begins with a double quote.
532532
"""
533533
fprogress(filepath, False, item)
534534
rval: Union[None, str] = None
535535

536536
if proc.stdin is not None:
537537
try:
538-
proc.stdin.write(("%s\n" % filepath).encode(defenc))
538+
proc.stdin.write(("%s\0" % filepath).encode(defenc))
539539
except OSError as e:
540540
# Pipe broke, usually because some error happened.
541541
raise fmakeexc() from e
@@ -1452,6 +1452,7 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik
14521452
# initialization.
14531453
self.entries # noqa: B018
14541454

1455+
args.append("-z")
14551456
args.append("--stdin")
14561457
kwargs["as_process"] = True
14571458
kwargs["istream"] = subprocess.PIPE

‎test/test_git.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,19 @@ def test_persistent_cat_file_command(self):
593593
self.assertEqual(typename, typename_two)
594594
self.assertEqual(size, size_two)
595595

596+
def test_object_header_rejects_an_embedded_line_feed(self):
597+
hexsha = "b2339455342180c7cc1e9bba3e9f181f7baa5167"
598+
git = Git(self.rorepo.working_dir)
599+
header = git.get_object_header(hexsha)
600+
601+
# A single trailing line feed is the request terminator, not a second request.
602+
self.assertEqual(git.get_object_header(hexsha + "\n"), header)
603+
604+
self.assertRaises(ValueError, git.get_object_header, "HEAD\nHEAD")
605+
606+
# The persistent command is still in step, so this is not HEAD's header.
607+
self.assertEqual(git.get_object_header(hexsha), header)
608+
596609
def test_version_info(self):
597610
"""The version_info attribute is a tuple of up to four ints."""
598611
v = self.git.version_info

‎test/test_index.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,6 +1878,27 @@ def test_checkout_pathlike(self, tmp_path, path_type, absolute, directory, conta
18781878
for name, data in files.items():
18791879
assert (tmp_path / name).read_bytes() == data
18801880

1881+
@pytest.mark.skipif(os.name == "nt", reason="Line feeds and quotes are not valid Windows filenames")
1882+
def test_checkout_sends_each_path_as_one_record(self, tmp_path):
1883+
with Repo.init(tmp_path) as repo:
1884+
nested = tmp_path / "nested"
1885+
nested.mkdir()
1886+
(nested / "first\noutside").write_bytes(b"nested")
1887+
(tmp_path / "outside").write_bytes(b"committed")
1888+
(tmp_path / '"quoted"').write_bytes(b"quoted")
1889+
repo.index.add(["nested", "outside", '"quoted"'])
1890+
1891+
(nested / "first\noutside").unlink()
1892+
(tmp_path / '"quoted"').unlink()
1893+
(tmp_path / "outside").write_bytes(b"local")
1894+
1895+
checked_out = {"nested/first\noutside", '"quoted"'}
1896+
assert set(repo.index.checkout(["nested", '"quoted"'], force=True)) == checked_out
1897+
assert (nested / "first\noutside").read_bytes() == b"nested"
1898+
assert (tmp_path / '"quoted"').read_bytes() == b"quoted"
1899+
# Neither "nested/first" nor "outside" was requested.
1900+
assert (tmp_path / "outside").read_bytes() == b"local"
1901+
18811902

18821903
class TestIndexUtils:
18831904
@pytest.mark.parametrize("file_path_type", [str, Path])

0 commit comments

Comments
 (0)