Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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