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
32 changes: 32 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,29 @@ 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 = [repo.git.GIT_PYTHON_GIT_EXECUTABLE, "interpret-trailers"] + trailer_args
proc: Git.AutoInterrupt = repo.git.execute( # type: ignore[call-overload]
cmd,
as_process=True,
istream=PIPE,
)
stdout_bytes, _ = proc.communicate(str(message).encode())
finalize_process(proc)
message = stdout_bytes.decode("utf8")
# 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