]> git.ipfire.org Git - thirdparty/git.git/commitdiff
fast-import: add '--signed-commits=<mode>' option
authorChristian Couder <christian.couder@gmail.com>
Wed, 17 Sep 2025 18:14:27 +0000 (20:14 +0200)
committerJunio C Hamano <gitster@pobox.com>
Wed, 17 Sep 2025 18:18:28 +0000 (11:18 -0700)
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>
Documentation/git-fast-import.adoc
builtin/fast-import.c
t/meson.build
t/t9305-fast-import-signatures.sh [new file with mode: 0755]

index 3144ffcdb689d563862e617fc43b5def731f4f05..90f242d0583d1098135c59c39b0d89bbe4b68f1a 100644 (file)
@@ -66,6 +66,11 @@ OPTIONS
        remote-helpers that use the `import` capability, as they are
        already trusted to run their own code.
 
+--signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::
+       Specify how to handle signed commits.  Behaves in the same way
+       as the same option in linkgit:git-fast-export[1], except that
+       default is 'verbatim' (instead of 'abort').
+
 Options for Frontends
 ~~~~~~~~~~~~~~~~~~~~~
 
index 2c35f9345d02d7c711ee522de5973b205c85ef51..2010e78475b32e2090336ae96a27af4786ca2a2e 100644 (file)
@@ -188,6 +188,8 @@ static int global_argc;
 static const char **global_argv;
 static const char *global_prefix;
 
+static enum sign_mode signed_commit_mode = SIGN_VERBATIM;
+
 /* Memory pools */
 static struct mem_pool fi_mem_pool = {
        .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)
        parse_data(&sig->data, 0, NULL);
 }
 
+static void discard_one_signature(void)
+{
+       struct strbuf data = STRBUF_INIT;
+
+       read_next_command();
+       parse_data(&data, 0, NULL);
+       strbuf_release(&data);
+}
+
 static void add_gpgsig_to_commit(struct strbuf *commit_data,
                                 const char *header,
                                 struct signature_data *sig)
@@ -2785,6 +2796,22 @@ static void store_signature(struct signature_data *stored_sig,
        }
 }
 
