3 files changed
@@ -1133,6 +1133,7 @@ def commit( | |||
| 1133 | 1133 | author_date: Union[datetime.datetime, str, None] = None, | |
| 1134 | 1134 | commit_date: Union[datetime.datetime, str, None] = None, | |
| 1135 | 1135 | skip_hooks: bool = False, | |
| 1136 | + trailers: Union[None, "Dict[str, str]", "List[Tuple[str, str]]"] = None, | ||
| 1136 | 1137 | ) -> Commit: | |
| 1137 | 1138 | """Commit the current default index file, creating a | |
| 1138 | 1139 | :class:`~git.objects.commit.Commit` object. | |
@@ -1169,6 +1170,7 @@ def commit( | |||
| 1169 | 1170 | committer=committer, | |
| 1170 | 1171 | author_date=author_date, | |
| 1171 | 1172 | commit_date=commit_date, | |
| 1173 | + trailers=trailers, | ||
| 1172 | 1174 | ) | |
| 1173 | 1175 | if not skip_hooks: | |
| 1174 | 1176 | run_commit_hook("post-commit", self) | |
@@ -570,6 +570,7 @@ def create_from_tree( | |||
| 570 | 570 | committer: Union[None, Actor] = None, | |
| 571 | 571 | author_date: Union[None, str, datetime.datetime] = None, | |
| 572 | 572 | commit_date: Union[None, str, datetime.datetime] = None, | |
| 573 | + trailers: Union[None, Dict[str, str], List[Tuple[str, str]]] = None, | ||
| 573 | 574 | ) -> "Commit": | |
| 574 | 575 | """Commit the given tree, creating a :class:`Commit` object. | |
| 575 | 576 | ||
@@ -609,6 +610,14 @@ def create_from_tree( | |||
| 609 | 610 | :param commit_date: | |
| 610 | 611 | The timestamp for the committer field. | |
| 611 | 612 | ||
| 613 | + :param trailers: | ||
| 614 | + Optional trailer key-value pairs to append to the commit message. | ||
| 615 | + Can be a dictionary mapping trailer keys to values, or a list of | ||
| 616 | + ``(key, value)`` tuples (useful when the same key appears multiple | ||
| 617 | + times, e.g. multiple ``Signed-off-by`` trailers). Trailers are | ||
| 618 | + appended using ``git interpret-trailers``. | ||
| 619 | + See :manpage:`git-interpret-trailers(1)`. | ||
| 620 | + | ||
| 612 | 621 | :return: | |
| 613 | 622 | :class:`Commit` object representing the new commit. | |
| 614 | 623 | ||
@@ -678,6 +687,27 @@ def create_from_tree( | |||
| 678 | 687 | tree = repo.tree(tree) | |
| 679 | 688 | # END tree conversion | |
| 680 | 689 | ||
| 690 | + # APPLY TRAILERS | ||
| 691 | + if trailers: | ||
| 692 | + trailer_args: List[str] = [] | ||
| 693 | + if isinstance(trailers, dict): | ||
| 694 | + for key, val in trailers.items(): | ||
| 695 | + trailer_args.append("--trailer") | ||
| 696 | + trailer_args.append(f"{key}: {val}") | ||
| 697 | + else: | ||
| 698 | + for key, val in trailers: | ||
| 699 | + trailer_args.append("--trailer") | ||
| 700 | + trailer_args.append(f"{key}: {val}") | ||
| 701 | + | ||
| 702 | + cmd = ["git", "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 | + message = proc.communicate(str(message).encode())[0].decode("utf8") | ||
| 709 | + # END apply trailers | ||
| 710 | + | ||
| 681 | 711 | # CREATE NEW COMMIT | |
| 682 | 712 | new_commit = cls( | |
| 683 | 713 | repo, | |
@@ -566,3 +566,77 @@ def test_commit_co_authors(self): | |||
| 566 | 566 | Actor("test_user_2", "another_user-email@github.com"), | |
| 567 | 567 | Actor("test_user_3", "test_user_3@github.com"), | |
| 568 | 568 | ] | |
| 569 | + | ||
| 570 | + @with_rw_directory | ||
| 571 | + def test_create_from_tree_with_trailers_dict(self, rw_dir): | ||
| 572 | + """Test that create_from_tree supports adding trailers via a dict.""" | ||
| 573 | + rw_repo = Repo.init(osp.join(rw_dir, "test_trailers_dict")) | ||
| 574 | + path = osp.join(str(rw_repo.working_tree_dir), "hello.txt") | ||
| 575 | + touch(path) | ||
| 576 | + rw_repo.index.add([path]) | ||
| 577 | + tree = rw_repo.index.write_tree() | ||
| 578 | + | ||
| 579 | + trailers = {"Issue": "123", "Signed-off-by": "Test User <test@test.com>"} | ||
| 580 | + commit = Commit.create_from_tree( | ||
| 581 | + rw_repo, | ||
| 582 | + tree, | ||
| 583 | + "Test commit with trailers", | ||
| 584 | + head=True, | ||
| 585 | + trailers=trailers, | ||
| 586 | + ) | ||
| 587 | + | ||
| 588 | + assert "Issue: 123" in commit.message | ||
| 589 | + assert "Signed-off-by: Test User <test@test.com>" in commit.message | ||
| 590 | + assert commit.trailers_dict == { | ||
| 591 | + "Issue": ["123"], | ||
| 592 | + "Signed-off-by": ["Test User <test@test.com>"], | ||
| 593 | + } | ||
| 594 | + | ||
| 595 | + @with_rw_directory | ||
| 596 | + def test_create_from_tree_with_trailers_list(self, rw_dir): | ||
| 597 | + """Test that create_from_tree supports adding trailers via a list of tuples.""" | ||
| 598 | + rw_repo = Repo.init(osp.join(rw_dir, "test_trailers_list")) | ||
| 599 | + path = osp.join(str(rw_repo.working_tree_dir), "hello.txt") | ||
| 600 | + touch(path) | ||
| 601 | + rw_repo.index.add([path]) | ||
| 602 | + tree = rw_repo.index.write_tree() | ||
| 603 | + | ||
| 604 | + trailers = [ | ||
| 605 | + ("Signed-off-by", "Alice <alice@example.com>"), | ||
| 606 | + ("Signed-off-by", "Bob <bob@example.com>"), | ||
| 607 | + ("Issue", "456"), | ||
| 608 | + ] | ||
| 609 | + commit = Commit.create_from_tree( | ||
| 610 | + rw_repo, | ||
| 611 | + tree, | ||
| 612 | + "Test commit with multiple trailers", | ||
| 613 | + head=True, | ||
| 614 | + trailers=trailers, | ||
| 615 | + ) | ||
| 616 | + | ||
| 617 | + assert "Signed-off-by: Alice <alice@example.com>" in commit.message | ||
| 618 | + assert "Signed-off-by: Bob <bob@example.com>" in commit.message | ||
| 619 | + assert "Issue: 456" in commit.message | ||
| 620 | + assert commit.trailers_dict == { | ||
| 621 | + "Signed-off-by": ["Alice <alice@example.com>", "Bob <bob@example.com>"], | ||
| 622 | + "Issue": ["456"], | ||
| 623 | + } | ||
| 624 | + | ||
| 625 | + @with_rw_directory | ||
| 626 | + def test_index_commit_with_trailers(self, rw_dir): | ||
| 627 | + """Test that IndexFile.commit() supports adding trailers.""" | ||
| 628 | + rw_repo = Repo.init(osp.join(rw_dir, "test_index_trailers")) | ||
| 629 | + path = osp.join(str(rw_repo.working_tree_dir), "hello.txt") | ||
| 630 | + touch(path) | ||
| 631 | + rw_repo.index.add([path]) | ||
| 632 | + | ||
| 633 | + trailers = {"Reviewed-by": "Reviewer <reviewer@example.com>"} | ||
| 634 | + commit = rw_repo.index.commit( | ||
| 635 | + "Test index commit with trailers", | ||
| 636 | + trailers=trailers, | ||
| 637 | + ) | ||
| 638 | + | ||
| 639 | + assert "Reviewed-by: Reviewer <reviewer@example.com>" in commit.message | ||
| 640 | + assert commit.trailers_dict == { | ||
| 641 | + "Reviewed-by": ["Reviewer <reviewer@example.com>"], | ||
| 642 | + } | ||
0 commit comments