fast-import: add '--signed-tags=<mode>' option

Recently, eaaddf5791 (fast-import: add '--signed-commits=<mode>' option, 2025-09-17) added support for controlling how signed commits are handled by `git fast-import`, but there is no option yet to decide about signed tags. To remediate that, let's add a '--signed-tags=<mode>' option to `git fast-import` too. With this, both `git fast-export` and `git fast-import` have both a '--signed-tags=<mode>' and a '--signed-commits=<mode>' supporting the same <mode>s. Signed-off-by: Christian Couder <chriscool@tuxfamily.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Christian Couder committed Oct 13, 2025 at 10:48 UTC d8ce08aa13b4dc6c4713ff9dc0b2ffacd5873d06
4 files changed +129
Documentation/git-fast-import.adoc
+5
@@ -66,6 +66,11 @@ fast-import stream! This option is enabled automatically for
66 remote-helpers that use the `import` capability, as they are
67 already trusted to run their own code.
68
69 +--signed-tags=(verbatim|warn-verbatim|warn-strip|strip|abort)::
70 + Specify how to handle signed tags. Behaves in the same way
71 + as the same option in linkgit:git-fast-export[1], except that
72 + default is 'verbatim' (instead of 'abort').
73 +
74 --signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::
75 Specify how to handle signed commits. Behaves in the same way
76 as the same option in linkgit:git-fast-export[1], except that
builtin/fast-import.c
+43
@@ -188,6 +188,7 @@ static int global_argc;
188 static const char **global_argv;
189 static const char *global_prefix;
190
191 +static enum sign_mode signed_tag_mode = SIGN_VERBATIM;
192 static enum sign_mode signed_commit_mode = SIGN_VERBATIM;
193
194 /* Memory pools */
@@ -2961,6 +2962,43 @@ static void parse_new_commit(const char *arg)
2962 b->last_commit = object_count_by_type[OBJ_COMMIT];
2963 }
2964
2965 +static void handle_tag_signature(struct strbuf *msg, const char *name)
2966 +{
2967 + size_t sig_offset = parse_signed_buffer(msg->buf, msg->len);
2968 +
2969 + /* If there is no signature, there is nothing to do. */
2970 + if (sig_offset >= msg->len)
2971 + return;
2972 +
2973 + switch (signed_tag_mode) {
2974 +
2975 + /* First, modes that don't change anything */
2976 + case SIGN_ABORT:
2977 + die(_("encountered signed tag; use "
2978 + "--signed-tags=<mode> to handle it"));
2979 + case SIGN_WARN_VERBATIM:
2980 + warning(_("importing a tag signature verbatim for tag '%s'"), name);
2981 + /* fallthru */
2982 + case SIGN_VERBATIM:
2983 + /* Nothing to do, the signature will be put into the imported tag. */
2984 + break;
2985 +
2986 + /* Second, modes that remove the signature */
2987 + case SIGN_WARN_STRIP:
2988 + warning(_("stripping a tag signature for tag '%s'"), name);
2989 + /* fallthru */
2990 + case SIGN_STRIP:
2991 + /* Truncate the buffer to remove the signature */
2992 + strbuf_setlen(msg, sig_offset);
2993 + break;
2994 +
2995 + /* Third, BUG */
2996 + default:
2997 + BUG("invalid signed_tag_mode value %d from tag '%s'",
2998 + signed_tag_mode, name);
2999 + }
3000 +}
3001 +
3002 static void parse_new_tag(const char *arg)
3003 {
3004 static struct strbuf msg = STRBUF_INIT;
@@ -3024,6 +3062,8 @@ static void parse_new_tag(const char *arg)
3062 /* tag payload/message */
3063 parse_data(&msg, 0, NULL);
3064
3065 + handle_tag_signature(&msg, t->name);
3066 +
3067 /* build the tag object */
3068 strbuf_reset(&new_data);
3069
@@ -3544,6 +3584,9 @@ static int parse_one_option(const char *option)
3584 } else if (skip_prefix(option, "signed-commits=", &option)) {
3585 if (parse_sign_mode(option, &signed_commit_mode))
3586 usagef(_("unknown --signed-commits mode '%s'"), option);
3587 + } else if (skip_prefix(option, "signed-tags=", &option)) {
3588 + if (parse_sign_mode(option, &signed_tag_mode))
3589 + usagef(_("unknown --signed-tags mode '%s'"), option);
3590 } else if (!strcmp(option, "quiet")) {
3591 show_stats = 0;
3592 quiet = 1;
t/meson.build
+1
@@ -1036,6 +1036,7 @@ integration_tests = [
1036 't9303-fast-import-compression.sh',
1037 't9304-fast-import-marks.sh',
1038 't9305-fast-import-signatures.sh',
1039 + 't9306-fast-import-signed-tags.sh',
1040 't9350-fast-export.sh',
1041 't9351-fast-export-anonymize.sh',
1042 't9400-git-cvsserver-server.sh',
t/t9306-fast-import-signed-tags.sh new
+80
@@ -0,0 +1,80 @@
1 +#!/bin/sh
2 +
3 +test_description='git fast-import --signed-tags=<mode>'
4 +
5 +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
6 +
7 +. ./test-lib.sh
8 +. "$TEST_DIRECTORY/lib-gpg.sh"
9 +
10 +test_expect_success 'set up unsigned initial commit and import repo' '
11 + test_commit first &&
12 + git init new
13 +'
14 +
15 +test_expect_success 'import no signed tag with --signed-tags=abort' '
16 + git fast-export --signed-tags=verbatim >output &&
17 + git -C new fast-import --quiet --signed-tags=abort <output
18 +'
19 +
20 +test_expect_success GPG 'set up OpenPGP signed tag' '
21 + git tag -s -m "OpenPGP signed tag" openpgp-signed first &&
22 + OPENPGP_SIGNED=$(git rev-parse --verify refs/tags/openpgp-signed) &&
23 + git fast-export --signed-tags=verbatim openpgp-signed >output
24 +'
25 +
26 +test_expect_success GPG 'import OpenPGP signed tag with --signed-tags=abort' '
27 + test_must_fail git -C new fast-import --quiet --signed-tags=abort <output
28 +'
29 +
30 +test_expect_success GPG 'import OpenPGP signed tag with --signed-tags=verbatim' '
31 + git -C new fast-import --quiet --signed-tags=verbatim <output >log 2>&1 &&
32 + IMPORTED=$(git -C new rev-parse --verify refs/tags/openpgp-signed) &&
33 + test $OPENPGP_SIGNED = $IMPORTED &&
34 + test_must_be_empty log
35 +'
36 +
37 +test_expect_success GPGSM 'setup X.509 signed tag' '
38 + test_config gpg.format x509 &&
39 + test_config user.signingkey $GIT_COMMITTER_EMAIL &&
40 +
41 + git tag -s -m "X.509 signed tag" x509-signed first &&
42 + X509_SIGNED=$(git rev-parse --verify refs/tags/x509-signed) &&
43 + git fast-export --signed-tags=verbatim x509-signed >output
44 +'
45 +
46 +test_expect_success GPGSM 'import X.509 signed tag with --signed-tags=warn-strip' '
47 + git -C new fast-import --quiet --signed-tags=warn-strip <output >log 2>&1 &&
48 + test_grep "stripping a tag signature for tag '\''x509-signed'\''" log &&
49 + IMPORTED=$(git -C new rev-parse --verify refs/tags/x509-signed) &&
50 + test $X509_SIGNED != $IMPORTED &&
51 + git -C new cat-file -p x509-signed >out &&
52 + test_grep ! "SIGNED MESSAGE" out
53 +'
54 +
55 +test_expect_success GPGSSH 'setup SSH signed tag' '
56 + test_config gpg.format ssh &&
57 + test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
58 +
59 + git tag -s -m "SSH signed tag" ssh-signed first &&
60 + SSH_SIGNED=$(git rev-parse --verify refs/tags/ssh-signed) &&
61 + git fast-export --signed-tags=verbatim ssh-signed >output
62 +'
63 +
64 +test_expect_success GPGSSH 'import SSH signed tag with --signed-tags=warn-verbatim' '
65 + git -C new fast-import --quiet --signed-tags=warn-verbatim <output >log 2>&1 &&
66 + test_grep "importing a tag signature verbatim for tag '\''ssh-signed'\''" log &&
67 + IMPORTED=$(git -C new rev-parse --verify refs/tags/ssh-signed) &&
68 + test $SSH_SIGNED = $IMPORTED
69 +'
70 +
71 +test_expect_success GPGSSH 'import SSH signed tag with --signed-tags=strip' '
72 + git -C new fast-import --quiet --signed-tags=strip <output >log 2>&1 &&
73 + test_must_be_empty log &&
74 + IMPORTED=$(git -C new rev-parse --verify refs/tags/ssh-signed) &&
75 + test $SSH_SIGNED != $IMPORTED &&
76 + git -C new cat-file -p ssh-signed >out &&
77 + test_grep ! "SSH SIGNATURE" out
78 +'
79 +
80 +test_done