{"thread":{"id":"64131","subject":"[PATCH v2 0/2] fast-import: start controlling how commit signatures are handled","startedAt":"2025-09-12T12:41:01Z","lastAt":"2025-09-17T18:24:00Z","messageCount":11,"participants":["Christian Couder","Patrick Steinhardt"],"isPatch":true,"patchVersion":2,"patchTotal":2},"messages":[{"id":"526179","messageId":"20250912124042.2523683-1-christian.couder@gmail.com","threadId":"64131","inReplyTo":null,"subject":"[PATCH v2 0/2] fast-import: start controlling how commit signatures are handled","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-12T12:40:40Z","receivedAt":"2025-09-12T12:41:01Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Tools like `git-filter-repo` should be able to control how commit\nsignatures are handled when regenerating repository content after it\nhas been filtered (see\nhttps://github.com/newren/git-filter-repo/issues/139). For this\npurpose, they need a way for `git fast-import` to control how commit\nsignatures are handled.\n\nThis small patch series starts to implement such a way by adding a new\n`--signed-commits=<mode>` option to `git fast-import`.\n\nFor now this new option behaves in a very similar way as the option\nwith the same name that already exists in `git fast-export`.\nEspecially it supports exactly the same <mode>s and the same aliases\nfor these modes. For example \"ignore\" is a synonym for \"verbatim\".\n\nIn the future I want to implement new <mode>s like \"strip-if-invalid\",\n\"re-sign\", \"re-sign-if-invalid\" that might be a bit more complex for\nthis option. But for now I prefer to start with the simple modes to\nvalidate the general design of the new option.\n\nIn the future I also plan to add a similar `--signed-tags=<mode>` so\nthat the import of tags can also be controlled. But I prefer to\nvalidate the general design of a single new option first.\n\nIn particular, I am interested in feedback about the following:\n\n  - Should we keep \"ignore\" as a synonym for \"verbatim\" and \"warn\" as\n    a synonym for \"warn-verbatim\"? My opinion is that they might be\n    confusing, so we might want to remove them for `git fast-import`\n    even if we keep them for `git fast-export`. The parsing code might\n    be a bit more complex if we do that though, so for now I have kept\n    the synonyms.\n\n  - Are we still fine with most <mode>s having a \"warn-*\" variant\n    (like the \"warn-strip\" variant of \"strip\" for example)? Or should\n    we have a separate `--verbose` or maybe `--signed-commits-verbose`\n    option dedicated to switching warnings on/off? I think it's good\n    to decide about this before the number of <mode>s increases a lot\n    with new <mode>s like \"strip-if-invalid\", \"re-sign\",\n    \"re-sign-if-invalid\" and possibly others.\n\nChanges since v1:\n=================\n\nThanks to Junio and Patrick for reviewing V1.\n\nIn patch 1/2:\n\n  - the commit message now says that a single option will be added in\n    the following commit\n\n  - instead of returning success in the `!parse_sign_mode(arg, val)`\n    case, we return error in the `parse_sign_mode(arg, val)` case,\n\n  - a '.' has been added at the end of a code comment.\n\nIn patch 2/2:\n\n  - the import_signature() function has been removed and we just call\n    parse_one_signature() in all cases except `SIGN_ABORT`, before\n    handling the signature according to `signed_commit_mode`,\n\n  - we now mark for translation all new strings that could be\n    displayed to users,\n\n  - usagef() is used instead of die() when the mode is unknown,\n\n  - the warning for `warn-verbatim` has been improved,\n\n  - the tests have been improved by using more idiomatic and\n    simplified commands\n\nCI tests:\n=========\n\nThey have all passed. See:\n\nhttps://github.com/chriscool/git/actions/runs/17672015528\n\nrange-diff vs V1:\n=================\n\n1:  7c8216a701 ! 1:  87149ae92d gpg-interface: refactor 'enum sign_mode' parsing\n    @@ Commit message\n         only command with '--signed-tags=<mode>' or '--signed-commits=<mode>'\n         options.\n     \n    -    In a following commit, we are going to add such options to `git\n    +    In a following commit, we are going to add a similar option to `git\n         fast-import`, which will be simpler, easier and cleaner if we can reuse\n         the 'enum sign_mode' defintion and parsing code.\n     \n    @@ builtin/fast-export.c: static struct hashmap anonymized_seeds;\n     -\telse if (!strcmp(arg, \"strip\"))\n     -\t\t*val = SIGN_STRIP;\n     -\telse\n    --\t\treturn error(\"Unknown %s mode: %s\", opt->long_name, arg);\n    --\treturn 0;\n     +\n    -+\tif (!parse_sign_mode(arg, val))\n    -+\t\treturn 0;\n    ++\tif (parse_sign_mode(arg, val))\n    + \t\treturn error(\"Unknown %s mode: %s\", opt->long_name, arg);\n     +\n    -+\treturn error(\"Unknown %s mode: %s\", opt->long_name, arg);\n    + \treturn 0;\n      }\n      \n    - static int parse_opt_tag_of_filtered_mode(const struct option *opt,\n     \n      ## gpg-interface.c ##\n     @@ gpg-interface.c: static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n    @@ gpg-interface.h: int check_signature(struct signature_check *sigc,\n      void print_signature_buffer(const struct signature_check *sigc,\n      \t\t\t    unsigned flags);\n      \n    -+/* Modes for --signed-tags=<mode> and --signed-commits=<mode> options */\n    ++/* Modes for --signed-tags=<mode> and --signed-commits=<mode> options. */\n     +enum sign_mode {\n     +\tSIGN_ABORT,\n     +\tSIGN_WARN_VERBATIM,\n2:  0294f05ae6 ! 2:  e34f015aea fast-import: add '--signed-commits=<mode>' option\n    @@ builtin/fast-import.c: static int global_argc;\n      /* Memory pools */\n      static struct mem_pool fi_mem_pool = {\n      \t.block_alloc = 2*1024*1024 - sizeof(struct mp_block),\n    -@@ builtin/fast-import.c: static void store_signature(struct signature_data *stored_sig,\n    - \t}\n    - }\n    - \n    -+/* Process signatures (up to 2: one \"sha1\" and one \"sha256\") */\n    -+static void import_signature(struct signature_data *sig_sha1,\n    -+\t\t\t     struct signature_data *sig_sha256,\n    -+\t\t\t     const char *v)\n    -+{\n    -+\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n    -+\n    -+\tparse_one_signature(&sig, v);\n    -+\n    -+\tif (!strcmp(sig.hash_algo, \"sha1\"))\n    -+\t\tstore_signature(sig_sha1, &sig, \"SHA-1\");\n    -+\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n    -+\t\tstore_signature(sig_sha256, &sig, \"SHA-256\");\n    -+\telse\n    -+\t\tBUG(\"parse_one_signature() returned unknown hash algo\");\n    -+}\n    -+\n    - static void parse_new_commit(const char *arg)\n    - {\n    - \tstatic struct strbuf msg = STRBUF_INIT;\n     @@ builtin/fast-import.c: static void parse_new_commit(const char *arg)\n      \tif (!committer)\n      \t\tdie(\"Expected committer but didn't get one\");\n      \n     -\t/* Process signatures (up to 2: one \"sha1\" and one \"sha256\") */\n      \twhile (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n    --\t\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n    --\n    + \t\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n    + \n     -\t\tparse_one_signature(&sig, v);\n    --\n    ++\t\tif (signed_commit_mode == SIGN_ABORT)\n    ++\t\t\tdie(_(\"encountered signed commit; use \"\n    ++\t\t\t      \"--signed-commits=<mode> to handle it\"));\n    + \n     -\t\tif (!strcmp(sig.hash_algo, \"sha1\"))\n     -\t\t\tstore_signature(&sig_sha1, &sig, \"SHA-1\");\n     -\t\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n     -\t\t\tstore_signature(&sig_sha256, &sig, \"SHA-256\");\n     -\t\telse\n     -\t\t\tBUG(\"parse_one_signature() returned unknown hash algo\");\n    --\n    -+\t\tstruct strbuf data = STRBUF_INIT;\n    ++\t\tparse_one_signature(&sig, v);\n    + \n     +\t\tswitch (signed_commit_mode) {\n     +\t\tcase SIGN_ABORT:\n    -+\t\t\tdie(\"encountered signed commit; use \"\n    -+\t\t\t    \"--signed-commits=<mode> to handle it\");\n    ++\t\t\tBUG(\"SIGN_ABORT should be handled before calling parse_one_signature()\");\n    ++\t\t\tbreak;\n     +\t\tcase SIGN_WARN_VERBATIM:\n    -+\t\t\twarning(\"importing a commit signature\");\n    ++\t\t\twarning(_(\"importing a commit signature verbatim\"));\n     +\t\t\t/* fallthru */\n     +\t\tcase SIGN_VERBATIM:\n    -+\t\t\timport_signature(&sig_sha1, &sig_sha256, v);\n    ++\t\t\tif (!strcmp(sig.hash_algo, \"sha1\"))\n    ++\t\t\t\tstore_signature(&sig_sha1, &sig, \"SHA-1\");\n    ++\t\t\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n    ++\t\t\t\tstore_signature(&sig_sha256, &sig, \"SHA-256\");\n    ++\t\t\telse\n    ++\t\t\t\tdie(_(\"parse_one_signature() returned unknown hash algo\"));\n     +\t\t\tbreak;\n     +\t\tcase SIGN_WARN_STRIP:\n    -+\t\t\twarning(\"stripping a commit signature\");\n    ++\t\t\twarning(_(\"stripping a commit signature\"));\n     +\t\t\t/* fallthru */\n     +\t\tcase SIGN_STRIP:\n    -+\t\t\t/* Read signature data and discard it */\n    -+\t\t\tread_next_command();\n    -+\t\t\tparse_data(&data, 0, NULL);\n    -+\t\t\tstrbuf_release(&data);\n    ++\t\t\t/* Just discard signature data */\n    ++\t\t\tstrbuf_release(&sig.data);\n    ++\t\t\tfree(sig.hash_algo);\n     +\t\t\tbreak;\n     +\t\t}\n      \t\tread_next_command();\n    @@ builtin/fast-import.c: static int parse_one_option(const char *option)\n      \t\toption_export_pack_edges(option);\n     +\t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n     +\t\tif (parse_sign_mode(option, &signed_commit_mode))\n    -+\t\t\tdie(\"unknown --signed-commits mode '%s'\", option);\n    ++\t\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), option);\n      \t} else if (!strcmp(option, \"quiet\")) {\n      \t\tshow_stats = 0;\n      \t\tquiet = 1;\n    @@ t/t9305-fast-import-signatures.sh (new)\n     +test_description='git fast-import --signed-commits=<mode>'\n     +\n     +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n    -+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     +\n     +. ./test-lib.sh\n     +. \"$TEST_DIRECTORY/lib-gpg.sh\"\n     +\n     +test_expect_success 'set up unsigned initial commit and import repo' '\n     +\ttest_commit first &&\n    -+\tmkdir new &&\n    -+\tgit --git-dir=new/.git init\n    ++\tgit init new\n     +'\n     +\n     +test_expect_success GPG 'set up OpenPGP signed commit' '\n    @@ t/t9305-fast-import-signatures.sh (new)\n     +\tSHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&\n     +\n     +\t# Check that the resulting SHA-1 commit has both signatures\n    -+\techo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&\n    ++\tgit -C explicit-sha256 cat-file -p $SHA1_B >out &&\n     +\ttest_grep -E \"^gpgsig \" out &&\n     +\ttest_grep -E \"^gpgsig-sha256 \" out\n     +'\n\n\nChristian Couder (2):\n  gpg-interface: refactor 'enum sign_mode' parsing\n  fast-import: add '--signed-commits=<mode>' option\n\n Documentation/git-fast-import.adoc |   5 ++\n builtin/fast-export.c              |  19 ++----\n builtin/fast-import.c              |  41 ++++++++---\n gpg-interface.c                    |  17 +++++\n gpg-interface.h                    |  15 ++++\n t/meson.build                      |   1 +\n t/t9305-fast-import-signatures.sh  | 106 +++++++++++++++++++++++++++++\n 7 files changed, 182 insertions(+), 22 deletions(-)\n create mode 100755 t/t9305-fast-import-signatures.sh\n\n-- \n2.51.0.195.gf8f8f06677\n\n"},{"id":"526180","messageId":"20250912124042.2523683-2-christian.couder@gmail.com","threadId":"64131","inReplyTo":"20250912124042.2523683-1-christian.couder@gmail.com","subject":"[PATCH v2 1/2] gpg-interface: refactor 'enum sign_mode' parsing","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-12T12:40:41Z","receivedAt":"2025-09-12T12:41:01Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The definition of 'enum sign_mode' as well as its parsing code are in\n\"builtin/fast-export.c\". This was fine because `git fast-export` was the\nonly command with '--signed-tags=<mode>' or '--signed-commits=<mode>'\noptions.\n\nIn a following commit, we are going to add a similar option to `git\nfast-import`, which will be simpler, easier and cleaner if we can reuse\nthe 'enum sign_mode' defintion and parsing code.\n\nSo let's move that definition and parsing code from\n\"builtin/fast-export.c\" to \"gpg-interface.{c,h}\".\n\nWhile at it, let's fix a small indentation issue with the arguments of\nparse_opt_sign_mode().\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/fast-export.c | 19 +++++--------------\n gpg-interface.c       | 17 +++++++++++++++++\n gpg-interface.h       | 15 +++++++++++++++\n 3 files changed, 37 insertions(+), 14 deletions(-)\n\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex c06ee0b213..dc2486f9a8 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -37,8 +37,6 @@ static const char *const fast_export_usage[] = {\n \tNULL\n };\n \n-enum sign_mode { SIGN_ABORT, SIGN_VERBATIM, SIGN_STRIP, SIGN_WARN_VERBATIM, SIGN_WARN_STRIP };\n-\n static int progress;\n static enum sign_mode signed_tag_mode = SIGN_ABORT;\n static enum sign_mode signed_commit_mode = SIGN_STRIP;\n@@ -59,23 +57,16 @@ static struct hashmap anonymized_seeds;\n static struct revision_sources revision_sources;\n \n static int parse_opt_sign_mode(const struct option *opt,\n-\t\t\t\t     const char *arg, int unset)\n+\t\t\t       const char *arg, int unset)\n {\n \tenum sign_mode *val = opt->value;\n+\n \tif (unset)\n \t\treturn 0;\n-\telse if (!strcmp(arg, \"abort\"))\n-\t\t*val = SIGN_ABORT;\n-\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n-\t\t*val = SIGN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n-\t\t*val = SIGN_WARN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-strip\"))\n-\t\t*val = SIGN_WARN_STRIP;\n-\telse if (!strcmp(arg, \"strip\"))\n-\t\t*val = SIGN_STRIP;\n-\telse\n+\n+\tif (parse_sign_mode(arg, val))\n \t\treturn error(\"Unknown %s mode: %s\", opt->long_name, arg);\n+\n \treturn 0;\n }\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 06e7fb5060..2f4f0e32cb 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -1125,3 +1125,20 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \tFREE_AND_NULL(ssh_signing_key_file);\n \treturn ret;\n }\n+\n+int parse_sign_mode(const char *arg, enum sign_mode *mode)\n+{\n+\tif (!strcmp(arg, \"abort\"))\n+\t\t*mode = SIGN_ABORT;\n+\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n+\t\t*mode = SIGN_VERBATIM;\n+\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n+\t\t*mode = SIGN_WARN_VERBATIM;\n+\telse if (!strcmp(arg, \"warn-strip\"))\n+\t\t*mode = SIGN_WARN_STRIP;\n+\telse if (!strcmp(arg, \"strip\"))\n+\t\t*mode = SIGN_STRIP;\n+\telse\n+\t\treturn -1;\n+\treturn 0;\n+}\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 60ddf8bbfa..50487aa148 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -104,4 +104,19 @@ int check_signature(struct signature_check *sigc,\n void print_signature_buffer(const struct signature_check *sigc,\n \t\t\t    unsigned flags);\n \n+/* Modes for --signed-tags=<mode> and --signed-commits=<mode> options. */\n+enum sign_mode {\n+\tSIGN_ABORT,\n+\tSIGN_WARN_VERBATIM,\n+\tSIGN_VERBATIM,\n+\tSIGN_WARN_STRIP,\n+\tSIGN_STRIP,\n+};\n+\n+/*\n+ * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n+ * otherwise.\n+ */\n+int parse_sign_mode(const char *arg, enum sign_mode *mode);\n+\n #endif\n-- \n2.51.0.195.gf8f8f06677\n\n"},{"id":"526181","messageId":"20250912124042.2523683-3-christian.couder@gmail.com","threadId":"64131","inReplyTo":"20250912124042.2523683-1-christian.couder@gmail.com","subject":"[PATCH v2 2/2] fast-import: add '--signed-commits=<mode>' option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-12T12:40:42Z","receivedAt":"2025-09-12T12:41:03Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A '--signed-commits=<mode>' option is already available when using\n`git fast-export` to decide what should be done at export time about\ncommit signatures. At import time though, there is no option, or\nother way, in `git fast-import` to decide about commit signatures.\n\nTo remediate that, let's add a '--signed-commits=<mode>' option to\n`git fast-import` too.\n\nFor now the supported <mode>s are the same as those supported by\n`git fast-export`.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/git-fast-import.adoc |   5 ++\n builtin/fast-import.c              |  41 ++++++++---\n t/meson.build                      |   1 +\n t/t9305-fast-import-signatures.sh  | 106 +++++++++++++++++++++++++++++\n 4 files changed, 145 insertions(+), 8 deletions(-)\n create mode 100755 t/t9305-fast-import-signatures.sh\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 3144ffcdb6..90f242d058 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -66,6 +66,11 @@ OPTIONS\n \tremote-helpers that use the `import` capability, as they are\n \talready trusted to run their own code.\n \n+--signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::\n+\tSpecify how to handle signed commits.  Behaves in the same way\n+\tas the same option in linkgit:git-fast-export[1], except that\n+\tdefault is 'verbatim' (instead of 'abort').\n+\n Options for Frontends\n ~~~~~~~~~~~~~~~~~~~~~\n \ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 2c35f9345d..890f05de4d 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -188,6 +188,8 @@ static int global_argc;\n static const char **global_argv;\n static const char *global_prefix;\n \n+static enum sign_mode signed_commit_mode = SIGN_VERBATIM;\n+\n /* Memory pools */\n static struct mem_pool fi_mem_pool = {\n \t.block_alloc = 2*1024*1024 - sizeof(struct mp_block),\n@@ -2817,19 +2819,39 @@ static void parse_new_commit(const char *arg)\n \tif (!committer)\n \t\tdie(\"Expected committer but didn't get one\");\n \n-\t/* Process signatures (up to 2: one \"sha1\" and one \"sha256\") */\n \twhile (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n \t\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n \n-\t\tparse_one_signature(&sig, v);\n+\t\tif (signed_commit_mode == SIGN_ABORT)\n+\t\t\tdie(_(\"encountered signed commit; use \"\n+\t\t\t      \"--signed-commits=<mode> to handle it\"));\n \n-\t\tif (!strcmp(sig.hash_algo, \"sha1\"))\n-\t\t\tstore_signature(&sig_sha1, &sig, \"SHA-1\");\n-\t\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n-\t\t\tstore_signature(&sig_sha256, &sig, \"SHA-256\");\n-\t\telse\n-\t\t\tBUG(\"parse_one_signature() returned unknown hash algo\");\n+\t\tparse_one_signature(&sig, v);\n \n+\t\tswitch (signed_commit_mode) {\n+\t\tcase SIGN_ABORT:\n+\t\t\tBUG(\"SIGN_ABORT should be handled before calling parse_one_signature()\");\n+\t\t\tbreak;\n+\t\tcase SIGN_WARN_VERBATIM:\n+\t\t\twarning(_(\"importing a commit signature verbatim\"));\n+\t\t\t/* fallthru */\n+\t\tcase SIGN_VERBATIM:\n+\t\t\tif (!strcmp(sig.hash_algo, \"sha1\"))\n+\t\t\t\tstore_signature(&sig_sha1, &sig, \"SHA-1\");\n+\t\t\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n+\t\t\t\tstore_signature(&sig_sha256, &sig, \"SHA-256\");\n+\t\t\telse\n+\t\t\t\tdie(_(\"parse_one_signature() returned unknown hash algo\"));\n+\t\t\tbreak;\n+\t\tcase SIGN_WARN_STRIP:\n+\t\t\twarning(_(\"stripping a commit signature\"));\n+\t\t\t/* fallthru */\n+\t\tcase SIGN_STRIP:\n+\t\t\t/* Just discard signature data */\n+\t\t\tstrbuf_release(&sig.data);\n+\t\t\tfree(sig.hash_algo);\n+\t\t\tbreak;\n+\t\t}\n \t\tread_next_command();\n \t}\n \n@@ -3501,6 +3523,9 @@ static int parse_one_option(const char *option)\n \t\toption_active_branches(option);\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n \t\toption_export_pack_edges(option);\n+\t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n+\t\tif (parse_sign_mode(option, &signed_commit_mode))\n+\t\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), option);\n \t} else if (!strcmp(option, \"quiet\")) {\n \t\tshow_stats = 0;\n \t\tquiet = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex 82af229be3..08ad6938e2 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -1032,6 +1032,7 @@ integration_tests = [\n   't9302-fast-import-unpack-limit.sh',\n   't9303-fast-import-compression.sh',\n   't9304-fast-import-marks.sh',\n+  't9305-fast-import-signatures.sh',\n   't9350-fast-export.sh',\n   't9351-fast-export-anonymize.sh',\n   't9400-git-cvsserver-server.sh',\ndiff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\nnew file mode 100755\nindex 0000000000..c2b4271658\n--- /dev/null\n+++ b/t/t9305-fast-import-signatures.sh\n@@ -0,0 +1,106 @@\n+#!/bin/sh\n+\n+test_description='git fast-import --signed-commits=<mode>'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success 'set up unsigned initial commit and import repo' '\n+\ttest_commit first &&\n+\tgit init new\n+'\n+\n+test_expect_success GPG 'set up OpenPGP signed commit' '\n+\tgit checkout -b openpgp-signing main &&\n+\techo \"Content for OpenPGP signing.\" >file-sign &&\n+\tgit add file-sign &&\n+\tgit commit -S -m \"OpenPGP signed commit\" &&\n+\tOPENPGP_SIGNING=$(git rev-parse --verify openpgp-signing)\n+'\n+\n+test_expect_success GPG 'import OpenPGP signature with --signed-commits=verbatim' '\n+\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\tgit -C new fast-import --quiet --signed-commits=verbatim <output >log 2>&1 &&\n+\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n+\ttest $OPENPGP_SIGNING = $IMPORTED &&\n+\ttest_must_be_empty log\n+'\n+\n+test_expect_success GPGSM 'set up X.509 signed commit' '\n+\tgit checkout -b x509-signing main &&\n+\ttest_config gpg.format x509 &&\n+\ttest_config user.signingkey $GIT_COMMITTER_EMAIL &&\n+\techo \"Content for X.509 signing.\" >file-sign &&\n+\tgit add file-sign &&\n+\tgit commit -S -m \"X.509 signed commit\" &&\n+\tX509_SIGNING=$(git rev-parse HEAD)\n+'\n+\n+test_expect_success GPGSM 'import X.509 signature fails with --signed-commits=abort' '\n+\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\ttest_must_fail git -C new fast-import --quiet --signed-commits=abort <output\n+'\n+\n+test_expect_success GPGSM 'import X.509 signature with --signed-commits=warn-verbatim' '\n+\tgit -C new fast-import --quiet --signed-commits=warn-verbatim <output >log 2>&1 &&\n+\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n+\ttest $X509_SIGNING = $IMPORTED &&\n+\ttest_grep \"importing a commit signature\" log\n+'\n+\n+test_expect_success GPGSSH 'set up SSH signed commit' '\n+\tgit checkout -b ssh-signing main &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\techo \"Content for SSH signing.\" >file-sign &&\n+\tgit add file-sign &&\n+\tgit commit -S -m \"SSH signed commit\" &&\n+\tSSH_SIGNING=$(git rev-parse HEAD)\n+'\n+\n+test_expect_success GPGSSH 'strip SSH signature with --signed-commits=strip' '\n+\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\tgit -C new fast-import --quiet --signed-commits=strip <output >log 2>&1 &&\n+\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n+\ttest $SSH_SIGNING != $IMPORTED &&\n+\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\ttest_grep ! -E \"^gpgsig\" actual &&\n+\ttest_must_be_empty log\n+'\n+\n+test_expect_success GPG 'setup a commit with dual OpenPGP signatures on its SHA-1 and SHA-256 formats' '\n+\t# Create a signed SHA-256 commit\n+\tgit init --object-format=sha256 explicit-sha256 &&\n+\tgit -C explicit-sha256 config extensions.compatObjectFormat sha1 &&\n+\tgit -C explicit-sha256 checkout -b dual-signed &&\n+\ttest_commit -C explicit-sha256 A &&\n+\techo B >explicit-sha256/B &&\n+\tgit -C explicit-sha256 add B &&\n+\ttest_tick &&\n+\tgit -C explicit-sha256 commit -S -m \"signed\" B &&\n+\tSHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&\n+\n+\t# Create the corresponding SHA-1 commit\n+\tSHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&\n+\n+\t# Check that the resulting SHA-1 commit has both signatures\n+\tgit -C explicit-sha256 cat-file -p $SHA1_B >out &&\n+\ttest_grep -E \"^gpgsig \" out &&\n+\ttest_grep -E \"^gpgsig-sha256 \" out\n+'\n+\n+test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=warn-strip' '\n+\tgit -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&\n+\ttest_grep -E \"^gpgsig sha1 openpgp\" output &&\n+\ttest_grep -E \"^gpgsig sha256 openpgp\" output &&\n+\tgit -C new fast-import --quiet --signed-commits=warn-strip <output >log 2>&1 &&\n+\tgit -C new cat-file commit refs/heads/dual-signed >actual &&\n+\ttest_grep ! -E \"^gpgsig \" actual &&\n+\ttest_grep ! -E \"^gpgsig-sha256 \" actual &&\n+\ttest_grep \"stripping a commit signature\" log >out &&\n+\ttest_line_count = 2 out\n+'\n+\n+test_done\n-- \n2.51.0.195.gf8f8f06677\n\n"},{"id":"526182","messageId":"CAP8UFD3guuRXR6DEEBLaO3AqzSfs_woj6WcrCxJ3krdObokD3A@mail.gmail.com","threadId":"64131","inReplyTo":"20250912124042.2523683-3-christian.couder@gmail.com","subject":"Re: [PATCH v2 2/2] fast-import: add '--signed-commits=<mode>' option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-12T12:45:37Z","receivedAt":"2025-09-12T12:45:52Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Sorry I forgot to use the\n--in-reply-to='20250910080839.2142651-1-christian.couder@gmail.com'\noption when sending this series. It is related to:\n\nhttps://lore.kernel.org/git/20250910080839.2142651-1-christian.couder@gmail.com/\n\nOn Fri, Sep 12, 2025 at 2:40 PM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> A '--signed-commits=<mode>' option is already available when using\n> `git fast-export` to decide what should be done at export time about\n> commit signatures. At import time though, there is no option, or\n> other way, in `git fast-import` to decide about commit signatures.\n>\n> To remediate that, let's add a '--signed-commits=<mode>' option to\n> `git fast-import` too.\n>\n> For now the supported <mode>s are the same as those supported by\n> `git fast-export`.\n>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>  Documentation/git-fast-import.adoc |   5 ++\n>  builtin/fast-import.c              |  41 ++++++++---\n>  t/meson.build                      |   1 +\n>  t/t9305-fast-import-signatures.sh  | 106 +++++++++++++++++++++++++++++\n>  4 files changed, 145 insertions(+), 8 deletions(-)\n>  create mode 100755 t/t9305-fast-import-signatures.sh\n>\n> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> index 3144ffcdb6..90f242d058 100644\n> --- a/Documentation/git-fast-import.adoc\n> +++ b/Documentation/git-fast-import.adoc\n> @@ -66,6 +66,11 @@ OPTIONS\n>         remote-helpers that use the `import` capability, as they are\n>         already trusted to run their own code.\n>\n> +--signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::\n> +       Specify how to handle signed commits.  Behaves in the same way\n> +       as the same option in linkgit:git-fast-export[1], except that\n> +       default is 'verbatim' (instead of 'abort').\n> +\n>  Options for Frontends\n>  ~~~~~~~~~~~~~~~~~~~~~\n>\n> diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> index 2c35f9345d..890f05de4d 100644\n> --- a/builtin/fast-import.c\n> +++ b/builtin/fast-import.c\n> @@ -188,6 +188,8 @@ static int global_argc;\n>  static const char **global_argv;\n>  static const char *global_prefix;\n>\n> +static enum sign_mode signed_commit_mode = SIGN_VERBATIM;\n> +\n>  /* Memory pools */\n>  static struct mem_pool fi_mem_pool = {\n>         .block_alloc = 2*1024*1024 - sizeof(struct mp_block),\n> @@ -2817,19 +2819,39 @@ static void parse_new_commit(const char *arg)\n>         if (!committer)\n>                 die(\"Expected committer but didn't get one\");\n>\n> -       /* Process signatures (up to 2: one \"sha1\" and one \"sha256\") */\n>         while (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n>                 struct signature_data sig = { NULL, NULL, STRBUF_INIT };\n>\n> -               parse_one_signature(&sig, v);\n> +               if (signed_commit_mode == SIGN_ABORT)\n> +                       die(_(\"encountered signed commit; use \"\n> +                             \"--signed-commits=<mode> to handle it\"));\n>\n> -               if (!strcmp(sig.hash_algo, \"sha1\"))\n> -                       store_signature(&sig_sha1, &sig, \"SHA-1\");\n> -               else if (!strcmp(sig.hash_algo, \"sha256\"))\n> -                       store_signature(&sig_sha256, &sig, \"SHA-256\");\n> -               else\n> -                       BUG(\"parse_one_signature() returned unknown hash algo\");\n> +               parse_one_signature(&sig, v);\n>\n> +               switch (signed_commit_mode) {\n> +               case SIGN_ABORT:\n> +                       BUG(\"SIGN_ABORT should be handled before calling parse_one_signature()\");\n> +                       break;\n> +               case SIGN_WARN_VERBATIM:\n> +                       warning(_(\"importing a commit signature verbatim\"));\n> +                       /* fallthru */\n> +               case SIGN_VERBATIM:\n> +                       if (!strcmp(sig.hash_algo, \"sha1\"))\n> +                               store_signature(&sig_sha1, &sig, \"SHA-1\");\n> +                       else if (!strcmp(sig.hash_algo, \"sha256\"))\n> +                               store_signature(&sig_sha256, &sig, \"SHA-256\");\n> +                       else\n> +                               die(_(\"parse_one_signature() returned unknown hash algo\"));\n> +                       break;\n> +               case SIGN_WARN_STRIP:\n> +                       warning(_(\"stripping a commit signature\"));\n> +                       /* fallthru */\n> +               case SIGN_STRIP:\n> +                       /* Just discard signature data */\n> +                       strbuf_release(&sig.data);\n> +                       free(sig.hash_algo);\n> +                       break;\n> +               }\n>                 read_next_command();\n>         }\n>\n> @@ -3501,6 +3523,9 @@ static int parse_one_option(const char *option)\n>                 option_active_branches(option);\n>         } else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n>                 option_export_pack_edges(option);\n> +       } else if (skip_prefix(option, \"signed-commits=\", &option)) {\n> +               if (parse_sign_mode(option, &signed_commit_mode))\n> +                       usagef(_(\"unknown --signed-commits mode '%s'\"), option);\n>         } else if (!strcmp(option, \"quiet\")) {\n>                 show_stats = 0;\n>                 quiet = 1;\n> diff --git a/t/meson.build b/t/meson.build\n> index 82af229be3..08ad6938e2 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -1032,6 +1032,7 @@ integration_tests = [\n>    't9302-fast-import-unpack-limit.sh',\n>    't9303-fast-import-compression.sh',\n>    't9304-fast-import-marks.sh',\n> +  't9305-fast-import-signatures.sh',\n>    't9350-fast-export.sh',\n>    't9351-fast-export-anonymize.sh',\n>    't9400-git-cvsserver-server.sh',\n> diff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\n> new file mode 100755\n> index 0000000000..c2b4271658\n> --- /dev/null\n> +++ b/t/t9305-fast-import-signatures.sh\n> @@ -0,0 +1,106 @@\n> +#!/bin/sh\n> +\n> +test_description='git fast-import --signed-commits=<mode>'\n> +\n> +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n> +\n> +. ./test-lib.sh\n> +. \"$TEST_DIRECTORY/lib-gpg.sh\"\n> +\n> +test_expect_success 'set up unsigned initial commit and import repo' '\n> +       test_commit first &&\n> +       git init new\n> +'\n> +\n> +test_expect_success GPG 'set up OpenPGP signed commit' '\n> +       git checkout -b openpgp-signing main &&\n> +       echo \"Content for OpenPGP signing.\" >file-sign &&\n> +       git add file-sign &&\n> +       git commit -S -m \"OpenPGP signed commit\" &&\n> +       OPENPGP_SIGNING=$(git rev-parse --verify openpgp-signing)\n> +'\n> +\n> +test_expect_success GPG 'import OpenPGP signature with --signed-commits=verbatim' '\n> +       git fast-export --signed-commits=verbatim openpgp-signing >output &&\n> +       git -C new fast-import --quiet --signed-commits=verbatim <output >log 2>&1 &&\n> +       IMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n> +       test $OPENPGP_SIGNING = $IMPORTED &&\n> +       test_must_be_empty log\n> +'\n> +\n> +test_expect_success GPGSM 'set up X.509 signed commit' '\n> +       git checkout -b x509-signing main &&\n> +       test_config gpg.format x509 &&\n> +       test_config user.signingkey $GIT_COMMITTER_EMAIL &&\n> +       echo \"Content for X.509 signing.\" >file-sign &&\n> +       git add file-sign &&\n> +       git commit -S -m \"X.509 signed commit\" &&\n> +       X509_SIGNING=$(git rev-parse HEAD)\n> +'\n> +\n> +test_expect_success GPGSM 'import X.509 signature fails with --signed-commits=abort' '\n> +       git fast-export --signed-commits=verbatim x509-signing >output &&\n> +       test_must_fail git -C new fast-import --quiet --signed-commits=abort <output\n> +'\n> +\n> +test_expect_success GPGSM 'import X.509 signature with --signed-commits=warn-verbatim' '\n> +       git -C new fast-import --quiet --signed-commits=warn-verbatim <output >log 2>&1 &&\n> +       IMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n> +       test $X509_SIGNING = $IMPORTED &&\n> +       test_grep \"importing a commit signature\" log\n> +'\n> +\n> +test_expect_success GPGSSH 'set up SSH signed commit' '\n> +       git checkout -b ssh-signing main &&\n> +       test_config gpg.format ssh &&\n> +       test_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n> +       echo \"Content for SSH signing.\" >file-sign &&\n> +       git add file-sign &&\n> +       git commit -S -m \"SSH signed commit\" &&\n> +       SSH_SIGNING=$(git rev-parse HEAD)\n> +'\n> +\n> +test_expect_success GPGSSH 'strip SSH signature with --signed-commits=strip' '\n> +       git fast-export --signed-commits=verbatim ssh-signing >output &&\n> +       git -C new fast-import --quiet --signed-commits=strip <output >log 2>&1 &&\n> +       IMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n> +       test $SSH_SIGNING != $IMPORTED &&\n> +       git -C new cat-file commit \"$IMPORTED\" >actual &&\n> +       test_grep ! -E \"^gpgsig\" actual &&\n> +       test_must_be_empty log\n> +'\n> +\n> +test_expect_success GPG 'setup a commit with dual OpenPGP signatures on its SHA-1 and SHA-256 formats' '\n> +       # Create a signed SHA-256 commit\n> +       git init --object-format=sha256 explicit-sha256 &&\n> +       git -C explicit-sha256 config extensions.compatObjectFormat sha1 &&\n> +       git -C explicit-sha256 checkout -b dual-signed &&\n> +       test_commit -C explicit-sha256 A &&\n> +       echo B >explicit-sha256/B &&\n> +       git -C explicit-sha256 add B &&\n> +       test_tick &&\n> +       git -C explicit-sha256 commit -S -m \"signed\" B &&\n> +       SHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&\n> +\n> +       # Create the corresponding SHA-1 commit\n> +       SHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&\n> +\n> +       # Check that the resulting SHA-1 commit has both signatures\n> +       git -C explicit-sha256 cat-file -p $SHA1_B >out &&\n> +       test_grep -E \"^gpgsig \" out &&\n> +       test_grep -E \"^gpgsig-sha256 \" out\n> +'\n> +\n> +test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=warn-strip' '\n> +       git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&\n> +       test_grep -E \"^gpgsig sha1 openpgp\" output &&\n> +       test_grep -E \"^gpgsig sha256 openpgp\" output &&\n> +       git -C new fast-import --quiet --signed-commits=warn-strip <output >log 2>&1 &&\n> +       git -C new cat-file commit refs/heads/dual-signed >actual &&\n> +       test_grep ! -E \"^gpgsig \" actual &&\n> +       test_grep ! -E \"^gpgsig-sha256 \" actual &&\n> +       test_grep \"stripping a commit signature\" log >out &&\n> +       test_line_count = 2 out\n> +'\n> +\n> +test_done\n> --\n> 2.51.0.195.gf8f8f06677\n>\n"},{"id":"526301","messageId":"aMex06JNPVeHaw_d@pks.im","threadId":"64131","inReplyTo":"20250912124042.2523683-3-christian.couder@gmail.com","subject":"Re: [PATCH v2 2/2] fast-import: add '--signed-commits=<mode>' option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-15T06:27:31Z","receivedAt":"2025-09-15T06:27:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Sep 12, 2025 at 02:40:42PM +0200, Christian Couder wrote:\n> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> index 3144ffcdb6..90f242d058 100644\n> --- a/Documentation/git-fast-import.adoc\n> +++ b/Documentation/git-fast-import.adoc\n> diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> index 2c35f9345d..890f05de4d 100644\n> --- a/builtin/fast-import.c\n> +++ b/builtin/fast-import.c\n> @@ -2817,19 +2819,39 @@ static void parse_new_commit(const char *arg)\n>  \tif (!committer)\n>  \t\tdie(\"Expected committer but didn't get one\");\n>  \n> -\t/* Process signatures (up to 2: one \"sha1\" and one \"sha256\") */\n>  \twhile (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n>  \t\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n>  \n> -\t\tparse_one_signature(&sig, v);\n> +\t\tif (signed_commit_mode == SIGN_ABORT)\n> +\t\t\tdie(_(\"encountered signed commit; use \"\n> +\t\t\t      \"--signed-commits=<mode> to handle it\"));\n>  \n> -\t\tif (!strcmp(sig.hash_algo, \"sha1\"))\n> -\t\t\tstore_signature(&sig_sha1, &sig, \"SHA-1\");\n> -\t\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n> -\t\t\tstore_signature(&sig_sha256, &sig, \"SHA-256\");\n> -\t\telse\n> -\t\t\tBUG(\"parse_one_signature() returned unknown hash algo\");\n> +\t\tparse_one_signature(&sig, v);\n>  \n> +\t\tswitch (signed_commit_mode) {\n> +\t\tcase SIGN_ABORT:\n> +\t\t\tBUG(\"SIGN_ABORT should be handled before calling parse_one_signature()\");\n> +\t\t\tbreak;\n\nLet's be defensive and convert this into a `default:` case so that any\nunhandled value will cause a BUG.\n\n> +\t\tcase SIGN_WARN_VERBATIM:\n> +\t\t\twarning(_(\"importing a commit signature verbatim\"));\n> +\t\t\t/* fallthru */\n> +\t\tcase SIGN_VERBATIM:\n> +\t\t\tif (!strcmp(sig.hash_algo, \"sha1\"))\n> +\t\t\t\tstore_signature(&sig_sha1, &sig, \"SHA-1\");\n> +\t\t\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n> +\t\t\t\tstore_signature(&sig_sha256, &sig, \"SHA-256\");\n> +\t\t\telse\n> +\t\t\t\tdie(_(\"parse_one_signature() returned unknown hash algo\"));\n> +\t\t\tbreak;\n> +\t\tcase SIGN_WARN_STRIP:\n> +\t\t\twarning(_(\"stripping a commit signature\"));\n> +\t\t\t/* fallthru */\n> +\t\tcase SIGN_STRIP:\n> +\t\t\t/* Just discard signature data */\n> +\t\t\tstrbuf_release(&sig.data);\n> +\t\t\tfree(sig.hash_algo);\n> +\t\t\tbreak;\n> +\t\t}\n>  \t\tread_next_command();\n>  \t}\n>  \n\nOther than that the patch looks good to me, thanks!\n\nPatrick\n"},{"id":"526336","messageId":"CAP8UFD3-3zjBBHP-Y9nLO-qEyhkWhD_pxFZhk6rzrC31LanDMQ@mail.gmail.com","threadId":"64131","inReplyTo":"aMex06JNPVeHaw_d@pks.im","subject":"Re: [PATCH v2 2/2] fast-import: add '--signed-commits=<mode>' option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-15T10:17:33Z","receivedAt":"2025-09-15T10:17:47Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Sep 15, 2025 at 8:27 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Fri, Sep 12, 2025 at 02:40:42PM +0200, Christian Couder wrote:\n\n> >\n> > -     /* Process signatures (up to 2: one \"sha1\" and one \"sha256\") */\n> >       while (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n> >               struct signature_data sig = { NULL, NULL, STRBUF_INIT };\n> >\n> > -             parse_one_signature(&sig, v);\n> > +             if (signed_commit_mode == SIGN_ABORT)\n> > +                     die(_(\"encountered signed commit; use \"\n> > +                           \"--signed-commits=<mode> to handle it\"));\n> >\n> > -             if (!strcmp(sig.hash_algo, \"sha1\"))\n> > -                     store_signature(&sig_sha1, &sig, \"SHA-1\");\n> > -             else if (!strcmp(sig.hash_algo, \"sha256\"))\n> > -                     store_signature(&sig_sha256, &sig, \"SHA-256\");\n> > -             else\n> > -                     BUG(\"parse_one_signature() returned unknown hash algo\");\n> > +             parse_one_signature(&sig, v);\n> >\n> > +             switch (signed_commit_mode) {\n> > +             case SIGN_ABORT:\n> > +                     BUG(\"SIGN_ABORT should be handled before calling parse_one_signature()\");\n> > +                     break;\n>\n> Let's be defensive and convert this into a `default:` case so that any\n> unhandled value will cause a BUG.\n\nOk, maybe something like BUG(\"invalid signed_commit_mode value %d\",\nsigned_commit_mode) then?\n\nNote that if we later develop new modes like \"re-sign\" or\n\"strip-if-invalid\", and users tries one such mode with an old version\nof Git, that should already be handled by the following code in\nparse_one_option():\n\n    } else if (skip_prefix(option, \"signed-commits=\", &option)) {\n        if (parse_sign_mode(option, &signed_commit_mode))\n            usagef(_(\"unknown --signed-commits mode '%s'\"), option);\n    } ...\n\nThanks,\nChristian.\n"},{"id":"526342","messageId":"aMfw4rboBOemzvr6@pks.im","threadId":"64131","inReplyTo":"CAP8UFD3-3zjBBHP-Y9nLO-qEyhkWhD_pxFZhk6rzrC31LanDMQ@mail.gmail.com","subject":"Re: [PATCH v2 2/2] fast-import: add '--signed-commits=<mode>' option","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-15T10:56:34Z","receivedAt":"2025-09-15T10:56:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 15, 2025 at 12:17:33PM +0200, Christian Couder wrote:\n> On Mon, Sep 15, 2025 at 8:27 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > On Fri, Sep 12, 2025 at 02:40:42PM +0200, Christian Couder wrote:\n> \n> > >\n> > > -     /* Process signatures (up to 2: one \"sha1\" and one \"sha256\") */\n> > >       while (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n> > >               struct signature_data sig = { NULL, NULL, STRBUF_INIT };\n> > >\n> > > -             parse_one_signature(&sig, v);\n> > > +             if (signed_commit_mode == SIGN_ABORT)\n> > > +                     die(_(\"encountered signed commit; use \"\n> > > +                           \"--signed-commits=<mode> to handle it\"));\n> > >\n> > > -             if (!strcmp(sig.hash_algo, \"sha1\"))\n> > > -                     store_signature(&sig_sha1, &sig, \"SHA-1\");\n> > > -             else if (!strcmp(sig.hash_algo, \"sha256\"))\n> > > -                     store_signature(&sig_sha256, &sig, \"SHA-256\");\n> > > -             else\n> > > -                     BUG(\"parse_one_signature() returned unknown hash algo\");\n> > > +             parse_one_signature(&sig, v);\n> > >\n> > > +             switch (signed_commit_mode) {\n> > > +             case SIGN_ABORT:\n> > > +                     BUG(\"SIGN_ABORT should be handled before calling parse_one_signature()\");\n> > > +                     break;\n> >\n> > Let's be defensive and convert this into a `default:` case so that any\n> > unhandled value will cause a BUG.\n> \n> Ok, maybe something like BUG(\"invalid signed_commit_mode value %d\",\n> signed_commit_mode) then?\n\nYeah, that should do the job.\n\nThanks!\n\nPatrick\n"},{"id":"526606","messageId":"20250917181427.3193500-1-christian.couder@gmail.com","threadId":"64131","inReplyTo":"20250912124042.2523683-1-christian.couder@gmail.com","subject":"[PATCH v3 0/2] fast-import: start controlling how commit signatures are handled","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-17T18:14:25Z","receivedAt":"2025-09-17T18:14:46Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Tools like `git-filter-repo` should be able to control how commit\nsignatures are handled when regenerating repository content after it\nhas been filtered (see\nhttps://github.com/newren/git-filter-repo/issues/139). For this\npurpose, they need a way for `git fast-import` to control how commit\nsignatures are handled.\n\nThis small patch series starts to implement such a way by adding a new\n`--signed-commits=<mode>` option to `git fast-import`.\n\nFor now this new option behaves in a very similar way as the option\nwith the same name that already exists in `git fast-export`.\nEspecially it supports exactly the same <mode>s and the same aliases\nfor these modes. For example \"ignore\" is a synonym for \"verbatim\".\n\nIn the future I want to implement new <mode>s like \"strip-if-invalid\",\n\"re-sign\", \"re-sign-if-invalid\" that might be a bit more complex for\nthis option. But for now I prefer to start with the simple modes to\nvalidate the general design of the new option.\n\nIn the future I also plan to add a similar `--signed-tags=<mode>` so\nthat the import of tags can also be controlled. But I prefer to\nvalidate the general design of a single new option first.\n\nIn particular, I am interested in feedback about the following:\n\n  - Should we keep \"ignore\" as a synonym for \"verbatim\" and \"warn\" as\n    a synonym for \"warn-verbatim\"? My opinion is that they might be\n    confusing, so we might want to remove them for `git fast-import`\n    even if we keep them for `git fast-export`. The parsing code might\n    be a bit more complex if we do that though, so for now I have kept\n    the synonyms.\n\n  - Are we still fine with most <mode>s having a \"warn-*\" variant\n    (like the \"warn-strip\" variant of \"strip\" for example)? Or should\n    we have a separate `--verbose` or maybe `--signed-commits-verbose`\n    option dedicated to switching warnings on/off? I think it's good\n    to decide about this before the number of <mode>s increases a lot\n    with new <mode>s like \"strip-if-invalid\", \"re-sign\",\n    \"re-sign-if-invalid\" and possibly others.\n\nChanges since v2:\n=================\n\nThanks to Junio and Patrick for reviewing previous versions.\n\nIn patch 2/2:\n\n  - The commit message has been improved to explain why new functions\n    are added and how the 'strip' and 'warn-strip' modes are handled.\n\n  - The code responsible for consuming a signature has been refactored\n    into two new functions: import_one_signature() and\n    discard_one_signature() functions. This makes it easier to follow\n    the logic and add new modes in the future.\n\n  - The SIGN_ABORT case is not handled separately from the other cases\n    anymore.\n\n  - Code comments have been added to the\n    `switch (signed_commit_mode) { ... }` that handles the different\n    cases, and a `default: ...` case that calls BUG() has been added\n    to it.\n\nCI tests:\n=========\n\nThey have all passed. See:\n\nhttps://github.com/chriscool/git/actions/runs/17804251337\n\nRange-diff vs V2:\n=================\n\n1:  87149ae92d = 1:  87149ae92d gpg-interface: refactor 'enum sign_mode' parsing\n2:  e34f015aea ! 2:  cf6ce66e1d fast-import: add '--signed-commits=<mode>' option\n    @@ Commit message\n         For now the supported <mode>s are the same as those supported by\n         `git fast-export`.\n     \n    +    The code responsible for consuming a signature is refactored into\n    +    the import_one_signature() and discard_one_signature() functions,\n    +    which makes it easier to follow the logic and add new modes in the\n    +    future.\n    +\n    +    In the 'strip' and 'warn-strip' modes, we deliberately use\n    +    discard_one_signature() to discard the signature without parsing it.\n    +    This ensures that even malformed signatures, which would cause the\n    +    parser to fail, can be successfully stripped from a commit.\n    +\n         Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n     \n      ## Documentation/git-fast-import.adoc ##\n    @@ builtin/fast-import.c: static int global_argc;\n      /* Memory pools */\n      static struct mem_pool fi_mem_pool = {\n      \t.block_alloc = 2*1024*1024 - sizeof(struct mp_block),\n    +@@ builtin/fast-import.c: static void parse_one_signature(struct signature_data *sig, const char *v)\n    + \tparse_data(&sig->data, 0, NULL);\n    + }\n    + \n    ++static void discard_one_signature(void)\n    ++{\n    ++\tstruct strbuf data = STRBUF_INIT;\n    ++\n    ++\tread_next_command();\n    ++\tparse_data(&data, 0, NULL);\n    ++\tstrbuf_release(&data);\n    ++}\n    ++\n    + static void add_gpgsig_to_commit(struct strbuf *commit_data,\n    + \t\t\t\t const char *header,\n    + \t\t\t\t struct signature_data *sig)\n    +@@ builtin/fast-import.c: static void store_signature(struct signature_data *stored_sig,\n    + \t}\n    + }\n    + \n    ++static void import_one_signature(struct signature_data *sig_sha1,\n    ++\t\t\t\t struct signature_data *sig_sha256,\n    ++\t\t\t\t const char *v)\n    ++{\n    ++\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n    ++\n    ++\tparse_one_signature(&sig, v);\n    ++\n    ++\tif (!strcmp(sig.hash_algo, \"sha1\"))\n    ++\t\tstore_signature(sig_sha1, &sig, \"SHA-1\");\n    ++\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n    ++\t\tstore_signature(sig_sha256, &sig, \"SHA-256\");\n    ++\telse\n    ++\t\tdie(_(\"parse_one_signature() returned unknown hash algo\"));\n    ++}\n    ++\n    + static void parse_new_commit(const char *arg)\n    + {\n    + \tstatic struct strbuf msg = STRBUF_INIT;\n     @@ builtin/fast-import.c: static void parse_new_commit(const char *arg)\n      \tif (!committer)\n      \t\tdie(\"Expected committer but didn't get one\");\n      \n     -\t/* Process signatures (up to 2: one \"sha1\" and one \"sha256\") */\n      \twhile (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n    - \t\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n    - \n    +-\t\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n    +-\n     -\t\tparse_one_signature(&sig, v);\n    -+\t\tif (signed_commit_mode == SIGN_ABORT)\n    -+\t\t\tdie(_(\"encountered signed commit; use \"\n    -+\t\t\t      \"--signed-commits=<mode> to handle it\"));\n    ++\t\tswitch (signed_commit_mode) {\n    ++\n    ++\t\t/* First, modes that don't need the signature to be parsed */\n    ++\t\tcase SIGN_ABORT:\n    ++\t\t\tdie(\"encountered signed commit; use \"\n    ++\t\t\t    \"--signed-commits=<mode> to handle it\");\n    ++\t\tcase SIGN_WARN_STRIP:\n    ++\t\t\twarning(_(\"stripping a commit signature\"));\n    ++\t\t\t/* fallthru */\n    ++\t\tcase SIGN_STRIP:\n    ++\t\t\tdiscard_one_signature();\n    ++\t\t\tbreak;\n      \n     -\t\tif (!strcmp(sig.hash_algo, \"sha1\"))\n     -\t\t\tstore_signature(&sig_sha1, &sig, \"SHA-1\");\n    @@ builtin/fast-import.c: static void parse_new_commit(const char *arg)\n     -\t\t\tstore_signature(&sig_sha256, &sig, \"SHA-256\");\n     -\t\telse\n     -\t\t\tBUG(\"parse_one_signature() returned unknown hash algo\");\n    -+\t\tparse_one_signature(&sig, v);\n    - \n    -+\t\tswitch (signed_commit_mode) {\n    -+\t\tcase SIGN_ABORT:\n    -+\t\t\tBUG(\"SIGN_ABORT should be handled before calling parse_one_signature()\");\n    -+\t\t\tbreak;\n    ++\t\t/* Second, modes that parse the signature */\n     +\t\tcase SIGN_WARN_VERBATIM:\n     +\t\t\twarning(_(\"importing a commit signature verbatim\"));\n     +\t\t\t/* fallthru */\n     +\t\tcase SIGN_VERBATIM:\n    -+\t\t\tif (!strcmp(sig.hash_algo, \"sha1\"))\n    -+\t\t\t\tstore_signature(&sig_sha1, &sig, \"SHA-1\");\n    -+\t\t\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n    -+\t\t\t\tstore_signature(&sig_sha256, &sig, \"SHA-256\");\n    -+\t\t\telse\n    -+\t\t\t\tdie(_(\"parse_one_signature() returned unknown hash algo\"));\n    -+\t\t\tbreak;\n    -+\t\tcase SIGN_WARN_STRIP:\n    -+\t\t\twarning(_(\"stripping a commit signature\"));\n    -+\t\t\t/* fallthru */\n    -+\t\tcase SIGN_STRIP:\n    -+\t\t\t/* Just discard signature data */\n    -+\t\t\tstrbuf_release(&sig.data);\n    -+\t\t\tfree(sig.hash_algo);\n    ++\t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n     +\t\t\tbreak;\n    + \n    ++\t\t/* Third, BUG */\n    ++\t\tdefault:\n    ++\t\t\tBUG(\"invalid signed_commit_mode value %d\", signed_commit_mode);\n     +\t\t}\n      \t\tread_next_command();\n      \t}\n\n\nChristian Couder (2):\n  gpg-interface: refactor 'enum sign_mode' parsing\n  fast-import: add '--signed-commits=<mode>' option\n\n Documentation/git-fast-import.adoc |   5 ++\n builtin/fast-export.c              |  19 ++----\n builtin/fast-import.c              |  63 ++++++++++++++---\n gpg-interface.c                    |  17 +++++\n gpg-interface.h                    |  15 ++++\n t/meson.build                      |   1 +\n t/t9305-fast-import-signatures.sh  | 106 +++++++++++++++++++++++++++++\n 7 files changed, 202 insertions(+), 24 deletions(-)\n create mode 100755 t/t9305-fast-import-signatures.sh\n\n-- \n2.51.0.195.ge34f015aea.dirty\n\n"},{"id":"526607","messageId":"20250917181427.3193500-2-christian.couder@gmail.com","threadId":"64131","inReplyTo":"20250917181427.3193500-1-christian.couder@gmail.com","subject":"[PATCH v3 1/2] gpg-interface: refactor 'enum sign_mode' parsing","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-17T18:14:26Z","receivedAt":"2025-09-17T18:14:46Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The definition of 'enum sign_mode' as well as its parsing code are in\n\"builtin/fast-export.c\". This was fine because `git fast-export` was the\nonly command with '--signed-tags=<mode>' or '--signed-commits=<mode>'\noptions.\n\nIn a following commit, we are going to add a similar option to `git\nfast-import`, which will be simpler, easier and cleaner if we can reuse\nthe 'enum sign_mode' defintion and parsing code.\n\nSo let's move that definition and parsing code from\n\"builtin/fast-export.c\" to \"gpg-interface.{c,h}\".\n\nWhile at it, let's fix a small indentation issue with the arguments of\nparse_opt_sign_mode().\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n builtin/fast-export.c | 19 +++++--------------\n gpg-interface.c       | 17 +++++++++++++++++\n gpg-interface.h       | 15 +++++++++++++++\n 3 files changed, 37 insertions(+), 14 deletions(-)\n\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex c06ee0b213..dc2486f9a8 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -37,8 +37,6 @@ static const char *const fast_export_usage[] = {\n \tNULL\n };\n \n-enum sign_mode { SIGN_ABORT, SIGN_VERBATIM, SIGN_STRIP, SIGN_WARN_VERBATIM, SIGN_WARN_STRIP };\n-\n static int progress;\n static enum sign_mode signed_tag_mode = SIGN_ABORT;\n static enum sign_mode signed_commit_mode = SIGN_STRIP;\n@@ -59,23 +57,16 @@ static struct hashmap anonymized_seeds;\n static struct revision_sources revision_sources;\n \n static int parse_opt_sign_mode(const struct option *opt,\n-\t\t\t\t     const char *arg, int unset)\n+\t\t\t       const char *arg, int unset)\n {\n \tenum sign_mode *val = opt->value;\n+\n \tif (unset)\n \t\treturn 0;\n-\telse if (!strcmp(arg, \"abort\"))\n-\t\t*val = SIGN_ABORT;\n-\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n-\t\t*val = SIGN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n-\t\t*val = SIGN_WARN_VERBATIM;\n-\telse if (!strcmp(arg, \"warn-strip\"))\n-\t\t*val = SIGN_WARN_STRIP;\n-\telse if (!strcmp(arg, \"strip\"))\n-\t\t*val = SIGN_STRIP;\n-\telse\n+\n+\tif (parse_sign_mode(arg, val))\n \t\treturn error(\"Unknown %s mode: %s\", opt->long_name, arg);\n+\n \treturn 0;\n }\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 06e7fb5060..2f4f0e32cb 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -1125,3 +1125,20 @@ static int sign_buffer_ssh(struct strbuf *buffer, struct strbuf *signature,\n \tFREE_AND_NULL(ssh_signing_key_file);\n \treturn ret;\n }\n+\n+int parse_sign_mode(const char *arg, enum sign_mode *mode)\n+{\n+\tif (!strcmp(arg, \"abort\"))\n+\t\t*mode = SIGN_ABORT;\n+\telse if (!strcmp(arg, \"verbatim\") || !strcmp(arg, \"ignore\"))\n+\t\t*mode = SIGN_VERBATIM;\n+\telse if (!strcmp(arg, \"warn-verbatim\") || !strcmp(arg, \"warn\"))\n+\t\t*mode = SIGN_WARN_VERBATIM;\n+\telse if (!strcmp(arg, \"warn-strip\"))\n+\t\t*mode = SIGN_WARN_STRIP;\n+\telse if (!strcmp(arg, \"strip\"))\n+\t\t*mode = SIGN_STRIP;\n+\telse\n+\t\treturn -1;\n+\treturn 0;\n+}\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 60ddf8bbfa..50487aa148 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -104,4 +104,19 @@ int check_signature(struct signature_check *sigc,\n void print_signature_buffer(const struct signature_check *sigc,\n \t\t\t    unsigned flags);\n \n+/* Modes for --signed-tags=<mode> and --signed-commits=<mode> options. */\n+enum sign_mode {\n+\tSIGN_ABORT,\n+\tSIGN_WARN_VERBATIM,\n+\tSIGN_VERBATIM,\n+\tSIGN_WARN_STRIP,\n+\tSIGN_STRIP,\n+};\n+\n+/*\n+ * Return 0 if `arg` can be parsed into an `enum sign_mode`. Return -1\n+ * otherwise.\n+ */\n+int parse_sign_mode(const char *arg, enum sign_mode *mode);\n+\n #endif\n-- \n2.51.0.195.ge34f015aea.dirty\n\n"},{"id":"526608","messageId":"20250917181427.3193500-3-christian.couder@gmail.com","threadId":"64131","inReplyTo":"20250917181427.3193500-1-christian.couder@gmail.com","subject":"[PATCH v3 2/2] fast-import: add '--signed-commits=<mode>' option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-17T18:14:27Z","receivedAt":"2025-09-17T18:14:48Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A '--signed-commits=<mode>' option is already available when using\n`git fast-export` to decide what should be done at export time about\ncommit signatures. At import time though, there is no option, or\nother way, in `git fast-import` to decide about commit signatures.\n\nTo remediate that, let's add a '--signed-commits=<mode>' option to\n`git fast-import` too.\n\nFor now the supported <mode>s are the same as those supported by\n`git fast-export`.\n\nThe code responsible for consuming a signature is refactored into\nthe import_one_signature() and discard_one_signature() functions,\nwhich makes it easier to follow the logic and add new modes in the\nfuture.\n\nIn the 'strip' and 'warn-strip' modes, we deliberately use\ndiscard_one_signature() to discard the signature without parsing it.\nThis ensures that even malformed signatures, which would cause the\nparser to fail, can be successfully stripped from a commit.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/git-fast-import.adoc |   5 ++\n builtin/fast-import.c              |  63 ++++++++++++++---\n t/meson.build                      |   1 +\n t/t9305-fast-import-signatures.sh  | 106 +++++++++++++++++++++++++++++\n 4 files changed, 165 insertions(+), 10 deletions(-)\n create mode 100755 t/t9305-fast-import-signatures.sh\n\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 3144ffcdb6..90f242d058 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -66,6 +66,11 @@ OPTIONS\n \tremote-helpers that use the `import` capability, as they are\n \talready trusted to run their own code.\n \n+--signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::\n+\tSpecify how to handle signed commits.  Behaves in the same way\n+\tas the same option in linkgit:git-fast-export[1], except that\n+\tdefault is 'verbatim' (instead of 'abort').\n+\n Options for Frontends\n ~~~~~~~~~~~~~~~~~~~~~\n \ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 2c35f9345d..2010e78475 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -188,6 +188,8 @@ static int global_argc;\n static const char **global_argv;\n static const char *global_prefix;\n \n+static enum sign_mode signed_commit_mode = SIGN_VERBATIM;\n+\n /* Memory pools */\n static struct mem_pool fi_mem_pool = {\n \t.block_alloc = 2*1024*1024 - sizeof(struct mp_block),\n@@ -2752,6 +2754,15 @@ static void parse_one_signature(struct signature_data *sig, const char *v)\n \tparse_data(&sig->data, 0, NULL);\n }\n \n+static void discard_one_signature(void)\n+{\n+\tstruct strbuf data = STRBUF_INIT;\n+\n+\tread_next_command();\n+\tparse_data(&data, 0, NULL);\n+\tstrbuf_release(&data);\n+}\n+\n static void add_gpgsig_to_commit(struct strbuf *commit_data,\n \t\t\t\t const char *header,\n \t\t\t\t struct signature_data *sig)\n@@ -2785,6 +2796,22 @@ static void store_signature(struct signature_data *stored_sig,\n \t}\n }\n \n+static void import_one_signature(struct signature_data *sig_sha1,\n+\t\t\t\t struct signature_data *sig_sha256,\n+\t\t\t\t const char *v)\n+{\n+\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n+\n+\tparse_one_signature(&sig, v);\n+\n+\tif (!strcmp(sig.hash_algo, \"sha1\"))\n+\t\tstore_signature(sig_sha1, &sig, \"SHA-1\");\n+\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n+\t\tstore_signature(sig_sha256, &sig, \"SHA-256\");\n+\telse\n+\t\tdie(_(\"parse_one_signature() returned unknown hash algo\"));\n+}\n+\n static void parse_new_commit(const char *arg)\n {\n \tstatic struct strbuf msg = STRBUF_INIT;\n@@ -2817,19 +2844,32 @@ static void parse_new_commit(const char *arg)\n \tif (!committer)\n \t\tdie(\"Expected committer but didn't get one\");\n \n-\t/* Process signatures (up to 2: one \"sha1\" and one \"sha256\") */\n \twhile (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n-\t\tstruct signature_data sig = { NULL, NULL, STRBUF_INIT };\n-\n-\t\tparse_one_signature(&sig, v);\n+\t\tswitch (signed_commit_mode) {\n+\n+\t\t/* First, modes that don't need the signature to be parsed */\n+\t\tcase SIGN_ABORT:\n+\t\t\tdie(\"encountered signed commit; use \"\n+\t\t\t    \"--signed-commits=<mode> to handle it\");\n+\t\tcase SIGN_WARN_STRIP:\n+\t\t\twarning(_(\"stripping a commit signature\"));\n+\t\t\t/* fallthru */\n+\t\tcase SIGN_STRIP:\n+\t\t\tdiscard_one_signature();\n+\t\t\tbreak;\n \n-\t\tif (!strcmp(sig.hash_algo, \"sha1\"))\n-\t\t\tstore_signature(&sig_sha1, &sig, \"SHA-1\");\n-\t\telse if (!strcmp(sig.hash_algo, \"sha256\"))\n-\t\t\tstore_signature(&sig_sha256, &sig, \"SHA-256\");\n-\t\telse\n-\t\t\tBUG(\"parse_one_signature() returned unknown hash algo\");\n+\t\t/* Second, modes that parse the signature */\n+\t\tcase SIGN_WARN_VERBATIM:\n+\t\t\twarning(_(\"importing a commit signature verbatim\"));\n+\t\t\t/* fallthru */\n+\t\tcase SIGN_VERBATIM:\n+\t\t\timport_one_signature(&sig_sha1, &sig_sha256, v);\n+\t\t\tbreak;\n \n+\t\t/* Third, BUG */\n+\t\tdefault:\n+\t\t\tBUG(\"invalid signed_commit_mode value %d\", signed_commit_mode);\n+\t\t}\n \t\tread_next_command();\n \t}\n \n@@ -3501,6 +3541,9 @@ static int parse_one_option(const char *option)\n \t\toption_active_branches(option);\n \t} else if (skip_prefix(option, \"export-pack-edges=\", &option)) {\n \t\toption_export_pack_edges(option);\n+\t} else if (skip_prefix(option, \"signed-commits=\", &option)) {\n+\t\tif (parse_sign_mode(option, &signed_commit_mode))\n+\t\t\tusagef(_(\"unknown --signed-commits mode '%s'\"), option);\n \t} else if (!strcmp(option, \"quiet\")) {\n \t\tshow_stats = 0;\n \t\tquiet = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex 82af229be3..08ad6938e2 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -1032,6 +1032,7 @@ integration_tests = [\n   't9302-fast-import-unpack-limit.sh',\n   't9303-fast-import-compression.sh',\n   't9304-fast-import-marks.sh',\n+  't9305-fast-import-signatures.sh',\n   't9350-fast-export.sh',\n   't9351-fast-export-anonymize.sh',\n   't9400-git-cvsserver-server.sh',\ndiff --git a/t/t9305-fast-import-signatures.sh b/t/t9305-fast-import-signatures.sh\nnew file mode 100755\nindex 0000000000..c2b4271658\n--- /dev/null\n+++ b/t/t9305-fast-import-signatures.sh\n@@ -0,0 +1,106 @@\n+#!/bin/sh\n+\n+test_description='git fast-import --signed-commits=<mode>'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success 'set up unsigned initial commit and import repo' '\n+\ttest_commit first &&\n+\tgit init new\n+'\n+\n+test_expect_success GPG 'set up OpenPGP signed commit' '\n+\tgit checkout -b openpgp-signing main &&\n+\techo \"Content for OpenPGP signing.\" >file-sign &&\n+\tgit add file-sign &&\n+\tgit commit -S -m \"OpenPGP signed commit\" &&\n+\tOPENPGP_SIGNING=$(git rev-parse --verify openpgp-signing)\n+'\n+\n+test_expect_success GPG 'import OpenPGP signature with --signed-commits=verbatim' '\n+\tgit fast-export --signed-commits=verbatim openpgp-signing >output &&\n+\tgit -C new fast-import --quiet --signed-commits=verbatim <output >log 2>&1 &&\n+\tIMPORTED=$(git -C new rev-parse --verify refs/heads/openpgp-signing) &&\n+\ttest $OPENPGP_SIGNING = $IMPORTED &&\n+\ttest_must_be_empty log\n+'\n+\n+test_expect_success GPGSM 'set up X.509 signed commit' '\n+\tgit checkout -b x509-signing main &&\n+\ttest_config gpg.format x509 &&\n+\ttest_config user.signingkey $GIT_COMMITTER_EMAIL &&\n+\techo \"Content for X.509 signing.\" >file-sign &&\n+\tgit add file-sign &&\n+\tgit commit -S -m \"X.509 signed commit\" &&\n+\tX509_SIGNING=$(git rev-parse HEAD)\n+'\n+\n+test_expect_success GPGSM 'import X.509 signature fails with --signed-commits=abort' '\n+\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\ttest_must_fail git -C new fast-import --quiet --signed-commits=abort <output\n+'\n+\n+test_expect_success GPGSM 'import X.509 signature with --signed-commits=warn-verbatim' '\n+\tgit -C new fast-import --quiet --signed-commits=warn-verbatim <output >log 2>&1 &&\n+\tIMPORTED=$(git -C new rev-parse --verify refs/heads/x509-signing) &&\n+\ttest $X509_SIGNING = $IMPORTED &&\n+\ttest_grep \"importing a commit signature\" log\n+'\n+\n+test_expect_success GPGSSH 'set up SSH signed commit' '\n+\tgit checkout -b ssh-signing main &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\techo \"Content for SSH signing.\" >file-sign &&\n+\tgit add file-sign &&\n+\tgit commit -S -m \"SSH signed commit\" &&\n+\tSSH_SIGNING=$(git rev-parse HEAD)\n+'\n+\n+test_expect_success GPGSSH 'strip SSH signature with --signed-commits=strip' '\n+\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\tgit -C new fast-import --quiet --signed-commits=strip <output >log 2>&1 &&\n+\tIMPORTED=$(git -C new rev-parse --verify refs/heads/ssh-signing) &&\n+\ttest $SSH_SIGNING != $IMPORTED &&\n+\tgit -C new cat-file commit \"$IMPORTED\" >actual &&\n+\ttest_grep ! -E \"^gpgsig\" actual &&\n+\ttest_must_be_empty log\n+'\n+\n+test_expect_success GPG 'setup a commit with dual OpenPGP signatures on its SHA-1 and SHA-256 formats' '\n+\t# Create a signed SHA-256 commit\n+\tgit init --object-format=sha256 explicit-sha256 &&\n+\tgit -C explicit-sha256 config extensions.compatObjectFormat sha1 &&\n+\tgit -C explicit-sha256 checkout -b dual-signed &&\n+\ttest_commit -C explicit-sha256 A &&\n+\techo B >explicit-sha256/B &&\n+\tgit -C explicit-sha256 add B &&\n+\ttest_tick &&\n+\tgit -C explicit-sha256 commit -S -m \"signed\" B &&\n+\tSHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&\n+\n+\t# Create the corresponding SHA-1 commit\n+\tSHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&\n+\n+\t# Check that the resulting SHA-1 commit has both signatures\n+\tgit -C explicit-sha256 cat-file -p $SHA1_B >out &&\n+\ttest_grep -E \"^gpgsig \" out &&\n+\ttest_grep -E \"^gpgsig-sha256 \" out\n+'\n+\n+test_expect_success GPG 'strip both OpenPGP signatures with --signed-commits=warn-strip' '\n+\tgit -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&\n+\ttest_grep -E \"^gpgsig sha1 openpgp\" output &&\n+\ttest_grep -E \"^gpgsig sha256 openpgp\" output &&\n+\tgit -C new fast-import --quiet --signed-commits=warn-strip <output >log 2>&1 &&\n+\tgit -C new cat-file commit refs/heads/dual-signed >actual &&\n+\ttest_grep ! -E \"^gpgsig \" actual &&\n+\ttest_grep ! -E \"^gpgsig-sha256 \" actual &&\n+\ttest_grep \"stripping a commit signature\" log >out &&\n+\ttest_line_count = 2 out\n+'\n+\n+test_done\n-- \n2.51.0.195.ge34f015aea.dirty\n\n"},{"id":"526609","messageId":"CAP8UFD3LNnDfc5KDGZUTvbHoMrHxN=m6KjH_c9GbDztpiUNazQ@mail.gmail.com","threadId":"64131","inReplyTo":"aMfw4rboBOemzvr6@pks.im","subject":"Re: [PATCH v2 2/2] fast-import: add '--signed-commits=<mode>' option","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-09-17T18:23:46Z","receivedAt":"2025-09-17T18:24:00Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Sep 15, 2025 at 12:56 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Mon, Sep 15, 2025 at 12:17:33PM +0200, Christian Couder wrote:\n> > On Mon, Sep 15, 2025 at 8:27 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > >\n> > > On Fri, Sep 12, 2025 at 02:40:42PM +0200, Christian Couder wrote:\n\n> > > >\n> > > > +             switch (signed_commit_mode) {\n> > > > +             case SIGN_ABORT:\n> > > > +                     BUG(\"SIGN_ABORT should be handled before calling parse_one_signature()\");\n> > > > +                     break;\n> > >\n> > > Let's be defensive and convert this into a `default:` case so that any\n> > > unhandled value will cause a BUG.\n> >\n> > Ok, maybe something like BUG(\"invalid signed_commit_mode value %d\",\n> > signed_commit_mode) then?\n>\n> Yeah, that should do the job.\n\nIn the V3 I just sent, I have changed the switch (...) { ... } but\nthere is a `default: ...` case with the BUG() instruction we\ndiscussed. Hopefully it still does the job.\n\nThanks.\n"}]}