+static void import_one_signature(struct signature_data *sig_sha1,
+                                struct signature_data *sig_sha256,
+                                const char *v)
+{
+       struct signature_data sig = { NULL, NULL, STRBUF_INIT };
+
+       parse_one_signature(&sig, v);
+
+       if (!strcmp(sig.hash_algo, "sha1"))
+               store_signature(sig_sha1, &sig, "SHA-1");
+       else if (!strcmp(sig.hash_algo, "sha256"))
+               store_signature(sig_sha256, &sig, "SHA-256");
+       else
+               die(_("parse_one_signature() returned unknown hash algo"));
+}
+
 static void parse_new_commit(const char *arg)
 {
        static struct strbuf msg = STRBUF_INIT;
@@ -2817,19 +2844,32 @@ static void parse_new_commit(const char *arg)
        if (!committer)
                die("Expected committer but didn't get one");
 
-       /* Process signatures (up to 2: one "sha1" and one "sha256") */
        while (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
-               struct signature_data sig = { NULL, NULL, STRBUF_INIT };
-
-               parse_one_signature(&sig, v);
+               switch (signed_commit_mode) {
+
+               /* First, modes that don't need the signature to be parsed */
+               case SIGN_ABORT:
+                       die("encountered signed commit; use "
+                           "--signed-commits=<mode> to handle it");
+               case SIGN_WARN_STRIP:
+                       warning(_("stripping a commit signature"));
+                       /* fallthru */
+               case SIGN_STRIP:
+                       discard_one_signature();
+                       break;
 
-               if (!strcmp(sig.hash_algo, "sha1"))
-                       store_signature(&sig_sha1, &sig, "SHA-1");
-               else if (!strcmp(sig.hash_algo, "sha256"))
-                       store_signature(&sig_sha256, &sig, "SHA-256");
-               else
-                       BUG("parse_one_signature() returned unknown hash algo");
+               /* Second, modes that parse the signature */
+               case SIGN_WARN_VERBATIM:
+                       warning(_("importing a commit signature verbatim"));
+                       /* fallthru */
+               case SIGN_VERBATIM:
+                       import_one_signature(&sig_sha1, &sig_sha256, v);
+                       break;
 
+               /* Third, BUG */
+               default:
+                       BUG("invalid signed_commit_mode value %d", signed_commit_mode);
+               }
                read_next_command();
        }
 
@@ -3501,6 +3541,9 @@ static int parse_one_option(const char *option)
                option_active_branches(option);
        } else if (skip_prefix(option, "export-pack-edges=", &option)) {
                option_export_pack_edges(option);
+       } else if (skip_prefix(option, "signed-commits=", &option)) {
+               if (parse_sign_mode(option, &signed_commit_mode))
+                       usagef(_("unknown --signed-commits mode '%s'"), option);
        } else if (!strcmp(option, "quiet")) {
                show_stats = 0;
                quiet = 1;
index 82af229be3efbecb0fce89ee69dda5a952888366..08ad6938e2e02f09e6a301b03c61bc1d5445ba55 100644 (file)
@@ -1032,6 +1032,7 @@ integration_tests = [
   't9302-fast-import-unpack-limit.sh',
   't9303-fast-import-compression.sh',
   't9304-fast-import-marks.sh',
+  't9305-fast-import-signatures.sh',
   't9350-fast-export.sh',
   't9351-fast-export-anonymize.sh',
   't9400-git-cvsserver-server.sh',
diff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh
new file mode 100755 (executable)
index 0000000..c2b4271
--- /dev/null
@@ -0,0 +1,106 @@
+#!/bin/sh
+
+test_description='git fast-import --signed-commits=<mode>'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-gpg.sh"
+
+test_expect_success 'set up unsigned initial commit and import repo' '
+       test_commit first &&
+       git init new
+'
+
+test_expect_success GPG 'set up OpenPGP signed commit' '
+       git checkout -b openpgp-signing main &&
+       echo "Content for OpenPGP signing." >file-sign &&
+       git add file-sign &&
+       git commit -S -m "OpenPGP signed commit" &&
+       OPENPGP_SIGNING=$(git rev-parse --verify openpgp-signing)
+'
+
+test_expect_success GPG 'import OpenPGP signature with --signed-commits=verbatim' '
+       git fast-export --signed-commits=verbatim openpgp-signing >output &&
+       git -C new fast-import --quiet --signed-commits=verbatim <output >log 2>&1 &&
+       IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&
+       test $OPENPGP_SIGNING = $IMPORTED &&
+       test_must_be_empty log
+'
+
+test_expect_success GPGSM 'set up X.509 signed commit' '
+       git checkout -b x509-signing main &&
+       test_config gpg.format x509 &&
+       test_config user.signingkey $GIT_COMMITTER_EMAIL &&
+       echo "Content for X.509 signing." >file-sign &&
+       git add file-sign &&
+       git commit -S -m "X.509 signed commit" &&
+       X509_SIGNING=$(git rev-parse HEAD)
+'
+
+test_expect_success GPGSM 'import X.509 signature fails with --signed-commits=abort' '
+       git fast-export --signed-commits=verbatim x509-signing >output &&
+       test_must_fail git -C new fast-import --quiet --signed-commits=abort <output
+'
+
+test_expect_success GPGSM 'import X.509 signature with --signed-commits=warn-verbatim' '
+       git -C new fast-import --quiet --signed-commits=warn-verbatim <output >log 2>&1 &&
+       IMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&
+       test $X509_SIGNING = $IMPORTED &&
+       test_grep "importing a commit signature" log
+'
+
+test_expect_success GPGSSH 'set up SSH signed commit' '
+       git checkout -b ssh-signing main &&
+       test_config gpg.format ssh &&
+       test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
+       echo "Content for SSH signing." >file-sign &&
+       git add file-sign &&
+       git commit -S -m "SSH signed commit" &&
+       SSH_SIGNING=$(git rev-parse HEAD)
+'
+
+test_expect_success GPGSSH 'strip SSH signature with --signed-commits=strip' '
+       git fast-export --signed-commits=verbatim ssh-signing >output &&
+       git -C new fast-import --quiet --signed-commits=strip <output >log 2>&1 &&
+       IMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&
+       test $SSH_SIGNING != $IMPORTED &&
+       git -C new cat-file commit "$IMPORTED" >actual &&
+       test_grep ! -E "^gpgsig" actual &&
+       test_must_be_empty log
+'
+
+test_expect_success GPG 'setup a commit with dual OpenPGP signatures on its SHA-1 and SHA-256 formats' '
+       # Create a signed SHA-256 commit
+       git init --object-format=sha256 explicit-sha256 &&
+       git -C explicit-sha256 config extensions.compatObjectFormat sha1 &&
+       git -C explicit-sha256 checkout -b dual-signed &&
+       test_commit -C explicit-sha256 A &&
+       echo B >explicit-sha256/B &&
+       git -C explicit-sha256 add B &&
+       test_tick &&
+       git -C explicit-sha256 commit -S -m "signed" B &&
+       SHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&
+
+       # Create the corresponding SHA-1 commit
+       SHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&
+
+       # Check that the resulting SHA-1 commit has both signatures
+       git -C explicit-sha256 cat-file -p $SHA1_B >out &&
+       test_grep -E "^gpgsig " out &&
+       test_grep -E "^gpgsig-sha256 " out
+'
+
+test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=warn-strip' '
+       git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&
+       test_grep -E "^gpgsig sha1 openpgp" output &&
+       test_grep -E "^gpgsig sha256 openpgp" output &&
+       git -C new fast-import --quiet --signed-commits=warn-strip <output >log 2>&1 &&
+       git -C new cat-file commit refs/heads/dual-signed >actual &&
+       test_grep ! -E "^gpgsig " actual &&
+       test_grep ! -E "^gpgsig-sha256 " actual &&
+       test_grep "stripping a commit signature" log >out &&
+       test_line_count = 2 out
+'
+
+test_done