X Tutup
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions git/index/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,7 @@ def commit(
author_date: Union[datetime.datetime, str, None] = None,
commit_date: Union[datetime.datetime, str, None] = None,
skip_hooks: bool = False,
trailers: Union[None, "Dict[str, str]", "List[Tuple[str, str]]"] = None,
) -> Commit:
"""Commit the current default index file, creating a
:class:`~git.objects.commit.Commit` object.
Expand Down Expand Up @@ -1169,6 +1170,7 @@ def commit(
committer=committer,
author_date=author_date,
commit_date=commit_date,
trailers=trailers,
)
if not skip_hooks:
run_commit_hook("post-commit", self)
Expand Down
30 changes: 30 additions & 0 deletions git/objects/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ def create_from_tree(
committer: Union[None, Actor] = None,
author_date: Union[None, str, datetime.datetime] = None,
commit_date: Union[None, str, datetime.datetime] = None,
trailers: Union[None, Dict[str, str], List[Tuple[str, str]]] = None,
) -> "Commit":
"""Commit the given tree, creating a :class:`Commit` object.
Expand Down Expand Up @@ -609,6 +610,14 @@ def create_from_tree(
:param commit_date:
The timestamp for the committer field.
:param trailers:
Optional trailer key-value pairs to append to the commit message.
Can be a dictionary mapping trailer keys to values, or a list of
``(key, value)`` tuples (useful when the same key appears multiple
times, e.g. multiple ``Signed-off-by`` trailers). Trailers are
appended using ``git interpret-trailers``.
See :manpage:`git-interpret-trailers(1)`.
:return:
:class:`Commit` object representing the new commit.
Expand Down Expand Up @@ -678,6 +687,27 @@ def create_from_tree(
tree = repo.tree(tree)
# END tree conversion

# APPLY TRAILERS
if trailers:
trailer_args: List[str] = []
if isinstance(trailers, dict):
for key, val in trailers.items():
trailer_args.append("--trailer")
trailer_args.append(f"{key}: {val}")
else:
for key, val in trailers:
trailer_args.append("--trailer")
trailer_args.append(f"{key}: {val}")

cmd = ["git", "interpret-trailers"] + trailer_args
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd hard-codes the git executable as the literal string "git". This bypasses GitPython’s configured executable (repo.git.GIT_PYTHON_GIT_EXECUTABLE) and can break environments that override it (custom path/wrapper). Consider building the command using the configured executable (and ideally aligning with the existing trailers_list implementation too).

Suggested change
cmd = ["git", "interpret-trailers"] + trailer_args
cmd = [repo.git.GIT_PYTHON_GIT_EXECUTABLE, "interpret-trailers"] + trailer_args

Copilot uses AI. Check for mistakes.
proc: Git.AutoInterrupt = repo.git.execute( # type: ignore[call-overload]
cmd,
as_process=True,
istream=PIPE,
)
message = proc.communicate(str(message).encode())[0].decode("utf8")
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running git interpret-trailers via repo.git.execute(..., as_process=True), non-zero exit codes are not surfaced unless the process is explicitly waited/finalized. After proc.communicate(...), call finalize_process(proc) (or proc.wait()) so failures (e.g., unsupported git version, config errors) raise GitCommandError instead of silently producing an incomplete/empty message.

Suggested change
message = proc.communicate(str(message).encode())[0].decode("utf8")
stdout_bytes, _ = proc.communicate(str(message).encode())
finalize_process(proc)
message = stdout_bytes.decode("utf8")

Copilot uses AI. Check for mistakes.
# END apply trailers

# CREATE NEW COMMIT
new_commit = cls(
repo,
Expand Down
74 changes: 74 additions & 0 deletions test/test_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,77 @@ def test_commit_co_authors(self):
Actor("test_user_2", "another_user-email@github.com"),
Actor("test_user_3", "test_user_3@github.com"),
]

@with_rw_directory
def test_create_from_tree_with_trailers_dict(self, rw_dir):
"""Test that create_from_tree supports adding trailers via a dict."""
rw_repo = Repo.init(osp.join(rw_dir, "test_trailers_dict"))
path = osp.join(str(rw_repo.working_tree_dir), "hello.txt")
touch(path)
rw_repo.index.add([path])
tree = rw_repo.index.write_tree()

trailers = {"Issue": "123", "Signed-off-by": "Test User <test@test.com>"}
commit = Commit.create_from_tree(
rw_repo,
tree,
"Test commit with trailers",
head=True,
trailers=trailers,
)

assert "Issue: 123" in commit.message
assert "Signed-off-by: Test User <test@test.com>" in commit.message
assert commit.trailers_dict == {
"Issue": ["123"],
"Signed-off-by": ["Test User <test@test.com>"],
}

@with_rw_directory
def test_create_from_tree_with_trailers_list(self, rw_dir):
"""Test that create_from_tree supports adding trailers via a list of tuples."""
rw_repo = Repo.init(osp.join(rw_dir, "test_trailers_list"))
path = osp.join(str(rw_repo.working_tree_dir), "hello.txt")
touch(path)
rw_repo.index.add([path])
tree = rw_repo.index.write_tree()

trailers = [
("Signed-off-by", "Alice <alice@example.com>"),
("Signed-off-by", "Bob <bob@example.com>"),
("Issue", "456"),
]
commit = Commit.create_from_tree(
rw_repo,
tree,
"Test commit with multiple trailers",
head=True,
trailers=trailers,
)

assert "Signed-off-by: Alice <alice@example.com>" in commit.message
assert "Signed-off-by: Bob <bob@example.com>" in commit.message
assert "Issue: 456" in commit.message
assert commit.trailers_dict == {
"Signed-off-by": ["Alice <alice@example.com>", "Bob <bob@example.com>"],
"Issue": ["456"],
}

@with_rw_directory
def test_index_commit_with_trailers(self, rw_dir):
"""Test that IndexFile.commit() supports adding trailers."""
rw_repo = Repo.init(osp.join(rw_dir, "test_index_trailers"))
path = osp.join(str(rw_repo.working_tree_dir), "hello.txt")
touch(path)
rw_repo.index.add([path])

trailers = {"Reviewed-by": "Reviewer <reviewer@example.com>"}
commit = rw_repo.index.commit(
"Test index commit with trailers",
trailers=trailers,
)

assert "Reviewed-by: Reviewer <reviewer@example.com>" in commit.message
assert commit.trailers_dict == {
"Reviewed-by": ["Reviewer <reviewer@example.com>"],
}
Loading
X Tutup