contrib/subtree: add -S/--gpg-sign

Allows optionally signing the commits that git subtree creates. This can be necessary when working in a repository that requires gpg signed commits. Signed-off-by: Patrik Weiskircher <patrik@pspdfkit.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrik Weiskircher committed Jun 4, 2025 at 14:16 UTC fea50ce4118c38d74d05c3595abe7756cabee4e1
3 files changed +145 -19
contrib/subtree/git-subtree.adoc
+13 -6
@@ -9,14 +9,14 @@ git-subtree - Merge subtrees together and split repository into subtrees
9 SYNOPSIS
10 --------
11 [verse]
12 -'git subtree' [<options>] -P <prefix> add <local-commit>
13 -'git subtree' [<options>] -P <prefix> add <repository> <remote-ref>
14 -'git subtree' [<options>] -P <prefix> merge <local-commit> [<repository>]
15 -'git subtree' [<options>] -P <prefix> split [<local-commit>]
12 +'git subtree' [<options>] -P <prefix> [-S[<keyid>]] add <local-commit>
13 +'git subtree' [<options>] -P <prefix> [-S[<keyid>]] add <repository> <remote-ref>
14 +'git subtree' [<options>] -P <prefix> [-S[<keyid>]] merge <local-commit> [<repository>]
15 +'git subtree' [<options>] -P <prefix> [-S[<keyid>]] split [<local-commit>]
16
17 [verse]
18 -'git subtree' [<options>] -P <prefix> pull <repository> <remote-ref>
19 -'git subtree' [<options>] -P <prefix> push <repository> <refspec>
18 +'git subtree' [<options>] -P <prefix> [-S[<keyid>]] pull <repository> <remote-ref>
19 +'git subtree' [<options>] -P <prefix> [-S[<keyid>]] push <repository> <refspec>
20
21 DESCRIPTION
22 -----------
@@ -149,6 +149,13 @@ OPTIONS FOR ALL COMMANDS
149 want to manipulate. This option is mandatory
150 for all commands.
151
152 +-S[<keyid>]::
153 +--gpg-sign[=<keyid>]::
154 +--no-gpg-sign::
155 + GPG-sign commits. The `keyid` argument is optional and
156 + defaults to the committer identity; `--no-gpg-sign` is useful to
157 + countermand a `--gpg-sign` option given earlier on the command line.
158 +
159 OPTIONS FOR 'add' AND 'merge' (ALSO: 'pull', 'split --rejoin', AND 'push --rejoin')
160 -----------------------------------------------------------------------------------
161 These options for 'add' and 'merge' may also be given to 'pull' (which
contrib/subtree/git-subtree.sh
+19 -13
@@ -26,12 +26,12 @@ then
26 fi
27
28 OPTS_SPEC="\
29 -git subtree add --prefix=<prefix> <commit>
30 -git subtree add --prefix=<prefix> <repository> <ref>
31 -git subtree merge --prefix=<prefix> <commit>
32 -git subtree split --prefix=<prefix> [<commit>]
33 -git subtree pull --prefix=<prefix> <repository> <ref>
34 -git subtree push --prefix=<prefix> <repository> <refspec>
29 +git subtree add --prefix=<prefix> [-S[=<key-id>]] <commit>
30 +git subtree add --prefix=<prefix> [-S[=<key-id>]] <repository> <ref>
31 +git subtree merge --prefix=<prefix> [-S[=<key-id>]] <commit>
32 +git subtree split --prefix=<prefix> [-S[=<key-id>]] [<commit>]
33 +git subtree pull --prefix=<prefix> [-S[=<key-id>]] <repository> <ref>
34 +git subtree push --prefix=<prefix> [-S[=<key-id>]] <repository> <refspec>
35 --
36 h,help! show the help
37 q,quiet! quiet
@@ -46,6 +46,7 @@ rejoin merge the new branch back into HEAD
46 options for 'add' and 'merge' (also: 'pull', 'split --rejoin', and 'push --rejoin')
47 squash merge subtree changes as a single commit
48 m,message!= use the given message as the commit message for the merge commit
49 +S,gpg-sign?key-id GPG-sign commits. The keyid argument is optional and defaults to the committer identity
50 "
51
52 indent=0
@@ -168,6 +169,7 @@ main () {
169 arg_split_annotate=
170 arg_addmerge_squash=
171 arg_addmerge_message=
172 + arg_gpg_sign=
173 while test $# -gt 0
174 do
175 opt="$1"
@@ -232,6 +234,9 @@ main () {
234 test -n "$allow_addmerge" || die_incompatible_opt "$opt" "$arg_command"
235 arg_addmerge_squash=
236 ;;
237 + --gpg-sign=* | --gpg-sign | --no-gpg-sign)
238 + arg_gpg_sign="$opt"
239 + ;;
240 --)
241 break
242 ;;
@@ -264,6 +269,7 @@ main () {
269 debug "quiet: {$arg_quiet}"
270 debug "dir: {$dir}"
271 debug "opts: {$*}"
272 + debug "gpg-sign: {$arg_gpg_sign}"
273 debug
274
275 "cmd_$arg_command" "$@"
@@ -529,7 +535,7 @@ copy_commit () {
535 printf "%s" "$arg_split_annotate"
536 cat
537 ) |
532 - git commit-tree "$2" $3 # reads the rest of stdin
538 + git commit-tree $arg_gpg_sign "$2" $3 # reads the rest of stdin
539 ) || die "fatal: can't copy commit $1"
540 }
541
@@ -675,10 +681,10 @@ new_squash_commit () {
681 if test -n "$old"
682 then
683 squash_msg "$dir" "$oldsub" "$newsub" |
678 - git commit-tree "$tree" -p "$old" || exit $?
684 + git commit-tree $arg_gpg_sign "$tree" -p "$old" || exit $?
685 else
686 squash_msg "$dir" "" "$newsub" |
681 - git commit-tree "$tree" || exit $?
687 + git commit-tree $arg_gpg_sign "$tree" || exit $?
688 fi
689 }
690
@@ -917,11 +923,11 @@ cmd_add_commit () {
923 then
924 rev=$(new_squash_commit "" "" "$rev") || exit $?
925 commit=$(add_squashed_msg "$rev" "$dir" |
920 - git commit-tree "$tree" $headp -p "$rev") || exit $?
926 + git commit-tree $arg_gpg_sign "$tree" $headp -p "$rev") || exit $?
927 else
928 revp=$(peel_committish "$rev") || exit $?
929 commit=$(add_msg "$dir" $headrev "$rev" |
924 - git commit-tree "$tree" $headp -p "$revp") || exit $?
930 + git commit-tree $arg_gpg_sign "$tree" $headp -p "$revp") || exit $?
931 fi
932 git reset "$commit" || exit $?
933
@@ -1072,9 +1078,9 @@ cmd_merge () {
1078 if test -n "$arg_addmerge_message"
1079 then
1080 git merge --no-ff -Xsubtree="$arg_prefix" \
1075 - --message="$arg_addmerge_message" "$rev"
1081 + --message="$arg_addmerge_message" $arg_gpg_sign "$rev"
1082 else
1077 - git merge --no-ff -Xsubtree="$arg_prefix" $rev
1083 + git merge --no-ff -Xsubtree="$arg_prefix" $arg_gpg_sign $rev
1084 fi
1085 }
1086
contrib/subtree/t/t7900-subtree.sh
+113
@@ -11,6 +11,7 @@ and push subcommands of git subtree.
11
12 TEST_DIRECTORY=$(pwd)/../../../t
13 . "$TEST_DIRECTORY"/test-lib.sh
14 +. "$TEST_DIRECTORY"/lib-gpg.sh
15
16 # Use our own wrapper around test-lib.sh's test_create_repo, in order
17 # to set log.date=relative. `git subtree` parses the output of `git
@@ -1563,4 +1564,116 @@ test_expect_success 'subtree descendant check' '
1564 )
1565 '
1566
1567 +test_expect_success GPG 'add subproj with GPG signing using -S flag' '
1568 + subtree_test_create_repo "$test_count" &&
1569 + subtree_test_create_repo "$test_count/sub proj" &&
1570 + test_create_commit "$test_count" main1 &&
1571 + test_create_commit "$test_count/sub proj" sub1 &&
1572 + (
1573 + cd "$test_count" &&
1574 + git fetch ./"sub proj" HEAD &&
1575 + git subtree add --prefix="sub dir" -S FETCH_HEAD &&
1576 + git verify-commit HEAD &&
1577 + test "$(last_commit_subject)" = "Add '\''sub dir/'\'' from commit '\''$(git rev-parse FETCH_HEAD)'\''"
1578 + )
1579 +'
1580 +
1581 +test_expect_success GPG 'add subproj with GPG signing using --gpg-sign flag' '
1582 + subtree_test_create_repo "$test_count" &&
1583 + subtree_test_create_repo "$test_count/sub proj" &&
1584 + test_create_commit "$test_count" main1 &&
1585 + test_create_commit "$test_count/sub proj" sub1 &&
1586 + (
1587 + cd "$test_count" &&
1588 + git fetch ./"sub proj" HEAD &&
1589 + git subtree add --prefix="sub dir" --gpg-sign FETCH_HEAD &&
1590 + git verify-commit HEAD &&
1591 + test "$(last_commit_subject)" = "Add '\''sub dir/'\'' from commit '\''$(git rev-parse FETCH_HEAD)'\''"
1592 + )
1593 +'
1594 +
1595 +test_expect_success GPG 'add subproj with GPG signing using specific key ID' '
1596 + subtree_test_create_repo "$test_count" &&
1597 + subtree_test_create_repo "$test_count/sub proj" &&
1598 + test_create_commit "$test_count" main1 &&
1599 + test_create_commit "$test_count/sub proj" sub1 &&
1600 + (
1601 + cd "$test_count" &&
1602 + git fetch ./"sub proj" HEAD &&
1603 + git subtree add --prefix="sub dir" -S"$GIT_COMMITTER_EMAIL" FETCH_HEAD &&
1604 + git verify-commit HEAD &&
1605 + test "$(last_commit_subject)" = "Add '\''sub dir/'\'' from commit '\''$(git rev-parse FETCH_HEAD)'\''"
1606 + )
1607 +'
1608 +
1609 +test_expect_success GPG 'merge with GPG signing' '
1610 + subtree_test_create_repo "$test_count" &&
1611 + subtree_test_create_repo "$test_count/sub proj" &&
1612 + test_create_commit "$test_count" main1 &&
1613 + test_create_commit "$test_count/sub proj" sub1 &&
1614 + (
1615 + cd "$test_count" &&
1616 + git fetch ./"sub proj" HEAD &&
1617 + git subtree add --prefix="sub dir" FETCH_HEAD
1618 + ) &&
1619 + test_create_commit "$test_count/sub proj" sub2 &&
1620 + (
1621 + cd "$test_count" &&
1622 + git fetch ./"sub proj" HEAD &&
1623 + git subtree merge --prefix="sub dir" -S FETCH_HEAD &&
1624 + git verify-commit HEAD
1625 + )
1626 +'
1627 +
1628 +test_expect_success GPG 'split with GPG signing and --rejoin' '
1629 + subtree_test_create_repo "$test_count" &&
1630 + subtree_test_create_repo "$test_count/sub proj" &&
1631 + test_create_commit "$test_count" main1 &&
1632 + test_create_commit "$test_count/sub proj" sub1 &&
1633 + (
1634 + cd "$test_count" &&
1635 + git fetch ./"sub proj" HEAD &&
1636 + git subtree add --prefix="sub dir" FETCH_HEAD
1637 + ) &&
1638 + test_create_commit "$test_count" "sub dir/main-sub1" &&
1639 + (
1640 + cd "$test_count" &&
1641 + git subtree split --prefix="sub dir" --rejoin -S &&
1642 + git verify-commit HEAD
1643 + )
1644 +'
1645 +
1646 +test_expect_success GPG 'add with --squash and GPG signing' '
1647 + subtree_test_create_repo "$test_count" &&
1648 + subtree_test_create_repo "$test_count/sub proj" &&
1649 + test_create_commit "$test_count" main1 &&
1650 + test_create_commit "$test_count/sub proj" sub1 &&
1651 + (
1652 + cd "$test_count" &&
1653 + git fetch ./"sub proj" HEAD &&
1654 + git subtree add --prefix="sub dir" --squash -S FETCH_HEAD &&
1655 + git verify-commit HEAD &&
1656 + # With --squash, the commit subject should reference the squash commit (first parent of merge)
1657 + squash_commit=$(git rev-parse HEAD^2) &&
1658 + test "$(last_commit_subject)" = "Merge commit '\''$squash_commit'\'' as '\''sub dir'\''"
1659 + )
1660 +'
1661 +
1662 +test_expect_success GPG 'pull with GPG signing' '
1663 + subtree_test_create_repo "$test_count" &&
1664 + subtree_test_create_repo "$test_count/sub proj" &&
1665 + test_create_commit "$test_count" main1 &&
1666 + test_create_commit "$test_count/sub proj" sub1 &&
1667 + (
1668 + cd "$test_count" &&
1669 + git subtree add --prefix="sub dir" ./"sub proj" HEAD
1670 + ) &&
1671 + test_create_commit "$test_count/sub proj" sub2 &&
1672 + (
1673 + cd "$test_count" &&
1674 + git subtree pull --prefix="sub dir" -S ./"sub proj" HEAD &&
1675 + git verify-commit HEAD
1676 + )
1677 +'
1678 +
1679 test_done