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

A '--signed-commits=<mode>' option is already available when using `git fast-export` to decide what should be done at export time about commit signatures. At import time though, there is no option, or other way, in `git fast-import` to decide about commit signatures. To remediate that, let's add a '--signed-commits=<mode>' option to `git fast-import` too. For now the supported <mode>s are the same as those supported by `git fast-export`. The code responsible for consuming a signature is refactored into the import_one_signature() and discard_one_signature() functions, which makes it easier to follow the logic and add new modes in the future. In the 'strip' and 'warn-strip' modes, we deliberately use discard_one_signature() to discard the signature without parsing it. This ensures that even malformed signatures, which would cause the parser to fail, can be successfully stripped from a commit. Signed-off-by: Christian Couder <chriscool@tuxfamily.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Christian Couder committed Sep 17, 2025 at 20:14 UTC eaaddf57912466414bce5bf81a24d1d69caf2e51
4 files changed +165 -10
Documentation/git-fast-import.adoc
+5
@@ -66,6 +66,11 @@ OPTIONS
66 remote-helpers that use the `import` capability, as they are
67 already trusted to run their own code.
68
69 +--signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::
70 + Specify how to handle signed commits. 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 Options for Frontends
75 ~~~~~~~~~~~~~~~~~~~~~
76
builtin/fast-import.c
+53 -10
@@ -188,6 +188,8 @@ static int global_argc;
188 static const char **global_argv;
189 static const char *global_prefix;
190
191 +static enum sign_mode signed_commit_mode = SIGN_VERBATIM;
192 +
193 /* Memory pools */
194 static struct mem_pool fi_mem_pool = {
195 .block_alloc = 2*1024*1024 - sizeof(struct mp_block),
@@ -2752,6 +2754,15 @@ static void parse_one_signature(struct signature_data *sig, const char *v)
2754 parse_data(&sig->data, 0, NULL);
2755 }
2756
2757 +static void discard_one_signature(void)
2758 +{
2759 + struct strbuf data = STRBUF_INIT;
2760 +
2761 + read_next_command();
2762 + parse_data(&data, 0, NULL);
2763 + strbuf_release(&data);
2764 +}
2765 +
2766 static void add_gpgsig_to_commit(struct strbuf *commit_data,
2767 const char *header,
2768 struct signature_data *sig)
@@ -2785,6 +2796,22 @@ static void store_signature(struct signature_data *stored_sig,
2796 }
2797 }
2798
2799 +static void import_one_signature(struct signature_data *sig_sha1,
2800 + struct signature_data *sig_sha256,
2801 + const char *v)
2802 +{
2803 + struct signature_data sig = { NULL, NULL, STRBUF_INIT };
2804 +
2805 + parse_one_signature(&sig, v);
2806 +
2807 + if (!strcmp(sig.hash_algo, "sha1"))
2808 + store_signature(sig_sha1, &sig, "SHA-1");
2809 + else if (!strcmp(sig.hash_algo, "sha256"))
2810 + store_signature(sig_sha256, &sig, "SHA-256");
2811 + else
2812 + die(_("parse_one_signature() returned unknown hash algo"));
2813 +}
2814 +
2815 static void parse_new_commit(const char *arg)
2816 {
2817 static struct strbuf msg = STRBUF_INIT;
@@ -2817,19 +2844,32 @@ static void parse_new_commit(const char *arg)
2844 if (!committer)
2845 die("Expected committer but didn't get one");
2846
2820 - /* Process signatures (up to 2: one "sha1" and one "sha256") */
2847 while (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
2822 - struct signature_data sig = { NULL, NULL, STRBUF_INIT };
2823 -
2824 - parse_one_signature(&sig, v);
2848 + switch (signed_commit_mode) {
2849 +
2850 + /* First, modes that don't need the signature to be parsed */
2851 + case SIGN_ABORT:
2852 + die("encountered signed commit; use "
2853 + "--signed-commits=<mode> to handle it");
2854 + case SIGN_WARN_STRIP:
2855 + warning(_("stripping a commit signature"));
2856 + /* fallthru */
2857 + case SIGN_STRIP:
2858 + discard_one_signature();
2859 + break;
2860
2826 - if (!strcmp(sig.hash_algo, "sha1"))
2827 - store_signature(&sig_sha1, &sig, "SHA-1");
2828 - else if (!strcmp(sig.hash_algo, "sha256"))
2829 - store_signature(&sig_sha256, &sig, "SHA-256");
2830 - else
2831 - BUG("parse_one_signature() returned unknown hash algo");
2861 + /* Second, modes that parse the signature */
2862 + case SIGN_WARN_VERBATIM:
2863 + warning(_("importing a commit signature verbatim"));
2864 + /* fallthru */
2865 + case SIGN_VERBATIM:
2866 + import_one_signature(&sig_sha1, &sig_sha256, v);
2867 + break;
2868
2869 + /* Third, BUG */
2870 + default:
2871 + BUG("invalid signed_commit_mode value %d", signed_commit_mode);
2872 + }
2873 read_next_command();
2874 }
2875
@@ -3501,6 +3541,9 @@ static int parse_one_option(const char *option)
3541 option_active_branches(option);
3542 } else if (skip_prefix(option, "export-pack-edges=", &option)) {
3543 option_export_pack_edges(option);
3544 + } else if (skip_prefix(option, "signed-commits=", &option)) {
3545 + if (parse_sign_mode(option, &signed_commit_mode))
3546 + usagef(_("unknown --signed-commits mode '%s'"), option);
3547 } else if (!strcmp(option, "quiet")) {
3548 show_stats = 0;
3549 quiet = 1;
t/meson.build
+1
@@ -1032,6 +1032,7 @@ integration_tests = [
1032 't9302-fast-import-unpack-limit.sh',
1033 't9303-fast-import-compression.sh',
1034 't9304-fast-import-marks.sh',
1035 + 't9305-fast-import-signatures.sh',
1036 't9350-fast-export.sh',
1037 't9351-fast-export-anonymize.sh',
1038 't9400-git-cvsserver-server.sh',
t/t9305-fast-import-signatures.sh new
+106
@@ -0,0 +1,106 @@
1 +#!/bin/sh
2 +
3 +test_description='git fast-import --signed-commits=<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 GPG 'set up OpenPGP signed commit' '
16 + git checkout -b openpgp-signing main &&
17 + echo "Content for OpenPGP signing." >file-sign &&
18 + git add file-sign &&
19 + git commit -S -m "OpenPGP signed commit" &&
20 + OPENPGP_SIGNING=$(git rev-parse --verify openpgp-signing)
21 +'
22 +
23 +test_expect_success GPG 'import OpenPGP signature with --signed-commits=verbatim' '
24 + git fast-export --signed-commits=verbatim openpgp-signing >output &&
25 + git -C new fast-import --quiet --signed-commits=verbatim <output >log 2>&1 &&
26 + IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&
27 + test $OPENPGP_SIGNING = $IMPORTED &&
28 + test_must_be_empty log
29 +'
30 +
31 +test_expect_success GPGSM 'set up X.509 signed commit' '
32 + git checkout -b x509-signing main &&
33 + test_config gpg.format x509 &&
34 + test_config user.signingkey $GIT_COMMITTER_EMAIL &&
35 + echo "Content for X.509 signing." >file-sign &&
36 + git add file-sign &&
37 + git commit -S -m "X.509 signed commit" &&
38 + X509_SIGNING=$(git rev-parse HEAD)
39 +'
40 +
41 +test_expect_success GPGSM 'import X.509 signature fails with --signed-commits=abort' '
42 + git fast-export --signed-commits=verbatim x509-signing >output &&
43 + test_must_fail git -C new fast-import --quiet --signed-commits=abort <output
44 +'
45 +
46 +test_expect_success GPGSM 'import X.509 signature with --signed-commits=warn-verbatim' '
47 + git -C new fast-import --quiet --signed-commits=warn-verbatim <output >log 2>&1 &&
48 + IMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&
49 + test $X509_SIGNING = $IMPORTED &&
50 + test_grep "importing a commit signature" log
51 +'
52 +
53 +test_expect_success GPGSSH 'set up SSH signed commit' '
54 + git checkout -b ssh-signing main &&
55 + test_config gpg.format ssh &&
56 + test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
57 + echo "Content for SSH signing." >file-sign &&
58 + git add file-sign &&
59 + git commit -S -m "SSH signed commit" &&
60 + SSH_SIGNING=$(git rev-parse HEAD)
61 +'
62 +
63 +test_expect_success GPGSSH 'strip SSH signature with --signed-commits=strip' '
64 + git fast-export --signed-commits=verbatim ssh-signing >output &&
65 + git -C new fast-import --quiet --signed-commits=strip <output >log 2>&1 &&
66 + IMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&
67 + test $SSH_SIGNING != $IMPORTED &&
68 + git -C new cat-file commit "$IMPORTED" >actual &&
69 + test_grep ! -E "^gpgsig" actual &&
70 + test_must_be_empty log
71 +'
72 +
73 +test_expect_success GPG 'setup a commit with dual OpenPGP signatures on its SHA-1 and SHA-256 formats' '
74 + # Create a signed SHA-256 commit
75 + git init --object-format=sha256 explicit-sha256 &&
76 + git -C explicit-sha256 config extensions.compatObjectFormat sha1 &&
77 + git -C explicit-sha256 checkout -b dual-signed &&
78 + test_commit -C explicit-sha256 A &&
79 + echo B >explicit-sha256/B &&
80 + git -C explicit-sha256 add B &&
81 + test_tick &&
82 + git -C explicit-sha256 commit -S -m "signed" B &&
83 + SHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&
84 +
85 + # Create the corresponding SHA-1 commit
86 + SHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&
87 +
88 + # Check that the resulting SHA-1 commit has both signatures
89 + git -C explicit-sha256 cat-file -p $SHA1_B >out &&
90 + test_grep -E "^gpgsig " out &&
91 + test_grep -E "^gpgsig-sha256 " out
92 +'
93 +
94 +test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=warn-strip' '
95 + git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&
96 + test_grep -E "^gpgsig sha1 openpgp" output &&
97 + test_grep -E "^gpgsig sha256 openpgp" output &&
98 + git -C new fast-import --quiet --signed-commits=warn-strip <output >log 2>&1 &&
99 + git -C new cat-file commit refs/heads/dual-signed >actual &&
100 + test_grep ! -E "^gpgsig " actual &&
101 + test_grep ! -E "^gpgsig-sha256 " actual &&
102 + test_grep "stripping a commit signature" log >out &&
103 + test_line_count = 2 out
104 +'
105 +
106 +test_done