Skip to content

Commit 28c34c5

Browse files
authored
Merge pull request #2125 from gitpython-developers/copilot/address-review-comments-2116
Harden commit trailer subprocess handling and align trailer I/O paths
2 parents b5b87dd + 633abdb commit 28c34c5

File tree

2 files changed

+88
-17
lines changed

2 files changed

+88
-17
lines changed

git/objects/commit.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -450,14 +450,7 @@ def trailers_list(self) -> List[Tuple[str, str]]:
450450
:return:
451451
List containing key-value tuples of whitespace stripped trailer information.
452452
"""
453-
cmd = ["git", "interpret-trailers", "--parse"]
454-
proc: Git.AutoInterrupt = self.repo.git.execute( # type: ignore[call-overload]
455-
cmd,
456-
as_process=True,
457-
istream=PIPE,
458-
)
459-
trailer: str = proc.communicate(str(self.message).encode())[0].decode("utf8")
460-
trailer = trailer.strip()
453+
trailer = self._interpret_trailers(self.repo, self.message, ["--parse"], encoding=self.encoding).strip()
461454

462455
if not trailer:
463456
return []
@@ -469,6 +462,27 @@ def trailers_list(self) -> List[Tuple[str, str]]:
469462

470463
return trailer_list
471464

465+
@classmethod
466+
def _interpret_trailers(
467+
cls,
468+
repo: "Repo",
469+
message: Union[str, bytes],
470+
trailer_args: Sequence[str],
471+
encoding: str = default_encoding,
472+
) -> str:
473+
message_bytes = message if isinstance(message, bytes) else message.encode(encoding, errors="strict")
474+
cmd = [repo.git.GIT_PYTHON_GIT_EXECUTABLE, "interpret-trailers", *trailer_args]
475+
proc: Git.AutoInterrupt = repo.git.execute( # type: ignore[call-overload]
476+
cmd,
477+
as_process=True,
478+
istream=PIPE,
479+
)
480+
try:
481+
stdout_bytes, _ = proc.communicate(message_bytes)
482+
return stdout_bytes.decode(encoding, errors="strict")
483+
finally:
484+
finalize_process(proc)
485+
472486
@property
473487
def trailers_dict(self) -> Dict[str, List[str]]:
474488
"""Get the trailers of the message as a dictionary.
@@ -699,15 +713,7 @@ def create_from_tree(
699713
trailer_args.append("--trailer")
700714
trailer_args.append(f"{key}: {val}")
701715

702-
cmd = [repo.git.GIT_PYTHON_GIT_EXECUTABLE, "interpret-trailers"] + trailer_args
703-
proc: Git.AutoInterrupt = repo.git.execute( # type: ignore[call-overload]
704-
cmd,
705-
as_process=True,
706-
istream=PIPE,
707-
)
708-
stdout_bytes, _ = proc.communicate(str(message).encode())
709-
finalize_process(proc)
710-
message = stdout_bytes.decode("utf8")
716+
message = cls._interpret_trailers(repo, str(message), trailer_args)
711717
# END apply trailers
712718

713719
# CREATE NEW COMMIT

test/test_commit.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,71 @@ def test_create_from_tree_with_trailers_list(self, rw_dir):
622622
"Issue": ["456"],
623623
}
624624

625+
@with_rw_directory
626+
def test_create_from_tree_with_non_utf8_trailers(self, rw_dir):
627+
"""Test that trailer creation and parsing respect the configured commit encoding."""
628+
rw_repo = Repo.init(osp.join(rw_dir, "test_trailers_non_utf8"))
629+
with rw_repo.config_writer() as writer:
630+
writer.set_value("i18n", "commitencoding", "ISO-8859-1")
631+
632+
path = osp.join(str(rw_repo.working_tree_dir), "hello.txt")
633+
touch(path)
634+
rw_repo.index.add([path])
635+
tree = rw_repo.index.write_tree()
636+
637+
commit = Commit.create_from_tree(
638+
rw_repo,
639+
tree,
640+
"Résumé",
641+
head=True,
642+
trailers={"Reviewed-by": "André <andre@example.com>"},
643+
)
644+
645+
assert commit.encoding == "ISO-8859-1"
646+
assert "Résumé" in commit.message
647+
assert "Reviewed-by: André <andre@example.com>" in commit.message
648+
assert commit.trailers_list == [("Reviewed-by", "André <andre@example.com>")]
649+
650+
@with_rw_directory
651+
def test_trailers_list_with_non_utf8_message_bytes(self, rw_dir):
652+
"""Test that trailer parsing handles non-UTF-8 commit message bytes."""
653+
rw_repo = Repo.init(osp.join(rw_dir, "test_trailers_non_utf8_bytes"))
654+
with rw_repo.config_writer() as writer:
655+
writer.set_value("i18n", "commitencoding", "ISO-8859-1")
656+
657+
path = osp.join(str(rw_repo.working_tree_dir), "hello.txt")
658+
touch(path)
659+
rw_repo.index.add([path])
660+
tree = rw_repo.index.write_tree()
661+
662+
commit = Commit.create_from_tree(
663+
rw_repo,
664+
tree,
665+
"Résumé",
666+
head=True,
667+
trailers={"Reviewed-by": "André <andre@example.com>"},
668+
)
669+
670+
bytes_commit = Commit(
671+
rw_repo,
672+
commit.binsha,
673+
message=commit.message.encode(commit.encoding),
674+
encoding=commit.encoding,
675+
)
676+
677+
assert bytes_commit.trailers_list == [("Reviewed-by", "André <andre@example.com>")]
678+
679+
def test_interpret_trailers_encodes_before_launching_process(self):
680+
"""Test that encoding failures happen before spawning interpret-trailers."""
681+
repo = Mock()
682+
repo.git = Mock()
683+
repo.git.GIT_PYTHON_GIT_EXECUTABLE = "git"
684+
685+
with self.assertRaises(UnicodeEncodeError):
686+
Commit._interpret_trailers(repo, "Euro: €", ["--parse"], encoding="ISO-8859-1")
687+
688+
repo.git.execute.assert_not_called()
689+
625690
@with_rw_directory
626691
def test_index_commit_with_trailers(self, rw_dir):
627692
"""Test that IndexFile.commit() supports adding trailers."""

0 commit comments

Comments
 (0)