{"thread":{"id":"63342","subject":"[PATCH] fast-(import|export): improve on the signature algorithm name","startedAt":"2025-04-24T20:39:32Z","lastAt":"2025-07-25T16:11:55Z","messageCount":65,"participants":["Christian Couder","Junio C Hamano","Elijah Newren","brian m. carlson","Patrick Steinhardt"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"516688","messageId":"20250424203904.909777-1-christian.couder@gmail.com","threadId":"63342","inReplyTo":null,"subject":"[PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-04-24T20:39:04Z","receivedAt":"2025-04-24T20:39:32Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\nsigned-commits, 2025-03-10), added support for signed commits.\n\nHowever, when processing signatures `git fast-export` outputs \"gpgsig\nsha1\" not just when it encounters an OpenPGP SHA-1 signature, but also\nwhen it encounters an SSH or X.509 signature. This is not very\ninformative to say the least, and this might prevent tools that process\nthe output from easily and properly handling signatures.\n\nLet's improve on that by reusing the existing code from\n\"gpg-interface.{c,h}\" to detect the signature algorithm, and then put\nthe signature algorithm name (like \"openpgp\", \"x509\" or \"ssh\") instead\nof \"sha1\" in the output. If we can't detect the signature algorithm we\nwill use \"unknown\". It might be a signature added by an external tool\nand we should likely keep it.\n\nSimilarly on the `git fast-import` side, let's use the existing code\nfrom \"gpg-interface.{c,h}\" to check if a signature algorithm name is\nvalid. In case of an \"unknown\" signature algorithm name, we will warn\nbut still keep it. Future work might implement several options to let\nusers deal with it in different ways, and might implement checking\nknown signatures too.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n\nThis is a follow up from cc/signed-fast-export-import that was merged\nby 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)\nand introduced the support for signed commits.\n\nThe format that this series implemented was lacking a bit, so the goal\nwith this patch is to improve it and handle signed commits a bit more\nconsistently in the code base. It also shows in the tests and in our\ndocumentation that SSH and X.509 signatures are supported.\n\n Documentation/git-fast-export.adoc |  5 +++\n Documentation/git-fast-import.adoc | 15 +++++++-\n builtin/fast-export.c              |  8 ++--\n builtin/fast-import.c              | 14 ++++---\n gpg-interface.c                    | 11 ++++++\n gpg-interface.h                    | 10 +++++\n t/t9350-fast-export.sh             | 60 +++++++++++++++++++++++++++++-\n 7 files changed, 112 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\nindex 413a527496..d03aeca781 100644\n--- a/Documentation/git-fast-export.adoc\n+++ b/Documentation/git-fast-export.adoc\n@@ -54,6 +54,11 @@ of tools that call 'git fast-export' but do not yet support\n '--signed-commits', you may set the environment variable\n 'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default\n from 'abort' to 'warn-strip'.\n++\n+When exported, signature starts with \"gpgsig <alg>\" where <alg> is the\n+signature algorithm name as identified by Git (e.g. \"openpgp\", \"x509\",\n+\"ssh\", or \"sha256\" for SHA-256 OpenPGP signatures), or \"unknown\" for\n+signatures that can't be identified.\n \n --tag-of-filtered-object=(abort|drop|rewrite)::\n \tSpecify how to handle tags whose tagged object is filtered out.\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 7b107f5e8e..50b6d2cc1d 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -521,7 +521,20 @@ The optional `gpgsig` command is used to include a PGP/GPG signature\n that signs the commit data.\n \n Here <alg> specifies which hashing algorithm is used for this\n-signature, either `sha1` or `sha256`.\n+signature. Current valid values are:\n+\n+* \"openpgp\" for SHA-1 OpenPGP signatures,\n+\n+* \"sha256\" for SHA-256 OpenPGP signatures,\n+\n+* \"x509\" for X.509 (GPGSM) signatures,\n+\n+* \"ssh\", for SSH signatures,\n+\n+* \"unknown\" for signatures that can't be identified (a warning is\n+  emitted).\n+\n+Signatures are not yet checked in the current implementation though.\n \n `encoding`\n ^^^^^^^^^^\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex 170126d41a..d00f02dc74 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -29,6 +29,7 @@\n #include \"quote.h\"\n #include \"remote.h\"\n #include \"blob.h\"\n+#include \"gpg-interface.h\"\n \n static const char *fast_export_usage[] = {\n \tN_(\"git fast-export [<rev-list-opts>]\"),\n@@ -700,9 +701,10 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t}\n \n \tif (*commit_buffer_cursor == '\\n') {\n-\t\tif ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor)))\n-\t\t\tsignature_alg = \"sha1\";\n-\t\telse if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n+\t\tif ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor))) {\n+\t\t\tconst char *name = get_signature_name(signature);\n+\t\t\tsignature_alg = name ? name : \"unknown\";\n+\t\t} else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n \t\t\tsignature_alg = \"sha256\";\n \t}\n \ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 63880b595c..59e991a03c 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -29,6 +29,7 @@\n #include \"commit-reach.h\"\n #include \"khash.h\"\n #include \"date.h\"\n+#include \"gpg-interface.h\"\n \n #define PACK_ID_BITS 16\n #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n@@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n \tif (sig_alg) {\n-\t\tif (!strcmp(sig_alg, \"sha1\"))\n-\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n-\t\telse if (!strcmp(sig_alg, \"sha256\"))\n+\t\tif (!strcmp(sig_alg, \"sha256\"))\n \t\t\tstrbuf_addstr(&new_data, \"gpgsig-sha256 \");\n-\t\telse\n-\t\t\tdie(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n+\t\telse if (valid_signature_name(sig_alg))\n+\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n+\t\telse if (!strcmp(sig_alg, \"unknown\")) {\n+\t\t\twarning(\"Unknown gpgsig algorithm name!\");\n+\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n+\t\t} else\n+\t\t\tdie(\"Invalid gpgsig algorithm name, got '%s'\", sig_alg);\n \t\tstring_list_split_in_place(&siglines, sig.buf, \"\\n\", -1);\n \t\tstrbuf_add_separated_string_list(&new_data, \"\\n \", &siglines);\n \t\tstrbuf_addch(&new_data, '\\n');\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 0896458de5..dc6ea904d0 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -144,6 +144,17 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n \treturn NULL;\n }\n \n+const char *get_signature_name(const char *buf)\n+{\n+\tstruct gpg_format *format = get_format_by_sig(buf);\n+\treturn format ? format->name : NULL;\n+}\n+\n+int valid_signature_name(const char *name)\n+{\n+\treturn (get_format_by_name(name) != NULL);\n+}\n+\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex e09f12e8d0..332707facc 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -47,6 +47,16 @@ struct signature_check {\n \n void signature_check_clear(struct signature_check *sigc);\n \n+/*\n+ * Return the name of the signature (like \"openpgp\", \"x509\" or \"ssh\").\n+ */\n+const char *get_signature_name(const char *buf);\n+\n+/*\n+ * Is the signature name valid (like \"openpgp\", \"x509\" or \"ssh\").\n+ */\n+int valid_signature_name(const char *name);\n+\n /*\n  * Look at a GPG signed tag object.  If such a signature exists, store it in\n  * signature and the signed content in payload.  Return 1 if a signature was\ndiff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\nindex dda9e7c3e7..2e2c83d153 100755\n--- a/t/t9350-fast-export.sh\n+++ b/t/t9350-fast-export.sh\n@@ -326,7 +326,7 @@ test_expect_success GPG 'signed-commits=abort' '\n test_expect_success GPG 'signed-commits=verbatim' '\n \n \tgit fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&\n-\tgrep \"^gpgsig sha\" output &&\n+\tgrep \"^gpgsig openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \t(\n \t\tcd new &&\n@@ -340,7 +340,7 @@ test_expect_success GPG 'signed-commits=verbatim' '\n test_expect_success GPG 'signed-commits=warn-verbatim' '\n \n \tgit fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&\n-\tgrep \"^gpgsig sha\" output &&\n+\tgrep \"^gpgsig openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \ttest -s err &&\n \t(\n@@ -381,6 +381,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '\n \n '\n \n+test_expect_success GPGSM 'setup x509 signed commit' '\n+\n+\tgit checkout -b x509-signing main &&\n+\ttest_config gpg.format x509 &&\n+\ttest_config user.signingkey $GIT_COMMITTER_EMAIL &&\n+\techo \"x509 content\" >file_for_x509 &&\n+\tgit add file_for_x509 &&\n+\tgit commit -S -m \"X.509 signed commit\" &&\n+\tX509_COMMIT=$(git rev-parse --verify HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSM 'x509 signature identified' '\n+\n+\tgit fast-export --signed-commits=verbatim --reencode=no x509-signing >output 2>err &&\n+\tgrep \"^gpgsig x509\" output &&\n+\ttest ! -s err &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tSTRIPPED=$(git rev-parse --verify refs/heads/x509-signing) &&\n+\t\ttest $X509_COMMIT = $STRIPPED\n+\t) <output &&\n+\ttest_might_fail git update-ref -d refs/heads/x509-signing\n+\n+'\n+\n+test_expect_success GPGSSH 'setup ssh signed commit' '\n+\n+\tgit checkout -b ssh-signing main &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\techo \"ssh content\" >file_for_ssh &&\n+\tgit add file_for_ssh &&\n+\tgit commit -S -m \"SSH signed commit\" &&\n+\tSSH_COMMIT=$(git rev-parse --verify HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSSH 'ssh signature identified' '\n+\n+\tgit fast-export --signed-commits=verbatim --reencode=no ssh-signing >output 2>err &&\n+\tgrep \"^gpgsig ssh\" output &&\n+\ttest ! -s err &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tSTRIPPED=$(git rev-parse --verify refs/heads/ssh-signing) &&\n+\t\ttest \"$SSH_COMMIT\" = \"$STRIPPED\"\n+\t) <output &&\n+\ttest_might_fail git update-ref -d refs/heads/ssh-signing\n+\n+'\n+\n test_expect_success 'setup submodule' '\n \n \ttest_config_global protocol.file.allow always &&\n-- \n2.49.0.392.g2fa1c74b07\n\n"},{"id":"516691","messageId":"xmqqselxtfyf.fsf@gitster.g","threadId":"63342","inReplyTo":"20250424203904.909777-1-christian.couder@gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-24T21:19:52Z","receivedAt":"2025-04-24T21:19:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\n> signed-commits, 2025-03-10), added support for signed commits.\n>\n> However, when processing signatures `git fast-export` outputs \"gpgsig\n> sha1\" not just when it encounters an OpenPGP SHA-1 signature, but also\n> when it encounters an SSH or X.509 signature. This is not very\n> informative to say the least, and this might prevent tools that process\n> the output from easily and properly handling signatures.\n>\n> Let's improve on that by reusing the existing code from\n> \"gpg-interface.{c,h}\" to detect the signature algorithm, and then put\n> the signature algorithm name (like \"openpgp\", \"x509\" or \"ssh\") instead\n> of \"sha1\" in the output. If we can't detect the signature algorithm we\n> will use \"unknown\". It might be a signature added by an external tool\n> and we should likely keep it.\n>\n> Similarly on the `git fast-import` side, let's use the existing code\n> from \"gpg-interface.{c,h}\" to check if a signature algorithm name is\n> valid. In case of an \"unknown\" signature algorithm name, we will warn\n> but still keep it. Future work might implement several options to let\n> users deal with it in different ways, and might implement checking\n> known signatures too.\n>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>\n> This is a follow up from cc/signed-fast-export-import that was merged\n> by 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)\n> and introduced the support for signed commits.\n>\n> The format that this series implemented was lacking a bit, so the goal\n> with this patch is to improve it and handle signed commits a bit more\n> consistently in the code base. It also shows in the tests and in our\n> documentation that SSH and X.509 signatures are supported.\n\nThanks.\n\nIt is a bit surprising and slightly sad that nobody bothered to\nreport/complain about the brokenness until the original author\nfollows up one month later X-<.  Nobody but the original author is\nusing this feature?  I would have expected that use of signed\ncommits were of high demand and many more people were actively\ninterested in the topic.\n\n>  '--signed-commits', you may set the environment variable\n>  'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default\n>  from 'abort' to 'warn-strip'.\n> ++\n> +When exported, signature starts with \"gpgsig <alg>\" where <alg> is the\n> +signature algorithm name as identified by Git (e.g. \"openpgp\", \"x509\",\n> +\"ssh\", or \"sha256\" for SHA-256 OpenPGP signatures), or \"unknown\" for\n> +signatures that can't be identified.\n\nNice to see these enumerated.  As we are not opening the choices of\n\"algorithms\" up to end-users by allowing custom signature routines\nto be plugged in, configured, or hooked into the system, it may make\nsense to make it clear that we will keep a canonical and exhausitve\nlist here, by saying \"one of these:\" followed by a bulleted list, \ninstead of a parenthesized examples (e.g.), like you did in the\nother documentation below.  Or perhaps refer to the other document\nfrom here so that we do not have to keep two lists in sync?\n\n> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> index 7b107f5e8e..50b6d2cc1d 100644\n> --- a/Documentation/git-fast-import.adoc\n> +++ b/Documentation/git-fast-import.adoc\n> @@ -521,7 +521,20 @@ The optional `gpgsig` command is used to include a PGP/GPG signature\n>  that signs the commit data.\n>  \n>  Here <alg> specifies which hashing algorithm is used for this\n> -signature, either `sha1` or `sha256`.\n> +signature. Current valid values are:\n> +\n> +* \"openpgp\" for SHA-1 OpenPGP signatures,\n> +\n> +* \"sha256\" for SHA-256 OpenPGP signatures,\n> +\n> +* \"x509\" for X.509 (GPGSM) signatures,\n> +\n> +* \"ssh\", for SSH signatures,\n> +\n> +* \"unknown\" for signatures that can't be identified (a warning is\n> +  emitted).\n> +\n> +Signatures are not yet checked in the current implementation though.\n\nExcellent.\n\n> diff --git a/builtin/fast-export.c b/builtin/fast-export.c\n> index 170126d41a..d00f02dc74 100644\n> --- a/builtin/fast-export.c\n> +++ b/builtin/fast-export.c\n> @@ -29,6 +29,7 @@\n>  #include \"quote.h\"\n>  #include \"remote.h\"\n>  #include \"blob.h\"\n> +#include \"gpg-interface.h\"\n>  \n>  static const char *fast_export_usage[] = {\n>  \tN_(\"git fast-export [<rev-list-opts>]\"),\n> @@ -700,9 +701,10 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n>  \t}\n>  \n>  \tif (*commit_buffer_cursor == '\\n') {\n> -\t\tif ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor)))\n> -\t\t\tsignature_alg = \"sha1\";\n> -\t\telse if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n> +\t\tif ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor))) {\n> +\t\t\tconst char *name = get_signature_name(signature);\n> +\t\t\tsignature_alg = name ? name : \"unknown\";\n> +\t\t} else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n>  \t\t\tsignature_alg = \"sha256\";\n\nThe original is bad enough but can we do something to these overly\nlong lines?\n\n>  \t}\n>  \n> diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> index 63880b595c..59e991a03c 100644\n> --- a/builtin/fast-import.c\n> +++ b/builtin/fast-import.c\n> @@ -29,6 +29,7 @@\n>  #include \"commit-reach.h\"\n>  #include \"khash.h\"\n>  #include \"date.h\"\n> +#include \"gpg-interface.h\"\n>  \n>  #define PACK_ID_BITS 16\n>  #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n> @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)\n>  \t\t\t\"encoding %s\\n\",\n>  \t\t\tencoding);\n>  \tif (sig_alg) {\n> -\t\tif (!strcmp(sig_alg, \"sha1\"))\n> -\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n> -\t\telse if (!strcmp(sig_alg, \"sha256\"))\n> +\t\tif (!strcmp(sig_alg, \"sha256\"))\n>  \t\t\tstrbuf_addstr(&new_data, \"gpgsig-sha256 \");\n> -\t\telse\n> -\t\t\tdie(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n> +\t\telse if (valid_signature_name(sig_alg))\n> +\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n> +\t\telse if (!strcmp(sig_alg, \"unknown\")) {\n> +\t\t\twarning(\"Unknown gpgsig algorithm name!\");\n> +\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n> +\t\t} else\n> +\t\t\tdie(\"Invalid gpgsig algorithm name, got '%s'\", sig_alg);\n\nHmph, we used to have special cases for sha1 and sha256 but now we\ncan handle sha1 with a more generic \"valid_signature_name()\" logic?\nAnd yet we need to still special case sha256?  Not that I trust the\nold code all that much and take deviations from the patterns in the\nold code as a sign of something not right...\n\nThe fast-export stream produced by the code with d9cb0e6f\n(fast-export, fast-import: add support for signed-commits,\n2025-03-10) used to identify a signature algorithm \"sha1\", but this\nnew version of fast-import lost the support for it, and will barf\nwhen seeing such an existing fast-export stream?  I am not sure what\nis going on around this code.\n\nI am not so worried about the other case, where the stream produced\nby fast-export contained in this version may or may not be readable\nby an older version of fast-import.\n\nI am puzzled enough, so I'll stop here for now.\n\nThanks.\n"},{"id":"516693","messageId":"CABPp-BFrau4F1a_+umYw0qu8HYz3dTPXZ9oiHhawTSZpgAoGLw@mail.gmail.com","threadId":"63342","inReplyTo":"20250424203904.909777-1-christian.couder@gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-04-24T21:41:45Z","receivedAt":"2025-04-24T21:41:59Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Apr 24, 2025 at 1:39 PM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\n> signed-commits, 2025-03-10), added support for signed commits.\n>\n> However, when processing signatures `git fast-export` outputs \"gpgsig\n> sha1\" not just when it encounters an OpenPGP SHA-1 signature, but also\n> when it encounters an SSH or X.509 signature. This is not very\n> informative to say the least, and this might prevent tools that process\n> the output from easily and properly handling signatures.\n>\n> Let's improve on that by reusing the existing code from\n> \"gpg-interface.{c,h}\" to detect the signature algorithm, and then put\n> the signature algorithm name (like \"openpgp\", \"x509\" or \"ssh\") instead\n> of \"sha1\" in the output. If we can't detect the signature algorithm we\n> will use \"unknown\". It might be a signature added by an external tool\n> and we should likely keep it.\n>\n> Similarly on the `git fast-import` side, let's use the existing code\n> from \"gpg-interface.{c,h}\" to check if a signature algorithm name is\n> valid. In case of an \"unknown\" signature algorithm name, we will warn\n> but still keep it. Future work might implement several options to let\n> users deal with it in different ways, and might implement checking\n> known signatures too.\n\nThe last sentence is somewhat ambiguous about whether it is only about\nthe \"unknown\" case or whether the second half of the sentence was\nswitching tracks to discuss something else about the known cases.  Do\nyou perhaps mean something like \"Future work might implement several\noptions to let users deal with an \"unknown\" signature algorithm, and\nwhen we have a valid signature algorithm, we may be able to not only\nverify the signature algorithm name but start verifying the signature\nitself to ensure it is valid as well.\"  ?\n\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>\n> This is a follow up from cc/signed-fast-export-import that was merged\n> by 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)\n> and introduced the support for signed commits.\n>\n> The format that this series implemented was lacking a bit, so the goal\n> with this patch is to improve it and handle signed commits a bit more\n> consistently in the code base. It also shows in the tests and in our\n> documentation that SSH and X.509 signatures are supported.\n>\n>  Documentation/git-fast-export.adoc |  5 +++\n>  Documentation/git-fast-import.adoc | 15 +++++++-\n>  builtin/fast-export.c              |  8 ++--\n>  builtin/fast-import.c              | 14 ++++---\n>  gpg-interface.c                    | 11 ++++++\n>  gpg-interface.h                    | 10 +++++\n>  t/t9350-fast-export.sh             | 60 +++++++++++++++++++++++++++++-\n>  7 files changed, 112 insertions(+), 11 deletions(-)\n>\n> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\n> index 413a527496..d03aeca781 100644\n> --- a/Documentation/git-fast-export.adoc\n> +++ b/Documentation/git-fast-export.adoc\n> @@ -54,6 +54,11 @@ of tools that call 'git fast-export' but do not yet support\n>  '--signed-commits', you may set the environment variable\n>  'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default\n>  from 'abort' to 'warn-strip'.\n> ++\n> +When exported, signature starts with \"gpgsig <alg>\" where <alg> is the\n> +signature algorithm name as identified by Git (e.g. \"openpgp\", \"x509\",\n> +\"ssh\", or \"sha256\" for SHA-256 OpenPGP signatures), or \"unknown\" for\n> +signatures that can't be identified.\n>\n>  --tag-of-filtered-object=(abort|drop|rewrite)::\n>         Specify how to handle tags whose tagged object is filtered out.\n> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> index 7b107f5e8e..50b6d2cc1d 100644\n> --- a/Documentation/git-fast-import.adoc\n> +++ b/Documentation/git-fast-import.adoc\n> @@ -521,7 +521,20 @@ The optional `gpgsig` command is used to include a PGP/GPG signature\n>  that signs the commit data.\n>\n>  Here <alg> specifies which hashing algorithm is used for this\n> -signature, either `sha1` or `sha256`.\n> +signature. Current valid values are:\n> +\n> +* \"openpgp\" for SHA-1 OpenPGP signatures,\n> +\n> +* \"sha256\" for SHA-256 OpenPGP signatures,\n> +\n> +* \"x509\" for X.509 (GPGSM) signatures,\n> +\n> +* \"ssh\", for SSH signatures,\n> +\n> +* \"unknown\" for signatures that can't be identified (a warning is\n> +  emitted).\n> +\n> +Signatures are not yet checked in the current implementation though.\n\nThanks for calling this out.\n\n>  `encoding`\n>  ^^^^^^^^^^\n> diff --git a/builtin/fast-export.c b/builtin/fast-export.c\n> index 170126d41a..d00f02dc74 100644\n> --- a/builtin/fast-export.c\n> +++ b/builtin/fast-export.c\n> @@ -29,6 +29,7 @@\n>  #include \"quote.h\"\n>  #include \"remote.h\"\n>  #include \"blob.h\"\n> +#include \"gpg-interface.h\"\n>\n>  static const char *fast_export_usage[] = {\n>         N_(\"git fast-export [<rev-list-opts>]\"),\n> @@ -700,9 +701,10 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n>         }\n>\n>         if (*commit_buffer_cursor == '\\n') {\n> -               if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor)))\n> -                       signature_alg = \"sha1\";\n> -               else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n> +               if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor))) {\n> +                       const char *name = get_signature_name(signature);\n> +                       signature_alg = name ? name : \"unknown\";\n> +               } else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n>                         signature_alg = \"sha256\";\n>         }\n>\n> diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> index 63880b595c..59e991a03c 100644\n> --- a/builtin/fast-import.c\n> +++ b/builtin/fast-import.c\n> @@ -29,6 +29,7 @@\n>  #include \"commit-reach.h\"\n>  #include \"khash.h\"\n>  #include \"date.h\"\n> +#include \"gpg-interface.h\"\n>\n>  #define PACK_ID_BITS 16\n>  #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n> @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)\n>                         \"encoding %s\\n\",\n>                         encoding);\n>         if (sig_alg) {\n> -               if (!strcmp(sig_alg, \"sha1\"))\n> -                       strbuf_addstr(&new_data, \"gpgsig \");\n> -               else if (!strcmp(sig_alg, \"sha256\"))\n> +               if (!strcmp(sig_alg, \"sha256\"))\n>                         strbuf_addstr(&new_data, \"gpgsig-sha256 \");\n> -               else\n> -                       die(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n> +               else if (valid_signature_name(sig_alg))\n> +                       strbuf_addstr(&new_data, \"gpgsig \");\n> +               else if (!strcmp(sig_alg, \"unknown\")) {\n> +                       warning(\"Unknown gpgsig algorithm name!\");\n> +                       strbuf_addstr(&new_data, \"gpgsig \");\n> +               } else\n> +                       die(\"Invalid gpgsig algorithm name, got '%s'\", sig_alg);\n>                 string_list_split_in_place(&siglines, sig.buf, \"\\n\", -1);\n>                 strbuf_add_separated_string_list(&new_data, \"\\n \", &siglines);\n>                 strbuf_addch(&new_data, '\\n');\n\nI'm not very familiar with gpg and other signatures, and was stuck\ntrying to parse this logic when a review from Junio came in, and I\ndecided to read it since he often \"thinks out loud\" to see if that'd\nexplain it better.  Sadly, didn't help... ;-)  But I'll watch for any\nfollow-up response you add over there.\n\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index 0896458de5..dc6ea904d0 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -144,6 +144,17 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n>         return NULL;\n>  }\n>\n> +const char *get_signature_name(const char *buf)\n> +{\n> +       struct gpg_format *format = get_format_by_sig(buf);\n> +       return format ? format->name : NULL;\n> +}\n> +\n> +int valid_signature_name(const char *name)\n> +{\n> +       return (get_format_by_name(name) != NULL);\n> +}\n> +\n>  void signature_check_clear(struct signature_check *sigc)\n>  {\n>         FREE_AND_NULL(sigc->payload);\n> diff --git a/gpg-interface.h b/gpg-interface.h\n> index e09f12e8d0..332707facc 100644\n> --- a/gpg-interface.h\n> +++ b/gpg-interface.h\n> @@ -47,6 +47,16 @@ struct signature_check {\n>\n>  void signature_check_clear(struct signature_check *sigc);\n>\n> +/*\n> + * Return the name of the signature (like \"openpgp\", \"x509\" or \"ssh\").\n> + */\n> +const char *get_signature_name(const char *buf);\n> +\n> +/*\n> + * Is the signature name valid (like \"openpgp\", \"x509\" or \"ssh\").\n> + */\n> +int valid_signature_name(const char *name);\n> +\n>  /*\n>   * Look at a GPG signed tag object.  If such a signature exists, store it in\n>   * signature and the signed content in payload.  Return 1 if a signature was\n> diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\n> index dda9e7c3e7..2e2c83d153 100755\n> --- a/t/t9350-fast-export.sh\n> +++ b/t/t9350-fast-export.sh\n> @@ -326,7 +326,7 @@ test_expect_success GPG 'signed-commits=abort' '\n>  test_expect_success GPG 'signed-commits=verbatim' '\n>\n>         git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&\n> -       grep \"^gpgsig sha\" output &&\n> +       grep \"^gpgsig openpgp\" output &&\n>         grep \"encoding ISO-8859-1\" output &&\n>         (\n>                 cd new &&\n> @@ -340,7 +340,7 @@ test_expect_success GPG 'signed-commits=verbatim' '\n>  test_expect_success GPG 'signed-commits=warn-verbatim' '\n>\n>         git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&\n> -       grep \"^gpgsig sha\" output &&\n> +       grep \"^gpgsig openpgp\" output &&\n>         grep \"encoding ISO-8859-1\" output &&\n>         test -s err &&\n>         (\n> @@ -381,6 +381,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '\n>\n>  '\n>\n> +test_expect_success GPGSM 'setup x509 signed commit' '\n> +\n> +       git checkout -b x509-signing main &&\n> +       test_config gpg.format x509 &&\n> +       test_config user.signingkey $GIT_COMMITTER_EMAIL &&\n> +       echo \"x509 content\" >file_for_x509 &&\n> +       git add file_for_x509 &&\n> +       git commit -S -m \"X.509 signed commit\" &&\n> +       X509_COMMIT=$(git rev-parse --verify HEAD) &&\n> +       git checkout main\n> +\n> +'\n> +\n> +test_expect_success GPGSM 'x509 signature identified' '\n> +\n> +       git fast-export --signed-commits=verbatim --reencode=no x509-signing >output 2>err &&\n\nIs --reencode=no important here or does this work with --reencode=yes\nas well?  (I understand the default being --reencode=abort and fact\nthat you are reusing an example that used a specialized encoding means\nyou need to specify something, was just curious if this particular\nvalue was important)\n\n> +       grep \"^gpgsig x509\" output &&\n> +       test ! -s err &&\n> +       (\n> +               cd new &&\n> +               git fast-import &&\n> +               STRIPPED=$(git rev-parse --verify refs/heads/x509-signing) &&\n> +               test $X509_COMMIT = $STRIPPED\n\nAh, --reencode=no is critical for the test, but only because you are\ntrying to ensure you get the same commit back.  Should there also be a\ntest for when something is tweaked, such as the encoding, and whether\nthe signature is still found?\n\n> +       ) <output &&\n> +       test_might_fail git update-ref -d refs/heads/x509-signing\n> +\n> +'\n> +\n> +test_expect_success GPGSSH 'setup ssh signed commit' '\n> +\n> +       git checkout -b ssh-signing main &&\n> +       test_config gpg.format ssh &&\n> +       test_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n> +       echo \"ssh content\" >file_for_ssh &&\n> +       git add file_for_ssh &&\n> +       git commit -S -m \"SSH signed commit\" &&\n> +       SSH_COMMIT=$(git rev-parse --verify HEAD) &&\n> +       git checkout main\n> +\n> +'\n> +\n> +test_expect_success GPGSSH 'ssh signature identified' '\n> +\n> +       git fast-export --signed-commits=verbatim --reencode=no ssh-signing >output 2>err &&\n\nOut of curiosity, any particular reason to export the entire history\ninstead of just the new commit you just added (though you'd likely\nwant --reference-excluded-parents if you did that, which really ought\nto be the default)?  Anyway...\n\n> +       grep \"^gpgsig ssh\" output &&\n> +       test ! -s err &&\n> +       (\n> +               cd new &&\n> +               git fast-import &&\n> +               STRIPPED=$(git rev-parse --verify refs/heads/ssh-signing) &&\n> +               test \"$SSH_COMMIT\" = \"$STRIPPED\"\n\nLooks like your two tests are for different signature types, but as\nnoted above, I'm kind of curious to see a test covering what happens\nwhen the resulting commit doesn't exactly match the original but still\nretains a signature.\n\n> +       ) <output &&\n> +       test_might_fail git update-ref -d refs/heads/ssh-signing\n> +\n> +'\n> +\n>  test_expect_success 'setup submodule' '\n>\n>         test_config_global protocol.file.allow always &&\n> --\n> 2.49.0.392.g2fa1c74b07\n"},{"id":"516694","messageId":"CABPp-BHudzADoYdBvoBZ1yDRj7Ra_V-or6ddAOV6nmXeMMpMaw@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqselxtfyf.fsf@gitster.g","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-04-24T21:59:01Z","receivedAt":"2025-04-24T21:59:13Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Apr 24, 2025 at 2:19 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n[...]\n> >\n> > This is a follow up from cc/signed-fast-export-import that was merged\n> > by 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)\n> > and introduced the support for signed commits.\n> >\n> > The format that this series implemented was lacking a bit, so the goal\n> > with this patch is to improve it and handle signed commits a bit more\n> > consistently in the code base. It also shows in the tests and in our\n> > documentation that SSH and X.509 signatures are supported.\n>\n> Thanks.\n>\n> It is a bit surprising and slightly sad that nobody bothered to\n> report/complain about the brokenness until the original author\n> follows up one month later X-<.  Nobody but the original author is\n> using this feature?  I would have expected that use of signed\n> commits were of high demand and many more people were actively\n> interested in the topic.\n\nSigned commits may be high demand, but we do have the intersection of\nsigned commits, usage of fast-export/fast-import, and using a\ndevelopment version of Git.  I suspect that intersection is somewhat\nsmall; my guess is that signed commits tends to be associated with\nlarge enterprisy things, development versions of git tend to be the\nopposite, and fast-export/fast-import are by design infrequently used\ntools for any given repo.\n\n(Also, not sure if this helps, but the original author was Luke, not\nChristian; Christian was the one who came by three years later to jump\nin and polish up the patches.)\n\n[...]\n> > @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)\n> >                       \"encoding %s\\n\",\n> >                       encoding);\n> >       if (sig_alg) {\n> > -             if (!strcmp(sig_alg, \"sha1\"))\n> > -                     strbuf_addstr(&new_data, \"gpgsig \");\n> > -             else if (!strcmp(sig_alg, \"sha256\"))\n> > +             if (!strcmp(sig_alg, \"sha256\"))\n> >                       strbuf_addstr(&new_data, \"gpgsig-sha256 \");\n> > -             else\n> > -                     die(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n> > +             else if (valid_signature_name(sig_alg))\n> > +                     strbuf_addstr(&new_data, \"gpgsig \");\n> > +             else if (!strcmp(sig_alg, \"unknown\")) {\n> > +                     warning(\"Unknown gpgsig algorithm name!\");\n> > +                     strbuf_addstr(&new_data, \"gpgsig \");\n> > +             } else\n> > +                     die(\"Invalid gpgsig algorithm name, got '%s'\", sig_alg);\n>\n> Hmph, we used to have special cases for sha1 and sha256 but now we\n> can handle sha1 with a more generic \"valid_signature_name()\" logic?\n> And yet we need to still special case sha256?  Not that I trust the\n> old code all that much and take deviations from the patterns in the\n> old code as a sign of something not right...\n>\n> The fast-export stream produced by the code with d9cb0e6f\n> (fast-export, fast-import: add support for signed-commits,\n> 2025-03-10) used to identify a signature algorithm \"sha1\", but this\n> new version of fast-import lost the support for it, and will barf\n> when seeing such an existing fast-export stream?  I am not sure what\n> is going on around this code.\n>\n> I am not so worried about the other case, where the stream produced\n> by fast-export contained in this version may or may not be readable\n> by an older version of fast-import.\n\nI certainly can't answer anything here as I know little about\nsignatures, but your comment brought up a different question for me:\nGiven that d9cb0e6ff8b3 (fast-export, fast-import: add support for\nsigned-commits, 2025-03-10) isn't part of any release (not even a\nrelease candidate), do we need to have backward compatibility with\nthat version?\n"},{"id":"516695","messageId":"aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net","threadId":"63342","inReplyTo":"20250424203904.909777-1-christian.couder@gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-04-24T22:05:18Z","receivedAt":"2025-04-24T22:05:21Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-04-24 at 20:39:04, Christian Couder wrote:\n>  Here <alg> specifies which hashing algorithm is used for this\n> -signature, either `sha1` or `sha256`.\n> +signature. Current valid values are:\n> +\n> +* \"openpgp\" for SHA-1 OpenPGP signatures,\n> +\n> +* \"sha256\" for SHA-256 OpenPGP signatures,\n> +\n> +* \"x509\" for X.509 (GPGSM) signatures,\n> +\n> +* \"ssh\", for SSH signatures,\n> +\n> +* \"unknown\" for signatures that can't be identified (a warning is\n> +  emitted).\n\nI don't think this is a good set of options.  We can have SHA-1 or\nSHA-256 options for any of the three.  If I create a SHA-256 commit and\nsign it with SSH, then it couldn't be exported with this type.\n\nIt is even possible and valid to create a signature over the SHA-1\ncontent of an object and sign it with one protocol, say, OpenPGP, and\nthen create a signature over the SHA-256 content of the object and sign\nit with a different one, such as SSH.  Git does not natively support\nthis, but it is possible to do by hand.\n\nThese should be separate fields: one for the hash algorithm and one for\nthe protocol.  Alternatively, we can just keep the hash algorithm field\nand parse the protocol by reading the first line, which will differ for\ndifferent protocols.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"516699","messageId":"xmqq1pthtbdg.fsf@gitster.g","threadId":"63342","inReplyTo":"CABPp-BHudzADoYdBvoBZ1yDRj7Ra_V-or6ddAOV6nmXeMMpMaw@mail.gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-24T22:58:51Z","receivedAt":"2025-04-24T22:58:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> The fast-export stream produced by the code with d9cb0e6f\n>> (fast-export, fast-import: add support for signed-commits,\n>> 2025-03-10) used to identify a signature algorithm \"sha1\", but this\n>> new version of fast-import lost the support for it, and will barf\n>> when seeing such an existing fast-export stream?  I am not sure what\n>> is going on around this code.\n>>\n>> I am not so worried about the other case, where the stream produced\n>> by fast-export contained in this version may or may not be readable\n>> by an older version of fast-import.\n>\n> I certainly can't answer anything here as I know little about\n> signatures, but your comment brought up a different question for me:\n> Given that d9cb0e6ff8b3 (fast-export, fast-import: add support for\n> signed-commits, 2025-03-10) isn't part of any release (not even a\n> release candidate), do we need to have backward compatibility with\n> that version?\n\nI think we will lose all the credibility if we said \"that's not in\nan official release, so we are free to break early adopters\", once\nsomething is in 'master'.  As some corp environment are know to run\n'next' and indeed we do encourage more folks to do so so that we can\ncatch breakages before they escape to 'master', I actually am equally\nworried about things in 'next'.\n"},{"id":"516702","messageId":"xmqqikmtrvkd.fsf@gitster.g","threadId":"63342","inReplyTo":"20250424203904.909777-1-christian.couder@gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-24T23:25:38Z","receivedAt":"2025-04-24T23:25:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> This is a follow up from cc/signed-fast-export-import that was merged\n> by 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)\n> and introduced the support for signed commits.\n\nI've looked at it and tried to test the result of merging it to\n'seen' locally, but for now I'll eject it from 'seen' before pushing\ntoday's integration result out, as the local tests seem to be\nfailing (and even though I haven't had time to positively identify\nthis patch is the culprit, as nothing else is affecting fast-export,\nI'd need to move with an educated guess and then dig deeper later to\nkeep the other topics moving).\n\nTest Summary Report\n-------------------\nt9350-fast-export.sh                             (Wstat: 256 (exited 1) Tests: 65 Failed: 11)\n  Failed tests:  24-25, 29, 31, 33, 35-37, 56-58\n  Non-zero exit status: 1\nFiles=1021, Tests=31986, 646 wallclock secs (16.20 usr  5.76 sys + 1053.65 cusr 8317.02 csys = 9392.63 CPU)\nResult: FAIL\ngmake[1]: *** [Makefile:78: prove] Error 1\ngmake[1]: Leaving directory '/usr/local/google/home/jch/w/buildfarm/seen/t'\ngmake: *** [Makefile:3287: test] Error 2\n"},{"id":"518913","messageId":"20250526103314.1542316-1-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250424203904.909777-1-christian.couder@gmail.com","subject":"[PATCH v2 0/6] extract algo information from signatures","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:33:08Z","receivedAt":"2025-05-26T10:33:38Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Around one month ago, I sent a patch that tried to improve on how `git\nfast-export` handled SSH and X.509 commit signatures:\n\nhttps://lore.kernel.org/git/20250424203904.909777-1-christian.couder@gmail.com/\n\nThis patch was showing a single string for the hash algorithm with the\nfollowing possible values:\n\n* \"openpgp\" for SHA-1 OpenPGP signatures,\n\n* \"sha256\" for SHA-256 OpenPGP signatures,\n\n* \"x509\" for X.509 (GPGSM) signatures,\n\n* \"ssh\", for SSH signatures,\n\n* \"unknown\" for signatures that can't be identified (a warning is\n  emitted).\n\nbrian m. carlson however replied that it would be better to show two\npieces of information instead of one: one for the hash algorithm and\none for the protocol.\n\nI have tried to do that but there were a number of issues. First it\nseems to be easier to extract information from signatures when\nchecking them. And if you check them, then it might be interesting to\nshow the result of the check.\n\nAlso for SSH signatures, it's difficult and not so informative to get\nthe hash algorithm. That's because the hash algorithm is often\nspecified by the key type (like \"RSA\", \"ECDSA\", \"Ed25519\", ...). For\nexample \"Ed25519\" has SHA-512 integrated into its design, and \"ECDSA\"\nand \"RSA\" are typically used with SHA-256. So for SSH signatures it\nseems better to just show the key type and not the hash algorithm.\n\nIn general I am not sure what users might want regarding commit\nsignatures when using fast-export. Some might not need much signature\ninformation at all, and for them checking signatures might just slow\nthe export process for no benefit, while others might want more\nsignature information even at the expense of a slower export.\n\nTo address this, I decided to focus first on extracting the hash\nalgorithm from OpenPGP/X.509 signatures and the key type from SSH\nsignature when checking signatures.\n\nTo test that, I thought that it could be interesting to add a\n`--summary` option to `verify-commit` that shows a concise, one-line\nsummary of the signature verification to standard output in the\n`STATUS FORMAT ALGORITHM` format, where:\n\n* STATUS is the result character (e.g., G, B, E, U, N, ...), similar\n  as what the \"%G?\" pretty format specifier shows,\n\n* FORMAT is the signature format (`openpgp`, `x509`, or `ssh`),\n\n* ALGORITHM is the hash algorithm used for GPG/GPGSM signatures\n  (e.g. `sha1`, `sha256`, ...), or the key type for SSH signatures\n  (`RSA`, `ECDSA`, `ED25519`, ...).\n\nIf we can agree on a concise format output for signature checks, then\nmaybe this format will be a good format to be used in the `git\nfast-export` output for users who are fine with signatures being\nchecked.\n\nWhat do you think?\n\nCI tests\n--------\n\nThey have all passed:\n\nhttps://github.com/chriscool/git/actions/runs/15248563563\n\nRange diff compared to v1\n-------------------------\n\nNo range diff as this series is a completely different approach to the\nproblem, and running range-diff shows completely different patches.\n\nChristian Couder (6):\n  gpg-interface: simplify ssh fingerprint parsing\n  gpg-interface: use left shift to define GPG_VERIFY_*\n  doc/verify-commit: update and improve the whole doc\n  gpg-interface: extract hash algorithm from signature status output\n  gpg-interface: extract SSH key type from signature status output\n  verify-commit: add a --summary flag\n\n Documentation/git-verify-commit.adoc |  53 +++++++++++--\n builtin/verify-commit.c              |   4 +-\n gpg-interface.c                      | 111 ++++++++++++++++++++++++++-\n gpg-interface.h                      |  16 +++-\n t/t7510-signed-commit.sh             |  24 ++++++\n t/t7528-signed-commit-ssh.sh         |  28 +++++++\n 6 files changed, 224 insertions(+), 12 deletions(-)\n\n-- \n2.49.0.609.g63c55177e5\n\n"},{"id":"518914","messageId":"20250526103314.1542316-2-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250526103314.1542316-1-christian.couder@gmail.com","subject":"[PATCH v2 1/6] gpg-interface: simplify ssh fingerprint parsing","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:33:09Z","receivedAt":"2025-05-26T10:33:39Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In \"gpg-interface.c\", the 'parse_ssh_output()' function takes a\n'struct signature_check *sigc' argument and populates many members of\nthis 'sigc' using information parsed from 'sigc->output' which\ncontains the ouput of an `ssh-keygen -Y ...` command that was used to\nverify an SSH signature.\n\nWhen it populates 'sigc->fingerprint' though, it uses\n`xstrdup(strstr(line, \"key \") + 4)` while `strstr(line, \"key \")` has\nalready been computed a few lines above and is already available in\nthe `key` variable.\n\nLet's simplify this.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n gpg-interface.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 0896458de5..e7af82d123 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -431,7 +431,7 @@ static void parse_ssh_output(struct signature_check *sigc)\n \n \tkey = strstr(line, \"key \");\n \tif (key) {\n-\t\tsigc->fingerprint = xstrdup(strstr(line, \"key \") + 4);\n+\t\tsigc->fingerprint = xstrdup(key + 4);\n \t\tsigc->key = xstrdup(sigc->fingerprint);\n \t} else {\n \t\t/*\n-- \n2.49.0.609.g63c55177e5\n\n"},{"id":"518915","messageId":"20250526103314.1542316-3-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250526103314.1542316-1-christian.couder@gmail.com","subject":"[PATCH v2 2/6] gpg-interface: use left shift to define GPG_VERIFY_*","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:33:10Z","receivedAt":"2025-05-26T10:33:41Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"In \"gpg-interface.h\", the definitions of the GPG_VERIFY_* boolean flags\nare currently using 1, 2 and 4 while we often prefer the bitwise left\nshift operator, `<<`, for that purpose to make it clearer that they are\nboolean.\n\nLet's use the left shift operator here too. Let's also fix an indent\nissue with \"4\" while at it.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n gpg-interface.h | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex e09f12e8d0..9a32dd6ce8 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -3,9 +3,9 @@\n \n struct strbuf;\n \n-#define GPG_VERIFY_VERBOSE\t\t1\n-#define GPG_VERIFY_RAW\t\t\t2\n-#define GPG_VERIFY_OMIT_STATUS\t4\n+#define GPG_VERIFY_VERBOSE\t(1<<0)\n+#define GPG_VERIFY_RAW\t\t(1<<1)\n+#define GPG_VERIFY_OMIT_STATUS\t(1<<2)\n \n enum signature_trust_level {\n \tTRUST_UNDEFINED,\n-- \n2.49.0.609.g63c55177e5\n\n"},{"id":"518916","messageId":"20250526103314.1542316-4-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250526103314.1542316-1-christian.couder@gmail.com","subject":"[PATCH v2 3/6] doc/verify-commit: update and improve the whole doc","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:33:11Z","receivedAt":"2025-05-26T10:33:42Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The documentation of the `git verify-commit` commands currently looks\nvery outdated and minimal. Especially it has the following issues:\n\n  - It only talks about verifying GPG signatures while the command\n    actually supports verifying other signatures like SSH ones.\n\n  - It's not clear what the exit code of the command is.\n\n  - It talks about the `<commit>...` arguments only as \"SHA-1\n    identifiers\" while SHA-256 as well as any committish is actually\n    supported.\n\nLet's fix all those issues by updating and improving the whole\ndocumentation.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/git-verify-commit.adoc | 36 ++++++++++++++++++++++++----\n 1 file changed, 31 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-verify-commit.adoc b/Documentation/git-verify-commit.adoc\nindex aee4c40eac..6a208a0c2a 100644\n--- a/Documentation/git-verify-commit.adoc\n+++ b/Documentation/git-verify-commit.adoc\n@@ -3,7 +3,7 @@ git-verify-commit(1)\n \n NAME\n ----\n-git-verify-commit - Check the GPG signature of commits\n+git-verify-commit - Check the signature of commits\n \n SYNOPSIS\n --------\n@@ -12,20 +12,46 @@ SYNOPSIS\n \n DESCRIPTION\n -----------\n-Validates the GPG signature created by 'git commit -S'.\n+Validates the cryptographic signature of commits. This is typically\n+a GPG signature created by 'git commit -S', but other signature\n+formats like SSH may also be verified depending on Git configuration\n+(see linkgit:git-config[1] and the `gpg.format` option).\n+\n+By default, the command prints human-readable verification results to\n+standard error.\n+\n+EXIT STATUS\n+-----------\n+If all the specified commits are successfully verified and their\n+signatures are good and trusted according to the configured trust\n+requirements, the command exits with 0.\n+\n+If any commit fails verification (e.g., due to a bad signature, a\n+missing or untrusted key), if a specified object cannot be found or is\n+not a commit object, or if another error occurs during verification,\n+the command exits with a non-zero status.\n \n OPTIONS\n -------\n --raw::\n-\tPrint the raw gpg status output to standard error instead of the normal\n-\thuman-readable output.\n+\tPrint the raw signature verification status output to standard\n+\terror instead of the normal human-readable output. The format\n+\tof this output is specific to the signature format being used.\n \n -v::\n --verbose::\n \tPrint the contents of the commit object before validating it.\n \n <commit>...::\n-\tSHA-1 identifiers of Git commit objects.\n+\tCommit objects to verify. Can be specified using any format\n+\taccepted by linkgit:git-rev-parse[1].\n+\n+SEE ALSO\n+--------\n+linkgit:git-commit[1],\n+linkgit:git-config[1],\n+linkgit:git-verify-tag[1],\n+linkgit:git-log[1]\n \n GIT\n ---\n-- \n2.49.0.609.g63c55177e5\n\n"},{"id":"518917","messageId":"20250526103314.1542316-5-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250526103314.1542316-1-christian.couder@gmail.com","subject":"[PATCH v2 4/6] gpg-interface: extract hash algorithm from signature status output","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:33:12Z","receivedAt":"2025-05-26T10:33:43Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"When using GPG/GPGSM to verify OpenPGP/X.509 signatures, the\nverification result (good/bad/etc.), signer, and key fingerprint are\nextracted from the output, but not the specific hash algorithm (e.g.,\n\"sha1\", \"sha256\") reported by GPG as having been used for the\nsignature itself.\n\nLet's improve the `gpg-interface` parsing logic to capture this\ninformation.\n\nThis information can be useful for Git commands or external tools\nthat process signature information. For example, it could be used\nwhen displaying signature verification results to users or when\nworking with various signature formats in tools like fast-export and\nfast-import.\n\nGPG provides the hash algorithm ID used for the signature within\nits machine-readable status output, specifically in the fields\nfollowing the `VALIDSIG` and `ERRSIG` keywords, as documented in\nGnuPG's `doc/DETAILS`.\n\nThe implementation follows RFC 4880 (OpenPGP Message Format) section\n9.4 for the mapping between hash algorithm IDs and their\ncorresponding names.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n gpg-interface.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++\n gpg-interface.h |  4 +++\n 2 files changed, 78 insertions(+)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex e7af82d123..15687ede43 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -153,6 +153,7 @@ void signature_check_clear(struct signature_check *sigc)\n \tFREE_AND_NULL(sigc->key);\n \tFREE_AND_NULL(sigc->fingerprint);\n \tFREE_AND_NULL(sigc->primary_key_fingerprint);\n+\tFREE_AND_NULL(sigc->sig_algo);\n }\n \n /* An exclusive status -- only one of them can appear in output */\n@@ -221,6 +222,65 @@ static int parse_gpg_trust_level(const char *level,\n \treturn 1;\n }\n \n+/* See RFC 4880: OpenPGP Message Format, section 9.4. Hash Algorithms */\n+static struct sigcheck_gpg_hash_algo {\n+\tconst char *id;\n+\tconst char *name;\n+} sigcheck_gpg_hash_algo[] = {\n+\t{ \"1\", \"md5\" },       /* deprecated */\n+\t{ \"2\", \"sha1\" },      /* mandatory */\n+\t{ \"3\", \"ripemd160\" },\n+\t{ \"8\", \"sha256\" },\n+\t{ \"9\", \"sha384\" },\n+\t{ \"10\", \"sha512\" },\n+\t{ \"11\", \"sha224\" },\n+};\n+\n+static const char *lookup_gpg_hash_algo(const char *algo_id)\n+{\n+\tif (!algo_id)\n+\t\treturn NULL;\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(sigcheck_gpg_hash_algo); i++) {\n+\t\tif (!strcmp(sigcheck_gpg_hash_algo[i].id, algo_id))\n+\t\t\treturn sigcheck_gpg_hash_algo[i].name;\n+\t}\n+\n+\treturn NULL;\n+}\n+\n+static char *extract_gpg_hash_algo(const char *args_start,\n+\t\t\t\t   const char *line_end,\n+\t\t\t\t   int field_index)\n+{\n+\tconst char *p = args_start;\n+\tint current_field = 0;\n+\tchar *result = NULL;\n+\n+\twhile (p < line_end && current_field < field_index) {\n+\t\t/* Skip to the end of the current field */\n+\t\twhile (p < line_end && *p != ' ')\n+\t\t\tp++;\n+\t\t/* Skip spaces to get to the start of the next field */\n+\t\twhile (p < line_end && *p == ' ') {\n+\t\t\tp++;\n+\t\t\tcurrent_field++;\n+\t\t}\n+\t}\n+\n+\tif (p < line_end && current_field == field_index) {\n+\t\t/* Found start of the target field */\n+\t\tconst char *algo_id_end = strchrnul(p, ' ');\n+\t\tchar *algo_id = xmemdupz(p, algo_id_end - p);\n+\t\tconst char *hash_algo = lookup_gpg_hash_algo(algo_id);\n+\t\tif (hash_algo)\n+\t\t\tresult = xstrdup(hash_algo);\n+\t\tfree(algo_id);\n+\t}\n+\n+\treturn result;\n+}\n+\n static void parse_gpg_output(struct signature_check *sigc)\n {\n \tconst char *buf = sigc->gpg_status;\n@@ -242,6 +302,18 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t/* Iterate over all search strings */\n \t\tfor (size_t i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {\n \t\t\tif (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) {\n+\n+\t\t\t\t/* Do we have hash algorithm? */\n+\t\t\t\tif (!sigc->sig_algo) {\n+\t\t\t\t\tconst char *line_end = strchrnul(line, '\\n');\n+\t\t\t\t\tif (!strcmp(sigcheck_gpg_status[i].check, \"VALIDSIG \"))\n+\t\t\t\t\t\t/* Hash algorithm is the 8th field in VALIDSIG */\n+\t\t\t\t\t\tsigc->sig_algo = extract_gpg_hash_algo(line, line_end, 7);\n+\t\t\t\t\telse if (!strcmp(sigcheck_gpg_status[i].check, \"ERRSIG \"))\n+\t\t\t\t\t\t/* Hash algorithm is the 3rd field in ERRSIG */\n+\t\t\t\t\t\tsigc->sig_algo = extract_gpg_hash_algo(line, line_end, 2);\n+\t\t\t\t}\n+\n \t\t\t\t/*\n \t\t\t\t * GOODSIG, BADSIG etc. can occur only once for\n \t\t\t\t * each signature.  Therefore, if we had more\n@@ -323,6 +395,7 @@ static void parse_gpg_output(struct signature_check *sigc)\n \t\t\t}\n \t\t}\n \t}\n+\n \treturn;\n \n error:\n@@ -332,6 +405,7 @@ static void parse_gpg_output(struct signature_check *sigc)\n \tFREE_AND_NULL(sigc->fingerprint);\n \tFREE_AND_NULL(sigc->signer);\n \tFREE_AND_NULL(sigc->key);\n+\tFREE_AND_NULL(sigc->sig_algo);\n }\n \n static int verify_gpg_signed_buffer(struct signature_check *sigc,\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 9a32dd6ce8..2b7701ca2c 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -42,6 +42,10 @@ struct signature_check {\n \tchar *key;\n \tchar *fingerprint;\n \tchar *primary_key_fingerprint;\n+\n+\t/* hash algo for GPG/GPGSM, key type for SSH */\n+\tchar *sig_algo;\n+\n \tenum signature_trust_level trust_level;\n };\n \n-- \n2.49.0.609.g63c55177e5\n\n"},{"id":"518918","messageId":"20250526103314.1542316-6-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250526103314.1542316-1-christian.couder@gmail.com","subject":"[PATCH v2 5/6] gpg-interface: extract SSH key type from signature status output","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:33:13Z","receivedAt":"2025-05-26T10:33:44Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A previous commit extracted the hash algorithm from GPG/GPGSM signature\nstatus output and stored it in a new 'sig_algo' member of 'struct\nsignature_check'.\n\nFor SSH signatures, it's more interesting and easier to extract the key\ntype (like \"RSA\", \"ECDSA\", \"Ed25519\", ...) rather than the hash\nalgorithm which often depends on the key type. For example \"Ed25519\"\nhas SHA-512 integrated into its design, and \"ECDSA\" and \"RSA\" are\ntypically used with SHA-256.\n\nLet's improve the `gpg-interface` parsing logic to capture the SSH key\ntype when parsing the SSH signature status output.\n\nSimilarly as the hash algorithm for GPG/GPGSM signatures, this\ninformation can be useful for Git commands or external tools that\nprocess signature information. For example, it could be used when\ndisplaying signature verification results to users or when working with\nvarious signature formats in tools like fast-export and fast-import.\n\nAs they share a common usage, we also store the SSH key type in the new\n'sig_algo' member of 'struct signature_check'.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n gpg-interface.c | 24 +++++++++++++++++++++++-\n 1 file changed, 23 insertions(+), 1 deletion(-)\n\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 15687ede43..182e579769 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -456,11 +456,27 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,\n \treturn ret;\n }\n \n+static char *extract_ssh_key_type(const char *type_start, const char *type_end)\n+{\n+\tif (!type_end || type_end <= type_start)\n+\t\treturn NULL;\n+\n+\t/* Back up over any spaces before \" key \" */\n+\twhile (type_end > type_start && *(type_end - 1) == ' ')\n+\t\ttype_end--;\n+\n+\tif (type_end <= type_start)\n+\t\treturn NULL;\n+\n+\treturn xmemdupz(type_start, type_end - type_start);\n+}\n+\n static void parse_ssh_output(struct signature_check *sigc)\n {\n \tconst char *line, *principal, *search;\n \tchar *to_free;\n \tchar *key = NULL;\n+\tconst char *after_last_with = NULL;\n \n \t/*\n \t * ssh-keygen output should be:\n@@ -485,8 +501,10 @@ static void parse_ssh_output(struct signature_check *sigc)\n \t\tprincipal = line;\n \t\tdo {\n \t\t\tsearch = strstr(line, \" with \");\n-\t\t\tif (search)\n+\t\t\tif (search) {\n \t\t\t\tline = search + 1;\n+\t\t\t\tafter_last_with = search + 6;\n+\t\t\t}\n \t\t} while (search != NULL);\n \t\tif (line == principal)\n \t\t\tgoto cleanup;\n@@ -499,6 +517,7 @@ static void parse_ssh_output(struct signature_check *sigc)\n \t\t/* Valid signature, but key unknown */\n \t\tsigc->result = 'G';\n \t\tsigc->trust_level = TRUST_UNDEFINED;\n+\t\tafter_last_with = line;\n \t} else {\n \t\tgoto cleanup;\n \t}\n@@ -507,6 +526,9 @@ static void parse_ssh_output(struct signature_check *sigc)\n \tif (key) {\n \t\tsigc->fingerprint = xstrdup(key + 4);\n \t\tsigc->key = xstrdup(sigc->fingerprint);\n+\n+\t\tif (after_last_with)\n+\t\t\tsigc->sig_algo = extract_ssh_key_type(after_last_with, key);\n \t} else {\n \t\t/*\n \t\t * Output did not match what we expected\n-- \n2.49.0.609.g63c55177e5\n\n"},{"id":"518919","messageId":"20250526103314.1542316-7-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250526103314.1542316-1-christian.couder@gmail.com","subject":"[PATCH v2 6/6] verify-commit: add a --summary flag","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:33:14Z","receivedAt":"2025-05-26T10:33:45Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"The current outputs, with `--raw`, `--verbose` or by default, from\n`git verify-commit` are all quite verbose and do not make it easy to\nquickly assess signature status.\n\nLet's add a new `--summary` option to `git verify-commit` that prints\na concise, one-line summary of the signature verification to standard\noutput.\n\nThis compact format is useful for scripts and tools that need to\nquickly parse signature verification results, while still being\nhuman-readable.\n\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n Documentation/git-verify-commit.adoc | 17 ++++++++++++++++-\n builtin/verify-commit.c              |  4 +++-\n gpg-interface.c                      | 11 +++++++++++\n gpg-interface.h                      |  6 ++++++\n t/t7510-signed-commit.sh             | 24 ++++++++++++++++++++++++\n t/t7528-signed-commit-ssh.sh         | 28 ++++++++++++++++++++++++++++\n 6 files changed, 88 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-verify-commit.adoc b/Documentation/git-verify-commit.adoc\nindex 6a208a0c2a..fb038ae0cf 100644\n--- a/Documentation/git-verify-commit.adoc\n+++ b/Documentation/git-verify-commit.adoc\n@@ -8,7 +8,7 @@ git-verify-commit - Check the signature of commits\n SYNOPSIS\n --------\n [verse]\n-'git verify-commit' [-v | --verbose] [--raw] <commit>...\n+'git verify-commit' [-v | --verbose] [--raw] [--summary] <commit>...\n \n DESCRIPTION\n -----------\n@@ -38,6 +38,21 @@ OPTIONS\n \terror instead of the normal human-readable output. The format\n \tof this output is specific to the signature format being used.\n \n+--summary::\n+\tPrint a one-line human-readable summary of the signature check\n+\tto standard output in the format: `STATUS FORMAT ALGORITHM`.\n++\n+STATUS is the result character (e.g., G, B, E, U, N, ...) shown by the\n+\"%G?\" pretty format specifier. See the \"Pretty Formats\" section in\n+linkgit:git-log[1].\n++\n+FORMAT indicates the signature format (`openpgp`, `x509`, or `ssh`) or\n+`?` if unknown.\n++\n+ALGORITHM is the hash algorithm used for GPG/GPGSM signatures\n+(e.g. `sha1`, `sha256`, ...), or the key type for SSH signatures\n+(`RSA`, `ECDSA`, `ED25519`, ...), or `?` if unknown.\n+\n -v::\n --verbose::\n \tPrint the contents of the commit object before validating it.\ndiff --git a/builtin/verify-commit.c b/builtin/verify-commit.c\nindex 5f749a30da..54b5b7d360 100644\n--- a/builtin/verify-commit.c\n+++ b/builtin/verify-commit.c\n@@ -14,7 +14,7 @@\n #include \"gpg-interface.h\"\n \n static const char * const verify_commit_usage[] = {\n-\t\tN_(\"git verify-commit [-v | --verbose] [--raw] <commit>...\"),\n+\t\tN_(\"git verify-commit [-v | --verbose] [--raw] [--summary] <commit>...\"),\n \t\tNULL\n };\n \n@@ -27,6 +27,7 @@ static int run_gpg_verify(struct commit *commit, unsigned flags)\n \n \tret = check_commit_signature(commit, &signature_check);\n \tprint_signature_buffer(&signature_check, flags);\n+\tprint_signature_summary(&signature_check, flags);\n \n \tsignature_check_clear(&signature_check);\n \treturn ret;\n@@ -60,6 +61,7 @@ int cmd_verify_commit(int argc,\n \tconst struct option verify_commit_options[] = {\n \t\tOPT__VERBOSE(&verbose, N_(\"print commit contents\")),\n \t\tOPT_BIT(0, \"raw\", &flags, N_(\"print raw gpg status output\"), GPG_VERIFY_RAW),\n+\t\tOPT_BIT(0, \"summary\", &flags, N_(\"print concise signature verification summary\"), GPG_VERIFY_SUMMARY),\n \t\tOPT_END()\n \t};\n \ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 182e579769..fc198715c4 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -153,6 +153,7 @@ void signature_check_clear(struct signature_check *sigc)\n \tFREE_AND_NULL(sigc->key);\n \tFREE_AND_NULL(sigc->fingerprint);\n \tFREE_AND_NULL(sigc->primary_key_fingerprint);\n+\tFREE_AND_NULL(sigc->format_name);\n \tFREE_AND_NULL(sigc->sig_algo);\n }\n \n@@ -756,6 +757,8 @@ int check_signature(struct signature_check *sigc,\n \tif (!fmt)\n \t\tdie(_(\"bad/incompatible signature '%s'\"), signature);\n \n+\tsigc->format_name = xstrdup(fmt->name);\n+\n \tif (parse_payload_metadata(sigc))\n \t\treturn 1;\n \n@@ -782,6 +785,14 @@ void print_signature_buffer(const struct signature_check *sigc, unsigned flags)\n \t\tfputs(output, stderr);\n }\n \n+void print_signature_summary(const struct signature_check *sigc, unsigned flags)\n+{\n+\tif (flags & GPG_VERIFY_SUMMARY)\n+\t\tprintf(\"%c %s %s\\n\", sigc->result,\n+\t\t       sigc->format_name ? sigc->format_name : \"?\",\n+\t\t       sigc->sig_algo ? sigc->sig_algo : \"?\");\n+}\n+\n size_t parse_signed_buffer(const char *buf, size_t size)\n {\n \tsize_t len = 0;\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex 2b7701ca2c..a9565757f6 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -6,6 +6,7 @@ struct strbuf;\n #define GPG_VERIFY_VERBOSE\t(1<<0)\n #define GPG_VERIFY_RAW\t\t(1<<1)\n #define GPG_VERIFY_OMIT_STATUS\t(1<<2)\n+#define GPG_VERIFY_SUMMARY\t(1<<3)\n \n enum signature_trust_level {\n \tTRUST_UNDEFINED,\n@@ -43,6 +44,9 @@ struct signature_check {\n \tchar *fingerprint;\n \tchar *primary_key_fingerprint;\n \n+\t/* \"openpgp\", \"x509\", \"ssh\" */\n+\tchar *format_name;\n+\n \t/* hash algo for GPG/GPGSM, key type for SSH */\n \tchar *sig_algo;\n \n@@ -95,5 +99,7 @@ int check_signature(struct signature_check *sigc,\n \t\t    const char *signature, size_t slen);\n void print_signature_buffer(const struct signature_check *sigc,\n \t\t\t    unsigned flags);\n+void print_signature_summary(const struct signature_check *sigc,\n+\t\t\t     unsigned flags);\n \n #endif\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex 39677e859a..47f40862f3 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -232,6 +232,30 @@ test_expect_success GPG2 'bare signature' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPG 'verify signatures with --summary' '\n+\t# GPG-signed commit\n+\tgit verify-commit --summary sixth-signed >actual &&\n+\ttest_grep \"^G openpgp sha1\" actual &&\n+\n+\t# Non-signed commit\n+\ttest_must_fail git verify-commit --summary seventh-unsigned >actual 2>&1 &&\n+\ttest_grep \"^N ? ?\" actual &&\n+\n+\t# Trusted signature with alternate key (hash used might depend on the OS)\n+\tgit verify-commit --summary eighth-signed-alt >actual &&\n+\ttest_grep -E \"^G openpgp sha(256|512)\" actual &&\n+\n+\t# Bad signature\n+\ttest_must_fail git verify-commit --summary $(cat forged1.commit) >actual 2>err &&\n+\ttest_grep \"^B openpgp ?\" actual\n+'\n+\n+test_expect_success GPG '--summary and --raw work together' '\n+\tgit verify-commit --summary --raw sixth-signed >actual 2>err &&\n+\ttest_grep \"^G openpgp sha1\" actual &&\n+\ttest_grep \"GOODSIG\" err\n+'\n+\n test_expect_success GPG 'show good signature with custom format' '\n \tcat >expect <<-\\EOF &&\n \tG\ndiff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh\nindex 065f780636..3d0e7d7859 100755\n--- a/t/t7528-signed-commit-ssh.sh\n+++ b/t/t7528-signed-commit-ssh.sh\n@@ -277,6 +277,34 @@ test_expect_success GPGSSH 'detect fudged signature with NUL' '\n \t! grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" actual2\n '\n \n+test_expect_success GPGSSH 'verify-commit --summary outputs format and key type for SSH signatures' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\n+\t# SSH-signed commit with ED25519 key\n+\tgit verify-commit --summary sixth-signed >actual &&\n+\ttest_grep \"^G ssh ED25519\" actual &&\n+\n+\t# SSH-signed commit with ECDSA key\n+\tgit verify-commit --summary thirteenth-signed-ecdsa >actual &&\n+\ttest_grep \"^G ssh ECDSA\" actual &&\n+\n+\t# Non-signed commit\n+\ttest_must_fail git verify-commit --summary seventh-unsigned >actual 2>&1 &&\n+\ttest_grep \"^N ? ?\" actual &&\n+\n+\t# Bad signature\n+\ttest_must_fail git verify-commit --summary $(cat forged1.commit) >actual 2>err &&\n+\ttest_grep \"^B ssh ?\" actual\n+'\n+\n+test_expect_success GPGSSH '--summary and --raw work together' '\n+\ttest_config gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\n+\tgit verify-commit --summary --raw sixth-signed >actual 2>err &&\n+\ttest_grep \"^G ssh ED25519\" actual &&\n+\ttest_grep \"${GPGSSH_GOOD_SIGNATURE_TRUSTED}\" err\n+'\n+\n test_expect_success GPGSSH 'amending already signed commit' '\n \ttest_config gpg.format ssh &&\n \ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n-- \n2.49.0.609.g63c55177e5\n\n"},{"id":"518920","messageId":"CAP8UFD3BbxDGg=pAPfgxkwtuCC1gsmjnyOQhJgZyu7R_hxOyAw@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqselxtfyf.fsf@gitster.g","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:34:10Z","receivedAt":"2025-05-26T10:34:23Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Apr 24, 2025 at 11:19 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n\n> > diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> > index 63880b595c..59e991a03c 100644\n> > --- a/builtin/fast-import.c\n> > +++ b/builtin/fast-import.c\n> > @@ -29,6 +29,7 @@\n> >  #include \"commit-reach.h\"\n> >  #include \"khash.h\"\n> >  #include \"date.h\"\n> > +#include \"gpg-interface.h\"\n> >\n> >  #define PACK_ID_BITS 16\n> >  #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n> > @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)\n> >                       \"encoding %s\\n\",\n> >                       encoding);\n> >       if (sig_alg) {\n> > -             if (!strcmp(sig_alg, \"sha1\"))\n> > -                     strbuf_addstr(&new_data, \"gpgsig \");\n> > -             else if (!strcmp(sig_alg, \"sha256\"))\n> > +             if (!strcmp(sig_alg, \"sha256\"))\n> >                       strbuf_addstr(&new_data, \"gpgsig-sha256 \");\n> > -             else\n> > -                     die(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n> > +             else if (valid_signature_name(sig_alg))\n> > +                     strbuf_addstr(&new_data, \"gpgsig \");\n> > +             else if (!strcmp(sig_alg, \"unknown\")) {\n> > +                     warning(\"Unknown gpgsig algorithm name!\");\n> > +                     strbuf_addstr(&new_data, \"gpgsig \");\n> > +             } else\n> > +                     die(\"Invalid gpgsig algorithm name, got '%s'\", sig_alg);\n>\n> Hmph, we used to have special cases for sha1 and sha256 but now we\n> can handle sha1 with a more generic \"valid_signature_name()\" logic?\n> And yet we need to still special case sha256?  Not that I trust the\n> old code all that much and take deviations from the patterns in the\n> old code as a sign of something not right...\n>\n> The fast-export stream produced by the code with d9cb0e6f\n> (fast-export, fast-import: add support for signed-commits,\n> 2025-03-10) used to identify a signature algorithm \"sha1\", but this\n> new version of fast-import lost the support for it, and will barf\n> when seeing such an existing fast-export stream?  I am not sure what\n> is going on around this code.\n>\n> I am not so worried about the other case, where the stream produced\n> by fast-export contained in this version may or may not be readable\n> by an older version of fast-import.\n>\n> I am puzzled enough, so I'll stop here for now.\n\nI am also not sure about the best way to approach this, so I tried a\nnew approach in v2.\n\nAnyway thanks for your comments! I will address them if we decide that\nthe approach in this patch is still worth pursuing.\n"},{"id":"518921","messageId":"CAP8UFD2+h6hthbHQP_hmd5xXXCwizvSOd5tgGDHJ=V_eW1sgsw@mail.gmail.com","threadId":"63342","inReplyTo":"CABPp-BFrau4F1a_+umYw0qu8HYz3dTPXZ9oiHhawTSZpgAoGLw@mail.gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:34:48Z","receivedAt":"2025-05-26T10:35:01Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Apr 24, 2025 at 11:41 PM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Thu, Apr 24, 2025 at 1:39 PM Christian Couder\n> <christian.couder@gmail.com> wrote:\n> >\n> > A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\n> > signed-commits, 2025-03-10), added support for signed commits.\n> >\n> > However, when processing signatures `git fast-export` outputs \"gpgsig\n> > sha1\" not just when it encounters an OpenPGP SHA-1 signature, but also\n> > when it encounters an SSH or X.509 signature. This is not very\n> > informative to say the least, and this might prevent tools that process\n> > the output from easily and properly handling signatures.\n> >\n> > Let's improve on that by reusing the existing code from\n> > \"gpg-interface.{c,h}\" to detect the signature algorithm, and then put\n> > the signature algorithm name (like \"openpgp\", \"x509\" or \"ssh\") instead\n> > of \"sha1\" in the output. If we can't detect the signature algorithm we\n> > will use \"unknown\". It might be a signature added by an external tool\n> > and we should likely keep it.\n> >\n> > Similarly on the `git fast-import` side, let's use the existing code\n> > from \"gpg-interface.{c,h}\" to check if a signature algorithm name is\n> > valid. In case of an \"unknown\" signature algorithm name, we will warn\n> > but still keep it. Future work might implement several options to let\n> > users deal with it in different ways, and might implement checking\n> > known signatures too.\n>\n> The last sentence is somewhat ambiguous about whether it is only about\n> the \"unknown\" case or whether the second half of the sentence was\n> switching tracks to discuss something else about the known cases.\n\nYeah, the second half of the sentence was about switching tracks to\ndiscuss other things we might do in the future.\n\n> Do\n> you perhaps mean something like \"Future work might implement several\n> options to let users deal with an \"unknown\" signature algorithm, and\n> when we have a valid signature algorithm, we may be able to not only\n> verify the signature algorithm name but start verifying the signature\n> itself to ensure it is valid as well.\"  ?\n\nYeah, that was the idea. Thanks for spelling it in a better way than I did.\n\nIn v2, I have actually tried an approach based on verifying\nsignatures, and I'd be happy to know your opinion about the different\napproaches.\n\nThanks!\n\n[...]\n\n> > +Signatures are not yet checked in the current implementation though.\n>\n> Thanks for calling this out.\n\n[...]\n\n> > @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)\n> >                         \"encoding %s\\n\",\n> >                         encoding);\n> >         if (sig_alg) {\n> > -               if (!strcmp(sig_alg, \"sha1\"))\n> > -                       strbuf_addstr(&new_data, \"gpgsig \");\n> > -               else if (!strcmp(sig_alg, \"sha256\"))\n> > +               if (!strcmp(sig_alg, \"sha256\"))\n> >                         strbuf_addstr(&new_data, \"gpgsig-sha256 \");\n> > -               else\n> > -                       die(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n> > +               else if (valid_signature_name(sig_alg))\n> > +                       strbuf_addstr(&new_data, \"gpgsig \");\n> > +               else if (!strcmp(sig_alg, \"unknown\")) {\n> > +                       warning(\"Unknown gpgsig algorithm name!\");\n> > +                       strbuf_addstr(&new_data, \"gpgsig \");\n> > +               } else\n> > +                       die(\"Invalid gpgsig algorithm name, got '%s'\", sig_alg);\n> >                 string_list_split_in_place(&siglines, sig.buf, \"\\n\", -1);\n> >                 strbuf_add_separated_string_list(&new_data, \"\\n \", &siglines);\n> >                 strbuf_addch(&new_data, '\\n');\n>\n> I'm not very familiar with gpg and other signatures, and was stuck\n> trying to parse this logic when a review from Junio came in, and I\n> decided to read it since he often \"thinks out loud\" to see if that'd\n> explain it better.  Sadly, didn't help... ;-)  But I'll watch for any\n> follow-up response you add over there.\n\nYeah, if we want to continue in the direction of this patch, let's\ndiscuss it over there.\n\n> > @@ -381,6 +381,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '\n> >\n> >  '\n> >\n> > +test_expect_success GPGSM 'setup x509 signed commit' '\n> > +\n> > +       git checkout -b x509-signing main &&\n> > +       test_config gpg.format x509 &&\n> > +       test_config user.signingkey $GIT_COMMITTER_EMAIL &&\n> > +       echo \"x509 content\" >file_for_x509 &&\n> > +       git add file_for_x509 &&\n> > +       git commit -S -m \"X.509 signed commit\" &&\n> > +       X509_COMMIT=$(git rev-parse --verify HEAD) &&\n> > +       git checkout main\n> > +\n> > +'\n> > +\n> > +test_expect_success GPGSM 'x509 signature identified' '\n> > +\n> > +       git fast-export --signed-commits=verbatim --reencode=no x509-signing >output 2>err &&\n>\n> Is --reencode=no important here or does this work with --reencode=yes\n> as well?  (I understand the default being --reencode=abort and fact\n> that you are reusing an example that used a specialized encoding means\n> you need to specify something, was just curious if this particular\n> value was important)\n>\n> > +       grep \"^gpgsig x509\" output &&\n> > +       test ! -s err &&\n> > +       (\n> > +               cd new &&\n> > +               git fast-import &&\n> > +               STRIPPED=$(git rev-parse --verify refs/heads/x509-signing) &&\n> > +               test $X509_COMMIT = $STRIPPED\n>\n> Ah, --reencode=no is critical for the test, but only because you are\n> trying to ensure you get the same commit back.\n\nYeah, I think it's useful to check that we can get the same commit back.\n\n> Should there also be a\n> test for when something is tweaked, such as the encoding, and whether\n> the signature is still found?\n\n[...]\n\n> > +test_expect_success GPGSSH 'ssh signature identified' '\n> > +\n> > +       git fast-export --signed-commits=verbatim --reencode=no ssh-signing >output 2>err &&\n>\n> Out of curiosity, any particular reason to export the entire history\n> instead of just the new commit you just added (though you'd likely\n> want --reference-excluded-parents if you did that, which really ought\n> to be the default)?  Anyway...\n\nThanks for your review and all your comments!\n\nAs I am not sure which approach is best and I am trying a different\napproach in v2, it might not be worth discussing some of the details\nof the approach I took in this v1 anymore. So I will skip answering\nsome of your specific comments unless we decide to go back to that\napproach later.\n"},{"id":"518922","messageId":"CAP8UFD1VamVNnoNbD4OFysB3aJSBQciZ4zL-DowCokFqobEzVA@mail.gmail.com","threadId":"63342","inReplyTo":"aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:35:03Z","receivedAt":"2025-05-26T10:35:16Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Apr 25, 2025 at 12:05 AM brian m. carlson\n<sandals@crustytoothpaste.net> wrote:\n>\n> On 2025-04-24 at 20:39:04, Christian Couder wrote:\n> >  Here <alg> specifies which hashing algorithm is used for this\n> > -signature, either `sha1` or `sha256`.\n> > +signature. Current valid values are:\n> > +\n> > +* \"openpgp\" for SHA-1 OpenPGP signatures,\n> > +\n> > +* \"sha256\" for SHA-256 OpenPGP signatures,\n> > +\n> > +* \"x509\" for X.509 (GPGSM) signatures,\n> > +\n> > +* \"ssh\", for SSH signatures,\n> > +\n> > +* \"unknown\" for signatures that can't be identified (a warning is\n> > +  emitted).\n>\n> I don't think this is a good set of options.  We can have SHA-1 or\n> SHA-256 options for any of the three.  If I create a SHA-256 commit and\n> sign it with SSH, then it couldn't be exported with this type.\n>\n> It is even possible and valid to create a signature over the SHA-1\n> content of an object and sign it with one protocol, say, OpenPGP, and\n> then create a signature over the SHA-256 content of the object and sign\n> it with a different one, such as SSH.  Git does not natively support\n> this, but it is possible to do by hand.\n>\n> These should be separate fields: one for the hash algorithm and one for\n> the protocol.\n\nYeah, I agree that the set of options is not ideal and it would be\nbetter if it was possible to get these two separate fields.\n\n> Alternatively, we can just keep the hash algorithm field\n> and parse the protocol by reading the first line, which will differ for\n> different protocols.\n\nI am not sure it's easy to get all the information without checking\nthe signature. I have tried a different approach based on checking the\nsignature in the v2.\n\nThanks for the review!\n"},{"id":"518923","messageId":"CAP8UFD0OdqnoFeYY+7y-No_x_DknapoLzvqvsy-+x_602sYQbg@mail.gmail.com","threadId":"63342","inReplyTo":"xmqq1pthtbdg.fsf@gitster.g","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-05-26T10:35:49Z","receivedAt":"2025-05-26T10:36:02Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Apr 25, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> >> The fast-export stream produced by the code with d9cb0e6f\n> >> (fast-export, fast-import: add support for signed-commits,\n> >> 2025-03-10) used to identify a signature algorithm \"sha1\", but this\n> >> new version of fast-import lost the support for it, and will barf\n> >> when seeing such an existing fast-export stream?  I am not sure what\n> >> is going on around this code.\n> >>\n> >> I am not so worried about the other case, where the stream produced\n> >> by fast-export contained in this version may or may not be readable\n> >> by an older version of fast-import.\n> >\n> > I certainly can't answer anything here as I know little about\n> > signatures, but your comment brought up a different question for me:\n> > Given that d9cb0e6ff8b3 (fast-export, fast-import: add support for\n> > signed-commits, 2025-03-10) isn't part of any release (not even a\n> > release candidate), do we need to have backward compatibility with\n> > that version?\n>\n> I think we will lose all the credibility if we said \"that's not in\n> an official release, so we are free to break early adopters\", once\n> something is in 'master'.\n\nI agree that we should have at least said in big letters that the\nimproved support for signed commits in fast-export/import is very\nexperimental and very likely to change in the future.\n\nWe could still do so. This could give us a bit of time and flexibility\nuntil we agree on and implement something better and backward\ncompatible. (Hopefully the v2 will help us move forward.)\n\n> As some corp environment are know to run\n> 'next' and indeed we do encourage more folks to do so so that we can\n> catch breakages before they escape to 'master', I actually am equally\n> worried about things in 'next'.\n\nYeah, right.\n\nOn the other hand I think it's fair for us to experiment in some\nareas, at least when we document that we are experimenting, which\nmeans that running 'master' or 'next' and blindly using any feature we\nhave just added is not the right thing to do if people don't want to\nbe exposed to not just bugs but also some possible backward\nincompatibilities in some areas.\n\nSo yeah, we should definitely have said that we are experimenting. Let\nme know if you want me to prepare a patch to add such wordings on top\nof d9cb0e6f (fast-export, fast-import: add support for signed-commits,\n2025-03-10). Otherwise we will have to be backward compatible, which\nis not ideal, but might not be a very big issue either.\n"},{"id":"518948","messageId":"CABPp-BFbF=rp6FZjyL+Fm5TqNZZdhfjS1sK-CBQ_=wtvFmfdLw@mail.gmail.com","threadId":"63342","inReplyTo":"20250526103314.1542316-1-christian.couder@gmail.com","subject":"Re: [PATCH v2 0/6] extract algo information from signatures","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-05-26T16:03:43Z","receivedAt":"2025-05-26T16:03:56Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, May 26, 2025 at 3:33 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> Around one month ago, I sent a patch that tried to improve on how `git\n> fast-export` handled SSH and X.509 commit signatures:\n>\n> https://lore.kernel.org/git/20250424203904.909777-1-christian.couder@gmail.com/\n>\n> This patch was showing a single string for the hash algorithm with the\n> following possible values:\n>\n> * \"openpgp\" for SHA-1 OpenPGP signatures,\n>\n> * \"sha256\" for SHA-256 OpenPGP signatures,\n>\n> * \"x509\" for X.509 (GPGSM) signatures,\n>\n> * \"ssh\", for SSH signatures,\n>\n> * \"unknown\" for signatures that can't be identified (a warning is\n>   emitted).\n>\n> brian m. carlson however replied that it would be better to show two\n> pieces of information instead of one: one for the hash algorithm and\n> one for the protocol.\n>\n> I have tried to do that but there were a number of issues. First it\n> seems to be easier to extract information from signatures when\n> checking them. And if you check them, then it might be interesting to\n> show the result of the check.\n>\n> Also for SSH signatures, it's difficult and not so informative to get\n> the hash algorithm. That's because the hash algorithm is often\n> specified by the key type (like \"RSA\", \"ECDSA\", \"Ed25519\", ...). For\n> example \"Ed25519\" has SHA-512 integrated into its design, and \"ECDSA\"\n> and \"RSA\" are typically used with SHA-256. So for SSH signatures it\n> seems better to just show the key type and not the hash algorithm.\n>\n> In general I am not sure what users might want regarding commit\n> signatures when using fast-export. Some might not need much signature\n> information at all, and for them checking signatures might just slow\n> the export process for no benefit, while others might want more\n> signature information even at the expense of a slower export.\n\nI'd like to propose that the following are the possible uses that\nusers might have regarding commit signatures with\nfast-export/fast-import (if anyone has additional usecases, let me\nknow):\n\n(A) Make fast-export include signatures, and make fast-import include\nthem unconditionally (even if invalid)\n(B) Similar to (A), but make *fast-import* check them and either error\nout or drop them if they become invalid\n(C) Simliar to (B), but make *fast-import* re-sign the commit if they\nbecome invalid\n(D) Similar to (A), but make *fast-import* re-sign the commit even if\nthe signature would have been valid\n\nNote that in the above, there might be additional processing between\nwhen fast-export runs and when fast-import does (e.g. by filter-repo\nor a similar tool, or even the user editing by hand).\n\n> To address this, I decided to focus first on extracting the hash\n> algorithm from OpenPGP/X.509 signatures and the key type from SSH\n> signature when checking signatures.\n>\n> To test that, I thought that it could be interesting to add a\n> `--summary` option to `verify-commit` that shows a concise, one-line\n> summary of the signature verification to standard output in the\n> `STATUS FORMAT ALGORITHM` format, where:\n>\n> * STATUS is the result character (e.g., G, B, E, U, N, ...), similar\n>   as what the \"%G?\" pretty format specifier shows,\n>\n> * FORMAT is the signature format (`openpgp`, `x509`, or `ssh`),\n>\n> * ALGORITHM is the hash algorithm used for GPG/GPGSM signatures\n>   (e.g. `sha1`, `sha256`, ...), or the key type for SSH signatures\n>   (`RSA`, `ECDSA`, `ED25519`, ...).\n\nThis sounds like it might be a nice feature extension to the\nverify-commit builtin.  I don't see how it helps implement signature\nhandling in fast-export/fast-import, though.\n\n> If we can agree on a concise format output for signature checks, then\n> maybe this format will be a good format to be used in the `git\n> fast-export` output for users who are fine with signatures being\n> checked.\n>\n> What do you think?\n\nMaybe I'm missing something, but it seems to me that checking\nsignatures *in fast-export* would be a complete waste of time.  For\nusecases (A) & (D), checking signatures at all is a waste of time.\nFor usecases (B) & (C), checking signatures in fast-export is\nthrowaway work because whether or not the signatures are valid at the\ntime fast-export runs, and even in the rare usecase where there is no\nadditional processing between fast-export and fast-import (such as by\nfilter-repo), the signatures would still need to be re-checked by\nfast-import anyway.  (Note that a simple `git fast-export ... | git\nfast-import` is *not* guaranteed to get the same commit hashes even\nwhen there are no commit signatures; that only happens when the\nhistory is sufficiently canonical).\n"},{"id":"518994","messageId":"xmqqzfeyqdye.fsf@gitster.g","threadId":"63342","inReplyTo":"CAP8UFD0OdqnoFeYY+7y-No_x_DknapoLzvqvsy-+x_602sYQbg@mail.gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-27T15:18:49Z","receivedAt":"2025-05-27T15:18:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> I agree that we should have at least said in big letters that the\n> improved support for signed commits in fast-export/import is very\n> experimental and very likely to change in the future.\n>\n> We could still do so. This could give us a bit of time and flexibility\n> until we agree on and implement something better and backward\n> compatible. (Hopefully the v2 will help us move forward.)\n\nOK, as the next release is approaching, perhaps we do a bit of\ndocumentation update to address that \"we are experimenting\" and\nnothing else, and leave the v2 updates for the next cycle?\n\nThanks.\n"},{"id":"519095","messageId":"xmqq8qmgsky8.fsf@gitster.g","threadId":"63342","inReplyTo":"xmqqzfeyqdye.fsf@gitster.g","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-28T17:29:19Z","receivedAt":"2025-05-28T17:29:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n>> I agree that we should have at least said in big letters that the\n>> improved support for signed commits in fast-export/import is very\n>> experimental and very likely to change in the future.\n>>\n>> We could still do so. This could give us a bit of time and flexibility\n>> until we agree on and implement something better and backward\n>> compatible. (Hopefully the v2 will help us move forward.)\n>\n> OK, as the next release is approaching, perhaps we do a bit of\n> documentation update to address that \"we are experimenting\" and\n> nothing else, and leave the v2 updates for the next cycle?\n\n---- >8 ----\nSubject: [PATCH] fast-export: --signed-commits is experimental\n\nAs the design of signature handling is still being discussed, it is\nlikely that the data stream produced by the code in Git 2.50 would\nhave to be changed in such a way that is not backward compatible.\n\nMark the feature as experimental and discourge its use for now.\n\nAlso flip the default on the generation side to \"strip\"; users of\nexisting versions would not have passed --signed-commits=strip and\nwill be broken by this change if the default is made to abort, and\nwill be encouraged by the error message to produce data stream with\nfuture breakage guarantees by passing --signed-commits option.\n\nAs we tone down the default behaviour, we no longer need the\nFAST_EXPORT_SIGNED_COMMITS_NOABORT environment variable, which was\nnot discoverable enough.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/RelNotes/2.50.0.adoc |  4 +++-\n Documentation/git-fast-export.adoc | 12 +++++-------\n Documentation/git-fast-import.adoc |  3 +++\n builtin/fast-export.c              |  7 +------\n t/t9350-fast-export.sh             | 20 ++++----------------\n 5 files changed, 16 insertions(+), 30 deletions(-)\n\ndiff --git a/Documentation/RelNotes/2.50.0.adoc b/Documentation/RelNotes/2.50.0.adoc\nindex c6c34d1a1d..9a1cdf0dc0 100644\n--- a/Documentation/RelNotes/2.50.0.adoc\n+++ b/Documentation/RelNotes/2.50.0.adoc\n@@ -100,7 +100,9 @@ Performance, Internal Implementation, Development Support etc.\n  * \"git fsck\" becomes more careful when checking the refs.\n \n  * \"git fast-export | git fast-import\" learns to deal with commit and\n-   tag objects with embedded signatures a bit better.\n+   tag objects with embedded signatures a bit better.  This is highly\n+   experimental and the format of the data stream may change in the\n+   future without compatibility guarantees.\n \n  * The code paths to check whether a refname X is available (by seeing\n    if another ref X/Y exists, etc.) have been optimized.\ndiff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\nindex 413a527496..43bbb4f63c 100644\n--- a/Documentation/git-fast-export.adoc\n+++ b/Documentation/git-fast-export.adoc\n@@ -46,14 +46,12 @@ resulting tag will have an invalid signature.\n \n --signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::\n \tSpecify how to handle signed commits.  Behaves exactly as\n-\t'--signed-tags', but for commits.  Default is 'abort'.\n+\t'--signed-tags', but for commits.  Default is 'strip', which\n+\tis the same as how earlier versions of this command without\n+\tthis option behaved.\n +\n-Earlier versions this command that did not have '--signed-commits'\n-behaved as if '--signed-commits=strip'.  As an escape hatch for users\n-of tools that call 'git fast-export' but do not yet support\n-'--signed-commits', you may set the environment variable\n-'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default\n-from 'abort' to 'warn-strip'.\n+NOTE: This is highly experimental and the format of the data stream may\n+change in the future without compatibility guarantees.\n \n --tag-of-filtered-object=(abort|drop|rewrite)::\n \tSpecify how to handle tags whose tagged object is filtered out.\ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 7b107f5e8e..250d866652 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -523,6 +523,9 @@ that signs the commit data.\n Here <alg> specifies which hashing algorithm is used for this\n signature, either `sha1` or `sha256`.\n \n+NOTE: This is highly experimental and the format of the data stream may\n+change in the future without compatibility guarantees.\n+\n `encoding`\n ^^^^^^^^^^\n The optional `encoding` command indicates the encoding of the commit\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex 37c01d6c6f..fcf6b00d5f 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -39,7 +39,7 @@ enum sign_mode { SIGN_ABORT, SIGN_VERBATIM, SIGN_STRIP, SIGN_WARN_VERBATIM, SIGN\n \n static int progress;\n static enum sign_mode signed_tag_mode = SIGN_ABORT;\n-static enum sign_mode signed_commit_mode = SIGN_ABORT;\n+static enum sign_mode signed_commit_mode = SIGN_STRIP;\n static enum tag_of_filtered_mode { TAG_FILTERING_ABORT, DROP, REWRITE } tag_of_filtered_mode = TAG_FILTERING_ABORT;\n static enum reencode_mode { REENCODE_ABORT, REENCODE_YES, REENCODE_NO } reencode_mode = REENCODE_ABORT;\n static int fake_missing_tagger;\n@@ -1269,7 +1269,6 @@ int cmd_fast_export(int argc,\n \t\t    const char *prefix,\n \t\t    struct repository *repo UNUSED)\n {\n-\tconst char *env_signed_commits_noabort;\n \tstruct rev_info revs;\n \tstruct commit *commit;\n \tchar *export_filename = NULL,\n@@ -1327,10 +1326,6 @@ int cmd_fast_export(int argc,\n \tif (argc == 1)\n \t\tusage_with_options (fast_export_usage, options);\n \n-\tenv_signed_commits_noabort = getenv(\"FAST_EXPORT_SIGNED_COMMITS_NOABORT\");\n-\tif (env_signed_commits_noabort && *env_signed_commits_noabort)\n-\t\tsigned_commit_mode = SIGN_WARN_STRIP;\n-\n \t/* we handle encodings */\n \tgit_config(git_default_config, NULL);\n \ndiff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\nindex dda9e7c3e7..76619765fc 100755\n--- a/t/t9350-fast-export.sh\n+++ b/t/t9350-fast-export.sh\n@@ -299,22 +299,10 @@ test_expect_success GPG 'set up signed commit' '\n \n '\n \n-test_expect_success GPG 'signed-commits default' '\n-\n-\tsane_unset FAST_EXPORT_SIGNED_COMMITS_NOABORT &&\n-\ttest_must_fail git fast-export --reencode=no commit-signing &&\n-\n-\tFAST_EXPORT_SIGNED_COMMITS_NOABORT=1 git fast-export --reencode=no commit-signing >output 2>err &&\n-\t! grep ^gpgsig output &&\n-\tgrep \"^encoding ISO-8859-1\" output &&\n-\ttest -s err &&\n-\tsed \"s/commit-signing/commit-strip-signing/\" output | (\n-\t\tcd new &&\n-\t\tgit fast-import &&\n-\t\tSTRIPPED=$(git rev-parse --verify refs/heads/commit-strip-signing) &&\n-\t\ttest $COMMIT_SIGNING != $STRIPPED\n-\t)\n-\n+test_expect_success GPG 'signed-commits default is same as strip' '\n+\tgit fast-export --reencode=no commit-signing >out1 2>err &&\n+\tgit fast-export --reencode=no --signed-commits=strip commit-signing >out2 &&\n+\ttest_cmp out1 out2\n '\n \n test_expect_success GPG 'signed-commits=abort' '\n-- \n2.50.0-rc0-134-gb29a910c2a\n\n"},{"id":"519097","messageId":"CABPp-BGe6r-X7NOiBFEvLGZH+GQvLkOYWn+qf3ZzzgT4sGAyOg@mail.gmail.com","threadId":"63342","inReplyTo":"xmqq8qmgsky8.fsf@gitster.g","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-05-28T20:06:42Z","receivedAt":"2025-05-28T20:06:54Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, May 28, 2025 at 10:29 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n> > Christian Couder <christian.couder@gmail.com> writes:\n> >\n> >> I agree that we should have at least said in big letters that the\n> >> improved support for signed commits in fast-export/import is very\n> >> experimental and very likely to change in the future.\n> >>\n> >> We could still do so. This could give us a bit of time and flexibility\n> >> until we agree on and implement something better and backward\n> >> compatible. (Hopefully the v2 will help us move forward.)\n> >\n> > OK, as the next release is approaching, perhaps we do a bit of\n> > documentation update to address that \"we are experimenting\" and\n> > nothing else, and leave the v2 updates for the next cycle?\n>\n> ---- >8 ----\n> Subject: [PATCH] fast-export: --signed-commits is experimental\n>\n> As the design of signature handling is still being discussed, it is\n> likely that the data stream produced by the code in Git 2.50 would\n> have to be changed in such a way that is not backward compatible.\n>\n> Mark the feature as experimental and discourge its use for now.\n\nI think this is a very good thing to do.\n\nminor nit: discourge -> discourage\n\n> Also flip the default on the generation side to \"strip\"; users of\n> existing versions would not have passed --signed-commits=strip and\n> will be broken by this change if the default is made to abort, and\n> will be encouraged by the error message to produce data stream with\n> future breakage guarantees by passing --signed-commits option.\n\nSo...git-filter-repo runs fast-export and has limited flexibility\nabout which options it passes to fast-export under the hood, so this\nchange would save me from the patch I was planning to add to\nfilter-repo.  So that's evidence in support of your statement, but the\n\"will be broken\" statement appears to me to be incongruent with past\ndeprecations and changes of default that we have gone through.  Often\nwhen we have deprecated or changed an option our process was to first\nproduce an error and update documentation and wait a while, then go\nand change the default after a sufficiently long time.  Here, we had\nkind of stopped at just producing the error with no plans to take\nanother step.  If that was the route we took in the past, what makes\nthis considered a breakage and not the other changes we made?\n\n(Just curious, I'm not against this change.)\n\n> As we tone down the default behaviour, we no longer need the\n> FAST_EXPORT_SIGNED_COMMITS_NOABORT environment variable, which was\n> not discoverable enough.\n\nMakes sense.\n>\n> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n> ---\n>  Documentation/RelNotes/2.50.0.adoc |  4 +++-\n>  Documentation/git-fast-export.adoc | 12 +++++-------\n>  Documentation/git-fast-import.adoc |  3 +++\n>  builtin/fast-export.c              |  7 +------\n>  t/t9350-fast-export.sh             | 20 ++++----------------\n>  5 files changed, 16 insertions(+), 30 deletions(-)\n>\n> diff --git a/Documentation/RelNotes/2.50.0.adoc b/Documentation/RelNotes/2.50.0.adoc\n> index c6c34d1a1d..9a1cdf0dc0 100644\n> --- a/Documentation/RelNotes/2.50.0.adoc\n> +++ b/Documentation/RelNotes/2.50.0.adoc\n> @@ -100,7 +100,9 @@ Performance, Internal Implementation, Development Support etc.\n>   * \"git fsck\" becomes more careful when checking the refs.\n>\n>   * \"git fast-export | git fast-import\" learns to deal with commit and\n> -   tag objects with embedded signatures a bit better.\n> +   tag objects with embedded signatures a bit better.  This is highly\n> +   experimental and the format of the data stream may change in the\n> +   future without compatibility guarantees.\n>\n>   * The code paths to check whether a refname X is available (by seeing\n>     if another ref X/Y exists, etc.) have been optimized.\n> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\n> index 413a527496..43bbb4f63c 100644\n> --- a/Documentation/git-fast-export.adoc\n> +++ b/Documentation/git-fast-export.adoc\n> @@ -46,14 +46,12 @@ resulting tag will have an invalid signature.\n>\n>  --signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::\n>         Specify how to handle signed commits.  Behaves exactly as\n> -       '--signed-tags', but for commits.  Default is 'abort'.\n> +       '--signed-tags', but for commits.  Default is 'strip', which\n> +       is the same as how earlier versions of this command without\n> +       this option behaved.\n>  +\n> -Earlier versions this command that did not have '--signed-commits'\n> -behaved as if '--signed-commits=strip'.  As an escape hatch for users\n> -of tools that call 'git fast-export' but do not yet support\n> -'--signed-commits', you may set the environment variable\n> -'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default\n> -from 'abort' to 'warn-strip'.\n> +NOTE: This is highly experimental and the format of the data stream may\n> +change in the future without compatibility guarantees.\n>\n>  --tag-of-filtered-object=(abort|drop|rewrite)::\n>         Specify how to handle tags whose tagged object is filtered out.\n> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> index 7b107f5e8e..250d866652 100644\n> --- a/Documentation/git-fast-import.adoc\n> +++ b/Documentation/git-fast-import.adoc\n> @@ -523,6 +523,9 @@ that signs the commit data.\n>  Here <alg> specifies which hashing algorithm is used for this\n>  signature, either `sha1` or `sha256`.\n>\n> +NOTE: This is highly experimental and the format of the data stream may\n> +change in the future without compatibility guarantees.\n> +\n>  `encoding`\n>  ^^^^^^^^^^\n>  The optional `encoding` command indicates the encoding of the commit\n> diff --git a/builtin/fast-export.c b/builtin/fast-export.c\n> index 37c01d6c6f..fcf6b00d5f 100644\n> --- a/builtin/fast-export.c\n> +++ b/builtin/fast-export.c\n> @@ -39,7 +39,7 @@ enum sign_mode { SIGN_ABORT, SIGN_VERBATIM, SIGN_STRIP, SIGN_WARN_VERBATIM, SIGN\n>\n>  static int progress;\n>  static enum sign_mode signed_tag_mode = SIGN_ABORT;\n> -static enum sign_mode signed_commit_mode = SIGN_ABORT;\n> +static enum sign_mode signed_commit_mode = SIGN_STRIP;\n>  static enum tag_of_filtered_mode { TAG_FILTERING_ABORT, DROP, REWRITE } tag_of_filtered_mode = TAG_FILTERING_ABORT;\n>  static enum reencode_mode { REENCODE_ABORT, REENCODE_YES, REENCODE_NO } reencode_mode = REENCODE_ABORT;\n>  static int fake_missing_tagger;\n> @@ -1269,7 +1269,6 @@ int cmd_fast_export(int argc,\n>                     const char *prefix,\n>                     struct repository *repo UNUSED)\n>  {\n> -       const char *env_signed_commits_noabort;\n>         struct rev_info revs;\n>         struct commit *commit;\n>         char *export_filename = NULL,\n> @@ -1327,10 +1326,6 @@ int cmd_fast_export(int argc,\n>         if (argc == 1)\n>                 usage_with_options (fast_export_usage, options);\n>\n> -       env_signed_commits_noabort = getenv(\"FAST_EXPORT_SIGNED_COMMITS_NOABORT\");\n> -       if (env_signed_commits_noabort && *env_signed_commits_noabort)\n> -               signed_commit_mode = SIGN_WARN_STRIP;\n> -\n>         /* we handle encodings */\n>         git_config(git_default_config, NULL);\n>\n> diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\n> index dda9e7c3e7..76619765fc 100755\n> --- a/t/t9350-fast-export.sh\n> +++ b/t/t9350-fast-export.sh\n> @@ -299,22 +299,10 @@ test_expect_success GPG 'set up signed commit' '\n>\n>  '\n>\n> -test_expect_success GPG 'signed-commits default' '\n> -\n> -       sane_unset FAST_EXPORT_SIGNED_COMMITS_NOABORT &&\n> -       test_must_fail git fast-export --reencode=no commit-signing &&\n> -\n> -       FAST_EXPORT_SIGNED_COMMITS_NOABORT=1 git fast-export --reencode=no commit-signing >output 2>err &&\n> -       ! grep ^gpgsig output &&\n> -       grep \"^encoding ISO-8859-1\" output &&\n> -       test -s err &&\n> -       sed \"s/commit-signing/commit-strip-signing/\" output | (\n> -               cd new &&\n> -               git fast-import &&\n> -               STRIPPED=$(git rev-parse --verify refs/heads/commit-strip-signing) &&\n> -               test $COMMIT_SIGNING != $STRIPPED\n> -       )\n> -\n> +test_expect_success GPG 'signed-commits default is same as strip' '\n> +       git fast-export --reencode=no commit-signing >out1 2>err &&\n> +       git fast-export --reencode=no --signed-commits=strip commit-signing >out2 &&\n> +       test_cmp out1 out2\n>  '\n>\n>  test_expect_success GPG 'signed-commits=abort' '\n> --\n> 2.50.0-rc0-134-gb29a910c2a\n\nLooks good to me.\n"},{"id":"519102","messageId":"xmqq4ix4qtva.fsf@gitster.g","threadId":"63342","inReplyTo":"CABPp-BGe6r-X7NOiBFEvLGZH+GQvLkOYWn+qf3ZzzgT4sGAyOg@mail.gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-28T21:59:37Z","receivedAt":"2025-05-28T21:59:41Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> Often\n> when we have deprecated or changed an option our process was to first\n> produce an error and update documentation and wait a while, then go\n> and change the default after a sufficiently long time.  Here, we had\n> kind of stopped at just producing the error with no plans to take\n> another step.  If that was the route we took in the past, what makes\n> this considered a breakage and not the other changes we made?\n>\n> (Just curious, I'm not against this change.)\n\nWhat is wrong is the behaviour change in the original, which luckily\nis not in any released versions (except for 2.50-rc0, which should\nnot count, as I think we should do this toning-down before -rc1).\n\nWe used to silently ignore and strip commit signatures and that has\nalways been the behaviour the existing users have relied upon; we\nstarted requiring these existing users to either explicitly pass\n--signed-c=strip or set an environmtne variable.  A new feature\nshould be opt-in to make the transition smoother, but the topic did\nnot follow that pattern.\n\nI view this last-minute band-aid patch that flips the default back\nto what it used to be as remedying that mistake in the original\nseries.\n"},{"id":"519114","messageId":"CABPp-BFRhSB0zTY1m+gYOpfvmuTvN5Pu3STawJwx9P-ayJw6bA@mail.gmail.com","threadId":"63342","inReplyTo":"xmqq4ix4qtva.fsf@gitster.g","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-05-28T23:15:45Z","receivedAt":"2025-05-28T23:15:57Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, May 28, 2025 at 2:59 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> > Often\n> > when we have deprecated or changed an option our process was to first\n> > produce an error and update documentation and wait a while, then go\n> > and change the default after a sufficiently long time.  Here, we had\n> > kind of stopped at just producing the error with no plans to take\n> > another step.  If that was the route we took in the past, what makes\n> > this considered a breakage and not the other changes we made?\n> >\n> > (Just curious, I'm not against this change.)\n>\n> What is wrong is the behaviour change in the original, which luckily\n> is not in any released versions (except for 2.50-rc0, which should\n> not count, as I think we should do this toning-down before -rc1).\n>\n> We used to silently ignore and strip commit signatures and that has\n> always been the behaviour the existing users have relied upon; we\n> started requiring these existing users to either explicitly pass\n> --signed-c=strip or set an environmtne variable.  A new feature\n> should be opt-in to make the transition smoother, but the topic did\n> not follow that pattern.\n>\n> I view this last-minute band-aid patch that flips the default back\n> to what it used to be as remedying that mistake in the original\n> series.\n\nAn earlier version of the series did keep the stripping, but you were\nthe one who objected\n(https://lore.kernel.org/git/xmqqfszbcazc.fsf@gitster.g/):\n\n\"\"\"\nWhy deliberate inconsistency?  I am not sure \"historically we did a\nwrong thing\" is a good reason (if we view that silently stripping\nwas a disservice to the users, aborting would be a bugfix).\n\"\"\"\n\nand later in the series\n(https://lore.kernel.org/git/xmqqim44fyjj.fsf@gitster.g/), you added:\n\n\"\"\"\nThanks.  The \"filter-repo already gets bug reports from the users\"\nis a valuable input when deciding if it is reasonable to sell the\nbehaviour change as a bugfix to our users.\n\"\"\"\n\nPersonally, I kind of think abort makes more sense as the default --\nat some point.  So I'm curious if you've just changed your mind\ncompletely from before and are against changing the default at all,\nwhether you think there's more steps that should be done before we\nchange the default, or whether this new flag feeling incomplete and\ndevelopment related to it being slow moving means we should wait off\nuntil we have a better picture of where it'll end up and leave the\ndefaults alone until we get there.  (I'm kind of leaning towards the\n3rd of those, but am curious if I'm misaligned with your vision.)\n"},{"id":"519131","messageId":"xmqqwma0nm6c.fsf@gitster.g","threadId":"63342","inReplyTo":"CABPp-BFRhSB0zTY1m+gYOpfvmuTvN5Pu3STawJwx9P-ayJw6bA@mail.gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-05-29T03:14:03Z","receivedAt":"2025-05-29T03:14:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> Personally, I kind of think abort makes more sense as the default --\n> at some point.\n\nOh, of course.\n\n> So I'm curious if you've just changed your mind\n> completely from before and are against changing the default at all,\n\nYes, after seeing that the representation of the signature algorithm\nand encapsulation format was not as well thought out as I thought it\nwould already be and its design still being discussed, I realized\nthat the new feature was way premature to have in the release.  At\nsome point, when things mature and we are reasonably sure we will\nnot have to make incompatible changes in the data stream, we might\nneed to switch, and the best default might turn out to be to refuse\nto work unless the end-user makes an explicit choice, but as the\ndesign of the feature stands now, I have a feeling that it is a bit\npremature.  Certainly not ready for general consumption.\n\nOf course, I could have just reverted the merge of the original\ntopic and give it a chance for a fresh restart the next cycle, but a\nnew feature clearly marked \"highly experimental\" would hopefully set\nthe end-user expectation straight, as long as the default is \"do not\ndo anything different from before\", which is the safest choice for a\nfeature whose design is still wobbly.\n\n"},{"id":"519476","messageId":"CAP8UFD2i-ayL7vs=Y46DpYkv64ROBj0YHNHEbbaG76mLTncyEQ@mail.gmail.com","threadId":"63342","inReplyTo":"xmqq8qmgsky8.fsf@gitster.g","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-02T15:56:10Z","receivedAt":"2025-06-02T15:56:23Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"(Sorry for the late answer to this.)\n\nOn Wed, May 28, 2025 at 7:29 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n> > Christian Couder <christian.couder@gmail.com> writes:\n> >\n> >> I agree that we should have at least said in big letters that the\n> >> improved support for signed commits in fast-export/import is very\n> >> experimental and very likely to change in the future.\n> >>\n> >> We could still do so. This could give us a bit of time and flexibility\n> >> until we agree on and implement something better and backward\n> >> compatible. (Hopefully the v2 will help us move forward.)\n> >\n> > OK, as the next release is approaching, perhaps we do a bit of\n> > documentation update to address that \"we are experimenting\" and\n> > nothing else, and leave the v2 updates for the next cycle?\n\nThanks for this. I agree that it's the best approach.\n\n> ---- >8 ----\n> Subject: [PATCH] fast-export: --signed-commits is experimental\n>\n> As the design of signature handling is still being discussed, it is\n> likely that the data stream produced by the code in Git 2.50 would\n> have to be changed in such a way that is not backward compatible.\n>\n> Mark the feature as experimental and discourge its use for now.\n\nYeah, right.\n\n> Also flip the default on the generation side to \"strip\"; users of\n> existing versions would not have passed --signed-commits=strip and\n> will be broken by this change if the default is made to abort, and\n> will be encouraged by the error message to produce data stream with\n> future breakage guarantees by passing --signed-commits option.\n>\n> As we tone down the default behaviour, we no longer need the\n> FAST_EXPORT_SIGNED_COMMITS_NOABORT environment variable, which was\n> not discoverable enough.\n>\n> Signed-off-by: Junio C Hamano <gitster@pobox.com>\n> ---\n>  Documentation/RelNotes/2.50.0.adoc |  4 +++-\n>  Documentation/git-fast-export.adoc | 12 +++++-------\n>  Documentation/git-fast-import.adoc |  3 +++\n>  builtin/fast-export.c              |  7 +------\n>  t/t9350-fast-export.sh             | 20 ++++----------------\n>  5 files changed, 16 insertions(+), 30 deletions(-)\n>\n> diff --git a/Documentation/RelNotes/2.50.0.adoc b/Documentation/RelNotes/2.50.0.adoc\n> index c6c34d1a1d..9a1cdf0dc0 100644\n> --- a/Documentation/RelNotes/2.50.0.adoc\n> +++ b/Documentation/RelNotes/2.50.0.adoc\n> @@ -100,7 +100,9 @@ Performance, Internal Implementation, Development Support etc.\n>   * \"git fsck\" becomes more careful when checking the refs.\n>\n>   * \"git fast-export | git fast-import\" learns to deal with commit and\n> -   tag objects with embedded signatures a bit better.\n> +   tag objects with embedded signatures a bit better.  This is highly\n> +   experimental and the format of the data stream may change in the\n> +   future without compatibility guarantees.\n>\n>   * The code paths to check whether a refname X is available (by seeing\n>     if another ref X/Y exists, etc.) have been optimized.\n> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\n> index 413a527496..43bbb4f63c 100644\n> --- a/Documentation/git-fast-export.adoc\n> +++ b/Documentation/git-fast-export.adoc\n> @@ -46,14 +46,12 @@ resulting tag will have an invalid signature.\n>\n>  --signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::\n>         Specify how to handle signed commits.  Behaves exactly as\n> -       '--signed-tags', but for commits.  Default is 'abort'.\n> +       '--signed-tags', but for commits.  Default is 'strip', which\n> +       is the same as how earlier versions of this command without\n> +       this option behaved.\n>  +\n> -Earlier versions this command that did not have '--signed-commits'\n> -behaved as if '--signed-commits=strip'.  As an escape hatch for users\n> -of tools that call 'git fast-export' but do not yet support\n> -'--signed-commits', you may set the environment variable\n> -'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default\n> -from 'abort' to 'warn-strip'.\n> +NOTE: This is highly experimental and the format of the data stream may\n> +change in the future without compatibility guarantees.\n\nI wonder if it should say that the default is likely to change too?\n\n>  --tag-of-filtered-object=(abort|drop|rewrite)::\n>         Specify how to handle tags whose tagged object is filtered out.\n> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> index 7b107f5e8e..250d866652 100644\n> --- a/Documentation/git-fast-import.adoc\n> +++ b/Documentation/git-fast-import.adoc\n> @@ -523,6 +523,9 @@ that signs the commit data.\n>  Here <alg> specifies which hashing algorithm is used for this\n>  signature, either `sha1` or `sha256`.\n>\n> +NOTE: This is highly experimental and the format of the data stream may\n> +change in the future without compatibility guarantees.\n> +\n>  `encoding`\n>  ^^^^^^^^^^\n>  The optional `encoding` command indicates the encoding of the commit\n> diff --git a/builtin/fast-export.c b/builtin/fast-export.c\n> index 37c01d6c6f..fcf6b00d5f 100644\n> --- a/builtin/fast-export.c\n> +++ b/builtin/fast-export.c\n> @@ -39,7 +39,7 @@ enum sign_mode { SIGN_ABORT, SIGN_VERBATIM, SIGN_STRIP, SIGN_WARN_VERBATIM, SIGN\n>\n>  static int progress;\n>  static enum sign_mode signed_tag_mode = SIGN_ABORT;\n> -static enum sign_mode signed_commit_mode = SIGN_ABORT;\n> +static enum sign_mode signed_commit_mode = SIGN_STRIP;\n>  static enum tag_of_filtered_mode { TAG_FILTERING_ABORT, DROP, REWRITE } tag_of_filtered_mode = TAG_FILTERING_ABORT;\n>  static enum reencode_mode { REENCODE_ABORT, REENCODE_YES, REENCODE_NO } reencode_mode = REENCODE_ABORT;\n>  static int fake_missing_tagger;\n> @@ -1269,7 +1269,6 @@ int cmd_fast_export(int argc,\n>                     const char *prefix,\n>                     struct repository *repo UNUSED)\n>  {\n> -       const char *env_signed_commits_noabort;\n>         struct rev_info revs;\n>         struct commit *commit;\n>         char *export_filename = NULL,\n> @@ -1327,10 +1326,6 @@ int cmd_fast_export(int argc,\n>         if (argc == 1)\n>                 usage_with_options (fast_export_usage, options);\n>\n> -       env_signed_commits_noabort = getenv(\"FAST_EXPORT_SIGNED_COMMITS_NOABORT\");\n> -       if (env_signed_commits_noabort && *env_signed_commits_noabort)\n> -               signed_commit_mode = SIGN_WARN_STRIP;\n> -\n>         /* we handle encodings */\n>         git_config(git_default_config, NULL);\n>\n> diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\n> index dda9e7c3e7..76619765fc 100755\n> --- a/t/t9350-fast-export.sh\n> +++ b/t/t9350-fast-export.sh\n> @@ -299,22 +299,10 @@ test_expect_success GPG 'set up signed commit' '\n>\n>  '\n>\n> -test_expect_success GPG 'signed-commits default' '\n> -\n> -       sane_unset FAST_EXPORT_SIGNED_COMMITS_NOABORT &&\n> -       test_must_fail git fast-export --reencode=no commit-signing &&\n> -\n> -       FAST_EXPORT_SIGNED_COMMITS_NOABORT=1 git fast-export --reencode=no commit-signing >output 2>err &&\n> -       ! grep ^gpgsig output &&\n> -       grep \"^encoding ISO-8859-1\" output &&\n> -       test -s err &&\n> -       sed \"s/commit-signing/commit-strip-signing/\" output | (\n> -               cd new &&\n> -               git fast-import &&\n> -               STRIPPED=$(git rev-parse --verify refs/heads/commit-strip-signing) &&\n> -               test $COMMIT_SIGNING != $STRIPPED\n> -       )\n> -\n> +test_expect_success GPG 'signed-commits default is same as strip' '\n\nHere also maybe we should say that the default could change in case\nadvanced users look at test cases to get hints at what is cast in\nstone?\n\n> +       git fast-export --reencode=no commit-signing >out1 2>err &&\n> +       git fast-export --reencode=no --signed-commits=strip commit-signing >out2 &&\n> +       test_cmp out1 out2\n>  '\n\nOtherwise the patch looks good to me.\n"},{"id":"519477","messageId":"CAP8UFD3c3QCqyt5OS_mXRgye47LircGUhyrM1gYR=bRbN64i8A@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqwma0nm6c.fsf@gitster.g","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-02T15:56:13Z","receivedAt":"2025-06-02T15:56:28Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, May 29, 2025 at 5:14 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> > Personally, I kind of think abort makes more sense as the default --\n> > at some point.\n>\n> Oh, of course.\n>\n> > So I'm curious if you've just changed your mind\n> > completely from before and are against changing the default at all,\n>\n> Yes, after seeing that the representation of the signature algorithm\n> and encapsulation format was not as well thought out as I thought it\n> would already be and its design still being discussed, I realized\n> that the new feature was way premature to have in the release.  At\n> some point, when things mature and we are reasonably sure we will\n> not have to make incompatible changes in the data stream, we might\n> need to switch, and the best default might turn out to be to refuse\n> to work unless the end-user makes an explicit choice, but as the\n> design of the feature stands now, I have a feeling that it is a bit\n> premature.  Certainly not ready for general consumption.\n\nDiscouraging the use of the feature and saying it's highly\nexperimental should hint that the default might change in the future,\nbut maybe we should explicitly say so?\n\n> Of course, I could have just reverted the merge of the original\n> topic and give it a chance for a fresh restart the next cycle, but a\n> new feature clearly marked \"highly experimental\" would hopefully set\n> the end-user expectation straight, as long as the default is \"do not\n> do anything different from before\", which is the safest choice for a\n> feature whose design is still wobbly.\n\nYeah, I agree with this approach in general.\n"},{"id":"519482","messageId":"xmqqtt4yt8rz.fsf@gitster.g","threadId":"63342","inReplyTo":"CAP8UFD2i-ayL7vs=Y46DpYkv64ROBj0YHNHEbbaG76mLTncyEQ@mail.gmail.com","subject":"Re: [PATCH] fast-(import|export): improve on the signature algorithm name","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-06-02T16:20:32Z","receivedAt":"2025-06-02T16:20:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n>> -Earlier versions this command that did not have '--signed-commits'\n>> -behaved as if '--signed-commits=strip'.  As an escape hatch for users\n>> -of tools that call 'git fast-export' but do not yet support\n>> -'--signed-commits', you may set the environment variable\n>> -'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default\n>> -from 'abort' to 'warn-strip'.\n>> +NOTE: This is highly experimental and the format of the data stream may\n>> +change in the future without compatibility guarantees.\n>\n> I wonder if it should say that the default is likely to change too?\n\nIt is to be decided in the future and we have no plan to do so\nbefore the feature loses \"experimental\" label, no?  Those who are\nopting into a highly experimental feature would know that already.\n\n>> +test_expect_success GPG 'signed-commits default is same as strip' '\n>\n> Here also maybe we should say that the default could change in case\n> advanced users look at test cases to get hints at what is cast in\n> stone?\n\nThe same answer applies.\n"},{"id":"519510","messageId":"aD4i7YhUnT5Kgew-@tapette.crustytoothpaste.net","threadId":"63342","inReplyTo":"20250526103314.1542316-1-christian.couder@gmail.com","subject":"Re: [PATCH v2 0/6] extract algo information from signatures","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-06-02T22:17:17Z","receivedAt":"2025-06-02T22:17:20Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-05-26 at 10:33:08, Christian Couder wrote:\n> Around one month ago, I sent a patch that tried to improve on how `git\n> fast-export` handled SSH and X.509 commit signatures:\n> \n> https://lore.kernel.org/git/20250424203904.909777-1-christian.couder@gmail.com/\n> \n> This patch was showing a single string for the hash algorithm with the\n> following possible values:\n> \n> * \"openpgp\" for SHA-1 OpenPGP signatures,\n> \n> * \"sha256\" for SHA-256 OpenPGP signatures,\n> \n> * \"x509\" for X.509 (GPGSM) signatures,\n> \n> * \"ssh\", for SSH signatures,\n> \n> * \"unknown\" for signatures that can't be identified (a warning is\n>   emitted).\n> \n> brian m. carlson however replied that it would be better to show two\n> pieces of information instead of one: one for the hash algorithm and\n> one for the protocol.\n\nActually, what I was saying is that we should have one for the hash\nalgorithm that is used in the Git object.  I don't care about the hash\nalgorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's\nsigned with SHA-512 or SHA-256), but we can have multiple signatures in\na single commit such that there's both a SHA-1 signature and a SHA-256\nsignature.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"520354","messageId":"20250618151821.528627-1-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250526103314.1542316-1-christian.couder@gmail.com","subject":"[PATCH v3] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-18T15:18:21Z","receivedAt":"2025-06-18T15:18:43Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\nsigned-commits, 2025-03-10), added support for signed commits to\nfast-export and fast-import.\n\nWhen a signed commit is processed, fast-export can output either\n\"gpgsig sha1\" or \"gpgsig sha256\" depending on whether the signed\ncommit uses the SHA-1 or SHA-256 Git object format.\n\nHowever, this implementation has a number of limitations:\n\n  - the output format was not properly described in the documentation,\n  - the output format is not very informative as it doesn't even say\n    if the signature is an OpenPGP, an SSH, or an X509 signature,\n  - the implementation doesn't support having both one signature on\n    the SHA-1 object and one on the SHA-256 object.\n\nLet's improve on these limitations by improving fast-export and\nfast-import so that:\n\n  - both one signature on the SHA-1 object and one on the SHA-256\n    object can be exported and imported,\n  - if there is more than one signature on the SHA-1 object or on\n    the SHA-256 object, a warning is emitted,\n  - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n    where <git-hash-algo> is the Git object format as before, and\n    <signature-format> is the signature type (\"openpgp\", \"x509\",\n    \"ssh\" or \"unknown\",\n  - the output is properly documented.\n\nNote that it could be even better to be able to export and import\nmore than one signature on the SHA-1 object and on the SHA-256\nobject, but other parts of Git don't handle that well for now, so\nthis is left for future improvements.\n\nHelped-by: brian m. carlson <sandals@crustytoothpaste.net>\nHelped-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n\nThe v1 of this patch series simply replaced the generic \"sha1\" or\n\"sha256\" in the 'gpgsig' command that `git fast-export` produces and\n`git fast-import` consumes with the signature format (e.g., openpgp,\nssh).\n\nIn the v2, a different approach was tried in the direction of possibly\nmaking `git fast-export` check signatures to be able to get more\ninformation from them.\n\nIn this v3, the approach is more similar to v1, but takes into account\nwhat was learned from the previous approaches. The signature format\n(e.g., openpgp, ssh) is added to the 'gpgsig' command, while both a\nSHA-1 and a SHA-256 signature from the same commit can be processed\nand a warning are emitted for each additional signature with the same\nhash type.\n\nThere are no tests in this v3 with both a SHA-1 and a SHA-256\nsignature on the same commit though, as I am not sure yet how to best\ngenerate a commit with such signatures. Suggestions welcome!\n\nAs the patch is very different from both v1 and v2 no range-diff is\nprovided.\n\nI have tried to launch CI tests but they seem blocked for 24 minutes at\nthe \"config\" stage.\n\nThanks to brian, Elijah and Junio who commented on the v1 and v2.\n\n Documentation/git-fast-export.adoc |  17 +++++\n Documentation/git-fast-import.adoc |  31 ++++++--\n builtin/fast-export.c              |  67 ++++++++++++++---\n builtin/fast-import.c              | 117 +++++++++++++++++++++++------\n gpg-interface.c                    |  12 +++\n gpg-interface.h                    |  12 +++\n t/t9350-fast-export.sh             |  60 ++++++++++++++-\n 7 files changed, 274 insertions(+), 42 deletions(-)\n\ndiff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\nindex 43bbb4f63c..64198f2186 100644\n--- a/Documentation/git-fast-export.adoc\n+++ b/Documentation/git-fast-export.adoc\n@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.\n \tis the same as how earlier versions of this command without\n \tthis option behaved.\n +\n+When exported, a signature starts with:\n++\n+gpgsig <git-hash-algo> <signature-format>\n++\n+where <git-hash-algo> is the Git object hash so either \"sha1\" or\n+\"sha256\", and <signature-format> is the signature type, so \"openpgp\",\n+\"x509\", \"ssh\" or \"unknown\".\n++\n+For example, an OpenPGP signature on a SHA-1 commit starts with\n+`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit\n+starts with `gpgsig sha256 ssh`.\n++\n+Currently for a given commit, at most one signature for the SHA-1\n+object and one signature for the SHA-256 object are exported, each\n+with their respective <git-hash-algo> identifier. A warning is\n+emitted for each additional signature found.\n++\n NOTE: This is highly experimental and the format of the data stream may\n change in the future without compatibility guarantees.\n \ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 250d866652..db5e5c8da5 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -445,7 +445,7 @@ one).\n \toriginal-oid?\n \t('author' (SP <name>)? SP LT <email> GT SP <when> LF)?\n \t'committer' (SP <name>)? SP LT <email> GT SP <when> LF\n-\t('gpgsig' SP <alg> LF data)?\n+\t('gpgsig' SP <algo> SP <format> LF data)?\n \t('encoding' SP <encoding> LF)?\n \tdata\n \t('from' SP <commit-ish> LF)?\n@@ -518,13 +518,32 @@ their syntax.\n ^^^^^^^^\n \n The optional `gpgsig` command is used to include a PGP/GPG signature\n-that signs the commit data.\n+or other cryptographic signature that signs the commit data.\n \n-Here <alg> specifies which hashing algorithm is used for this\n-signature, either `sha1` or `sha256`.\n+....\n+\t'gpgsig' SP <git-hash-algo> SP <signature-format> LF\n+\tdata\n+....\n+\n+The `gpgsig` command takes two arguments:\n+\n+* `<git-hash-algo>` specifies which Git object format this signature\n+  applies to, either `sha1` or `sha256`.\n+\n+* `<signature-format>` specifies the type of signature, such as\n+  `openpgp`, `x509`, `ssh`, or `unknown`.\n+\n+A commit may have at most one signature for the SHA-1 object format\n+(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n+(stored in the \"gpgsig-sha256\" header).\n+\n+See below for a detailed description of the `data` command which\n+contains the raw signature data.\n+\n+Signatures are not yet checked in the current implementation though.\n \n-NOTE: This is highly experimental and the format of the data stream may\n-change in the future without compatibility guarantees.\n+NOTE: This is highly experimental and the format of the `gpgsig`\n+command may change in the future without compatibility guarantees.\n \n `encoding`\n ^^^^^^^^^^\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex fcf6b00d5f..332c036ee4 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -29,6 +29,7 @@\n #include \"quote.h\"\n #include \"remote.h\"\n #include \"blob.h\"\n+#include \"gpg-interface.h\"\n \n static const char *const fast_export_usage[] = {\n \tN_(\"git fast-export [<rev-list-opts>]\"),\n@@ -652,6 +653,30 @@ static const char *find_commit_multiline_header(const char *msg,\n \treturn strbuf_detach(&val, NULL);\n }\n \n+static void print_signature(const char *signature, const char *object_hash)\n+{\n+\tif (!signature)\n+\t\treturn;\n+\n+\tprintf(\"gpgsig %s %s\\ndata %u\\n%s\",\n+\t       object_hash,\n+\t       get_signature_format(signature),\n+\t       (unsigned)strlen(signature),\n+\t       signature);\n+}\n+\n+static void warn_on_extra_sig(const char **pos, struct commit *commit, int is_sha1)\n+{\n+\tconst char *header = is_sha1 ? \"gpgsig\" : \"gpgsig-sha256\";\n+\tconst char *extra_sig = find_commit_multiline_header(*pos + 1, header, pos);\n+\tif (extra_sig) {\n+\t\tconst char *hash = is_sha1 ? \"SHA-1\" : \"SHA-256\";\n+\t\twarning(\"more than one %s signature found on commit %s, using only the first one\",\n+\t\t\thash, oid_to_hex(&commit->object.oid));\n+\t\tfree((char *)extra_sig);\n+\t}\n+}\n+\n static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t\t\t  struct string_list *paths_of_changed_objects)\n {\n@@ -660,7 +685,8 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \tconst char *author, *author_end, *committer, *committer_end;\n \tconst char *encoding = NULL;\n \tsize_t encoding_len;\n-\tconst char *signature_alg = NULL, *signature = NULL;\n+\tconst char *sig_sha1 = NULL;\n+\tconst char *sig_sha256 = NULL;\n \tconst char *message;\n \tchar *reencoded = NULL;\n \tstruct commit_list *p;\n@@ -700,10 +726,28 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t}\n \n \tif (*commit_buffer_cursor == '\\n') {\n-\t\tif ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor)))\n-\t\t\tsignature_alg = \"sha1\";\n-\t\telse if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n-\t\t\tsignature_alg = \"sha256\";\n+\t\tconst char *sig_cursor = commit_buffer_cursor;\n+\t\tconst char *after_sha1 = commit_buffer_cursor;\n+\t\tconst char *after_sha256 = commit_buffer_cursor;\n+\n+\t\t/*\n+\t\t * Find the first signature for each hash algorithm.\n+\t\t * The searches must start from the same position.\n+\t\t */\n+\t\tsig_sha1 = find_commit_multiline_header(sig_cursor + 1,\n+\t\t\t\t\t\t\t\"gpgsig\",\n+\t\t\t\t\t\t\t&after_sha1);\n+\t\tsig_sha256 = find_commit_multiline_header(sig_cursor + 1,\n+\t\t\t\t\t\t\t  \"gpgsig-sha256\",\n+\t\t\t\t\t\t\t  &after_sha256);\n+\n+\t\t/* Warn on any additional signatures, as they will be ignored. */\n+\t\tif (sig_sha1)\n+\t\t\twarn_on_extra_sig(&after_sha1, commit, 1);\n+\t\tif (sig_sha256)\n+\t\t\twarn_on_extra_sig(&after_sha256, commit, 0);\n+\n+\t\tcommit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;\n \t}\n \n \tmessage = strstr(commit_buffer_cursor, \"\\n\\n\");\n@@ -769,7 +813,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \tprintf(\"%.*s\\n%.*s\\n\",\n \t       (int)(author_end - author), author,\n \t       (int)(committer_end - committer), committer);\n-\tif (signature) {\n+\tif (sig_sha1 || sig_sha256) {\n \t\tswitch (signed_commit_mode) {\n \t\tcase SIGN_ABORT:\n \t\t\tdie(\"encountered signed commit %s; use \"\n@@ -780,19 +824,18 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t\t\t\toid_to_hex(&commit->object.oid));\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n-\t\t\tprintf(\"gpgsig %s\\ndata %u\\n%s\",\n-\t\t\t       signature_alg,\n-\t\t\t       (unsigned)strlen(signature),\n-\t\t\t       signature);\n+\t\t\tprint_signature(sig_sha1, \"sha1\");\n+\t\t\tprint_signature(sig_sha256, \"sha256\");\n \t\t\tbreak;\n \t\tcase SIGN_WARN_STRIP:\n-\t\t\twarning(\"stripping signature from commit %s\",\n+\t\t\twarning(\"stripping signature(s) from commit %s\",\n \t\t\t\toid_to_hex(&commit->object.oid));\n \t\t\t/* fallthru */\n \t\tcase SIGN_STRIP:\n \t\t\tbreak;\n \t\t}\n-\t\tfree((char *)signature);\n+\t\tfree((char *)sig_sha1);\n+\t\tfree((char *)sig_sha256);\n \t}\n \tif (!reencoded && encoding)\n \t\tprintf(\"encoding %.*s\\n\", (int)encoding_len, encoding);\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b2839c5f43..48ce8ebb77 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -29,6 +29,7 @@\n #include \"commit-reach.h\"\n #include \"khash.h\"\n #include \"date.h\"\n+#include \"gpg-interface.h\"\n \n #define PACK_ID_BITS 16\n #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n@@ -2718,15 +2719,86 @@ static struct hash_list *parse_merge(unsigned int *count)\n \treturn list;\n }\n \n+struct signature_data {\n+\tchar *hash_algo;      /* \"sha1\" or \"sha256\" */\n+\tchar *sig_format;     /* \"openpgp\", \"x509\", \"ssh\", \"unknown\" */\n+\tstruct strbuf data;   /* The actual signature data */\n+};\n+\n+static void parse_one_signature(struct signature_data *sig, const char *v)\n+{\n+\tchar *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n+\tchar *space = strchr(args, ' ');\n+\n+\tif (!space)\n+\t\tdie(\"Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', \"\n+\t\t    \"got 'gpgsig %s'\", args);\n+\t*space++ = '\\0';\n+\n+\tsig->hash_algo = args;\n+\tsig->sig_format = space;\n+\n+\t/* Remove any trailing newline from format */\n+\tspace = strchr(sig->sig_format, '\\n');\n+\tif (space)\n+\t\t*space = '\\0';\n+\n+\t/* Validate hash algorithm */\n+\tif (strcmp(sig->hash_algo, \"sha1\") &&\n+\t    strcmp(sig->hash_algo, \"sha256\"))\n+\t\tdie(\"Unknown git hash algorithm in gpgsig: '%s'\", sig->hash_algo);\n+\n+\t/* Validate signature format */\n+\tif (!valid_signature_format(sig->sig_format))\n+\t\tdie(\"Invalid signature format in gpgsig: '%s'\", sig->sig_format);\n+\tif (!strcmp(sig->sig_format, \"unknown\"))\n+\t\twarning(\"'unknown' signature format in gpgsig\");\n+\n+\t/* Read signature data */\n+\tread_next_command();\n+\tparse_data(&sig->data, 0, NULL);\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+{\n+\tstruct string_list siglines = STRING_LIST_INIT_NODUP;\n+\n+\tif (!sig->hash_algo)\n+\t\treturn;\n+\n+\tstrbuf_addstr(commit_data, header);\n+\tstring_list_split_in_place(&siglines, sig->data.buf, \"\\n\", -1);\n+\tstrbuf_add_separated_string_list(commit_data, \"\\n \", &siglines);\n+\tstrbuf_addch(commit_data, '\\n');\n+\tstring_list_clear(&siglines, 1);\n+\tstrbuf_release(&sig->data);\n+\tfree(sig->hash_algo);\n+}\n+\n+static void store_signature(struct signature_data *stored_sig,\n+\t\t\t    struct signature_data *new_sig,\n+\t\t\t    const char *hash_type)\n+{\n+\tif (stored_sig->hash_algo) {\n+\t\twarning(\"Multiple %s signatures found, ignoring additional signature\",\n+\t\t\thash_type);\n+\t\tstrbuf_release(&new_sig->data);\n+\t\tfree(new_sig->hash_algo);\n+\t} else {\n+\t\t*stored_sig = *new_sig;\n+\t}\n+}\n+\n static void parse_new_commit(const char *arg)\n {\n-\tstatic struct strbuf sig = STRBUF_INIT;\n \tstatic struct strbuf msg = STRBUF_INIT;\n-\tstruct string_list siglines = STRING_LIST_INIT_NODUP;\n+\tstruct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };\n+\tstruct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };\n \tstruct branch *b;\n \tchar *author = NULL;\n \tchar *committer = NULL;\n-\tchar *sig_alg = NULL;\n \tchar *encoding = NULL;\n \tstruct hash_list *merge_list = NULL;\n \tunsigned int merge_count;\n@@ -2750,13 +2822,23 @@ static void parse_new_commit(const char *arg)\n \t}\n \tif (!committer)\n \t\tdie(\"Expected committer but didn't get one\");\n-\tif (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n-\t\tsig_alg = xstrdup(v);\n-\t\tread_next_command();\n-\t\tparse_data(&sig, 0, NULL);\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+\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\tread_next_command();\n-\t} else\n-\t\tstrbuf_setlen(&sig, 0);\n+\t}\n+\n \tif (skip_prefix(command_buf.buf, \"encoding \", &v)) {\n \t\tencoding = xstrdup(v);\n \t\tread_next_command();\n@@ -2830,23 +2912,14 @@ static void parse_new_commit(const char *arg)\n \t\tstrbuf_addf(&new_data,\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n-\tif (sig_alg) {\n-\t\tif (!strcmp(sig_alg, \"sha1\"))\n-\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n-\t\telse if (!strcmp(sig_alg, \"sha256\"))\n-\t\t\tstrbuf_addstr(&new_data, \"gpgsig-sha256 \");\n-\t\telse\n-\t\t\tdie(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n-\t\tstring_list_split_in_place(&siglines, sig.buf, \"\\n\", -1);\n-\t\tstrbuf_add_separated_string_list(&new_data, \"\\n \", &siglines);\n-\t\tstrbuf_addch(&new_data, '\\n');\n-\t}\n+\n+\tadd_gpgsig_to_commit(&new_data, \"gpgsig \", &sig_sha1);\n+\tadd_gpgsig_to_commit(&new_data, \"gpgsig-sha256 \", &sig_sha256);\n+\n \tstrbuf_addch(&new_data, '\\n');\n \tstrbuf_addbuf(&new_data, &msg);\n-\tstring_list_clear(&siglines, 1);\n \tfree(author);\n \tfree(committer);\n-\tfree(sig_alg);\n \tfree(encoding);\n \n \tif (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 0896458de5..6f2d87475f 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n \treturn NULL;\n }\n \n+const char *get_signature_format(const char *buf)\n+{\n+\tstruct gpg_format *format = get_format_by_sig(buf);\n+\treturn format ? format->name : \"unknown\";\n+}\n+\n+int valid_signature_format(const char *format)\n+{\n+       return (!!get_format_by_name(format) ||\n+\t       !strcmp(format, \"unknown\"));\n+}\n+\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex e09f12e8d0..60ddf8bbfa 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -47,6 +47,18 @@ struct signature_check {\n \n void signature_check_clear(struct signature_check *sigc);\n \n+/*\n+ * Return the format of the signature (like \"openpgp\", \"x509\", \"ssh\"\n+ * or \"unknown\").\n+ */\n+const char *get_signature_format(const char *buf);\n+\n+/*\n+ * Is the signature format valid (like \"openpgp\", \"x509\", \"ssh\" or\n+ * \"unknown\")\n+ */\n+int valid_signature_format(const char *format);\n+\n /*\n  * Look at a GPG signed tag object.  If such a signature exists, store it in\n  * signature and the signed content in payload.  Return 1 if a signature was\ndiff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\nindex 76619765fc..74dd23cb77 100755\n--- a/t/t9350-fast-export.sh\n+++ b/t/t9350-fast-export.sh\n@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '\n test_expect_success GPG 'signed-commits=verbatim' '\n \n \tgit fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&\n-\tgrep \"^gpgsig sha\" output &&\n+\ttest_grep -E \"^gpgsig sha(1|256) openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \t(\n \t\tcd new &&\n@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '\n test_expect_success GPG 'signed-commits=warn-verbatim' '\n \n \tgit fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&\n-\tgrep \"^gpgsig sha\" output &&\n+\ttest_grep -E \"^gpgsig sha(1|256) openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \ttest -s err &&\n \t(\n@@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '\n \n '\n \n+test_expect_success GPGSM 'setup X.509 signed commit' '\n+\n+\tgit checkout -b x509-signing main &&\n+\ttest_config gpg.format x509 &&\n+\ttest_config user.signingkey $GIT_COMMITTER_EMAIL &&\n+\techo \"X.509 content\" >file &&\n+\tgit add file &&\n+\tgit commit -S -m \"X.509 signed commit\" &&\n+\tX509_COMMIT=$(git rev-parse HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSM 'round-trip X.509 signed commit' '\n+\n+\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\tgrep \"^gpgsig sha1 x509\" output &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/x509-signing >actual &&\n+\t\tgrep \"^gpgsig \" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/x509-signing) &&\n+\t\ttest $X509_COMMIT = $IMPORTED\n+\t) <output\n+\n+'\n+\n+test_expect_success GPGSSH 'setup SSH signed commit' '\n+\n+\tgit checkout -b ssh-signing main &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\techo \"SSH content\" >file &&\n+\tgit add file &&\n+\tgit commit -S -m \"SSH signed commit\" &&\n+\tSSH_COMMIT=$(git rev-parse HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSSH 'round-trip SSH signed commit' '\n+\n+\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\tgrep \"^gpgsig sha1 ssh\" output &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/ssh-signing >actual &&\n+\t\tgrep \"^gpgsig \" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/ssh-signing) &&\n+\t\ttest $SSH_COMMIT = $IMPORTED\n+\t) <output\n+\n+'\n+\n test_expect_success 'setup submodule' '\n \n \ttest_config_global protocol.file.allow always &&\n-- \n2.50.0.2.g875523421d\n\n"},{"id":"520406","messageId":"20250619133630.727274-1-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250618151821.528627-1-christian.couder@gmail.com","subject":"[PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-19T13:36:30Z","receivedAt":"2025-06-19T13:36:52Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\nsigned-commits, 2025-03-10), added support for signed commits to\nfast-export and fast-import.\n\nWhen a signed commit is processed, fast-export can output either\n\"gpgsig sha1\" or \"gpgsig sha256\" depending on whether the signed\ncommit uses the SHA-1 or SHA-256 Git object format.\n\nHowever, this implementation has a number of limitations:\n\n  - the output format was not properly described in the documentation,\n  - the output format is not very informative as it doesn't even say\n    if the signature is an OpenPGP, an SSH, or an X509 signature,\n  - the implementation doesn't support having both one signature on\n    the SHA-1 object and one on the SHA-256 object.\n\nLet's improve on these limitations by improving fast-export and\nfast-import so that:\n\n  - both one signature on the SHA-1 object and one on the SHA-256\n    object can be exported and imported,\n  - if there is more than one signature on the SHA-1 object or on\n    the SHA-256 object, a warning is emitted,\n  - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n    where <git-hash-algo> is the Git object format as before, and\n    <signature-format> is the signature type (\"openpgp\", \"x509\",\n    \"ssh\" or \"unknown\",\n  - the output is properly documented.\n\nNote that it could be even better to be able to export and import\nmore than one signature on the SHA-1 object and on the SHA-256\nobject, but other parts of Git don't handle that well for now, so\nthis is left for future improvements.\n\nHelped-by: brian m. carlson <sandals@crustytoothpaste.net>\nHelped-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n\nThis v4 is just about fixing a few bugs in the tests using the SHA-256\nobject format compared to the v3. (I had issues with CI tests on v3,\nso I sent it without waiting for the results.)\n\nThe v1 of this patch series simply replaced the generic \"sha1\" or\n\"sha256\" in the 'gpgsig' command that `git fast-export` produces and\n`git fast-import` consumes with the signature format (e.g., openpgp,\nssh).\n\nIn the v2, a different approach was tried in the direction of possibly\nmaking `git fast-export` check signatures to be able to get more\ninformation from them.\n\nIn this v4 and in v3, the approach is more similar to v1, but takes\ninto account what was learned from the previous approaches. The\nsignature format (e.g., openpgp, ssh) is added to the 'gpgsig'\ncommand, while both a SHA-1 and a SHA-256 signature from the same\ncommit can be processed and a warning are emitted for each additional\nsignature with the same hash type.\n\nThere are no tests in this v4 and in v3 with both a SHA-1 and a\nSHA-256 signature on the same commit though, as I am not sure yet how\nto best generate a commit with such signatures. Suggestions welcome!\n\nThanks to brian, Elijah and Junio who commented on the v1 and v2.\n\nRange-diff with v3:\n\n1:  d28cf238fb ! 1:  b3d8b13c0e fast-(import|export): improve on commit signature output format\n    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '\n     +test_expect_success GPGSM 'round-trip X.509 signed commit' '\n     +\n     +  git fast-export --signed-commits=verbatim x509-signing >output &&\n    -+  grep \"^gpgsig sha1 x509\" output &&\n    ++  test_grep -E \"^gpgsig sha(1|256) x509\" output &&\n     +  (\n     +          cd new &&\n     +          git fast-import &&\n     +          git cat-file commit refs/heads/x509-signing >actual &&\n    -+          grep \"^gpgsig \" actual &&\n    ++          grep \"^gpgsig\" actual &&\n     +          IMPORTED=$(git rev-parse refs/heads/x509-signing) &&\n     +          test $X509_COMMIT = $IMPORTED\n     +  ) <output\n    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '\n     +test_expect_success GPGSSH 'round-trip SSH signed commit' '\n     +\n     +  git fast-export --signed-commits=verbatim ssh-signing >output &&\n    -+  grep \"^gpgsig sha1 ssh\" output &&\n    ++  test_grep -E \"^gpgsig sha(1|256) ssh\" output &&\n     +  (\n     +          cd new &&\n     +          git fast-import &&\n     +          git cat-file commit refs/heads/ssh-signing >actual &&\n    -+          grep \"^gpgsig \" actual &&\n    ++          grep \"^gpgsig\" actual &&\n     +          IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&\n     +          test $SSH_COMMIT = $IMPORTED\n     +  ) <output\n\n\nCI tests:\n\nThey have all passed:\n\nhttps://github.com/chriscool/git/actions/runs/15758360209\n\n\n Documentation/git-fast-export.adoc |  17 +++++\n Documentation/git-fast-import.adoc |  31 ++++++--\n builtin/fast-export.c              |  67 ++++++++++++++---\n builtin/fast-import.c              | 117 +++++++++++++++++++++++------\n gpg-interface.c                    |  12 +++\n gpg-interface.h                    |  12 +++\n t/t9350-fast-export.sh             |  60 ++++++++++++++-\n 7 files changed, 274 insertions(+), 42 deletions(-)\n\ndiff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\nindex 43bbb4f63c..64198f2186 100644\n--- a/Documentation/git-fast-export.adoc\n+++ b/Documentation/git-fast-export.adoc\n@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.\n \tis the same as how earlier versions of this command without\n \tthis option behaved.\n +\n+When exported, a signature starts with:\n++\n+gpgsig <git-hash-algo> <signature-format>\n++\n+where <git-hash-algo> is the Git object hash so either \"sha1\" or\n+\"sha256\", and <signature-format> is the signature type, so \"openpgp\",\n+\"x509\", \"ssh\" or \"unknown\".\n++\n+For example, an OpenPGP signature on a SHA-1 commit starts with\n+`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit\n+starts with `gpgsig sha256 ssh`.\n++\n+Currently for a given commit, at most one signature for the SHA-1\n+object and one signature for the SHA-256 object are exported, each\n+with their respective <git-hash-algo> identifier. A warning is\n+emitted for each additional signature found.\n++\n NOTE: This is highly experimental and the format of the data stream may\n change in the future without compatibility guarantees.\n \ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 250d866652..db5e5c8da5 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -445,7 +445,7 @@ one).\n \toriginal-oid?\n \t('author' (SP <name>)? SP LT <email> GT SP <when> LF)?\n \t'committer' (SP <name>)? SP LT <email> GT SP <when> LF\n-\t('gpgsig' SP <alg> LF data)?\n+\t('gpgsig' SP <algo> SP <format> LF data)?\n \t('encoding' SP <encoding> LF)?\n \tdata\n \t('from' SP <commit-ish> LF)?\n@@ -518,13 +518,32 @@ their syntax.\n ^^^^^^^^\n \n The optional `gpgsig` command is used to include a PGP/GPG signature\n-that signs the commit data.\n+or other cryptographic signature that signs the commit data.\n \n-Here <alg> specifies which hashing algorithm is used for this\n-signature, either `sha1` or `sha256`.\n+....\n+\t'gpgsig' SP <git-hash-algo> SP <signature-format> LF\n+\tdata\n+....\n+\n+The `gpgsig` command takes two arguments:\n+\n+* `<git-hash-algo>` specifies which Git object format this signature\n+  applies to, either `sha1` or `sha256`.\n+\n+* `<signature-format>` specifies the type of signature, such as\n+  `openpgp`, `x509`, `ssh`, or `unknown`.\n+\n+A commit may have at most one signature for the SHA-1 object format\n+(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n+(stored in the \"gpgsig-sha256\" header).\n+\n+See below for a detailed description of the `data` command which\n+contains the raw signature data.\n+\n+Signatures are not yet checked in the current implementation though.\n \n-NOTE: This is highly experimental and the format of the data stream may\n-change in the future without compatibility guarantees.\n+NOTE: This is highly experimental and the format of the `gpgsig`\n+command may change in the future without compatibility guarantees.\n \n `encoding`\n ^^^^^^^^^^\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex fcf6b00d5f..332c036ee4 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -29,6 +29,7 @@\n #include \"quote.h\"\n #include \"remote.h\"\n #include \"blob.h\"\n+#include \"gpg-interface.h\"\n \n static const char *const fast_export_usage[] = {\n \tN_(\"git fast-export [<rev-list-opts>]\"),\n@@ -652,6 +653,30 @@ static const char *find_commit_multiline_header(const char *msg,\n \treturn strbuf_detach(&val, NULL);\n }\n \n+static void print_signature(const char *signature, const char *object_hash)\n+{\n+\tif (!signature)\n+\t\treturn;\n+\n+\tprintf(\"gpgsig %s %s\\ndata %u\\n%s\",\n+\t       object_hash,\n+\t       get_signature_format(signature),\n+\t       (unsigned)strlen(signature),\n+\t       signature);\n+}\n+\n+static void warn_on_extra_sig(const char **pos, struct commit *commit, int is_sha1)\n+{\n+\tconst char *header = is_sha1 ? \"gpgsig\" : \"gpgsig-sha256\";\n+\tconst char *extra_sig = find_commit_multiline_header(*pos + 1, header, pos);\n+\tif (extra_sig) {\n+\t\tconst char *hash = is_sha1 ? \"SHA-1\" : \"SHA-256\";\n+\t\twarning(\"more than one %s signature found on commit %s, using only the first one\",\n+\t\t\thash, oid_to_hex(&commit->object.oid));\n+\t\tfree((char *)extra_sig);\n+\t}\n+}\n+\n static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t\t\t  struct string_list *paths_of_changed_objects)\n {\n@@ -660,7 +685,8 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \tconst char *author, *author_end, *committer, *committer_end;\n \tconst char *encoding = NULL;\n \tsize_t encoding_len;\n-\tconst char *signature_alg = NULL, *signature = NULL;\n+\tconst char *sig_sha1 = NULL;\n+\tconst char *sig_sha256 = NULL;\n \tconst char *message;\n \tchar *reencoded = NULL;\n \tstruct commit_list *p;\n@@ -700,10 +726,28 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t}\n \n \tif (*commit_buffer_cursor == '\\n') {\n-\t\tif ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor)))\n-\t\t\tsignature_alg = \"sha1\";\n-\t\telse if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n-\t\t\tsignature_alg = \"sha256\";\n+\t\tconst char *sig_cursor = commit_buffer_cursor;\n+\t\tconst char *after_sha1 = commit_buffer_cursor;\n+\t\tconst char *after_sha256 = commit_buffer_cursor;\n+\n+\t\t/*\n+\t\t * Find the first signature for each hash algorithm.\n+\t\t * The searches must start from the same position.\n+\t\t */\n+\t\tsig_sha1 = find_commit_multiline_header(sig_cursor + 1,\n+\t\t\t\t\t\t\t\"gpgsig\",\n+\t\t\t\t\t\t\t&after_sha1);\n+\t\tsig_sha256 = find_commit_multiline_header(sig_cursor + 1,\n+\t\t\t\t\t\t\t  \"gpgsig-sha256\",\n+\t\t\t\t\t\t\t  &after_sha256);\n+\n+\t\t/* Warn on any additional signatures, as they will be ignored. */\n+\t\tif (sig_sha1)\n+\t\t\twarn_on_extra_sig(&after_sha1, commit, 1);\n+\t\tif (sig_sha256)\n+\t\t\twarn_on_extra_sig(&after_sha256, commit, 0);\n+\n+\t\tcommit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;\n \t}\n \n \tmessage = strstr(commit_buffer_cursor, \"\\n\\n\");\n@@ -769,7 +813,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \tprintf(\"%.*s\\n%.*s\\n\",\n \t       (int)(author_end - author), author,\n \t       (int)(committer_end - committer), committer);\n-\tif (signature) {\n+\tif (sig_sha1 || sig_sha256) {\n \t\tswitch (signed_commit_mode) {\n \t\tcase SIGN_ABORT:\n \t\t\tdie(\"encountered signed commit %s; use \"\n@@ -780,19 +824,18 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t\t\t\toid_to_hex(&commit->object.oid));\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n-\t\t\tprintf(\"gpgsig %s\\ndata %u\\n%s\",\n-\t\t\t       signature_alg,\n-\t\t\t       (unsigned)strlen(signature),\n-\t\t\t       signature);\n+\t\t\tprint_signature(sig_sha1, \"sha1\");\n+\t\t\tprint_signature(sig_sha256, \"sha256\");\n \t\t\tbreak;\n \t\tcase SIGN_WARN_STRIP:\n-\t\t\twarning(\"stripping signature from commit %s\",\n+\t\t\twarning(\"stripping signature(s) from commit %s\",\n \t\t\t\toid_to_hex(&commit->object.oid));\n \t\t\t/* fallthru */\n \t\tcase SIGN_STRIP:\n \t\t\tbreak;\n \t\t}\n-\t\tfree((char *)signature);\n+\t\tfree((char *)sig_sha1);\n+\t\tfree((char *)sig_sha256);\n \t}\n \tif (!reencoded && encoding)\n \t\tprintf(\"encoding %.*s\\n\", (int)encoding_len, encoding);\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b2839c5f43..48ce8ebb77 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -29,6 +29,7 @@\n #include \"commit-reach.h\"\n #include \"khash.h\"\n #include \"date.h\"\n+#include \"gpg-interface.h\"\n \n #define PACK_ID_BITS 16\n #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n@@ -2718,15 +2719,86 @@ static struct hash_list *parse_merge(unsigned int *count)\n \treturn list;\n }\n \n+struct signature_data {\n+\tchar *hash_algo;      /* \"sha1\" or \"sha256\" */\n+\tchar *sig_format;     /* \"openpgp\", \"x509\", \"ssh\", \"unknown\" */\n+\tstruct strbuf data;   /* The actual signature data */\n+};\n+\n+static void parse_one_signature(struct signature_data *sig, const char *v)\n+{\n+\tchar *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n+\tchar *space = strchr(args, ' ');\n+\n+\tif (!space)\n+\t\tdie(\"Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', \"\n+\t\t    \"got 'gpgsig %s'\", args);\n+\t*space++ = '\\0';\n+\n+\tsig->hash_algo = args;\n+\tsig->sig_format = space;\n+\n+\t/* Remove any trailing newline from format */\n+\tspace = strchr(sig->sig_format, '\\n');\n+\tif (space)\n+\t\t*space = '\\0';\n+\n+\t/* Validate hash algorithm */\n+\tif (strcmp(sig->hash_algo, \"sha1\") &&\n+\t    strcmp(sig->hash_algo, \"sha256\"))\n+\t\tdie(\"Unknown git hash algorithm in gpgsig: '%s'\", sig->hash_algo);\n+\n+\t/* Validate signature format */\n+\tif (!valid_signature_format(sig->sig_format))\n+\t\tdie(\"Invalid signature format in gpgsig: '%s'\", sig->sig_format);\n+\tif (!strcmp(sig->sig_format, \"unknown\"))\n+\t\twarning(\"'unknown' signature format in gpgsig\");\n+\n+\t/* Read signature data */\n+\tread_next_command();\n+\tparse_data(&sig->data, 0, NULL);\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+{\n+\tstruct string_list siglines = STRING_LIST_INIT_NODUP;\n+\n+\tif (!sig->hash_algo)\n+\t\treturn;\n+\n+\tstrbuf_addstr(commit_data, header);\n+\tstring_list_split_in_place(&siglines, sig->data.buf, \"\\n\", -1);\n+\tstrbuf_add_separated_string_list(commit_data, \"\\n \", &siglines);\n+\tstrbuf_addch(commit_data, '\\n');\n+\tstring_list_clear(&siglines, 1);\n+\tstrbuf_release(&sig->data);\n+\tfree(sig->hash_algo);\n+}\n+\n+static void store_signature(struct signature_data *stored_sig,\n+\t\t\t    struct signature_data *new_sig,\n+\t\t\t    const char *hash_type)\n+{\n+\tif (stored_sig->hash_algo) {\n+\t\twarning(\"Multiple %s signatures found, ignoring additional signature\",\n+\t\t\thash_type);\n+\t\tstrbuf_release(&new_sig->data);\n+\t\tfree(new_sig->hash_algo);\n+\t} else {\n+\t\t*stored_sig = *new_sig;\n+\t}\n+}\n+\n static void parse_new_commit(const char *arg)\n {\n-\tstatic struct strbuf sig = STRBUF_INIT;\n \tstatic struct strbuf msg = STRBUF_INIT;\n-\tstruct string_list siglines = STRING_LIST_INIT_NODUP;\n+\tstruct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };\n+\tstruct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };\n \tstruct branch *b;\n \tchar *author = NULL;\n \tchar *committer = NULL;\n-\tchar *sig_alg = NULL;\n \tchar *encoding = NULL;\n \tstruct hash_list *merge_list = NULL;\n \tunsigned int merge_count;\n@@ -2750,13 +2822,23 @@ static void parse_new_commit(const char *arg)\n \t}\n \tif (!committer)\n \t\tdie(\"Expected committer but didn't get one\");\n-\tif (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n-\t\tsig_alg = xstrdup(v);\n-\t\tread_next_command();\n-\t\tparse_data(&sig, 0, NULL);\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+\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\tread_next_command();\n-\t} else\n-\t\tstrbuf_setlen(&sig, 0);\n+\t}\n+\n \tif (skip_prefix(command_buf.buf, \"encoding \", &v)) {\n \t\tencoding = xstrdup(v);\n \t\tread_next_command();\n@@ -2830,23 +2912,14 @@ static void parse_new_commit(const char *arg)\n \t\tstrbuf_addf(&new_data,\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n-\tif (sig_alg) {\n-\t\tif (!strcmp(sig_alg, \"sha1\"))\n-\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n-\t\telse if (!strcmp(sig_alg, \"sha256\"))\n-\t\t\tstrbuf_addstr(&new_data, \"gpgsig-sha256 \");\n-\t\telse\n-\t\t\tdie(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n-\t\tstring_list_split_in_place(&siglines, sig.buf, \"\\n\", -1);\n-\t\tstrbuf_add_separated_string_list(&new_data, \"\\n \", &siglines);\n-\t\tstrbuf_addch(&new_data, '\\n');\n-\t}\n+\n+\tadd_gpgsig_to_commit(&new_data, \"gpgsig \", &sig_sha1);\n+\tadd_gpgsig_to_commit(&new_data, \"gpgsig-sha256 \", &sig_sha256);\n+\n \tstrbuf_addch(&new_data, '\\n');\n \tstrbuf_addbuf(&new_data, &msg);\n-\tstring_list_clear(&siglines, 1);\n \tfree(author);\n \tfree(committer);\n-\tfree(sig_alg);\n \tfree(encoding);\n \n \tif (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 0896458de5..6f2d87475f 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n \treturn NULL;\n }\n \n+const char *get_signature_format(const char *buf)\n+{\n+\tstruct gpg_format *format = get_format_by_sig(buf);\n+\treturn format ? format->name : \"unknown\";\n+}\n+\n+int valid_signature_format(const char *format)\n+{\n+       return (!!get_format_by_name(format) ||\n+\t       !strcmp(format, \"unknown\"));\n+}\n+\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex e09f12e8d0..60ddf8bbfa 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -47,6 +47,18 @@ struct signature_check {\n \n void signature_check_clear(struct signature_check *sigc);\n \n+/*\n+ * Return the format of the signature (like \"openpgp\", \"x509\", \"ssh\"\n+ * or \"unknown\").\n+ */\n+const char *get_signature_format(const char *buf);\n+\n+/*\n+ * Is the signature format valid (like \"openpgp\", \"x509\", \"ssh\" or\n+ * \"unknown\")\n+ */\n+int valid_signature_format(const char *format);\n+\n /*\n  * Look at a GPG signed tag object.  If such a signature exists, store it in\n  * signature and the signed content in payload.  Return 1 if a signature was\ndiff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\nindex 76619765fc..3559a33c72 100755\n--- a/t/t9350-fast-export.sh\n+++ b/t/t9350-fast-export.sh\n@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '\n test_expect_success GPG 'signed-commits=verbatim' '\n \n \tgit fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&\n-\tgrep \"^gpgsig sha\" output &&\n+\ttest_grep -E \"^gpgsig sha(1|256) openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \t(\n \t\tcd new &&\n@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '\n test_expect_success GPG 'signed-commits=warn-verbatim' '\n \n \tgit fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&\n-\tgrep \"^gpgsig sha\" output &&\n+\ttest_grep -E \"^gpgsig sha(1|256) openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \ttest -s err &&\n \t(\n@@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '\n \n '\n \n+test_expect_success GPGSM 'setup X.509 signed commit' '\n+\n+\tgit checkout -b x509-signing main &&\n+\ttest_config gpg.format x509 &&\n+\ttest_config user.signingkey $GIT_COMMITTER_EMAIL &&\n+\techo \"X.509 content\" >file &&\n+\tgit add file &&\n+\tgit commit -S -m \"X.509 signed commit\" &&\n+\tX509_COMMIT=$(git rev-parse HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSM 'round-trip X.509 signed commit' '\n+\n+\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\ttest_grep -E \"^gpgsig sha(1|256) x509\" output &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/x509-signing >actual &&\n+\t\tgrep \"^gpgsig\" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/x509-signing) &&\n+\t\ttest $X509_COMMIT = $IMPORTED\n+\t) <output\n+\n+'\n+\n+test_expect_success GPGSSH 'setup SSH signed commit' '\n+\n+\tgit checkout -b ssh-signing main &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\techo \"SSH content\" >file &&\n+\tgit add file &&\n+\tgit commit -S -m \"SSH signed commit\" &&\n+\tSSH_COMMIT=$(git rev-parse HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSSH 'round-trip SSH signed commit' '\n+\n+\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\ttest_grep -E \"^gpgsig sha(1|256) ssh\" output &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/ssh-signing >actual &&\n+\t\tgrep \"^gpgsig\" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/ssh-signing) &&\n+\t\ttest $SSH_COMMIT = $IMPORTED\n+\t) <output\n+\n+'\n+\n test_expect_success 'setup submodule' '\n \n \ttest_config_global protocol.file.allow always &&\n-- \n2.50.0.2.g875523421d\n\n"},{"id":"520407","messageId":"CAP8UFD28y7_AUGndCyPDTc0f0odufdTAW5vReM5pMGeCcAerYg@mail.gmail.com","threadId":"63342","inReplyTo":"aD4i7YhUnT5Kgew-@tapette.crustytoothpaste.net","subject":"Re: [PATCH v2 0/6] extract algo information from signatures","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-19T13:37:12Z","receivedAt":"2025-06-19T13:37:26Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Jun 3, 2025 at 12:17 AM brian m. carlson\n<sandals@crustytoothpaste.net> wrote:\n>\n> On 2025-05-26 at 10:33:08, Christian Couder wrote:\n\n> > brian m. carlson however replied that it would be better to show two\n> > pieces of information instead of one: one for the hash algorithm and\n> > one for the protocol.\n>\n> Actually, what I was saying is that we should have one for the hash\n> algorithm that is used in the Git object.  I don't care about the hash\n> algorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's\n> signed with SHA-512 or SHA-256), but we can have multiple signatures in\n> a single commit such that there's both a SHA-1 signature and a SHA-256\n> signature.\n\nThanks for clarifying. I think that's what I implemented in v3 and v4.\n"},{"id":"520408","messageId":"CAP8UFD2zO0Apy5jOrbFdBjq2F73mo3dbESaBqYCG2hsKBamFPg@mail.gmail.com","threadId":"63342","inReplyTo":"CABPp-BFbF=rp6FZjyL+Fm5TqNZZdhfjS1sK-CBQ_=wtvFmfdLw@mail.gmail.com","subject":"Re: [PATCH v2 0/6] extract algo information from signatures","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-19T13:38:12Z","receivedAt":"2025-06-19T13:38:25Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, May 26, 2025 at 6:03 PM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Mon, May 26, 2025 at 3:33 AM Christian Couder\n> <christian.couder@gmail.com> wrote:\n\n> I'd like to propose that the following are the possible uses that\n> users might have regarding commit signatures with\n> fast-export/fast-import (if anyone has additional usecases, let me\n> know):\n>\n> (A) Make fast-export include signatures, and make fast-import include\n> them unconditionally (even if invalid)\n> (B) Similar to (A), but make *fast-import* check them and either error\n> out or drop them if they become invalid\n> (C) Simliar to (B), but make *fast-import* re-sign the commit if they\n> become invalid\n> (D) Similar to (A), but make *fast-import* re-sign the commit even if\n> the signature would have been valid\n>\n> Note that in the above, there might be additional processing between\n> when fast-export runs and when fast-import does (e.g. by filter-repo\n> or a similar tool, or even the user editing by hand).\n\nI agree that they are likely to be the most important use cases, and I\nam fine with working on these use cases.\n\n> > To address this, I decided to focus first on extracting the hash\n> > algorithm from OpenPGP/X.509 signatures and the key type from SSH\n> > signature when checking signatures.\n> >\n> > To test that, I thought that it could be interesting to add a\n> > `--summary` option to `verify-commit` that shows a concise, one-line\n> > summary of the signature verification to standard output in the\n> > `STATUS FORMAT ALGORITHM` format, where:\n> >\n> > * STATUS is the result character (e.g., G, B, E, U, N, ...), similar\n> >   as what the \"%G?\" pretty format specifier shows,\n> >\n> > * FORMAT is the signature format (`openpgp`, `x509`, or `ssh`),\n> >\n> > * ALGORITHM is the hash algorithm used for GPG/GPGSM signatures\n> >   (e.g. `sha1`, `sha256`, ...), or the key type for SSH signatures\n> >   (`RSA`, `ECDSA`, `ED25519`, ...).\n>\n> This sounds like it might be a nice feature extension to the\n> verify-commit builtin.  I don't see how it helps implement signature\n> handling in fast-export/fast-import, though.\n\nFair enough. In the v3 and v4, I changed the approach and dropped all of this.\n\n> > If we can agree on a concise format output for signature checks, then\n> > maybe this format will be a good format to be used in the `git\n> > fast-export` output for users who are fine with signatures being\n> > checked.\n> >\n> > What do you think?\n>\n> Maybe I'm missing something, but it seems to me that checking\n> signatures *in fast-export* would be a complete waste of time.  For\n> usecases (A) & (D), checking signatures at all is a waste of time.\n> For usecases (B) & (C), checking signatures in fast-export is\n> throwaway work because whether or not the signatures are valid at the\n> time fast-export runs, and even in the rare usecase where there is no\n> additional processing between fast-export and fast-import (such as by\n> filter-repo), the signatures would still need to be re-checked by\n> fast-import anyway.  (Note that a simple `git fast-export ... | git\n> fast-import` is *not* guaranteed to get the same commit hashes even\n> when there are no commit signatures; that only happens when the\n> history is sufficiently canonical).\n\nYeah, right. In v3 and v4, I dropped this in favor of something\nsimpler similar to what was in the v1 patch, and after that I plan to\nwork on checking signatures in fast-import soon. Thanks.\n"},{"id":"520410","messageId":"xmqqbjqjdbmj.fsf@gitster.g","threadId":"63342","inReplyTo":"20250619133630.727274-1-christian.couder@gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-06-19T14:55:32Z","receivedAt":"2025-06-19T14:55:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> This v4 is just about fixing a few bugs in the tests using the SHA-256\n> object format compared to the v3. (I had issues with CI tests on v3,\n> so I sent it without waiting for the results.)\n\nThanks.\n\nI am not sure if \"I am happy is either 1 or 256\" is what you really\nwant, though.  The test presumably knows what algorithm is being\nused during its run, so wouldn't you want to say more like \"I know I\nused sha256, and I expect seeing sha256, ah, I see sha256 and even\nbetter I see no sha1, so I am very happy\"?\n\n> There are no tests in this v4 and in v3 with both a SHA-1 and a\n> SHA-256 signature on the same commit though, as I am not sure yet how\n> to best generate a commit with such signatures. Suggestions welcome!\n\nGood point to fill potential gaps.  If we had such a commit, then\nwould these tests say \"I know I want both 1 and 256, and I do see\none instance each of 1 and 256, so I am happy\"?\n\n"},{"id":"520425","messageId":"CABPp-BF6ZjfJ2MSHkhXQwv00=63qG3wLU8ie=_WVxnWTffFpRA@mail.gmail.com","threadId":"63342","inReplyTo":"20250619133630.727274-1-christian.couder@gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-06-19T21:44:30Z","receivedAt":"2025-06-19T21:44:43Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Jun 19, 2025 at 6:36 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\n> signed-commits, 2025-03-10), added support for signed commits to\n> fast-export and fast-import.\n>\n> When a signed commit is processed, fast-export can output either\n> \"gpgsig sha1\" or \"gpgsig sha256\" depending on whether the signed\n> commit uses the SHA-1 or SHA-256 Git object format.\n>\n> However, this implementation has a number of limitations:\n>\n>   - the output format was not properly described in the documentation,\n\nThanks for working on fixing this.\n\n>   - the output format is not very informative as it doesn't even say\n>     if the signature is an OpenPGP, an SSH, or an X509 signature,\n\nWhy would it need to say what type of signature it is?  Don't the\nascii armor lines have e.g. \"----BEGIN PGP SIGNATURE----\" and \"----END\nPGP SIGNATURE----\" around it, which fast-import can read as well as\nfast-export?  Is the idea that we strip those lines and now need to\nreplace the information we lost?\n\n>   - the implementation doesn't support having both one signature on\n>     the SHA-1 object and one on the SHA-256 object.\n\nNot sure I understand this; more questions around this later.\n\n> Let's improve on these limitations by improving fast-export and\n> fast-import so that:\n>\n>   - both one signature on the SHA-1 object and one on the SHA-256\n>     object can be exported and imported,\n>   - if there is more than one signature on the SHA-1 object or on\n>     the SHA-256 object, a warning is emitted,\n>   - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n>     where <git-hash-algo> is the Git object format as before, and\n>     <signature-format> is the signature type (\"openpgp\", \"x509\",\n>     \"ssh\" or \"unknown\",\n>   - the output is properly documented.\n\nPerhaps this was discussed in an earlier round, but if so I either\nforgot or missed it.  What value does <git-hash-algo> and\n<signature-format> provide?  How are they intended to be used?\n\nIs the <signature-format> merely self-inflicted pain from stripping\nthe ascii armor lines?  If so, would it make more sense to just\ninclude those armor lines as-is in the fast-export stream and let\nfast-import process it?  Then we wouldn't have to introduce all the\noutputting and parsing of this new <signature-format> field and worry\nabout the new special \"unknown\" status.\n\nIs the <git-hash-algo> due to the fact that we have separate `gpgsig`\nand `gpgsig-sha256` commit headers and we want to use that information\nto avoid writing these headers to the wrong-sized objects (and/or to\navoid checking whether the signature is valid on the wrong-sized\nobjects)?  If so, could that be spelled out in the docs as well,\nespecially since it appears that the intent of these headers is left\nunimplemented due to not changing fast-import to do anything with\nthem?\n\nAnd if <git-hash-algo>'s purpose is to ensure they are only used when\nwriting same-sized object as what was exported, then...isn't that a\nbug?  This series was started because people wanted to be able to do\nthings like keeping signature even when they are no longer valid or\nresigning commits that have a no longer commit signature (among other\nuses), but that would mean that if someone exports a sha1 repository\nand imports it as sha256, we don't want to ignore the fact that the\nsha1 commit was signed for those usecases.\n\nIf, however, the <git-hash-algo>'s purpose is merely as a performance\noptimization that fast-import can employ in the cases where it checks\nfor signatures being valid, so it can avoid checking when it know the\nhash size isn't even the same, then it could make sense.\n\nBut, short of that performance optimization, it's unclear to me\nwhether we'd lose anything by simply exporting \"gpgsig <contents of\nsignature header from original object, including the armor lines>\",\nand dropping the <git-hash-algo> and <signature-format> lines\nentirely.  Am I missing something?  (I may well be; I don't know much\nabout signing stuff beyond the very basics, and don't mess with signed\ncommits or tags much myself.)\n\n[...]\n> There are no tests in this v4 and in v3 with both a SHA-1 and a\n> SHA-256 signature on the same commit though, as I am not sure yet how\n> to best generate a commit with such signatures. Suggestions welcome!\n\nIf no suggestions are forthcoming, it feels odd to implement this with\nno tests.  Would it make sense to leave it out until we know how to\ntest it?  (More questions on this below...)\n\n[...]\n> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\n> index 43bbb4f63c..64198f2186 100644\n> --- a/Documentation/git-fast-export.adoc\n> +++ b/Documentation/git-fast-export.adoc\n> @@ -50,6 +50,23 @@ resulting tag will have an invalid signature.\n>         is the same as how earlier versions of this command without\n>         this option behaved.\n>  +\n> +When exported, a signature starts with:\n> ++\n> +gpgsig <git-hash-algo> <signature-format>\n> ++\n> +where <git-hash-algo> is the Git object hash so either \"sha1\" or\n> +\"sha256\", and <signature-format> is the signature type, so \"openpgp\",\n> +\"x509\", \"ssh\" or \"unknown\".\n> ++\n> +For example, an OpenPGP signature on a SHA-1 commit starts with\n> +`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit\n> +starts with `gpgsig sha256 ssh`.\n\nI had a number of comments/questions on this above already.\n\n> ++\n> +Currently for a given commit, at most one signature for the SHA-1\n> +object and one signature for the SHA-256 object are exported, each\n> +with their respective <git-hash-algo> identifier.\n\nWait..does this mean fast-export is obligated to walk over both all\nsha1 commits and all \"equivalent\" sha256 commits when exporting a\nrepo?  I thought most operations on the repo would walk over only one\nor the other; walking over both seems to be against the spirit of the\n\"fast\" in \"fast-export\".  Am I missing something?  (Possibly related\nquestion: Does \"git log\" bother walking over both, or does it only\nwalk over one?)  Even if this really is wanted by some users,\nshouldn't they manually request it rather than making exports slow for\neveryone else by default?\n\n> +A warning is\n> +emitted for each additional signature found.\n\nWhy?  This seems odd to me.  Why not merely export them all, and let\nfast-import throw warnings or errors if it sees more than one and is\nnot yet prepared to handle multiple signatures?\n\n> ++\n>  NOTE: This is highly experimental and the format of the data stream may\n>  change in the future without compatibility guarantees.\n>\n> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> index 250d866652..db5e5c8da5 100644\n> --- a/Documentation/git-fast-import.adoc\n> +++ b/Documentation/git-fast-import.adoc\n> @@ -445,7 +445,7 @@ one).\n>         original-oid?\n>         ('author' (SP <name>)? SP LT <email> GT SP <when> LF)?\n>         'committer' (SP <name>)? SP LT <email> GT SP <when> LF\n> -       ('gpgsig' SP <alg> LF data)?\n> +       ('gpgsig' SP <algo> SP <format> LF data)?\n>         ('encoding' SP <encoding> LF)?\n>         data\n>         ('from' SP <commit-ish> LF)?\n> @@ -518,13 +518,32 @@ their syntax.\n>  ^^^^^^^^\n>\n>  The optional `gpgsig` command is used to include a PGP/GPG signature\n> -that signs the commit data.\n> +or other cryptographic signature that signs the commit data.\n\nGood catch.\n\n>\n> -Here <alg> specifies which hashing algorithm is used for this\n> -signature, either `sha1` or `sha256`.\n> +....\n> +       'gpgsig' SP <git-hash-algo> SP <signature-format> LF\n> +       data\n> +....\n> +\n> +The `gpgsig` command takes two arguments:\n> +\n> +* `<git-hash-algo>` specifies which Git object format this signature\n> +  applies to, either `sha1` or `sha256`.\n> +\n> +* `<signature-format>` specifies the type of signature, such as\n> +  `openpgp`, `x509`, `ssh`, or `unknown`.\n> +\n> +A commit may have at most one signature for the SHA-1 object format\n> +(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n> +(stored in the \"gpgsig-sha256\" header).\n\nWhy?  Does this mean hg-fast-export or hg-fast-import (or a\njj-fast-export or jj-fast-import) wouldn't be allowed to specify\nmultiple signatures?  The fast-export and fast-import streams are\noften used for interoperation with other VCSes, but as far as I can\ntell, you're encoding a restriction on what's allowed that isn't an\nactual problem for git but just a not-yet-implemented state.  If I've\nunderstood correctly that the restriction is merely due to the current\nimplementation, perhaps the wording could be changed to not list it as\nan encoding restriction, but as a current fast-import limitation?\n\nAlso, I'm slightly uncomfortable with \"the SHA-1 object format\" and\n\"the SHA-256 object format\" because of the fact that these tools are\nused for interoperability with similarly-named tools from other VCSes.\nI think it'd be better to just treat them equally as \"here was/were\nsome signature(s) on the object in the original repo; importers can\nchoose what to do with them\".\n\n> +\n> +See below for a detailed description of the `data` command which\n> +contains the raw signature data.\n> +\n> +Signatures are not yet checked in the current implementation though.\n\n...but what does happen with those signatures?  Dropped?  Kept as-is?\nCan we just spell this out a bit more clearly?  e.g.\n\n\"Signatures are not checked in the current implementation; they are\nused as-is, which may mean the signatures are invalid in the imported\nrepository.\"\n\n> -NOTE: This is highly experimental and the format of the data stream may\n> -change in the future without compatibility guarantees.\n> +NOTE: This is highly experimental and the format of the `gpgsig`\n> +command may change in the future without compatibility guarantees.\n\nGood clarification.\n\n\nI briefly skimmed the implementation and test files, and didn't see\nany problems...but I think it probably makes more sense to get aligned\non the goals of the format and how these fields are meant to be used\nbefore diving into those details closer.\n\nThanks for working on this topic.\n"},{"id":"520504","messageId":"CAP8UFD24LayEi-=PREqbiyC7PeJbYEP1ghOZ+T-ma=DZVaPtSg@mail.gmail.com","threadId":"63342","inReplyTo":"CABPp-BF6ZjfJ2MSHkhXQwv00=63qG3wLU8ie=_WVxnWTffFpRA@mail.gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-06-20T16:12:36Z","receivedAt":"2025-06-20T16:12:51Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Jun 19, 2025 at 11:44 PM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Thu, Jun 19, 2025 at 6:36 AM Christian Couder\n> <christian.couder@gmail.com> wrote:\n\n> > However, this implementation has a number of limitations:\n> >\n> >   - the output format was not properly described in the documentation,\n>\n> Thanks for working on fixing this.\n>\n> >   - the output format is not very informative as it doesn't even say\n> >     if the signature is an OpenPGP, an SSH, or an X509 signature,\n>\n> Why would it need to say what type of signature it is?  Don't the\n> ascii armor lines have e.g. \"----BEGIN PGP SIGNATURE----\" and \"----END\n> PGP SIGNATURE----\" around it, which fast-import can read as well as\n> fast-export?  Is the idea that we strip those lines and now need to\n> replace the information we lost?\n\nIn https://lore.kernel.org/git/aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net/\nbrian said:\n\n\"These should be separate fields: one for the hash algorithm and one for\nthe protocol.  Alternatively, we can just keep the hash algorithm field\nand parse the protocol by reading the first line, which will differ for\ndifferent protocols.\"\n\nIt would have been nice if you had then said that you prefer not to\nhave the protocol.\n\nMy opinion was that it was better for tools processing fast-export\noutput to have the protocol as they have to parse the \"gppsig ...\"\nline anyway. So it should be easier for them than to parse the ascii\narmor lines.\n\n> >   - the implementation doesn't support having both one signature on\n> >     the SHA-1 object and one on the SHA-256 object.\n>\n> Not sure I understand this; more questions around this later.\n\nIn https://lore.kernel.org/git/aD4i7YhUnT5Kgew-@tapette.crustytoothpaste.net/\nbrian said:\n\n\"Actually, what I was saying is that we should have one for the hash\nalgorithm that is used in the Git object.  I don't care about the hash\nalgorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's\nsigned with SHA-512 or SHA-256), but we can have multiple signatures in\na single commit such that there's both a SHA-1 signature and a SHA-256\nsignature.\"\n\nso I implemented the possibility that there's both a SHA-1 signature\nand a SHA-256 signature in a single commit.\n\nIf you disagreed with what brian suggested, it would have been nice to\nreply to brian then.\n\n> > Let's improve on these limitations by improving fast-export and\n> > fast-import so that:\n> >\n> >   - both one signature on the SHA-1 object and one on the SHA-256\n> >     object can be exported and imported,\n> >   - if there is more than one signature on the SHA-1 object or on\n> >     the SHA-256 object, a warning is emitted,\n> >   - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n> >     where <git-hash-algo> is the Git object format as before, and\n> >     <signature-format> is the signature type (\"openpgp\", \"x509\",\n> >     \"ssh\" or \"unknown\",\n> >   - the output is properly documented.\n>\n> Perhaps this was discussed in an earlier round, but if so I either\n> forgot or missed it.  What value does <git-hash-algo> and\n> <signature-format> provide?  How are they intended to be used?\n\nMy opinion on this is that in cases like this we might not always know\nwhat could be useful for tools or users in general, so it might be\nbetter to provide more information that can be easily discarded if not\nuseful rather than not enough information.\n\nbrian seemed to say that <git-hash-algo> and <signature-format> are\nimportant so I just prefered to have both, especially as they are not\ncostly to get.\n\n> Is the <signature-format> merely self-inflicted pain from stripping\n> the ascii armor lines?  If so, would it make more sense to just\n> include those armor lines as-is in the fast-export stream and let\n> fast-import process it?\n\nThe ascii armor lines are kept in the fast-export stream, but\nfast-export's ouput is supposed to be processed somehow sometimes\nbefore being fed back to fast-import, so I think we should make it\neasy for tools or people processing the output to get signature\ninformation.\n\n>  Then we wouldn't have to introduce all the\n> outputting and parsing of this new <signature-format> field and worry\n> about the new special \"unknown\" status.\n\nI think it's a trade-off. If there is a consensus that it's very\nunlikely to help anyone, I am fine with removing it.\n\n> Is the <git-hash-algo> due to the fact that we have separate `gpgsig`\n> and `gpgsig-sha256` commit headers and we want to use that information\n> to avoid writing these headers to the wrong-sized objects (and/or to\n> avoid checking whether the signature is valid on the wrong-sized\n> objects)?\n\nYeah, I think it could help with that, but brian might better answer that.\n\n> If so, could that be spelled out in the docs as well,\n> especially since it appears that the intent of these headers is left\n> unimplemented due to not changing fast-import to do anything with\n> them?\n\nfast-import puts back the `gpgsig` or `gpgsig-sha256` headers\ndepending on <git-hash-algo>, so it's useful at least for that. I will\nimprove the docs about it.\n\n> And if <git-hash-algo>'s purpose is to ensure they are only used when\n> writing same-sized object as what was exported, then...isn't that a\n> bug?\n\nI am not sure I understand what would be the bug.\n\n> This series was started because people wanted to be able to do\n> things like keeping signature even when they are no longer valid or\n> resigning commits that have a no longer commit signature (among other\n> uses), but that would mean that if someone exports a sha1 repository\n> and imports it as sha256, we don't want to ignore the fact that the\n> sha1 commit was signed for those usecases.\n>\n> If, however, the <git-hash-algo>'s purpose is merely as a performance\n> optimization that fast-import can employ in the cases where it checks\n> for signatures being valid, so it can avoid checking when it know the\n> hash size isn't even the same, then it could make sense.\n\nI think it could be used for that, but I'd like brian's opinion about this.\n\n> But, short of that performance optimization, it's unclear to me\n> whether we'd lose anything by simply exporting \"gpgsig <contents of\n> signature header from original object, including the armor lines>\",\n> and dropping the <git-hash-algo> and <signature-format> lines\n> entirely.  Am I missing something?  (I may well be; I don't know much\n> about signing stuff beyond the very basics, and don't mess with signed\n> commits or tags much myself.)\n\nI don't know much about signing either, that's why I have tried to\nrely on brian's suggestion on this.\n\n> [...]\n> > There are no tests in this v4 and in v3 with both a SHA-1 and a\n> > SHA-256 signature on the same commit though, as I am not sure yet how\n> > to best generate a commit with such signatures. Suggestions welcome!\n>\n> If no suggestions are forthcoming, it feels odd to implement this with\n> no tests.  Would it make sense to leave it out until we know how to\n> test it?  (More questions on this below...)\n\nI had hoped that implementing this and then asking how best to test\nwith both a SHA-1 and a SHA-256 signature on the same commit would be\nbetter to move things forward.\n\n> > +Currently for a given commit, at most one signature for the SHA-1\n> > +object and one signature for the SHA-256 object are exported, each\n> > +with their respective <git-hash-algo> identifier.\n>\n> Wait..does this mean fast-export is obligated to walk over both all\n> sha1 commits and all \"equivalent\" sha256 commits when exporting a\n> repo?  I thought most operations on the repo would walk over only one\n> or the other; walking over both seems to be against the spirit of the\n> \"fast\" in \"fast-export\".  Am I missing something?  (Possibly related\n> question: Does \"git log\" bother walking over both, or does it only\n> walk over one?)  Even if this really is wanted by some users,\n> shouldn't they manually request it rather than making exports slow for\n> everyone else by default?\n\nNo fast-export doesn't walk over both the sha1 commits and all\n\"equivalent\" sha256 commits, it can just process at most 2 signatures\nfor a given commit, one with the `gpgsig` header and one with the\n`gpgsig-sha256` header. I will try to reword the doc to make it\nclearer.\n\n> > +A warning is\n> > +emitted for each additional signature found.\n>\n> Why?  This seems odd to me.  Why not merely export them all, and let\n> fast-import throw warnings or errors if it sees more than one and is\n> not yet prepared to handle multiple signatures?\n\nI am not against implementing both export and import support for any\nnumber of signatures, but it seems that Git in general doesn't support\nmore than one `gpgsig` and one `gpgsig-sha256` signature well, and\nthat's also what brian suggested supporting. It's also better than\njust 1 signature max which is the current state.\n\n> > -Here <alg> specifies which hashing algorithm is used for this\n> > -signature, either `sha1` or `sha256`.\n> > +....\n> > +       'gpgsig' SP <git-hash-algo> SP <signature-format> LF\n> > +       data\n> > +....\n> > +\n> > +The `gpgsig` command takes two arguments:\n> > +\n> > +* `<git-hash-algo>` specifies which Git object format this signature\n> > +  applies to, either `sha1` or `sha256`.\n> > +\n> > +* `<signature-format>` specifies the type of signature, such as\n> > +  `openpgp`, `x509`, `ssh`, or `unknown`.\n> > +\n> > +A commit may have at most one signature for the SHA-1 object format\n> > +(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n> > +(stored in the \"gpgsig-sha256\" header).\n>\n> Why?  Does this mean hg-fast-export or hg-fast-import (or a\n> jj-fast-export or jj-fast-import) wouldn't be allowed to specify\n> multiple signatures?  The fast-export and fast-import streams are\n> often used for interoperation with other VCSes, but as far as I can\n> tell, you're encoding a restriction on what's allowed that isn't an\n> actual problem for git but just a not-yet-implemented state.\n\nI would be fine with rewording this to say that the current\nimplementation allows at most one signature for the SHA-1 object\nformat and one for the SHA-256 object format to be imported. But as\nfar as I understand additional signatures on top of that might not be\nvery well supported by Git in general, so maybe I should mention that\ntoo?\n\n> If I've\n> understood correctly that the restriction is merely due to the current\n> implementation, perhaps the wording could be changed to not list it as\n> an encoding restriction, but as a current fast-import limitation?\n\nIf that current fast-import limitation was lifted, we should probably\nstill say that additional signatures might not be very well supported\nby Git in general.\n\n> Also, I'm slightly uncomfortable with \"the SHA-1 object format\" and\n> \"the SHA-256 object format\" because of the fact that these tools are\n> used for interoperability with similarly-named tools from other VCSes.\n> I think it'd be better to just treat them equally as \"here was/were\n> some signature(s) on the object in the original repo; importers can\n> choose what to do with them\".\n\nI don't think giving that information prevents tools from other VCSes\nfrom doing what they want with the signatures.\n\n> > +See below for a detailed description of the `data` command which\n> > +contains the raw signature data.\n> > +\n> > +Signatures are not yet checked in the current implementation though.\n>\n> ...but what does happen with those signatures?  Dropped?  Kept as-is?\n\nThis section (about the `gpgsig` command) starts with:\n\n\"The optional `gpgsig` command is used to include a PGP/GPG signature\nthat signs the commit data.\"\n\nso yeah they are kept as-is.\n\n> Can we just spell this out a bit more clearly?  e.g.\n>\n> \"Signatures are not checked in the current implementation; they are\n> used as-is, which may mean the signatures are invalid in the imported\n> repository.\"\n\nIt feels a bit redundant but, yeah, the part about possibly invalid\nsignatures might clarify things, so I am fine with spelling it out\nlike this. Thanks.\n\n[...]\n\n> I briefly skimmed the implementation and test files, and didn't see\n> any problems...but I think it probably makes more sense to get aligned\n> on the goals of the format and how these fields are meant to be used\n> before diving into those details closer.\n\nYeah, I would have been happy if we could have been aligned with the\ngoals of the format and the fields earlier, but better late than\nnever.\n\n> Thanks for working on this topic.\n\nThanks for reviewing.\n"},{"id":"520511","messageId":"xmqqfrfu2pa1.fsf@gitster.g","threadId":"63342","inReplyTo":"CAP8UFD24LayEi-=PREqbiyC7PeJbYEP1ghOZ+T-ma=DZVaPtSg@mail.gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-06-20T19:20:38Z","receivedAt":"2025-06-20T19:20:43Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\ntldr; 2 brief requests.  Please\n\n - Be gentle to people and expect that it is normal for them to be\n   off of the list for a few weeks (or even more), not able to give\n   a timely comments;\n\n - Fully stand behind your own patch (unless it is an RFC), even if\n   some of the idea came from elsewhere.\n\n>> Why would it need to say what type of signature it is?  Don't the\n>> ascii armor lines have e.g. \"----BEGIN PGP SIGNATURE----\" and \"----END\n>> PGP SIGNATURE----\" around it, which fast-import can read as well as\n>> fast-export?  Is the idea that we strip those lines and now need to\n>> replace the information we lost?\n>\n> In https://lore.kernel.org/git/aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net/\n> brian said:\n>\n> \"These should be separate fields: one for the hash algorithm and one for\n> the protocol.  Alternatively, we can just keep the hash algorithm field\n> and parse the protocol by reading the first line, which will differ for\n> different protocols.\"\n>\n> It would have been nice if you had then said that you prefer not to\n> have the protocol.\n\nLet's remember to be gentle for those who give varluable feedbacks\nbut may not be always on this list.  A late comment on a topic that\nhas not hit 'next' is much better than a late comment after the\ntopic hits 'next', or no comment at all.\n\nAlso, even if the idea came from somebody else, if you agreed to the\nidea and made it part of your submission, then it would be better to\nexplain it in your own words, in the most appropriate way to answer\nthe question asked (e.g. the original from Brian and the question by\nElijah may have stress on different aspect of the problem).\n\n> My opinion was that it was better for tools processing fast-export\n> output to have the protocol as they have to parse the \"gppsig ...\"\n> line anyway. So it should be easier for them than to parse the ascii\n> armor lines.\n\nAnd if you do not agree with Brian's, perhaps discuss a bit more to\n(1) either convince yourself that Brian's idea is better and rewrite\nyour code to adopt the idea, or (2) explain the reason why your \"the\nimporter reads and parses anyway\" is better design and stick to it.\n\n> ...\n> Yeah, I would have been happy if we could have been aligned with the\n> goals of the format and the fields earlier, but better late than\n> never.\n>\n>> Thanks for working on this topic.\n>\n> Thanks for reviewing.\n\nThanks.\n"},{"id":"520781","messageId":"CABPp-BE8b8Ju3zaB04DzQJoSKwNkHcHTw8q95ux_bb4hOpboBg@mail.gmail.com","threadId":"63342","inReplyTo":"CAP8UFD24LayEi-=PREqbiyC7PeJbYEP1ghOZ+T-ma=DZVaPtSg@mail.gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-06-26T19:11:56Z","receivedAt":"2025-06-26T19:12:08Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Jun 20, 2025 at 9:12 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> On Thu, Jun 19, 2025 at 11:44 PM Elijah Newren <newren@gmail.com> wrote:\n> >\n> > On Thu, Jun 19, 2025 at 6:36 AM Christian Couder\n> > <christian.couder@gmail.com> wrote:\n>\n> > > However, this implementation has a number of limitations:\n> > >\n> > >   - the output format was not properly described in the documentation,\n> >\n> > Thanks for working on fixing this.\n> >\n> > >   - the output format is not very informative as it doesn't even say\n> > >     if the signature is an OpenPGP, an SSH, or an X509 signature,\n> >\n> > Why would it need to say what type of signature it is?  Don't the\n> > ascii armor lines have e.g. \"----BEGIN PGP SIGNATURE----\" and \"----END\n> > PGP SIGNATURE----\" around it, which fast-import can read as well as\n> > fast-export?  Is the idea that we strip those lines and now need to\n> > replace the information we lost?\n>\n> In https://lore.kernel.org/git/aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net/\n> brian said:\n>\n> \"These should be separate fields: one for the hash algorithm and one for\n> the protocol.  Alternatively, we can just keep the hash algorithm field\n> and parse the protocol by reading the first line, which will differ for\n> different protocols.\"\n>\n> It would have been nice if you had then said that you prefer not to\n> have the protocol.\n>\n> My opinion was that it was better for tools processing fast-export\n> output to have the protocol as they have to parse the \"gppsig ...\"\n> line anyway. So it should be easier for them than to parse the ascii\n> armor lines.\n>\n> > >   - the implementation doesn't support having both one signature on\n> > >     the SHA-1 object and one on the SHA-256 object.\n> >\n> > Not sure I understand this; more questions around this later.\n>\n> In https://lore.kernel.org/git/aD4i7YhUnT5Kgew-@tapette.crustytoothpaste.net/\n> brian said:\n>\n> \"Actually, what I was saying is that we should have one for the hash\n> algorithm that is used in the Git object.  I don't care about the hash\n> algorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's\n> signed with SHA-512 or SHA-256), but we can have multiple signatures in\n> a single commit such that there's both a SHA-1 signature and a SHA-256\n> signature.\"\n>\n> so I implemented the possibility that there's both a SHA-1 signature\n> and a SHA-256 signature in a single commit.\n>\n> If you disagreed with what brian suggested, it would have been nice to\n> reply to brian then.\n\nSorry about that.  I hadn't read the whole thread; further, I was\npreviously trying to avoid the details of signatures beyond the basics\nand hoping to leave those details to others, but between v2 and some\nwords you had in this version plus deciding to just take a closer look\nwhile on vacation (as I was last week), I decided to try to look a bit\ncloser.\n\nTurns out, though, that despite my attempts to read a bit more\ndocumentation and code in the project, I still had a fundamental\nmisunderstanding last week when I was responding.  I had been assuming\nthat the gpgsig headers would appear on sha1-commits and gpgsig-sha256\nheaders would appear on sha256 commits.  I didn't understand that both\ntypes of headers can appear on both types of commits (so that, for\nexample, gpgsig-sha256 would be use the sha256-commit as the source\nand sign that data but write the result to a sha1-commit). I talked\nwith brian this morning to ask a few questions and he clarified this\nfor me.\n\n(Side note: It might be nice to clarify this in\nDocumentation/technical/hash-function-transition.adoc; that document\ndid not dispel this confusion when I read it last week.)\n\n> > > Let's improve on these limitations by improving fast-export and\n> > > fast-import so that:\n> > >\n> > >   - both one signature on the SHA-1 object and one on the SHA-256\n> > >     object can be exported and imported,\n\nAnd the part I didn't understand the first time around is that e.g.\nthe SHA-1 commit can include both the signatures on the SHA-1 object\nand on the SHA-256 object.  (Similarly, the SHA-256 commit can include\nboth signatures as well.)\n\n> > >   - if there is more than one signature on the SHA-1 object or on\n> > >     the SHA-256 object, a warning is emitted,\n\nThis is now ambiguous to me.  more than one signature using the SHA-1\nas source for signing, or more than one signature recorded within the\nSHA-1 commit?  I think you mean the former.\n\n> > >   - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n> > >     where <git-hash-algo> is the Git object format as before, and\n> > >     <signature-format> is the signature type (\"openpgp\", \"x509\",\n> > >     \"ssh\" or \"unknown\",\n> > >   - the output is properly documented.\n> >\n> > Perhaps this was discussed in an earlier round, but if so I either\n> > forgot or missed it.  What value does <git-hash-algo> and\n> > <signature-format> provide?  How are they intended to be used?\n>\n> My opinion on this is that in cases like this we might not always know\n> what could be useful for tools or users in general, so it might be\n> better to provide more information that can be easily discarded if not\n> useful rather than not enough information.\n>\n> brian seemed to say that <git-hash-algo> and <signature-format> are\n> important so I just prefered to have both, especially as they are not\n> costly to get.\n\nSo, with my new understanding, <git-hash-algo> is necessary because if\nyou only had the gpg signature on a commit object, you don't know what\nit was a signature of -- it might be a signature of that commit\nobject, or it might be a signature of it's \"compatibility\" object\ncreated with a different git hashing algorithm.  So, fast-import --\nregardless of whether it is writing into a sha1 repo or a sha256 repo\n-- won't be able to know how to check the validity of the gpg\nsignature unless it knows whether to check the signature of the sha1\ncommit or the sha256 commit.  And if the repository that fast-import\nis writing to isn't writing compatibility objects for interoperation\nat all, then it won't be able to check any compatibility signatures at\nall; it'll only be able to check signatures of the actual object it\nwants to write.\n\n> > Is the <signature-format> merely self-inflicted pain from stripping\n> > the ascii armor lines?  If so, would it make more sense to just\n> > include those armor lines as-is in the fast-export stream and let\n> > fast-import process it?\n>\n> The ascii armor lines are kept in the fast-export stream, but\n> fast-export's ouput is supposed to be processed somehow sometimes\n> before being fed back to fast-import, so I think we should make it\n> easy for tools or people processing the output to get signature\n> information.\n\nAh, just for convenience of the importer/processor; fair enough.\n\n> > Is the <git-hash-algo> due to the fact that we have separate `gpgsig`\n> > and `gpgsig-sha256` commit headers and we want to use that information\n> > to avoid writing these headers to the wrong-sized objects (and/or to\n> > avoid checking whether the signature is valid on the wrong-sized\n> > objects)?\n>\n> Yeah, I think it could help with that, but brian might better answer that.\n\nI think the answer is actually no, this isn't at all what these are\nabout.  These are about knowing how to check the validity of the\nsignatures, because the signatures aren't necessarily associated with\nthe object they were read from; they could be a signature of its\ncompatibility object instead.\n\n> > If so, could that be spelled out in the docs as well,\n> > especially since it appears that the intent of these headers is left\n> > unimplemented due to not changing fast-import to do anything with\n> > them?\n>\n> fast-import puts back the `gpgsig` or `gpgsig-sha256` headers\n> depending on <git-hash-algo>, so it's useful at least for that. I will\n> improve the docs about it.\n>\n> > And if <git-hash-algo>'s purpose is to ensure they are only used when\n> > writing same-sized object as what was exported, then...isn't that a\n> > bug?\n>\n> I am not sure I understand what would be the bug.\n\nIt's not relevant given my misunderstanding, but to explain what I was\nthinking last week (note that the first two bullets are my mistake):\n\n  * gpgsig headers only occur on sha1 commits and are the signatures\nof the sha1 commit sans the gpgsig header\n  * gpgsig-sha256 headers are similar with respect to sha256 commits\n  * we were using <git-hash-algo> as an optimization to only sign\ncommits in fast-import if the repository we were writing to was using\nthe same hash function as the repo we were writing from\n\nIn such a case, that'd be a bug for anyone that wanted a\n'resign-commits-that-were-previously-signed-if-the-signature-becomes-invalid'.\nAlthough a signature of a sha256 commit clearly won't verify if we\nexported a sha256 repo and imported it as a sha1 repo and tried to\nverify that signature using a sha1 commit, that doesn't mean we should\njust ignore the fact that the sha256 commit had a signature.  The user\nclearly expressed the intent to resign any commits that had a\nsignature before but where the signature is no longer valid.\n\n> > This series was started because people wanted to be able to do\n> > things like keeping signature even when they are no longer valid or\n> > resigning commits that have a no longer commit signature (among other\n> > uses), but that would mean that if someone exports a sha1 repository\n> > and imports it as sha256, we don't want to ignore the fact that the\n> > sha1 commit was signed for those usecases.\n> >\n> > If, however, the <git-hash-algo>'s purpose is merely as a performance\n> > optimization that fast-import can employ in the cases where it checks\n> > for signatures being valid, so it can avoid checking when it know the\n> > hash size isn't even the same, then it could make sense.\n>\n> I think it could be used for that, but I'd like brian's opinion about this.\n\nNo, I was just wrong; it can't be used as such a performance\noptimization.  A signature of a sha256 commit stored in a sha1 commit\n(or a signature of a sha1 commit stored in a sha256 commit) isn't\naccidental, it's part of the intentional design of the hash-function\ntransition.\n\nBut, it might mean that the rules for what to do with signatures in\nfast-import need to be more complex than what I previously\nwrote...which I'll comment on a bit below.\n\n> > Wait..does this mean fast-export is obligated to walk over both all\n> > sha1 commits and all \"equivalent\" sha256 commits when exporting a\n> > repo?  I thought most operations on the repo would walk over only one\n> > or the other; walking over both seems to be against the spirit of the\n> > \"fast\" in \"fast-export\".  Am I missing something?  (Possibly related\n> > question: Does \"git log\" bother walking over both, or does it only\n> > walk over one?)  Even if this really is wanted by some users,\n> > shouldn't they manually request it rather than making exports slow for\n> > everyone else by default?\n>\n> No fast-export doesn't walk over both the sha1 commits and all\n> \"equivalent\" sha256 commits, it can just process at most 2 signatures\n> for a given commit, one with the `gpgsig` header and one with the\n> `gpgsig-sha256` header. I will try to reword the doc to make it\n> clearer.\n\nRight, but fast-import might need to *write* both sha1 commits and\nsha256 commits (or at least write one of them and keep a mapping\nbetween the two in memory) in order to correctly verify and process\nboth gpgsig headers...or at least, it will be unable to verify\nsignatures of compatibility objects if people do not have the\nappropriate extensions.compatObjectFormat config setting set within\nthe repository they are importing to.\n\nThis actually expands the usecases I mentioned previously a bit.\nThose usecases were:\n\n(A) Make fast-export include signatures, and make fast-import include\nthem unconditionally (even if invalid)\n(B) Similar to (A), but make *fast-import* check them and either error\nout or drop them if they become invalid\n(C) Simliar to (B), but make *fast-import* re-sign the commit if they\nbecome invalid\n(D) Similar to (A), but make *fast-import* re-sign the commit even if\nthe signature would have been valid\n\nCases (B) and (C) might either need special care or need to bifurcate\ninto additional options given that we can have two signatures on a\ncommit.  \"Validity of gpg signature stored in the commit\" now becomes\n\"Validity of gpg signatureS stored in the commit\".  Whereas before we\nonly considered the cases of \"no valid signatures\" and \"all valid\nsignatures\", we also need to worry about the case where one signature\nis valid and the other is either known to be invalid or simply cannot\nbe checked because this repo isn't set up to write compatibility\nobjects.\n\n> > > +A warning is\n> > > +emitted for each additional signature found.\n> >\n> > Why?  This seems odd to me.  Why not merely export them all, and let\n> > fast-import throw warnings or errors if it sees more than one and is\n> > not yet prepared to handle multiple signatures?\n>\n> I am not against implementing both export and import support for any\n> number of signatures, but it seems that Git in general doesn't support\n> more than one `gpgsig` and one `gpgsig-sha256` signature well, and\n> that's also what brian suggested supporting. It's also better than\n> just 1 signature max which is the current state.\n[...]\n> > If I've\n> > understood correctly that the restriction is merely due to the current\n> > implementation, perhaps the wording could be changed to not list it as\n> > an encoding restriction, but as a current fast-import limitation?\n>\n> If that current fast-import limitation was lifted, we should probably\n> still say that additional signatures might not be very well supported\n> by Git in general.\n\nAh!  I thought you were saying that fast-import specifically didn't\nhave good support for more than one signature (and I was assuming the\nrest of Git did) and were using that as the reason to avoid handling\nmore than 1.\n\nSorry about that, I'm following you on this point now.\n\n> > Also, I'm slightly uncomfortable with \"the SHA-1 object format\" and\n> > \"the SHA-256 object format\" because of the fact that these tools are\n> > used for interoperability with similarly-named tools from other VCSes.\n> > I think it'd be better to just treat them equally as \"here was/were\n> > some signature(s) on the object in the original repo; importers can\n> > choose what to do with them\".\n>\n> I don't think giving that information prevents tools from other VCSes\n> from doing what they want with the signatures.\n\nYeah, sorry, this was fallout from my misunderstanding about commits\nhaving signatures from their compatibility object.  That's just kind\nof fundamental to this hash-function transition, so ignore what I\nwrote here.\n\n> [...]\n>\n> > I briefly skimmed the implementation and test files, and didn't see\n> > any problems...but I think it probably makes more sense to get aligned\n> > on the goals of the format and how these fields are meant to be used\n> > before diving into those details closer.\n>\n> Yeah, I would have been happy if we could have been aligned with the\n> goals of the format and the fields earlier, but better late than\n> never.\n\nI think the proposal for the fields in this version make sense now,\nbut it might have been easier to get alignment if (1) we could get\nreal testcases of the multiple signature case, and (2) we could see\nthe actual code for fast-import to deal with the signatures (trying to\nguess the needs of the importer and the meanings of the fields we\nexport is supposed to be is harder when their usage has been left\nunimplemented).  That said, I also understand you wanting to avoid\nimplementing too much and throwing things away if we disagreed on\nearly decisions.\n\nAnyway, I'm on board with these new fields and their purpose, though\nI'm still curious if we start diverging when we run across surprises\nas we dig further into the implementation.\n"},{"id":"521484","messageId":"xmqqbjpv1ucb.fsf@gitster.g","threadId":"63342","inReplyTo":"20250619133630.727274-1-christian.couder@gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-07T22:58:44Z","receivedAt":"2025-07-07T22:58:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> This v4 is just about fixing a few bugs in the tests using the SHA-256\n> object format compared to the v3. (I had issues with CI tests on v3,\n> so I sent it without waiting for the results.)\n\nWe haven't heard much after a few comments were posted on this\nlatest round, since Elijah's\n<20250619133630.727274-1-christian.couder@gmail.com>; I understand\nthat it would be the author's turn to respond (the response does not\nnecessarily have to be with an updated iteration).  If so, let me\nmark the topic as Stalled in the draft of the latest issue of the\n\"What's cooking\" report.\n\nThanks.\n"},{"id":"521498","messageId":"CAP8UFD223ja7jKU+wb6TiGkc9frh5dt1rCJkOkk+O+J2MPokrw@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqbjpv1ucb.fsf@gitster.g","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-08T03:35:47Z","receivedAt":"2025-07-08T03:36:01Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Jul 8, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > This v4 is just about fixing a few bugs in the tests using the SHA-256\n> > object format compared to the v3. (I had issues with CI tests on v3,\n> > so I sent it without waiting for the results.)\n>\n> We haven't heard much after a few comments were posted on this\n> latest round, since Elijah's\n> <20250619133630.727274-1-christian.couder@gmail.com>; I understand\n> that it would be the author's turn to respond (the response does not\n> necessarily have to be with an updated iteration).  If so, let me\n> mark the topic as Stalled in the draft of the latest issue of the\n> \"What's cooking\" report.\n\nI will hopefully send a v5 later today.\n\nThanks.\n"},{"id":"521500","messageId":"xmqqwm8jxoj3.fsf@gitster.g","threadId":"63342","inReplyTo":"CAP8UFD223ja7jKU+wb6TiGkc9frh5dt1rCJkOkk+O+J2MPokrw@mail.gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-08T05:03:12Z","receivedAt":"2025-07-08T05:03:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> On Tue, Jul 8, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:\n>>\n>> Christian Couder <christian.couder@gmail.com> writes:\n>>\n>> > This v4 is just about fixing a few bugs in the tests using the SHA-256\n>> > object format compared to the v3. (I had issues with CI tests on v3,\n>> > so I sent it without waiting for the results.)\n>>\n>> We haven't heard much after a few comments were posted on this\n>> latest round, since Elijah's\n>> <20250619133630.727274-1-christian.couder@gmail.com>; I understand\n>> that it would be the author's turn to respond (the response does not\n>> necessarily have to be with an updated iteration).  If so, let me\n>> mark the topic as Stalled in the draft of the latest issue of the\n>> \"What's cooking\" report.\n>\n> I will hopefully send a v5 later today.\n\nThanks.\n\nBy the way, I noticed that you often do not respond to reviews until\nthe last minute, at the same time as when you send your next\niteration, or even soon after doing so.\n\nThat is quite different from how other contributors operate, i.e.\nrespond and engage in discussions triggered by the reviews, and\nafter people involved in discussion got an (even rough) idea of what\nthe right next step would be, if not a total consensus, send the\nnext iteration.\n\nI do not know which style is more efficient form of cooperation, but\nit somewhat makes my job harder, if I do not hear much _heartbeats_\nafter I see review comments on the list.  I do not mind waiting for\nseeing the next round for quite a while---after all, any substantial\n(re)work takes time.  And responding to reviews may need thinking\nthings through carefully, which may take some time, so I would not\ndemand an immediate response, either.  But it would be nearly\nimpossible to feel the current status of such a topic---a few review\ncomments are seen, the author goes silent for a while, we cannot\ntell if the author is working on a new iteration or where the author\nand reviewers agree and disagree.\n\nAlso a review response that comes at the same time or immediately\nafter a new iteration is already sent out makes it look like the\nauthor is refusing to continue discussion and reviewers are not\nwelcome to make follow-up suggestions during such a discussion.\n\nInstead, the next iteration comes as a fait accompli, and makes it\nless useful to continue the review discussion on the previous round\nby responding to such a late response.\n\n"},{"id":"521504","messageId":"aGy82TiRFcij5V_9@pks.im","threadId":"63342","inReplyTo":"xmqqwm8jxoj3.fsf@gitster.g","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-08T06:38:17Z","receivedAt":"2025-07-08T06:38:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Jul 07, 2025 at 10:03:12PM -0700, Junio C Hamano wrote:\n> Christian Couder <christian.couder@gmail.com> writes:\n> \n> > On Tue, Jul 8, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:\n> >>\n> >> Christian Couder <christian.couder@gmail.com> writes:\n> >>\n> >> > This v4 is just about fixing a few bugs in the tests using the SHA-256\n> >> > object format compared to the v3. (I had issues with CI tests on v3,\n> >> > so I sent it without waiting for the results.)\n> >>\n> >> We haven't heard much after a few comments were posted on this\n> >> latest round, since Elijah's\n> >> <20250619133630.727274-1-christian.couder@gmail.com>; I understand\n> >> that it would be the author's turn to respond (the response does not\n> >> necessarily have to be with an updated iteration).  If so, let me\n> >> mark the topic as Stalled in the draft of the latest issue of the\n> >> \"What's cooking\" report.\n> >\n> > I will hopefully send a v5 later today.\n> \n> Thanks.\n> \n> By the way, I noticed that you often do not respond to reviews until\n> the last minute, at the same time as when you send your next\n> iteration, or even soon after doing so.\n> \n> That is quite different from how other contributors operate, i.e.\n> respond and engage in discussions triggered by the reviews, and\n> after people involved in discussion got an (even rough) idea of what\n> the right next step would be, if not a total consensus, send the\n> next iteration.\n> \n> I do not know which style is more efficient form of cooperation, but\n> it somewhat makes my job harder, if I do not hear much _heartbeats_\n> after I see review comments on the list.  I do not mind waiting for\n> seeing the next round for quite a while---after all, any substantial\n> (re)work takes time.  And responding to reviews may need thinking\n> things through carefully, which may take some time, so I would not\n> demand an immediate response, either.  But it would be nearly\n> impossible to feel the current status of such a topic---a few review\n> comments are seen, the author goes silent for a while, we cannot\n> tell if the author is working on a new iteration or where the author\n> and reviewers agree and disagree.\n> \n> Also a review response that comes at the same time or immediately\n> after a new iteration is already sent out makes it look like the\n> author is refusing to continue discussion and reviewers are not\n> welcome to make follow-up suggestions during such a discussion.\n> \n> Instead, the next iteration comes as a fait accompli, and makes it\n> less useful to continue the review discussion on the previous round\n> by responding to such a late response.\n\nI agree with your points. Overall, a fast response cycle is key to good\ncollaboration from my point of view. I think it not only makes your life\nas a maintainer easier, but it also makes the reviewer feel like they\nare being heard and is the prerequisite for good discussion.\n\nOn our team's handbook page [1] we have the following couple of bullet\npoints regarding how to respond to reviews:\n\n  * Respond to feedback that you have received as fast as possible. A\n    fast exchange is a prerequisite for a fruitful discussion and\n    ensures that you keep momentum.\n\n  * On the other hand, it shouldn’t be necessary to respond to every\n    small typo correction in case you will send out the next version\n    soon anyway. If it will take a while before you send the next\n    version though it is nice to acknowledge nits, but mention that you\n    will hold off sending a new version of the series until you got more\n    feedback.\n\n  * Consider the viewpoint of the other person and be ready to disagree and\n    commit. Do not ignore feedback that you have received, as that will lead\n    to frustration and a decreased likelihood for that person to review your\n    future patch series.\n\nPatrick\n\n[1]: https://handbook.gitlab.com/handbook/engineering/infrastructure-platforms/data-access/git/#iteration\n"},{"id":"521519","messageId":"CAP8UFD0aXSvepjvwHu_dDcoEn5j6p=HxSEMgftotiZg3B1Hzmw@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqbjqjdbmj.fsf@gitster.g","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-08T09:16:34Z","receivedAt":"2025-07-08T09:16:48Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Jun 19, 2025 at 4:55 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > This v4 is just about fixing a few bugs in the tests using the SHA-256\n> > object format compared to the v3. (I had issues with CI tests on v3,\n> > so I sent it without waiting for the results.)\n>\n> Thanks.\n>\n> I am not sure if \"I am happy is either 1 or 256\" is what you really\n> want, though.  The test presumably knows what algorithm is being\n> used during its run, so wouldn't you want to say more like \"I know I\n> used sha256, and I expect seeing sha256, ah, I see sha256 and even\n> better I see no sha1, so I am very happy\"?\n\nYeah, I agree it's better to have tests say things like \"I know I used\nsha256, and I expect to see sha256\". So in v5 tests now use:\n\n  test_grep -E \"^gpgsig $GIT_DEFAULT_HASH x509\" output\n\nas \"$GIT_DEFAULT_HASH\" should be either \"sha1\" or \"sha256\" depending\non the current hash.\n\nI am not sure \"and even better I see no sha1\" is worth it then though,\nso I haven't added that.\n\n> > There are no tests in this v4 and in v3 with both a SHA-1 and a\n> > SHA-256 signature on the same commit though, as I am not sure yet how\n> > to best generate a commit with such signatures. Suggestions welcome!\n>\n> Good point to fill potential gaps.  If we had such a commit, then\n> would these tests say \"I know I want both 1 and 256, and I do see\n> one instance each of 1 and 256, so I am happy\"?\n\nThere is a test with such a commit in the v5 I am about to send and\nyeah it checks that there is one instance of 1 and 256.\n"},{"id":"521520","messageId":"CAP8UFD1M2UTnqpd5KG=GQfofUiJL3Z3ZvJBiTe2nZP4a2i68Ow@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqfrfu2pa1.fsf@gitster.g","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-08T09:16:47Z","receivedAt":"2025-07-08T09:17:01Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Jun 20, 2025 at 9:20 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> tldr; 2 brief requests.  Please\n>\n>  - Be gentle to people and expect that it is normal for them to be\n>    off of the list for a few weeks (or even more), not able to give\n>    a timely comments;\n>\n>  - Fully stand behind your own patch (unless it is an RFC), even if\n>    some of the idea came from elsewhere.\n\nOk I will keep these in mind.\n\n> >> Why would it need to say what type of signature it is?  Don't the\n> >> ascii armor lines have e.g. \"----BEGIN PGP SIGNATURE----\" and \"----END\n> >> PGP SIGNATURE----\" around it, which fast-import can read as well as\n> >> fast-export?  Is the idea that we strip those lines and now need to\n> >> replace the information we lost?\n> >\n> > In https://lore.kernel.org/git/aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net/\n> > brian said:\n> >\n> > \"These should be separate fields: one for the hash algorithm and one for\n> > the protocol.  Alternatively, we can just keep the hash algorithm field\n> > and parse the protocol by reading the first line, which will differ for\n> > different protocols.\"\n> >\n> > It would have been nice if you had then said that you prefer not to\n> > have the protocol.\n>\n> Let's remember to be gentle for those who give varluable feedbacks\n> but may not be always on this list.  A late comment on a topic that\n> has not hit 'next' is much better than a late comment after the\n> topic hits 'next', or no comment at all.\n\nI agree that the comment was valuable, sorry if it appeared otherwise.\n\n> Also, even if the idea came from somebody else, if you agreed to the\n> idea and made it part of your submission, then it would be better to\n> explain it in your own words, in the most appropriate way to answer\n> the question asked (e.g. the original from Brian and the question by\n> Elijah may have stress on different aspect of the problem).\n>\n> > My opinion was that it was better for tools processing fast-export\n> > output to have the protocol as they have to parse the \"gppsig ...\"\n> > line anyway. So it should be easier for them than to parse the ascii\n> > armor lines.\n>\n> And if you do not agree with Brian's, perhaps discuss a bit more to\n> (1) either convince yourself that Brian's idea is better and rewrite\n> your code to adopt the idea, or (2) explain the reason why your \"the\n> importer reads and parses anyway\" is better design and stick to it.\n\nI actually agreed with brian about \"These should be separate fields:\none for the hash algorithm and one for the protocol.\" Anyway it seems\nthat we all agree now on this.\n"},{"id":"521521","messageId":"CAP8UFD0Xi+9rA+M_0j0gGpyLTmLYB2u5PT2--cRFqqyrV4NFYg@mail.gmail.com","threadId":"63342","inReplyTo":"CABPp-BE8b8Ju3zaB04DzQJoSKwNkHcHTw8q95ux_bb4hOpboBg@mail.gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-08T09:16:59Z","receivedAt":"2025-07-08T09:17:14Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Jun 26, 2025 at 9:12 PM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Fri, Jun 20, 2025 at 9:12 AM Christian Couder\n> <christian.couder@gmail.com> wrote:\n\n> > In https://lore.kernel.org/git/aD4i7YhUnT5Kgew-@tapette.crustytoothpaste.net/\n> > brian said:\n> >\n> > \"Actually, what I was saying is that we should have one for the hash\n> > algorithm that is used in the Git object.  I don't care about the hash\n> > algorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's\n> > signed with SHA-512 or SHA-256), but we can have multiple signatures in\n> > a single commit such that there's both a SHA-1 signature and a SHA-256\n> > signature.\"\n> >\n> > so I implemented the possibility that there's both a SHA-1 signature\n> > and a SHA-256 signature in a single commit.\n> >\n> > If you disagreed with what brian suggested, it would have been nice to\n> > reply to brian then.\n>\n> Sorry about that.  I hadn't read the whole thread; further, I was\n> previously trying to avoid the details of signatures beyond the basics\n> and hoping to leave those details to others, but between v2 and some\n> words you had in this version plus deciding to just take a closer look\n> while on vacation (as I was last week), I decided to try to look a bit\n> closer.\n\nThanks for taking a closer look.\n\n> Turns out, though, that despite my attempts to read a bit more\n> documentation and code in the project, I still had a fundamental\n> misunderstanding last week when I was responding.  I had been assuming\n> that the gpgsig headers would appear on sha1-commits and gpgsig-sha256\n> headers would appear on sha256 commits.  I didn't understand that both\n> types of headers can appear on both types of commits (so that, for\n> example, gpgsig-sha256 would be use the sha256-commit as the source\n> and sign that data but write the result to a sha1-commit). I talked\n> with brian this morning to ask a few questions and he clarified this\n> for me.\n>\n> (Side note: It might be nice to clarify this in\n> Documentation/technical/hash-function-transition.adoc; that document\n> did not dispel this confusion when I read it last week.)\n\nI agree that it would help to improve that document. I think it's a\nseparate topic though.\n\n> And the part I didn't understand the first time around is that e.g.\n> the SHA-1 commit can include both the signatures on the SHA-1 object\n> and on the SHA-256 object.  (Similarly, the SHA-256 commit can include\n> both signatures as well.)\n>\n> > > >   - if there is more than one signature on the SHA-1 object or on\n> > > >     the SHA-256 object, a warning is emitted,\n>\n> This is now ambiguous to me.  more than one signature using the SHA-1\n> as source for signing, or more than one signature recorded within the\n> SHA-1 commit?  I think you mean the former.\n\nYes, it's the former. I am not sure there is a simple and short way to\ndisambiguate these meanings.\n\n> > My opinion on this is that in cases like this we might not always know\n> > what could be useful for tools or users in general, so it might be\n> > better to provide more information that can be easily discarded if not\n> > useful rather than not enough information.\n> >\n> > brian seemed to say that <git-hash-algo> and <signature-format> are\n> > important so I just prefered to have both, especially as they are not\n> > costly to get.\n>\n> So, with my new understanding, <git-hash-algo> is necessary because if\n> you only had the gpg signature on a commit object, you don't know what\n> it was a signature of -- it might be a signature of that commit\n> object, or it might be a signature of it's \"compatibility\" object\n> created with a different git hashing algorithm.\n\nI agree that it's important for this reason.\n\n> So, fast-import --\n> regardless of whether it is writing into a sha1 repo or a sha256 repo\n> -- won't be able to know how to check the validity of the gpg\n> signature unless it knows whether to check the signature of the sha1\n> commit or the sha256 commit. And if the repository that fast-import> is writing to isn't writing compatibility objects for interoperation\n> at all, then it won't be able to check any compatibility signatures at\n> all; it'll only be able to check signatures of the actual object it\n> wants to write.\n\nYeah, right.\n\n> > > Is the <signature-format> merely self-inflicted pain from stripping\n> > > the ascii armor lines?  If so, would it make more sense to just\n> > > include those armor lines as-is in the fast-export stream and let\n> > > fast-import process it?\n> >\n> > The ascii armor lines are kept in the fast-export stream, but\n> > fast-export's ouput is supposed to be processed somehow sometimes\n> > before being fed back to fast-import, so I think we should make it\n> > easy for tools or people processing the output to get signature\n> > information.\n>\n> Ah, just for convenience of the importer/processor; fair enough.\n\nYeah, right.\n\n> > > Is the <git-hash-algo> due to the fact that we have separate `gpgsig`\n> > > and `gpgsig-sha256` commit headers and we want to use that information\n> > > to avoid writing these headers to the wrong-sized objects (and/or to\n> > > avoid checking whether the signature is valid on the wrong-sized\n> > > objects)?\n> >\n> > Yeah, I think it could help with that, but brian might better answer that.\n>\n> I think the answer is actually no, this isn't at all what these are\n> about.  These are about knowing how to check the validity of the\n> signatures, because the signatures aren't necessarily associated with\n> the object they were read from; they could be a signature of its\n> compatibility object instead.\n\nFirst I am now not sure what \"wrong-sized objects\" meant in your\nprevious sentence. I thought it meant \"object using SHA-1 vs object\nusing SHA-256\". In that case, the <git-hash-algo> could indeed help\ncheck the signature on the right object.\n\nAlso git fast-import has to recreate the `gpgsig` and `gpgsig-sha256`\ncommit headers when importing signatures, so <git-hash-algo> can help\nit with that too.\n\n> > > If so, could that be spelled out in the docs as well,\n> > > especially since it appears that the intent of these headers is left\n> > > unimplemented due to not changing fast-import to do anything with\n> > > them?\n> >\n> > fast-import puts back the `gpgsig` or `gpgsig-sha256` headers\n> > depending on <git-hash-algo>, so it's useful at least for that. I will\n> > improve the docs about it.\n\nIn v5, I have improved the fast-import documentation with the following:\n\n* `<git-hash-algo>` specifies which Git object format this signature\n  applies to, either `sha1` or `sha256`. This allows to know which\n  representation of the commit was signed (the SHA-1 or the SHA-256\n  version) which helps with both signature verification and\n  interoperability between repos with different hash functions.\n\n* `<signature-format>` specifies the type of signature, such as\n  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for\n  tools that process the stream, so they don't have to parse the ASCII\n  armor to identify the signature type.\n\nThe commit message is improved too.\n\n> > > And if <git-hash-algo>'s purpose is to ensure they are only used when\n> > > writing same-sized object as what was exported, then...isn't that a\n> > > bug?\n> >\n> > I am not sure I understand what would be the bug.\n>\n> It's not relevant given my misunderstanding, but to explain what I was\n> thinking last week (note that the first two bullets are my mistake):\n>\n>   * gpgsig headers only occur on sha1 commits and are the signatures\n> of the sha1 commit sans the gpgsig header\n>   * gpgsig-sha256 headers are similar with respect to sha256 commits\n>   * we were using <git-hash-algo> as an optimization to only sign\n> commits in fast-import if the repository we were writing to was using\n> the same hash function as the repo we were writing from\n>\n> In such a case, that'd be a bug for anyone that wanted a\n> 'resign-commits-that-were-previously-signed-if-the-signature-becomes-invalid'.\n> Although a signature of a sha256 commit clearly won't verify if we\n> exported a sha256 repo and imported it as a sha1 repo and tried to\n> verify that signature using a sha1 commit, that doesn't mean we should\n> just ignore the fact that the sha256 commit had a signature.  The user\n> clearly expressed the intent to resign any commits that had a\n> signature before but where the signature is no longer valid.\n\nI see.\n\n> > > This series was started because people wanted to be able to do\n> > > things like keeping signature even when they are no longer valid or\n> > > resigning commits that have a no longer commit signature (among other\n> > > uses), but that would mean that if someone exports a sha1 repository\n> > > and imports it as sha256, we don't want to ignore the fact that the\n> > > sha1 commit was signed for those usecases.\n> > >\n> > > If, however, the <git-hash-algo>'s purpose is merely as a performance\n> > > optimization that fast-import can employ in the cases where it checks\n> > > for signatures being valid, so it can avoid checking when it know the\n> > > hash size isn't even the same, then it could make sense.\n> >\n> > I think it could be used for that, but I'd like brian's opinion about this.\n>\n> No, I was just wrong; it can't be used as such a performance\n> optimization.  A signature of a sha256 commit stored in a sha1 commit\n> (or a signature of a sha1 commit stored in a sha256 commit) isn't\n> accidental, it's part of the intentional design of the hash-function\n> transition.\n>\n> But, it might mean that the rules for what to do with signatures in\n> fast-import need to be more complex than what I previously\n> wrote...which I'll comment on a bit below.\n\nOk.\n\n> > > Wait..does this mean fast-export is obligated to walk over both all\n> > > sha1 commits and all \"equivalent\" sha256 commits when exporting a\n> > > repo?  I thought most operations on the repo would walk over only one\n> > > or the other; walking over both seems to be against the spirit of the\n> > > \"fast\" in \"fast-export\".  Am I missing something?  (Possibly related\n> > > question: Does \"git log\" bother walking over both, or does it only\n> > > walk over one?)  Even if this really is wanted by some users,\n> > > shouldn't they manually request it rather than making exports slow for\n> > > everyone else by default?\n> >\n> > No fast-export doesn't walk over both the sha1 commits and all\n> > \"equivalent\" sha256 commits, it can just process at most 2 signatures\n> > for a given commit, one with the `gpgsig` header and one with the\n> > `gpgsig-sha256` header. I will try to reword the doc to make it\n> > clearer.\n\nIn v5 all the signatures are exported, so the doc is now:\n\n\"While all the signatures of a commit are exported, an importer may\nchoose to accept only some of them. For example\nlinkgit:git-fast-import[1] currently stores at most one signature per\nGit hash algorithm in each commit.\"\n\nI hope it avoids misunderstandings.\n\n> Right, but fast-import might need to *write* both sha1 commits and\n> sha256 commits (or at least write one of them and keep a mapping\n> between the two in memory) in order to correctly verify and process\n> both gpgsig headers...or at least, it will be unable to verify\n> signatures of compatibility objects if people do not have the\n> appropriate extensions.compatObjectFormat config setting set within\n> the repository they are importing to.\n\nYeah, it seems to me that when extensions.compatObjectFormat is set,\nthen a compatibility mapping between objects can be used. I don't\nthink using it should be part of this patch though. When I will work\non making it possible for fast-import to check signatures, I will try\nto use it to write both sha1 commits and sha256 commits, and check\nboth signatures.\n\n> This actually expands the usecases I mentioned previously a bit.\n> Those usecases were:\n>\n> (A) Make fast-export include signatures, and make fast-import include\n> them unconditionally (even if invalid)\n> (B) Similar to (A), but make *fast-import* check them and either error\n> out or drop them if they become invalid\n> (C) Simliar to (B), but make *fast-import* re-sign the commit if they\n> become invalid\n> (D) Similar to (A), but make *fast-import* re-sign the commit even if\n> the signature would have been valid\n>\n> Cases (B) and (C) might either need special care or need to bifurcate\n> into additional options given that we can have two signatures on a\n> commit.  \"Validity of gpg signature stored in the commit\" now becomes\n> \"Validity of gpg signatureS stored in the commit\".  Whereas before we\n> only considered the cases of \"no valid signatures\" and \"all valid\n> signatures\", we also need to worry about the case where one signature\n> is valid and the other is either known to be invalid or simply cannot\n> be checked because this repo isn't set up to write compatibility\n> objects.\n\nYeah, I agree that there might be new cases to consider. I don't think\nthis affects this patch though as long as it allows one signature for\neach hash to be exported and imported which is the case.\n\n> > Yeah, I would have been happy if we could have been aligned with the\n> > goals of the format and the fields earlier, but better late than\n> > never.\n>\n> I think the proposal for the fields in this version make sense now,\n> but it might have been easier to get alignment if (1) we could get\n> real testcases of the multiple signature case,\n\nThere is such a multiple signature test case in v5.\n\n> and (2) we could see\n> the actual code for fast-import to deal with the signatures (trying to\n> guess the needs of the importer and the meanings of the fields we\n> export is supposed to be is harder when their usage has been left\n> unimplemented).  That said, I also understand you wanting to avoid\n> implementing too much and throwing things away if we disagreed on\n> early decisions.\n\nYeah, I prefer to move forward step by step on this.\n\n> Anyway, I'm on board with these new fields and their purpose, though\n> I'm still curious if we start diverging when we run across surprises\n> as we dig further into the implementation.\n\nAs we have clearly warned that the current implementation is\nexperimental, I think we should have more flexibility to adapt the\nformats and behaviors if we run across surprises in the next steps.\n\nThanks for your thoughts and reviews.\n"},{"id":"521522","messageId":"20250708091738.4072857-1-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250619133630.727274-1-christian.couder@gmail.com","subject":"[PATCH v5] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-08T09:17:38Z","receivedAt":"2025-07-08T09:18:03Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\nsigned-commits, 2025-03-10), added support for signed commits to\nfast-export and fast-import.\n\nWhen a signed commit is processed, fast-export can output either\n\"gpgsig sha1\" or \"gpgsig sha256\" depending on whether the signed\ncommit uses the SHA-1 or SHA-256 Git object format.\n\nHowever, this implementation has a number of limitations:\n\n  - the output format was not properly described in the documentation,\n  - the output format is not very informative as it doesn't even say\n    if the signature is an OpenPGP, an SSH, or an X509 signature,\n  - the implementation doesn't support having both one signature on\n    the SHA-1 object and one on the SHA-256 object.\n\nLet's improve on these limitations by improving fast-export and\nfast-import so that:\n\n  - all the signatures are exported,\n  - at most one signature on the SHA-1 object and one on the SHA-256\n    are imported,\n  - if there is more than one signature on the SHA-1 object or on\n    the SHA-256 object, fast-import emits a warning for each\n    additional signature,\n  - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n    where <git-hash-algo> is the Git object format as before, and\n    <signature-format> is the signature type (\"openpgp\", \"x509\",\n    \"ssh\" or \"unknown\"),\n  - the output is properly documented.\n\nAbout the output format:\n\n  - <git-hash-algo> allows to know which representation of the commit\n    was signed (the SHA-1 or the SHA-256 version) which helps with\n    both signature verification and interoperability between repos\n    with different hash functions,\n\n  - <signature-format> helps tools that process the fast-export\n    stream, so they don't have to parse the ASCII armor to identify\n    the signature type.\n\nIt could be even better to be able to import more than one signature\non the SHA-1 object and on the SHA-256 object, but other parts of\nGit don't handle that well for now, so this is left for future\nimprovements.\n\nHelped-by: brian m. carlson <sandals@crustytoothpaste.net>\nHelped-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n\nThis v5 is similar in spirit to v4, especially the format of the\n\"gpgsig ...\" command didn't change, but there are a number of\nimprovements:\n\n  - All the signatures are now exported. On the import side, still at\n    most one signature for SHA-1 and one for SHA-256 are imported (and\n    a warning is still emitted for additional signatures) though.\n\n  - The code makes sure each signature ends with a LF character. While\n    this is not mandatory, it is encouraged and makes the output more\n    human readable and 'grep'able.\n\n  - A test with both a SHA-1 and a SHA-256 signature on the same\n    commit has been added.\n\n  - Some tests check that either \"sha1\" or \"sha256\" is in the \"gpgsig\n    ...\"  command instead of matching \"sha(1|256)\".\n\n  - The format of the \"gpgsig ...\" command is better explained both in\n    the commit message and in the fast-import documentation.\n\n  - There are some typo fixes, lines wrapped, and a few other such\n    small changes.\n\nThanks to brian, Elijah and Junio who commented on the v1 and v2.\n\nCI tests:\n\nThey have all passed:\n\nhttps://github.com/chriscool/git/actions/runs/16136587558\n\nRange-diff with v4:\n\n1:  b3d8b13c0e ! 1:  5791617d7d fast-(import|export): improve on commit signature output format\n    @@ Commit message\n         Let's improve on these limitations by improving fast-export and\n         fast-import so that:\n     \n    -      - both one signature on the SHA-1 object and one on the SHA-256\n    -        object can be exported and imported,\n    +      - all the signatures are exported,\n    +      - at most one signature on the SHA-1 object and one on the SHA-256\n    +        are imported,\n           - if there is more than one signature on the SHA-1 object or on\n    -        the SHA-256 object, a warning is emitted,\n    +        the SHA-256 object, fast-import emits a warning for each\n    +        additional signature,\n           - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n             where <git-hash-algo> is the Git object format as before, and\n             <signature-format> is the signature type (\"openpgp\", \"x509\",\n    -        \"ssh\" or \"unknown\",\n    +        \"ssh\" or \"unknown\"),\n           - the output is properly documented.\n     \n    -    Note that it could be even better to be able to export and import\n    -    more than one signature on the SHA-1 object and on the SHA-256\n    -    object, but other parts of Git don't handle that well for now, so\n    -    this is left for future improvements.\n    +    About the output format:\n    +\n    +      - <git-hash-algo> allows to know which representation of the commit\n    +        was signed (the SHA-1 or the SHA-256 version) which helps with\n    +        both signature verification and interoperability between repos\n    +        with different hash functions,\n    +\n    +      - <signature-format> helps tools that process the fast-export\n    +        stream, so they don't have to parse the ASCII armor to identify\n    +        the signature type.\n    +\n    +    It could be even better to be able to import more than one signature\n    +    on the SHA-1 object and on the SHA-256 object, but other parts of\n    +    Git don't handle that well for now, so this is left for future\n    +    improvements.\n     \n         Helped-by: brian m. carlson <sandals@crustytoothpaste.net>\n         Helped-by: Elijah Newren <newren@gmail.com>\n    @@ Documentation/git-fast-export.adoc: resulting tag will have an invalid signature\n     +`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit\n     +starts with `gpgsig sha256 ssh`.\n     ++\n    -+Currently for a given commit, at most one signature for the SHA-1\n    -+object and one signature for the SHA-256 object are exported, each\n    -+with their respective <git-hash-algo> identifier. A warning is\n    -+emitted for each additional signature found.\n    ++While all the signatures of a commit are exported, an importer may\n    ++choose to accept only some of them. For example\n    ++linkgit:git-fast-import[1] currently stores at most one signature per\n    ++Git hash algorithm in each commit.\n     ++\n      NOTE: This is highly experimental and the format of the data stream may\n      change in the future without compatibility guarantees.\n    @@ Documentation/git-fast-import.adoc: their syntax.\n     +The `gpgsig` command takes two arguments:\n     +\n     +* `<git-hash-algo>` specifies which Git object format this signature\n    -+  applies to, either `sha1` or `sha256`.\n    ++  applies to, either `sha1` or `sha256`. This allows to know which\n    ++  representation of the commit was signed (the SHA-1 or the SHA-256\n    ++  version) which helps with both signature verification and\n    ++  interoperability between repos with different hash functions.\n     +\n     +* `<signature-format>` specifies the type of signature, such as\n    -+  `openpgp`, `x509`, `ssh`, or `unknown`.\n    ++  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for\n    ++  tools that process the stream, so they don't have to parse the ASCII\n    ++  armor to identify the signature type.\n     +\n     +A commit may have at most one signature for the SHA-1 object format\n     +(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n    @@ builtin/fast-export.c: static const char *find_commit_multiline_header(const cha\n     +\tif (!signature)\n     +\t\treturn;\n     +\n    -+\tprintf(\"gpgsig %s %s\\ndata %u\\n%s\",\n    ++\tprintf(\"gpgsig %s %s\\ndata %u\\n%s\\n\",\n     +\t       object_hash,\n     +\t       get_signature_format(signature),\n     +\t       (unsigned)strlen(signature),\n     +\t       signature);\n     +}\n     +\n    -+static void warn_on_extra_sig(const char **pos, struct commit *commit, int is_sha1)\n    ++static const char *append_signatures_for_header(struct string_list *signatures,\n    ++\t\t\t\t\t\tconst char *pos,\n    ++\t\t\t\t\t\tconst char *header,\n    ++\t\t\t\t\t\tconst char *object_hash)\n     +{\n    -+\tconst char *header = is_sha1 ? \"gpgsig\" : \"gpgsig-sha256\";\n    -+\tconst char *extra_sig = find_commit_multiline_header(*pos + 1, header, pos);\n    -+\tif (extra_sig) {\n    -+\t\tconst char *hash = is_sha1 ? \"SHA-1\" : \"SHA-256\";\n    -+\t\twarning(\"more than one %s signature found on commit %s, using only the first one\",\n    -+\t\t\thash, oid_to_hex(&commit->object.oid));\n    -+\t\tfree((char *)extra_sig);\n    ++\tconst char *signature;\n    ++\tconst char *start = pos;\n    ++\tconst char *end = pos;\n    ++\n    ++\twhile ((signature = find_commit_multiline_header(start + 1,\n    ++\t\t\t\t\t\t\t header,\n    ++\t\t\t\t\t\t\t &end))) {\n    ++\t\tstring_list_append(signatures, signature)->util = (void *)object_hash;\n    ++\t\tfree((char *)signature);\n    ++\t\tstart = end;\n     +\t}\n    ++\n    ++\treturn end;\n     +}\n     +\n      static void handle_commit(struct commit *commit, struct rev_info *rev,\n    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\n      \tconst char *encoding = NULL;\n      \tsize_t encoding_len;\n     -\tconst char *signature_alg = NULL, *signature = NULL;\n    -+\tconst char *sig_sha1 = NULL;\n    -+\tconst char *sig_sha256 = NULL;\n    ++\tstruct string_list signatures = STRING_LIST_INIT_DUP;\n      \tconst char *message;\n      \tchar *reencoded = NULL;\n      \tstruct commit_list *p;\n    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\n     -\t\t\tsignature_alg = \"sha1\";\n     -\t\telse if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n     -\t\t\tsignature_alg = \"sha256\";\n    -+\t\tconst char *sig_cursor = commit_buffer_cursor;\n    -+\t\tconst char *after_sha1 = commit_buffer_cursor;\n    -+\t\tconst char *after_sha256 = commit_buffer_cursor;\n    -+\n    -+\t\t/*\n    -+\t\t * Find the first signature for each hash algorithm.\n    -+\t\t * The searches must start from the same position.\n    -+\t\t */\n    -+\t\tsig_sha1 = find_commit_multiline_header(sig_cursor + 1,\n    -+\t\t\t\t\t\t\t\"gpgsig\",\n    -+\t\t\t\t\t\t\t&after_sha1);\n    -+\t\tsig_sha256 = find_commit_multiline_header(sig_cursor + 1,\n    -+\t\t\t\t\t\t\t  \"gpgsig-sha256\",\n    -+\t\t\t\t\t\t\t  &after_sha256);\n    -+\n    -+\t\t/* Warn on any additional signatures, as they will be ignored. */\n    -+\t\tif (sig_sha1)\n    -+\t\t\twarn_on_extra_sig(&after_sha1, commit, 1);\n    -+\t\tif (sig_sha256)\n    -+\t\t\twarn_on_extra_sig(&after_sha256, commit, 0);\n    -+\n    ++\t\tconst char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n    ++\t\t\t\t\t\t\t\t      \"gpgsig\", \"sha1\");\n    ++\t\tconst char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n    ++\t\t\t\t\t\t\t\t\t\"gpgsig-sha256\", \"sha256\");\n     +\t\tcommit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;\n      \t}\n      \n    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\n      \t       (int)(author_end - author), author,\n      \t       (int)(committer_end - committer), committer);\n     -\tif (signature) {\n    -+\tif (sig_sha1 || sig_sha256) {\n    ++\tif (signatures.nr) {\n      \t\tswitch (signed_commit_mode) {\n      \t\tcase SIGN_ABORT:\n      \t\t\tdie(\"encountered signed commit %s; use \"\n    -@@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct rev_info *rev,\n    - \t\t\t\toid_to_hex(&commit->object.oid));\n    + \t\t\t    \"--signed-commits=<mode> to handle it\",\n    + \t\t\t    oid_to_hex(&commit->object.oid));\n    + \t\tcase SIGN_WARN_VERBATIM:\n    +-\t\t\twarning(\"exporting signed commit %s\",\n    +-\t\t\t\toid_to_hex(&commit->object.oid));\n    ++\t\t\twarning(\"exporting %\"PRIuMAX\" signature(s) for commit %s\",\n    ++\t\t\t\t(uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));\n      \t\t\t/* fallthru */\n      \t\tcase SIGN_VERBATIM:\n     -\t\t\tprintf(\"gpgsig %s\\ndata %u\\n%s\",\n     -\t\t\t       signature_alg,\n     -\t\t\t       (unsigned)strlen(signature),\n     -\t\t\t       signature);\n    -+\t\t\tprint_signature(sig_sha1, \"sha1\");\n    -+\t\t\tprint_signature(sig_sha256, \"sha256\");\n    ++\t\t\tfor (size_t i = 0; i < signatures.nr; i++) {\n    ++\t\t\t\tstruct string_list_item *item = &signatures.items[i];\n    ++\t\t\t\tprint_signature(item->string, item->util);\n    ++\t\t\t}\n      \t\t\tbreak;\n      \t\tcase SIGN_WARN_STRIP:\n     -\t\t\twarning(\"stripping signature from commit %s\",\n    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\n      \t\t\tbreak;\n      \t\t}\n     -\t\tfree((char *)signature);\n    -+\t\tfree((char *)sig_sha1);\n    -+\t\tfree((char *)sig_sha256);\n    ++\t\tstring_list_clear(&signatures, 0);\n      \t}\n      \tif (!reencoded && encoding)\n      \t\tprintf(\"encoding %.*s\\n\", (int)encoding_len, encoding);\n    @@ builtin/fast-import.c: static struct hash_list *parse_merge(unsigned int *count)\n     +\t\t\t    const char *hash_type)\n     +{\n     +\tif (stored_sig->hash_algo) {\n    -+\t\twarning(\"Multiple %s signatures found, ignoring additional signature\",\n    ++\t\twarning(\"multiple %s signatures found, \"\n    ++\t\t\t\"ignoring additional signature\",\n     +\t\t\thash_type);\n     +\t\tstrbuf_release(&new_sig->data);\n     +\t\tfree(new_sig->hash_algo);\n    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=abort' '\n      \n      \tgit fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&\n     -\tgrep \"^gpgsig sha\" output &&\n    -+\ttest_grep -E \"^gpgsig sha(1|256) openpgp\" output &&\n    ++\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n      \tgrep \"encoding ISO-8859-1\" output &&\n      \t(\n      \t\tcd new &&\n    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=verbatim' '\n      \n      \tgit fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&\n     -\tgrep \"^gpgsig sha\" output &&\n    -+\ttest_grep -E \"^gpgsig sha(1|256) openpgp\" output &&\n    ++\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n      \tgrep \"encoding ISO-8859-1\" output &&\n      \ttest -s err &&\n      \t(\n    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '\n     +test_expect_success GPGSM 'round-trip X.509 signed commit' '\n     +\n     +\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n    -+\ttest_grep -E \"^gpgsig sha(1|256) x509\" output &&\n    ++\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH x509\" output &&\n     +\t(\n     +\t\tcd new &&\n     +\t\tgit fast-import &&\n    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '\n     +test_expect_success GPGSSH 'round-trip SSH signed commit' '\n     +\n     +\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n    -+\ttest_grep -E \"^gpgsig sha(1|256) ssh\" output &&\n    ++\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH ssh\" output &&\n     +\t(\n     +\t\tcd new &&\n     +\t\tgit fast-import &&\n    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '\n      test_expect_success 'setup submodule' '\n      \n      \ttest_config_global protocol.file.allow always &&\n    +@@ t/t9350-fast-export.sh: test_expect_success 'fast-export handles --end-of-options' '\n    + \ttest_cmp expect actual\n    + '\n    + \n    ++test_expect_success GPG 'setup a commit with dual 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    ++\techo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&\n    ++\ttest_grep -E \"^gpgsig \" out &&\n    ++\ttest_grep -E \"^gpgsig-sha256 \" out\n    ++'\n    ++\n    ++test_expect_success GPG 'export and import of doubly signed commit' '\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    ++\n    ++\t(\n    ++\t\tcd new &&\n    ++\t\tgit fast-import &&\n    ++\t\tgit cat-file commit refs/heads/dual-signed >actual &&\n    ++\t\ttest_grep -E \"^gpgsig \" actual &&\n    ++\t\ttest_grep -E \"^gpgsig-sha256 \" actual &&\n    ++\t\tIMPORTED=$(git rev-parse refs/heads/dual-signed) &&\n    ++\t\tif test \"$GIT_DEFAULT_HASH\" = \"sha1\"\n    ++\t\tthen\n    ++\t\t\ttest $SHA1_B = $IMPORTED\n    ++\t\telse\n    ++\t\t\ttest $SHA256_B = $IMPORTED\n    ++\t\tfi\n    ++\t) <output\n    ++'\n    ++\n    + test_done\n\n\n Documentation/git-fast-export.adoc |  17 +++++\n Documentation/git-fast-import.adoc |  36 +++++++--\n builtin/fast-export.c              |  62 +++++++++++----\n builtin/fast-import.c              | 118 +++++++++++++++++++++++------\n gpg-interface.c                    |  12 +++\n gpg-interface.h                    |  12 +++\n t/t9350-fast-export.sh             | 102 ++++++++++++++++++++++++-\n 7 files changed, 315 insertions(+), 44 deletions(-)\n\ndiff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\nindex 43bbb4f63c..297b57bb2e 100644\n--- a/Documentation/git-fast-export.adoc\n+++ b/Documentation/git-fast-export.adoc\n@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.\n \tis the same as how earlier versions of this command without\n \tthis option behaved.\n +\n+When exported, a signature starts with:\n++\n+gpgsig <git-hash-algo> <signature-format>\n++\n+where <git-hash-algo> is the Git object hash so either \"sha1\" or\n+\"sha256\", and <signature-format> is the signature type, so \"openpgp\",\n+\"x509\", \"ssh\" or \"unknown\".\n++\n+For example, an OpenPGP signature on a SHA-1 commit starts with\n+`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit\n+starts with `gpgsig sha256 ssh`.\n++\n+While all the signatures of a commit are exported, an importer may\n+choose to accept only some of them. For example\n+linkgit:git-fast-import[1] currently stores at most one signature per\n+Git hash algorithm in each commit.\n++\n NOTE: This is highly experimental and the format of the data stream may\n change in the future without compatibility guarantees.\n \ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 250d866652..89dec1108f 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -445,7 +445,7 @@ one).\n \toriginal-oid?\n \t('author' (SP <name>)? SP LT <email> GT SP <when> LF)?\n \t'committer' (SP <name>)? SP LT <email> GT SP <when> LF\n-\t('gpgsig' SP <alg> LF data)?\n+\t('gpgsig' SP <algo> SP <format> LF data)?\n \t('encoding' SP <encoding> LF)?\n \tdata\n \t('from' SP <commit-ish> LF)?\n@@ -518,13 +518,37 @@ their syntax.\n ^^^^^^^^\n \n The optional `gpgsig` command is used to include a PGP/GPG signature\n-that signs the commit data.\n+or other cryptographic signature that signs the commit data.\n \n-Here <alg> specifies which hashing algorithm is used for this\n-signature, either `sha1` or `sha256`.\n+....\n+\t'gpgsig' SP <git-hash-algo> SP <signature-format> LF\n+\tdata\n+....\n+\n+The `gpgsig` command takes two arguments:\n+\n+* `<git-hash-algo>` specifies which Git object format this signature\n+  applies to, either `sha1` or `sha256`. This allows to know which\n+  representation of the commit was signed (the SHA-1 or the SHA-256\n+  version) which helps with both signature verification and\n+  interoperability between repos with different hash functions.\n+\n+* `<signature-format>` specifies the type of signature, such as\n+  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for\n+  tools that process the stream, so they don't have to parse the ASCII\n+  armor to identify the signature type.\n+\n+A commit may have at most one signature for the SHA-1 object format\n+(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n+(stored in the \"gpgsig-sha256\" header).\n+\n+See below for a detailed description of the `data` command which\n+contains the raw signature data.\n+\n+Signatures are not yet checked in the current implementation though.\n \n-NOTE: This is highly experimental and the format of the data stream may\n-change in the future without compatibility guarantees.\n+NOTE: This is highly experimental and the format of the `gpgsig`\n+command may change in the future without compatibility guarantees.\n \n `encoding`\n ^^^^^^^^^^\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex fcf6b00d5f..7b4e6a6e41 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -29,6 +29,7 @@\n #include \"quote.h\"\n #include \"remote.h\"\n #include \"blob.h\"\n+#include \"gpg-interface.h\"\n \n static const char *const fast_export_usage[] = {\n \tN_(\"git fast-export [<rev-list-opts>]\"),\n@@ -652,6 +653,38 @@ static const char *find_commit_multiline_header(const char *msg,\n \treturn strbuf_detach(&val, NULL);\n }\n \n+static void print_signature(const char *signature, const char *object_hash)\n+{\n+\tif (!signature)\n+\t\treturn;\n+\n+\tprintf(\"gpgsig %s %s\\ndata %u\\n%s\\n\",\n+\t       object_hash,\n+\t       get_signature_format(signature),\n+\t       (unsigned)strlen(signature),\n+\t       signature);\n+}\n+\n+static const char *append_signatures_for_header(struct string_list *signatures,\n+\t\t\t\t\t\tconst char *pos,\n+\t\t\t\t\t\tconst char *header,\n+\t\t\t\t\t\tconst char *object_hash)\n+{\n+\tconst char *signature;\n+\tconst char *start = pos;\n+\tconst char *end = pos;\n+\n+\twhile ((signature = find_commit_multiline_header(start + 1,\n+\t\t\t\t\t\t\t header,\n+\t\t\t\t\t\t\t &end))) {\n+\t\tstring_list_append(signatures, signature)->util = (void *)object_hash;\n+\t\tfree((char *)signature);\n+\t\tstart = end;\n+\t}\n+\n+\treturn end;\n+}\n+\n static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t\t\t  struct string_list *paths_of_changed_objects)\n {\n@@ -660,7 +693,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \tconst char *author, *author_end, *committer, *committer_end;\n \tconst char *encoding = NULL;\n \tsize_t encoding_len;\n-\tconst char *signature_alg = NULL, *signature = NULL;\n+\tstruct string_list signatures = STRING_LIST_INIT_DUP;\n \tconst char *message;\n \tchar *reencoded = NULL;\n \tstruct commit_list *p;\n@@ -700,10 +733,11 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t}\n \n \tif (*commit_buffer_cursor == '\\n') {\n-\t\tif ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor)))\n-\t\t\tsignature_alg = \"sha1\";\n-\t\telse if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n-\t\t\tsignature_alg = \"sha256\";\n+\t\tconst char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n+\t\t\t\t\t\t\t\t      \"gpgsig\", \"sha1\");\n+\t\tconst char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n+\t\t\t\t\t\t\t\t\t\"gpgsig-sha256\", \"sha256\");\n+\t\tcommit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;\n \t}\n \n \tmessage = strstr(commit_buffer_cursor, \"\\n\\n\");\n@@ -769,30 +803,30 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \tprintf(\"%.*s\\n%.*s\\n\",\n \t       (int)(author_end - author), author,\n \t       (int)(committer_end - committer), committer);\n-\tif (signature) {\n+\tif (signatures.nr) {\n \t\tswitch (signed_commit_mode) {\n \t\tcase SIGN_ABORT:\n \t\t\tdie(\"encountered signed commit %s; use \"\n \t\t\t    \"--signed-commits=<mode> to handle it\",\n \t\t\t    oid_to_hex(&commit->object.oid));\n \t\tcase SIGN_WARN_VERBATIM:\n-\t\t\twarning(\"exporting signed commit %s\",\n-\t\t\t\toid_to_hex(&commit->object.oid));\n+\t\t\twarning(\"exporting %\"PRIuMAX\" signature(s) for commit %s\",\n+\t\t\t\t(uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n-\t\t\tprintf(\"gpgsig %s\\ndata %u\\n%s\",\n-\t\t\t       signature_alg,\n-\t\t\t       (unsigned)strlen(signature),\n-\t\t\t       signature);\n+\t\t\tfor (size_t i = 0; i < signatures.nr; i++) {\n+\t\t\t\tstruct string_list_item *item = &signatures.items[i];\n+\t\t\t\tprint_signature(item->string, item->util);\n+\t\t\t}\n \t\t\tbreak;\n \t\tcase SIGN_WARN_STRIP:\n-\t\t\twarning(\"stripping signature from commit %s\",\n+\t\t\twarning(\"stripping signature(s) from commit %s\",\n \t\t\t\toid_to_hex(&commit->object.oid));\n \t\t\t/* fallthru */\n \t\tcase SIGN_STRIP:\n \t\t\tbreak;\n \t\t}\n-\t\tfree((char *)signature);\n+\t\tstring_list_clear(&signatures, 0);\n \t}\n \tif (!reencoded && encoding)\n \t\tprintf(\"encoding %.*s\\n\", (int)encoding_len, encoding);\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b2839c5f43..332073d0f6 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -29,6 +29,7 @@\n #include \"commit-reach.h\"\n #include \"khash.h\"\n #include \"date.h\"\n+#include \"gpg-interface.h\"\n \n #define PACK_ID_BITS 16\n #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n@@ -2718,15 +2719,87 @@ static struct hash_list *parse_merge(unsigned int *count)\n \treturn list;\n }\n \n+struct signature_data {\n+\tchar *hash_algo;      /* \"sha1\" or \"sha256\" */\n+\tchar *sig_format;     /* \"openpgp\", \"x509\", \"ssh\", \"unknown\" */\n+\tstruct strbuf data;   /* The actual signature data */\n+};\n+\n+static void parse_one_signature(struct signature_data *sig, const char *v)\n+{\n+\tchar *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n+\tchar *space = strchr(args, ' ');\n+\n+\tif (!space)\n+\t\tdie(\"Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', \"\n+\t\t    \"got 'gpgsig %s'\", args);\n+\t*space++ = '\\0';\n+\n+\tsig->hash_algo = args;\n+\tsig->sig_format = space;\n+\n+\t/* Remove any trailing newline from format */\n+\tspace = strchr(sig->sig_format, '\\n');\n+\tif (space)\n+\t\t*space = '\\0';\n+\n+\t/* Validate hash algorithm */\n+\tif (strcmp(sig->hash_algo, \"sha1\") &&\n+\t    strcmp(sig->hash_algo, \"sha256\"))\n+\t\tdie(\"Unknown git hash algorithm in gpgsig: '%s'\", sig->hash_algo);\n+\n+\t/* Validate signature format */\n+\tif (!valid_signature_format(sig->sig_format))\n+\t\tdie(\"Invalid signature format in gpgsig: '%s'\", sig->sig_format);\n+\tif (!strcmp(sig->sig_format, \"unknown\"))\n+\t\twarning(\"'unknown' signature format in gpgsig\");\n+\n+\t/* Read signature data */\n+\tread_next_command();\n+\tparse_data(&sig->data, 0, NULL);\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+{\n+\tstruct string_list siglines = STRING_LIST_INIT_NODUP;\n+\n+\tif (!sig->hash_algo)\n+\t\treturn;\n+\n+\tstrbuf_addstr(commit_data, header);\n+\tstring_list_split_in_place(&siglines, sig->data.buf, \"\\n\", -1);\n+\tstrbuf_add_separated_string_list(commit_data, \"\\n \", &siglines);\n+\tstrbuf_addch(commit_data, '\\n');\n+\tstring_list_clear(&siglines, 1);\n+\tstrbuf_release(&sig->data);\n+\tfree(sig->hash_algo);\n+}\n+\n+static void store_signature(struct signature_data *stored_sig,\n+\t\t\t    struct signature_data *new_sig,\n+\t\t\t    const char *hash_type)\n+{\n+\tif (stored_sig->hash_algo) {\n+\t\twarning(\"multiple %s signatures found, \"\n+\t\t\t\"ignoring additional signature\",\n+\t\t\thash_type);\n+\t\tstrbuf_release(&new_sig->data);\n+\t\tfree(new_sig->hash_algo);\n+\t} else {\n+\t\t*stored_sig = *new_sig;\n+\t}\n+}\n+\n static void parse_new_commit(const char *arg)\n {\n-\tstatic struct strbuf sig = STRBUF_INIT;\n \tstatic struct strbuf msg = STRBUF_INIT;\n-\tstruct string_list siglines = STRING_LIST_INIT_NODUP;\n+\tstruct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };\n+\tstruct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };\n \tstruct branch *b;\n \tchar *author = NULL;\n \tchar *committer = NULL;\n-\tchar *sig_alg = NULL;\n \tchar *encoding = NULL;\n \tstruct hash_list *merge_list = NULL;\n \tunsigned int merge_count;\n@@ -2750,13 +2823,23 @@ static void parse_new_commit(const char *arg)\n \t}\n \tif (!committer)\n \t\tdie(\"Expected committer but didn't get one\");\n-\tif (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n-\t\tsig_alg = xstrdup(v);\n-\t\tread_next_command();\n-\t\tparse_data(&sig, 0, NULL);\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+\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\tread_next_command();\n-\t} else\n-\t\tstrbuf_setlen(&sig, 0);\n+\t}\n+\n \tif (skip_prefix(command_buf.buf, \"encoding \", &v)) {\n \t\tencoding = xstrdup(v);\n \t\tread_next_command();\n@@ -2830,23 +2913,14 @@ static void parse_new_commit(const char *arg)\n \t\tstrbuf_addf(&new_data,\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n-\tif (sig_alg) {\n-\t\tif (!strcmp(sig_alg, \"sha1\"))\n-\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n-\t\telse if (!strcmp(sig_alg, \"sha256\"))\n-\t\t\tstrbuf_addstr(&new_data, \"gpgsig-sha256 \");\n-\t\telse\n-\t\t\tdie(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n-\t\tstring_list_split_in_place(&siglines, sig.buf, \"\\n\", -1);\n-\t\tstrbuf_add_separated_string_list(&new_data, \"\\n \", &siglines);\n-\t\tstrbuf_addch(&new_data, '\\n');\n-\t}\n+\n+\tadd_gpgsig_to_commit(&new_data, \"gpgsig \", &sig_sha1);\n+\tadd_gpgsig_to_commit(&new_data, \"gpgsig-sha256 \", &sig_sha256);\n+\n \tstrbuf_addch(&new_data, '\\n');\n \tstrbuf_addbuf(&new_data, &msg);\n-\tstring_list_clear(&siglines, 1);\n \tfree(author);\n \tfree(committer);\n-\tfree(sig_alg);\n \tfree(encoding);\n \n \tif (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 0896458de5..6f2d87475f 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n \treturn NULL;\n }\n \n+const char *get_signature_format(const char *buf)\n+{\n+\tstruct gpg_format *format = get_format_by_sig(buf);\n+\treturn format ? format->name : \"unknown\";\n+}\n+\n+int valid_signature_format(const char *format)\n+{\n+       return (!!get_format_by_name(format) ||\n+\t       !strcmp(format, \"unknown\"));\n+}\n+\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex e09f12e8d0..60ddf8bbfa 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -47,6 +47,18 @@ struct signature_check {\n \n void signature_check_clear(struct signature_check *sigc);\n \n+/*\n+ * Return the format of the signature (like \"openpgp\", \"x509\", \"ssh\"\n+ * or \"unknown\").\n+ */\n+const char *get_signature_format(const char *buf);\n+\n+/*\n+ * Is the signature format valid (like \"openpgp\", \"x509\", \"ssh\" or\n+ * \"unknown\")\n+ */\n+int valid_signature_format(const char *format);\n+\n /*\n  * Look at a GPG signed tag object.  If such a signature exists, store it in\n  * signature and the signed content in payload.  Return 1 if a signature was\ndiff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\nindex 76619765fc..46700dbc40 100755\n--- a/t/t9350-fast-export.sh\n+++ b/t/t9350-fast-export.sh\n@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '\n test_expect_success GPG 'signed-commits=verbatim' '\n \n \tgit fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&\n-\tgrep \"^gpgsig sha\" output &&\n+\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \t(\n \t\tcd new &&\n@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '\n test_expect_success GPG 'signed-commits=warn-verbatim' '\n \n \tgit fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&\n-\tgrep \"^gpgsig sha\" output &&\n+\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \ttest -s err &&\n \t(\n@@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '\n \n '\n \n+test_expect_success GPGSM 'setup X.509 signed commit' '\n+\n+\tgit checkout -b x509-signing main &&\n+\ttest_config gpg.format x509 &&\n+\ttest_config user.signingkey $GIT_COMMITTER_EMAIL &&\n+\techo \"X.509 content\" >file &&\n+\tgit add file &&\n+\tgit commit -S -m \"X.509 signed commit\" &&\n+\tX509_COMMIT=$(git rev-parse HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSM 'round-trip X.509 signed commit' '\n+\n+\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH x509\" output &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/x509-signing >actual &&\n+\t\tgrep \"^gpgsig\" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/x509-signing) &&\n+\t\ttest $X509_COMMIT = $IMPORTED\n+\t) <output\n+\n+'\n+\n+test_expect_success GPGSSH 'setup SSH signed commit' '\n+\n+\tgit checkout -b ssh-signing main &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\techo \"SSH content\" >file &&\n+\tgit add file &&\n+\tgit commit -S -m \"SSH signed commit\" &&\n+\tSSH_COMMIT=$(git rev-parse HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSSH 'round-trip SSH signed commit' '\n+\n+\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH ssh\" output &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/ssh-signing >actual &&\n+\t\tgrep \"^gpgsig\" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/ssh-signing) &&\n+\t\ttest $SSH_COMMIT = $IMPORTED\n+\t) <output\n+\n+'\n+\n test_expect_success 'setup submodule' '\n \n \ttest_config_global protocol.file.allow always &&\n@@ -905,4 +961,46 @@ test_expect_success 'fast-export handles --end-of-options' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPG 'setup a commit with dual 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+\techo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&\n+\ttest_grep -E \"^gpgsig \" out &&\n+\ttest_grep -E \"^gpgsig-sha256 \" out\n+'\n+\n+test_expect_success GPG 'export and import of doubly signed commit' '\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+\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/dual-signed >actual &&\n+\t\ttest_grep -E \"^gpgsig \" actual &&\n+\t\ttest_grep -E \"^gpgsig-sha256 \" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/dual-signed) &&\n+\t\tif test \"$GIT_DEFAULT_HASH\" = \"sha1\"\n+\t\tthen\n+\t\t\ttest $SHA1_B = $IMPORTED\n+\t\telse\n+\t\t\ttest $SHA256_B = $IMPORTED\n+\t\tfi\n+\t) <output\n+'\n+\n test_done\n-- \n2.50.0.174.gcf836c1ec7\n\n"},{"id":"521524","messageId":"CAP8UFD3ZitsRz3ccAjmB_k+DhdYw+AJgf-2vPCGUaRAgWd9SEA@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqwm8jxoj3.fsf@gitster.g","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-08T10:17:35Z","receivedAt":"2025-07-08T10:17:49Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Jul 8, 2025 at 7:03 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > On Tue, Jul 8, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:\n\n> >> We haven't heard much after a few comments were posted on this\n> >> latest round, since Elijah's\n> >> <20250619133630.727274-1-christian.couder@gmail.com>; I understand\n> >> that it would be the author's turn to respond (the response does not\n> >> necessarily have to be with an updated iteration).  If so, let me\n> >> mark the topic as Stalled in the draft of the latest issue of the\n> >> \"What's cooking\" report.\n> >\n> > I will hopefully send a v5 later today.\n\nI just sent it and replied to the pending reviews about v4.\n\n> Thanks.\n>\n> By the way, I noticed that you often do not respond to reviews until\n> the last minute, at the same time as when you send your next\n> iteration, or even soon after doing so.\n\nYeah, right.\n\n> That is quite different from how other contributors operate, i.e.\n> respond and engage in discussions triggered by the reviews, and\n> after people involved in discussion got an (even rough) idea of what\n> the right next step would be, if not a total consensus, send the\n> next iteration.\n>\n> I do not know which style is more efficient form of cooperation, but\n> it somewhat makes my job harder, if I do not hear much _heartbeats_\n> after I see review comments on the list.  I do not mind waiting for\n> seeing the next round for quite a while---after all, any substantial\n> (re)work takes time.  And responding to reviews may need thinking\n> things through carefully, which may take some time, so I would not\n> demand an immediate response, either.  But it would be nearly\n> impossible to feel the current status of such a topic---a few review\n> comments are seen, the author goes silent for a while, we cannot\n> tell if the author is working on a new iteration or where the author\n> and reviewers agree and disagree.\n\nSorry if it makes your job harder.\n\nWhen I work on a number of different things, I alternate between\ntopics. Just after I send a new version of some series to the mailing\nlist, I usually start working on a different topic. These days for\nexample I alternate between this topic and the promisor-remote\ncapability topic. So it seems to me that if I were to respond to\nreviews right away, I could be switching topics all the time if there\nare discussions happening on several topics I work on.\n\nI know I still have to switch often anyway between topics because I\nmight be pinged internally at GitLab about some issues or because\nsomeone I mentor asks me a question privately, etc. And maybe for you\nor others switching topics often is not an issue, but when topics are\nquite complex I feel it makes it much harder for me to focus on what I\nam doing. I don't think I am the only one in this case by the way.\n\n> Also a review response that comes at the same time or immediately\n> after a new iteration is already sent out makes it look like the\n> author is refusing to continue discussion and reviewers are not\n> welcome to make follow-up suggestions during such a discussion.\n\nSorry if it looks like this. I am not refusing any discussion or\nfollow up suggestions. As you say above, responding to reviews may\nneed thinking and often working to try things out, and often it seems\nto me that I cannot really reply properly if I haven't worked enough\nto try some ideas.\n\nLet me take for example the v5 I just sent. It's only by researching\nand trying different ideas without knowing if they would work that I\nfound (after a long time) a way to write a proper test with both a\nSHA-1 and a SHA-256 signature on the same commit. It was the same for\nusing \"$GIT_DEFAULT_HASH\" instead of \"sha(1|256)\" in the tests.\n\nSo yeah, I could have replied early with \"I will do it in v5.\" or \"I\nwill try to do it in v5.\" or \"Ok\" or \"I will think about it.\" to most\nsuggestions I got, but what would have really been the value of a\nresponse with mostly those kinds of sentences in it?\n\n> Instead, the next iteration comes as a fait accompli,\n\nEven if I had replied with mostly \"I will do it in v5.\" or \"I will try\nto do it in v5.\", etc, to many suggestions, I could still have found\nor decided for some reasons to actually implement something else and\nuse those reasons to justify it. Would it have been less of a fait\naccompli?\n\n> and makes it\n> less useful to continue the review discussion on the previous round\n> by responding to such a late response.\n\nIn my opinion the discussion can continue with more useful and higher\nquality information, as I have worked significantly to think through\nand try to implement the suggestions that were made or to research and\nthen often implement other solutions. Yeah, it doesn't continue on the\nprevious round, but hopefully the new round is better, so ...\n"},{"id":"521528","messageId":"CAP8UFD1A+eV9hbmp4P3pC71+oSTrtLgxtWGyt++J8a+bk497qA@mail.gmail.com","threadId":"63342","inReplyTo":"aGy82TiRFcij5V_9@pks.im","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-08T11:08:05Z","receivedAt":"2025-07-08T11:08:19Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Jul 8, 2025 at 8:38 AM Patrick Steinhardt <ps@pks.im> wrote:\n\n> I agree with your points. Overall, a fast response cycle is key to good\n> collaboration from my point of view. I think it not only makes your life\n> as a maintainer easier, but it also makes the reviewer feel like they\n> are being heard and is the prerequisite for good discussion.\n\nI don't agree with \"is the prerequisite for good discussion\".  I think\nit's perfectly possible to have good long running discussions even\nwhen people's replies are delayed. And people can have vacations or\nweekends or private issues or work on other things which can delay\nsome replies. And when people work significantly for a long time on a\ntopic before replying, I really think it can increase the quality of\nthe discussion.\n\nAlso if a contributor comes back with improved patches that try to\nfollow closely what a reviewer suggested, then I think it can (and\nshould) make a reviewer feel like they have really been heard better\nthan just a hollow reply right away followed later by less well\nthought out patches.\n\nThis doesn't mean that I think there is no value in a fast response\ncycle. But I think it depends a lot on the circumstances.\n\nYeah, for someone new in the community I think it can often help a\nlot. And I encourage the contributors I mentor to respond soon.\n\nWhen someone has been part of the community for a long time, I think\nit's different though. It can happen, but it's much less likely that\nthey are going to disappear tomorrow or otherwise not follow up on\nfeedback they got from reviewers.\n\nIt doesn't mean that I think oldtimers should have some kind of\nprivilege, and yeah they should also try to give a good example. But\nwe should allow people to not always behave in a very formatted way.\nAs I mentioned above and in my reply to Junio, many things outside Git\ndevelopment can happen, and people can behave differently, have\ndifferent preferences in the way they work, and especially not like or\nwant to switch topics very often for example to keep a better mental\nfocus on what they are currently doing.\n\n> On our team's handbook page [1] we have the following couple of bullet\n> points regarding how to respond to reviews:\n\nYeah, I think they are likely to be good for newcomers.\n"},{"id":"521554","messageId":"xmqqbjpuwsbm.fsf@gitster.g","threadId":"63342","inReplyTo":"CAP8UFD1A+eV9hbmp4P3pC71+oSTrtLgxtWGyt++J8a+bk497qA@mail.gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-08T16:38:53Z","receivedAt":"2025-07-08T16:38:57Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> Also if a contributor comes back with improved patches that try to\n> follow closely what a reviewer suggested, then I think it can (and\n> should) make a reviewer feel like they have really been heard better\n> than just a hollow reply right away followed later by less well\n> thought out patches.\n\nThat is kind of \"better late than never\".  I would expect better\nthan that from more senior prominent contributors ;-)\n\nAnd I totally agree with you that reviews often deserve very well\nreasoned responses, which take time to prepare; a response that\ncomes as spinal reflex without much thought is often not very\nuseful.\n\nIt really depends on the definition of \"fast\" in \"fast response\".\n\nIf we need a week to come up with a newer iteration, it would be\nfair to expect that we can say something like \"I agree, I'll fix\",\n\"I am not convinced because ...\", \"I am skeptical but let me first\nsee how it pans out\", etc. by day #2 or #3, wouldn't it?  Upon\nrecieving a response at the same time or soon after an updated\niteration was sent, especially when the response is \"no, I do not\nthink so\", what is the reviewer expected to do?  Saying \"you may not\nthink so but here is another point that may make you reconsider\"\nwould be too late, so it would actively discourage continued\ndiscussion.\n\n> It doesn't mean that I think oldtimers should have some kind of\n> privilege, and yeah they should also try to give a good example. But\n> we should allow people to not always behave in a very formatted way.\n\nOld timers learn from experience how other old timers operate ;-)\nand I have learned to ignore the usual signal when anticipating what\nyour next iteration may look like (in other words, interim responses\nor lack thereof is usually a good signal for most developers, but\nnot for you---you tend to come back with your next iteration without\nmuch interim interactions).  But other contributors shouldn't be\nforced to.  That is what we need some community norm for.\n\n>> On our team's handbook page [1] we have the following couple of bullet\n>> points regarding how to respond to reviews:\n>\n> Yeah, I think they are likely to be good for newcomers.\n\nThe handbook here is gitlab's team handbook, and it may not apply to\nopen source Git development community, but \"this rule applies only\nto newcomers, I am above that rule\" is the same thing as saying\n\"oldtimers like me should have some kind of privilege\".  I do not\nknow what to think about this and what you said above.\n"},{"id":"521580","messageId":"xmqqv7o2s5to.fsf@gitster.g","threadId":"63342","inReplyTo":"20250708091738.4072857-1-christian.couder@gmail.com","subject":"Re: [PATCH v5] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-08T21:58:27Z","receivedAt":"2025-07-08T21:58:30Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\n> signed-commits, 2025-03-10), added support for signed commits to\n> fast-export and fast-import.\n> ...\n> It could be even better to be able to import more than one signature\n> on the SHA-1 object and on the SHA-256 object, but other parts of\n> Git don't handle that well for now, so this is left for future\n> improvements.\n>\n> Helped-by: brian m. carlson <sandals@crustytoothpaste.net>\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>\n> This v5 is similar in spirit to v4, especially the format of the\n> \"gpgsig ...\" command didn't change, but there are a number of\n> improvements:\n>\n>   - All the signatures are now exported. On the import side, still at\n>     most one signature for SHA-1 and one for SHA-256 are imported (and\n>     a warning is still emitted for additional signatures) though.\n>\n>   - The code makes sure each signature ends with a LF character. While\n>     this is not mandatory, it is encouraged and makes the output more\n>     human readable and 'grep'able.\n>\n>   - A test with both a SHA-1 and a SHA-256 signature on the same\n>     commit has been added.\n>\n>   - Some tests check that either \"sha1\" or \"sha256\" is in the \"gpgsig\n>     ...\"  command instead of matching \"sha(1|256)\".\n>\n>   - The format of the \"gpgsig ...\" command is better explained both in\n>     the commit message and in the fast-import documentation.\n>\n>   - There are some typo fixes, lines wrapped, and a few other such\n>     small changes.\n\nThanks.  Will queue.\n"},{"id":"521589","messageId":"CABPp-BF6OvH8oh=jG_8fWoC5gW+9E+wx=uDEk1uerJTOva5isg@mail.gmail.com","threadId":"63342","inReplyTo":"20250708091738.4072857-1-christian.couder@gmail.com","subject":"Re: [PATCH v5] fast-(import|export): improve on commit signature output format","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-07-08T23:08:06Z","receivedAt":"2025-07-08T23:08:19Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Jul 8, 2025 at 2:18 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\n> signed-commits, 2025-03-10), added support for signed commits to\n> fast-export and fast-import.\n>\n> When a signed commit is processed, fast-export can output either\n> \"gpgsig sha1\" or \"gpgsig sha256\" depending on whether the signed\n> commit uses the SHA-1 or SHA-256 Git object format.\n>\n> However, this implementation has a number of limitations:\n>\n>   - the output format was not properly described in the documentation,\n>   - the output format is not very informative as it doesn't even say\n>     if the signature is an OpenPGP, an SSH, or an X509 signature,\n>   - the implementation doesn't support having both one signature on\n>     the SHA-1 object and one on the SHA-256 object.\n>\n> Let's improve on these limitations by improving fast-export and\n> fast-import so that:\n>\n>   - all the signatures are exported,\n>   - at most one signature on the SHA-1 object and one on the SHA-256\n>     are imported,\n>   - if there is more than one signature on the SHA-1 object or on\n>     the SHA-256 object, fast-import emits a warning for each\n>     additional signature,\n>   - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n>     where <git-hash-algo> is the Git object format as before, and\n>     <signature-format> is the signature type (\"openpgp\", \"x509\",\n>     \"ssh\" or \"unknown\"),\n>   - the output is properly documented.\n>\n> About the output format:\n>\n>   - <git-hash-algo> allows to know which representation of the commit\n>     was signed (the SHA-1 or the SHA-256 version) which helps with\n>     both signature verification and interoperability between repos\n>     with different hash functions,\n>\n>   - <signature-format> helps tools that process the fast-export\n>     stream, so they don't have to parse the ASCII armor to identify\n>     the signature type.\n>\n> It could be even better to be able to import more than one signature\n> on the SHA-1 object and on the SHA-256 object, but other parts of\n> Git don't handle that well for now, so this is left for future\n> improvements.\n>\n> Helped-by: brian m. carlson <sandals@crustytoothpaste.net>\n> Helped-by: Elijah Newren <newren@gmail.com>\n> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>\n> ---\n>\n> This v5 is similar in spirit to v4, especially the format of the\n> \"gpgsig ...\" command didn't change, but there are a number of\n> improvements:\n>\n>   - All the signatures are now exported. On the import side, still at\n>     most one signature for SHA-1 and one for SHA-256 are imported (and\n>     a warning is still emitted for additional signatures) though.\n>\n>   - The code makes sure each signature ends with a LF character. While\n>     this is not mandatory, it is encouraged and makes the output more\n>     human readable and 'grep'able.\n>\n>   - A test with both a SHA-1 and a SHA-256 signature on the same\n>     commit has been added.\n>\n>   - Some tests check that either \"sha1\" or \"sha256\" is in the \"gpgsig\n>     ...\"  command instead of matching \"sha(1|256)\".\n>\n>   - The format of the \"gpgsig ...\" command is better explained both in\n>     the commit message and in the fast-import documentation.\n>\n>   - There are some typo fixes, lines wrapped, and a few other such\n>     small changes.\n>\n> Thanks to brian, Elijah and Junio who commented on the v1 and v2.\n>\n> CI tests:\n>\n> They have all passed:\n>\n> https://github.com/chriscool/git/actions/runs/16136587558\n>\n> Range-diff with v4:\n>\n> 1:  b3d8b13c0e ! 1:  5791617d7d fast-(import|export): improve on commit signature output format\n>     @@ Commit message\n>          Let's improve on these limitations by improving fast-export and\n>          fast-import so that:\n>\n>     -      - both one signature on the SHA-1 object and one on the SHA-256\n>     -        object can be exported and imported,\n>     +      - all the signatures are exported,\n>     +      - at most one signature on the SHA-1 object and one on the SHA-256\n>     +        are imported,\n>            - if there is more than one signature on the SHA-1 object or on\n>     -        the SHA-256 object, a warning is emitted,\n>     +        the SHA-256 object, fast-import emits a warning for each\n>     +        additional signature,\n>            - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n>              where <git-hash-algo> is the Git object format as before, and\n>              <signature-format> is the signature type (\"openpgp\", \"x509\",\n>     -        \"ssh\" or \"unknown\",\n>     +        \"ssh\" or \"unknown\"),\n>            - the output is properly documented.\n>\n>     -    Note that it could be even better to be able to export and import\n>     -    more than one signature on the SHA-1 object and on the SHA-256\n>     -    object, but other parts of Git don't handle that well for now, so\n>     -    this is left for future improvements.\n>     +    About the output format:\n>     +\n>     +      - <git-hash-algo> allows to know which representation of the commit\n>     +        was signed (the SHA-1 or the SHA-256 version) which helps with\n>     +        both signature verification and interoperability between repos\n>     +        with different hash functions,\n>     +\n>     +      - <signature-format> helps tools that process the fast-export\n>     +        stream, so they don't have to parse the ASCII armor to identify\n>     +        the signature type.\n>     +\n>     +    It could be even better to be able to import more than one signature\n>     +    on the SHA-1 object and on the SHA-256 object, but other parts of\n>     +    Git don't handle that well for now, so this is left for future\n>     +    improvements.\n>\n>          Helped-by: brian m. carlson <sandals@crustytoothpaste.net>\n>          Helped-by: Elijah Newren <newren@gmail.com>\n>     @@ Documentation/git-fast-export.adoc: resulting tag will have an invalid signature\n>      +`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit\n>      +starts with `gpgsig sha256 ssh`.\n>      ++\n>     -+Currently for a given commit, at most one signature for the SHA-1\n>     -+object and one signature for the SHA-256 object are exported, each\n>     -+with their respective <git-hash-algo> identifier. A warning is\n>     -+emitted for each additional signature found.\n>     ++While all the signatures of a commit are exported, an importer may\n>     ++choose to accept only some of them. For example\n>     ++linkgit:git-fast-import[1] currently stores at most one signature per\n>     ++Git hash algorithm in each commit.\n>      ++\n>       NOTE: This is highly experimental and the format of the data stream may\n>       change in the future without compatibility guarantees.\n>     @@ Documentation/git-fast-import.adoc: their syntax.\n>      +The `gpgsig` command takes two arguments:\n>      +\n>      +* `<git-hash-algo>` specifies which Git object format this signature\n>     -+  applies to, either `sha1` or `sha256`.\n>     ++  applies to, either `sha1` or `sha256`. This allows to know which\n>     ++  representation of the commit was signed (the SHA-1 or the SHA-256\n>     ++  version) which helps with both signature verification and\n>     ++  interoperability between repos with different hash functions.\n>      +\n>      +* `<signature-format>` specifies the type of signature, such as\n>     -+  `openpgp`, `x509`, `ssh`, or `unknown`.\n>     ++  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for\n>     ++  tools that process the stream, so they don't have to parse the ASCII\n>     ++  armor to identify the signature type.\n>      +\n>      +A commit may have at most one signature for the SHA-1 object format\n>      +(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n>     @@ builtin/fast-export.c: static const char *find_commit_multiline_header(const cha\n>      +  if (!signature)\n>      +          return;\n>      +\n>     -+  printf(\"gpgsig %s %s\\ndata %u\\n%s\",\n>     ++  printf(\"gpgsig %s %s\\ndata %u\\n%s\\n\",\n>      +         object_hash,\n>      +         get_signature_format(signature),\n>      +         (unsigned)strlen(signature),\n>      +         signature);\n>      +}\n>      +\n>     -+static void warn_on_extra_sig(const char **pos, struct commit *commit, int is_sha1)\n>     ++static const char *append_signatures_for_header(struct string_list *signatures,\n>     ++                                          const char *pos,\n>     ++                                          const char *header,\n>     ++                                          const char *object_hash)\n>      +{\n>     -+  const char *header = is_sha1 ? \"gpgsig\" : \"gpgsig-sha256\";\n>     -+  const char *extra_sig = find_commit_multiline_header(*pos + 1, header, pos);\n>     -+  if (extra_sig) {\n>     -+          const char *hash = is_sha1 ? \"SHA-1\" : \"SHA-256\";\n>     -+          warning(\"more than one %s signature found on commit %s, using only the first one\",\n>     -+                  hash, oid_to_hex(&commit->object.oid));\n>     -+          free((char *)extra_sig);\n>     ++  const char *signature;\n>     ++  const char *start = pos;\n>     ++  const char *end = pos;\n>     ++\n>     ++  while ((signature = find_commit_multiline_header(start + 1,\n>     ++                                                   header,\n>     ++                                                   &end))) {\n>     ++          string_list_append(signatures, signature)->util = (void *)object_hash;\n>     ++          free((char *)signature);\n>     ++          start = end;\n>      +  }\n>     ++\n>     ++  return end;\n>      +}\n>      +\n>       static void handle_commit(struct commit *commit, struct rev_info *rev,\n>     @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\n>         const char *encoding = NULL;\n>         size_t encoding_len;\n>      -  const char *signature_alg = NULL, *signature = NULL;\n>     -+  const char *sig_sha1 = NULL;\n>     -+  const char *sig_sha256 = NULL;\n>     ++  struct string_list signatures = STRING_LIST_INIT_DUP;\n>         const char *message;\n>         char *reencoded = NULL;\n>         struct commit_list *p;\n>     @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\n>      -                  signature_alg = \"sha1\";\n>      -          else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n>      -                  signature_alg = \"sha256\";\n>     -+          const char *sig_cursor = commit_buffer_cursor;\n>     -+          const char *after_sha1 = commit_buffer_cursor;\n>     -+          const char *after_sha256 = commit_buffer_cursor;\n>     -+\n>     -+          /*\n>     -+           * Find the first signature for each hash algorithm.\n>     -+           * The searches must start from the same position.\n>     -+           */\n>     -+          sig_sha1 = find_commit_multiline_header(sig_cursor + 1,\n>     -+                                                  \"gpgsig\",\n>     -+                                                  &after_sha1);\n>     -+          sig_sha256 = find_commit_multiline_header(sig_cursor + 1,\n>     -+                                                    \"gpgsig-sha256\",\n>     -+                                                    &after_sha256);\n>     -+\n>     -+          /* Warn on any additional signatures, as they will be ignored. */\n>     -+          if (sig_sha1)\n>     -+                  warn_on_extra_sig(&after_sha1, commit, 1);\n>     -+          if (sig_sha256)\n>     -+                  warn_on_extra_sig(&after_sha256, commit, 0);\n>     -+\n>     ++          const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n>     ++                                                                \"gpgsig\", \"sha1\");\n>     ++          const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n>     ++                                                                  \"gpgsig-sha256\", \"sha256\");\n>      +          commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;\n>         }\n>\n>     @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\n>                (int)(author_end - author), author,\n>                (int)(committer_end - committer), committer);\n>      -  if (signature) {\n>     -+  if (sig_sha1 || sig_sha256) {\n>     ++  if (signatures.nr) {\n>                 switch (signed_commit_mode) {\n>                 case SIGN_ABORT:\n>                         die(\"encountered signed commit %s; use \"\n>     -@@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct rev_info *rev,\n>     -                           oid_to_hex(&commit->object.oid));\n>     +                       \"--signed-commits=<mode> to handle it\",\n>     +                       oid_to_hex(&commit->object.oid));\n>     +           case SIGN_WARN_VERBATIM:\n>     +-                  warning(\"exporting signed commit %s\",\n>     +-                          oid_to_hex(&commit->object.oid));\n>     ++                  warning(\"exporting %\"PRIuMAX\" signature(s) for commit %s\",\n>     ++                          (uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));\n>                         /* fallthru */\n>                 case SIGN_VERBATIM:\n>      -                  printf(\"gpgsig %s\\ndata %u\\n%s\",\n>      -                         signature_alg,\n>      -                         (unsigned)strlen(signature),\n>      -                         signature);\n>     -+                  print_signature(sig_sha1, \"sha1\");\n>     -+                  print_signature(sig_sha256, \"sha256\");\n>     ++                  for (size_t i = 0; i < signatures.nr; i++) {\n>     ++                          struct string_list_item *item = &signatures.items[i];\n>     ++                          print_signature(item->string, item->util);\n>     ++                  }\n>                         break;\n>                 case SIGN_WARN_STRIP:\n>      -                  warning(\"stripping signature from commit %s\",\n>     @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r\n>                         break;\n>                 }\n>      -          free((char *)signature);\n>     -+          free((char *)sig_sha1);\n>     -+          free((char *)sig_sha256);\n>     ++          string_list_clear(&signatures, 0);\n>         }\n>         if (!reencoded && encoding)\n>                 printf(\"encoding %.*s\\n\", (int)encoding_len, encoding);\n>     @@ builtin/fast-import.c: static struct hash_list *parse_merge(unsigned int *count)\n>      +                      const char *hash_type)\n>      +{\n>      +  if (stored_sig->hash_algo) {\n>     -+          warning(\"Multiple %s signatures found, ignoring additional signature\",\n>     ++          warning(\"multiple %s signatures found, \"\n>     ++                  \"ignoring additional signature\",\n>      +                  hash_type);\n>      +          strbuf_release(&new_sig->data);\n>      +          free(new_sig->hash_algo);\n>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=abort' '\n>\n>         git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&\n>      -  grep \"^gpgsig sha\" output &&\n>     -+  test_grep -E \"^gpgsig sha(1|256) openpgp\" output &&\n>     ++  test_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n>         grep \"encoding ISO-8859-1\" output &&\n>         (\n>                 cd new &&\n>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=verbatim' '\n>\n>         git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&\n>      -  grep \"^gpgsig sha\" output &&\n>     -+  test_grep -E \"^gpgsig sha(1|256) openpgp\" output &&\n>     ++  test_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n>         grep \"encoding ISO-8859-1\" output &&\n>         test -s err &&\n>         (\n>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '\n>      +test_expect_success GPGSM 'round-trip X.509 signed commit' '\n>      +\n>      +  git fast-export --signed-commits=verbatim x509-signing >output &&\n>     -+  test_grep -E \"^gpgsig sha(1|256) x509\" output &&\n>     ++  test_grep -E \"^gpgsig $GIT_DEFAULT_HASH x509\" output &&\n>      +  (\n>      +          cd new &&\n>      +          git fast-import &&\n>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '\n>      +test_expect_success GPGSSH 'round-trip SSH signed commit' '\n>      +\n>      +  git fast-export --signed-commits=verbatim ssh-signing >output &&\n>     -+  test_grep -E \"^gpgsig sha(1|256) ssh\" output &&\n>     ++  test_grep -E \"^gpgsig $GIT_DEFAULT_HASH ssh\" output &&\n>      +  (\n>      +          cd new &&\n>      +          git fast-import &&\n>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '\n>       test_expect_success 'setup submodule' '\n>\n>         test_config_global protocol.file.allow always &&\n>     +@@ t/t9350-fast-export.sh: test_expect_success 'fast-export handles --end-of-options' '\n>     +   test_cmp expect actual\n>     + '\n>     +\n>     ++test_expect_success GPG 'setup a commit with dual 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>     ++  echo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&\n>     ++  test_grep -E \"^gpgsig \" out &&\n>     ++  test_grep -E \"^gpgsig-sha256 \" out\n>     ++'\n>     ++\n>     ++test_expect_success GPG 'export and import of doubly signed commit' '\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>     ++\n>     ++  (\n>     ++          cd new &&\n>     ++          git fast-import &&\n>     ++          git cat-file commit refs/heads/dual-signed >actual &&\n>     ++          test_grep -E \"^gpgsig \" actual &&\n>     ++          test_grep -E \"^gpgsig-sha256 \" actual &&\n>     ++          IMPORTED=$(git rev-parse refs/heads/dual-signed) &&\n>     ++          if test \"$GIT_DEFAULT_HASH\" = \"sha1\"\n>     ++          then\n>     ++                  test $SHA1_B = $IMPORTED\n>     ++          else\n>     ++                  test $SHA256_B = $IMPORTED\n>     ++          fi\n>     ++  ) <output\n>     ++'\n>     ++\n>     + test_done\n>\n>\n>  Documentation/git-fast-export.adoc |  17 +++++\n>  Documentation/git-fast-import.adoc |  36 +++++++--\n>  builtin/fast-export.c              |  62 +++++++++++----\n>  builtin/fast-import.c              | 118 +++++++++++++++++++++++------\n>  gpg-interface.c                    |  12 +++\n>  gpg-interface.h                    |  12 +++\n>  t/t9350-fast-export.sh             | 102 ++++++++++++++++++++++++-\n>  7 files changed, 315 insertions(+), 44 deletions(-)\n>\n> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\n> index 43bbb4f63c..297b57bb2e 100644\n> --- a/Documentation/git-fast-export.adoc\n> +++ b/Documentation/git-fast-export.adoc\n> @@ -50,6 +50,23 @@ resulting tag will have an invalid signature.\n>         is the same as how earlier versions of this command without\n>         this option behaved.\n>  +\n> +When exported, a signature starts with:\n> ++\n> +gpgsig <git-hash-algo> <signature-format>\n> ++\n> +where <git-hash-algo> is the Git object hash so either \"sha1\" or\n> +\"sha256\", and <signature-format> is the signature type, so \"openpgp\",\n> +\"x509\", \"ssh\" or \"unknown\".\n> ++\n> +For example, an OpenPGP signature on a SHA-1 commit starts with\n> +`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit\n> +starts with `gpgsig sha256 ssh`.\n> ++\n> +While all the signatures of a commit are exported, an importer may\n> +choose to accept only some of them. For example\n> +linkgit:git-fast-import[1] currently stores at most one signature per\n> +Git hash algorithm in each commit.\n> ++\n>  NOTE: This is highly experimental and the format of the data stream may\n>  change in the future without compatibility guarantees.\n>\n> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> index 250d866652..89dec1108f 100644\n> --- a/Documentation/git-fast-import.adoc\n> +++ b/Documentation/git-fast-import.adoc\n> @@ -445,7 +445,7 @@ one).\n>         original-oid?\n>         ('author' (SP <name>)? SP LT <email> GT SP <when> LF)?\n>         'committer' (SP <name>)? SP LT <email> GT SP <when> LF\n> -       ('gpgsig' SP <alg> LF data)?\n> +       ('gpgsig' SP <algo> SP <format> LF data)?\n>         ('encoding' SP <encoding> LF)?\n>         data\n>         ('from' SP <commit-ish> LF)?\n> @@ -518,13 +518,37 @@ their syntax.\n>  ^^^^^^^^\n>\n>  The optional `gpgsig` command is used to include a PGP/GPG signature\n> -that signs the commit data.\n> +or other cryptographic signature that signs the commit data.\n>\n> -Here <alg> specifies which hashing algorithm is used for this\n> -signature, either `sha1` or `sha256`.\n> +....\n> +       'gpgsig' SP <git-hash-algo> SP <signature-format> LF\n> +       data\n\nShould the `data` be moved to the line above, just to make it clear\nit's associated with it?  (Similar to the first line you changed in\ngit-fast-import.adoc?)\n\n> +....\n> +\n> +The `gpgsig` command takes two arguments:\n> +\n> +* `<git-hash-algo>` specifies which Git object format this signature\n> +  applies to, either `sha1` or `sha256`. This allows to know which\n> +  representation of the commit was signed (the SHA-1 or the SHA-256\n> +  version) which helps with both signature verification and\n> +  interoperability between repos with different hash functions.\n\nShould there also be a note added that fast-import is limited on what\nsignatures it can verify if extensions.compatObjectFormat is not set?\n\n> +\n> +* `<signature-format>` specifies the type of signature, such as\n> +  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for\n> +  tools that process the stream, so they don't have to parse the ASCII\n> +  armor to identify the signature type.\n> +\n> +A commit may have at most one signature for the SHA-1 object format\n> +(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n> +(stored in the \"gpgsig-sha256\" header).\n> +\n> +See below for a detailed description of the `data` command which\n> +contains the raw signature data.\n> +\n> +Signatures are not yet checked in the current implementation though.\n\n...or maybe that extra note could be added as a parenthetical comment\nhere for now?\n\n> -NOTE: This is highly experimental and the format of the data stream may\n> -change in the future without compatibility guarantees.\n> +NOTE: This is highly experimental and the format of the `gpgsig`\n> +command may change in the future without compatibility guarantees.\n>\n>  `encoding`\n>  ^^^^^^^^^^\n> diff --git a/builtin/fast-export.c b/builtin/fast-export.c\n> index fcf6b00d5f..7b4e6a6e41 100644\n> --- a/builtin/fast-export.c\n> +++ b/builtin/fast-export.c\n> @@ -29,6 +29,7 @@\n>  #include \"quote.h\"\n>  #include \"remote.h\"\n>  #include \"blob.h\"\n> +#include \"gpg-interface.h\"\n>\n>  static const char *const fast_export_usage[] = {\n>         N_(\"git fast-export [<rev-list-opts>]\"),\n> @@ -652,6 +653,38 @@ static const char *find_commit_multiline_header(const char *msg,\n>         return strbuf_detach(&val, NULL);\n>  }\n>\n> +static void print_signature(const char *signature, const char *object_hash)\n> +{\n> +       if (!signature)\n> +               return;\n> +\n> +       printf(\"gpgsig %s %s\\ndata %u\\n%s\\n\",\n> +              object_hash,\n> +              get_signature_format(signature),\n> +              (unsigned)strlen(signature),\n> +              signature);\n> +}\n> +\n> +static const char *append_signatures_for_header(struct string_list *signatures,\n> +                                               const char *pos,\n> +                                               const char *header,\n> +                                               const char *object_hash)\n> +{\n> +       const char *signature;\n> +       const char *start = pos;\n> +       const char *end = pos;\n> +\n> +       while ((signature = find_commit_multiline_header(start + 1,\n> +                                                        header,\n> +                                                        &end))) {\n> +               string_list_append(signatures, signature)->util = (void *)object_hash;\n> +               free((char *)signature);\n> +               start = end;\n> +       }\n> +\n> +       return end;\n> +}\n> +\n>  static void handle_commit(struct commit *commit, struct rev_info *rev,\n>                           struct string_list *paths_of_changed_objects)\n>  {\n> @@ -660,7 +693,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n>         const char *author, *author_end, *committer, *committer_end;\n>         const char *encoding = NULL;\n>         size_t encoding_len;\n> -       const char *signature_alg = NULL, *signature = NULL;\n> +       struct string_list signatures = STRING_LIST_INIT_DUP;\n>         const char *message;\n>         char *reencoded = NULL;\n>         struct commit_list *p;\n> @@ -700,10 +733,11 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n>         }\n>\n>         if (*commit_buffer_cursor == '\\n') {\n> -               if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor)))\n> -                       signature_alg = \"sha1\";\n> -               else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n> -                       signature_alg = \"sha256\";\n> +               const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n> +                                                                     \"gpgsig\", \"sha1\");\n> +               const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n> +                                                                       \"gpgsig-sha256\", \"sha256\");\n> +               commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;\n>         }\n>\n>         message = strstr(commit_buffer_cursor, \"\\n\\n\");\n> @@ -769,30 +803,30 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n>         printf(\"%.*s\\n%.*s\\n\",\n>                (int)(author_end - author), author,\n>                (int)(committer_end - committer), committer);\n> -       if (signature) {\n> +       if (signatures.nr) {\n>                 switch (signed_commit_mode) {\n>                 case SIGN_ABORT:\n>                         die(\"encountered signed commit %s; use \"\n>                             \"--signed-commits=<mode> to handle it\",\n>                             oid_to_hex(&commit->object.oid));\n>                 case SIGN_WARN_VERBATIM:\n> -                       warning(\"exporting signed commit %s\",\n> -                               oid_to_hex(&commit->object.oid));\n> +                       warning(\"exporting %\"PRIuMAX\" signature(s) for commit %s\",\n> +                               (uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));\n>                         /* fallthru */\n>                 case SIGN_VERBATIM:\n> -                       printf(\"gpgsig %s\\ndata %u\\n%s\",\n> -                              signature_alg,\n> -                              (unsigned)strlen(signature),\n> -                              signature);\n> +                       for (size_t i = 0; i < signatures.nr; i++) {\n> +                               struct string_list_item *item = &signatures.items[i];\n> +                               print_signature(item->string, item->util);\n> +                       }\n>                         break;\n>                 case SIGN_WARN_STRIP:\n> -                       warning(\"stripping signature from commit %s\",\n> +                       warning(\"stripping signature(s) from commit %s\",\n\nNice catch; that's the kind of thing that could be easily overlooked.\n\n>                                 oid_to_hex(&commit->object.oid));\n>                         /* fallthru */\n>                 case SIGN_STRIP:\n>                         break;\n>                 }\n> -               free((char *)signature);\n> +               string_list_clear(&signatures, 0);\n>         }\n>         if (!reencoded && encoding)\n>                 printf(\"encoding %.*s\\n\", (int)encoding_len, encoding);\n> diff --git a/builtin/fast-import.c b/builtin/fast-import.c\n> index b2839c5f43..332073d0f6 100644\n> --- a/builtin/fast-import.c\n> +++ b/builtin/fast-import.c\n> @@ -29,6 +29,7 @@\n>  #include \"commit-reach.h\"\n>  #include \"khash.h\"\n>  #include \"date.h\"\n> +#include \"gpg-interface.h\"\n>\n>  #define PACK_ID_BITS 16\n>  #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n> @@ -2718,15 +2719,87 @@ static struct hash_list *parse_merge(unsigned int *count)\n>         return list;\n>  }\n>\n> +struct signature_data {\n> +       char *hash_algo;      /* \"sha1\" or \"sha256\" */\n> +       char *sig_format;     /* \"openpgp\", \"x509\", \"ssh\", \"unknown\" */\n> +       struct strbuf data;   /* The actual signature data */\n> +};\n> +\n> +static void parse_one_signature(struct signature_data *sig, const char *v)\n> +{\n> +       char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n> +       char *space = strchr(args, ' ');\n> +\n> +       if (!space)\n> +               die(\"Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', \"\n> +                   \"got 'gpgsig %s'\", args);\n> +       *space++ = '\\0';\n> +\n> +       sig->hash_algo = args;\n> +       sig->sig_format = space;\n\nReally minor nitpick, but it might be clearer to pre-increment space\nhere than to increment it above.\n\n> +\n> +       /* Remove any trailing newline from format */\n> +       space = strchr(sig->sig_format, '\\n');\n> +       if (space)\n> +               *space = '\\0';\n> +\n> +       /* Validate hash algorithm */\n> +       if (strcmp(sig->hash_algo, \"sha1\") &&\n> +           strcmp(sig->hash_algo, \"sha256\"))\n> +               die(\"Unknown git hash algorithm in gpgsig: '%s'\", sig->hash_algo);\n> +\n> +       /* Validate signature format */\n> +       if (!valid_signature_format(sig->sig_format))\n> +               die(\"Invalid signature format in gpgsig: '%s'\", sig->sig_format);\n> +       if (!strcmp(sig->sig_format, \"unknown\"))\n> +               warning(\"'unknown' signature format in gpgsig\");\n> +\n> +       /* Read signature data */\n> +       read_next_command();\n> +       parse_data(&sig->data, 0, NULL);\n> +}\n> +\n> +static void add_gpgsig_to_commit(struct strbuf *commit_data,\n> +                                const char *header,\n> +                                struct signature_data *sig)\n> +{\n> +       struct string_list siglines = STRING_LIST_INIT_NODUP;\n> +\n> +       if (!sig->hash_algo)\n> +               return;\n> +\n> +       strbuf_addstr(commit_data, header);\n> +       string_list_split_in_place(&siglines, sig->data.buf, \"\\n\", -1);\n> +       strbuf_add_separated_string_list(commit_data, \"\\n \", &siglines);\n> +       strbuf_addch(commit_data, '\\n');\n> +       string_list_clear(&siglines, 1);\n> +       strbuf_release(&sig->data);\n> +       free(sig->hash_algo);\n> +}\n> +\n> +static void store_signature(struct signature_data *stored_sig,\n> +                           struct signature_data *new_sig,\n> +                           const char *hash_type)\n> +{\n> +       if (stored_sig->hash_algo) {\n> +               warning(\"multiple %s signatures found, \"\n> +                       \"ignoring additional signature\",\n> +                       hash_type);\n> +               strbuf_release(&new_sig->data);\n> +               free(new_sig->hash_algo);\n> +       } else {\n> +               *stored_sig = *new_sig;\n> +       }\n> +}\n> +\n>  static void parse_new_commit(const char *arg)\n>  {\n> -       static struct strbuf sig = STRBUF_INIT;\n>         static struct strbuf msg = STRBUF_INIT;\n> -       struct string_list siglines = STRING_LIST_INIT_NODUP;\n> +       struct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };\n> +       struct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };\n>         struct branch *b;\n>         char *author = NULL;\n>         char *committer = NULL;\n> -       char *sig_alg = NULL;\n>         char *encoding = NULL;\n>         struct hash_list *merge_list = NULL;\n>         unsigned int merge_count;\n> @@ -2750,13 +2823,23 @@ static void parse_new_commit(const char *arg)\n>         }\n>         if (!committer)\n>                 die(\"Expected committer but didn't get one\");\n> -       if (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n> -               sig_alg = xstrdup(v);\n> -               read_next_command();\n> -               parse_data(&sig, 0, NULL);\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> +\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> +\n>                 read_next_command();\n> -       } else\n> -               strbuf_setlen(&sig, 0);\n> +       }\n> +\n>         if (skip_prefix(command_buf.buf, \"encoding \", &v)) {\n>                 encoding = xstrdup(v);\n>                 read_next_command();\n> @@ -2830,23 +2913,14 @@ static void parse_new_commit(const char *arg)\n>                 strbuf_addf(&new_data,\n>                         \"encoding %s\\n\",\n>                         encoding);\n> -       if (sig_alg) {\n> -               if (!strcmp(sig_alg, \"sha1\"))\n> -                       strbuf_addstr(&new_data, \"gpgsig \");\n> -               else if (!strcmp(sig_alg, \"sha256\"))\n> -                       strbuf_addstr(&new_data, \"gpgsig-sha256 \");\n> -               else\n> -                       die(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n> -               string_list_split_in_place(&siglines, sig.buf, \"\\n\", -1);\n> -               strbuf_add_separated_string_list(&new_data, \"\\n \", &siglines);\n> -               strbuf_addch(&new_data, '\\n');\n> -       }\n> +\n> +       add_gpgsig_to_commit(&new_data, \"gpgsig \", &sig_sha1);\n> +       add_gpgsig_to_commit(&new_data, \"gpgsig-sha256 \", &sig_sha256);\n> +\n>         strbuf_addch(&new_data, '\\n');\n>         strbuf_addbuf(&new_data, &msg);\n> -       string_list_clear(&siglines, 1);\n>         free(author);\n>         free(committer);\n> -       free(sig_alg);\n>         free(encoding);\n>\n>         if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))\n> diff --git a/gpg-interface.c b/gpg-interface.c\n> index 0896458de5..6f2d87475f 100644\n> --- a/gpg-interface.c\n> +++ b/gpg-interface.c\n> @@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n>         return NULL;\n>  }\n>\n> +const char *get_signature_format(const char *buf)\n> +{\n> +       struct gpg_format *format = get_format_by_sig(buf);\n> +       return format ? format->name : \"unknown\";\n> +}\n> +\n> +int valid_signature_format(const char *format)\n> +{\n> +       return (!!get_format_by_name(format) ||\n> +              !strcmp(format, \"unknown\"));\n> +}\n> +\n>  void signature_check_clear(struct signature_check *sigc)\n>  {\n>         FREE_AND_NULL(sigc->payload);\n> diff --git a/gpg-interface.h b/gpg-interface.h\n> index e09f12e8d0..60ddf8bbfa 100644\n> --- a/gpg-interface.h\n> +++ b/gpg-interface.h\n> @@ -47,6 +47,18 @@ struct signature_check {\n>\n>  void signature_check_clear(struct signature_check *sigc);\n>\n> +/*\n> + * Return the format of the signature (like \"openpgp\", \"x509\", \"ssh\"\n> + * or \"unknown\").\n> + */\n> +const char *get_signature_format(const char *buf);\n> +\n> +/*\n> + * Is the signature format valid (like \"openpgp\", \"x509\", \"ssh\" or\n> + * \"unknown\")\n> + */\n> +int valid_signature_format(const char *format);\n> +\n>  /*\n>   * Look at a GPG signed tag object.  If such a signature exists, store it in\n>   * signature and the signed content in payload.  Return 1 if a signature was\n> diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\n> index 76619765fc..46700dbc40 100755\n> --- a/t/t9350-fast-export.sh\n> +++ b/t/t9350-fast-export.sh\n> @@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '\n>  test_expect_success GPG 'signed-commits=verbatim' '\n>\n>         git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&\n> -       grep \"^gpgsig sha\" output &&\n> +       test_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n>         grep \"encoding ISO-8859-1\" output &&\n>         (\n>                 cd new &&\n> @@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '\n>  test_expect_success GPG 'signed-commits=warn-verbatim' '\n>\n>         git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&\n> -       grep \"^gpgsig sha\" output &&\n> +       test_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n>         grep \"encoding ISO-8859-1\" output &&\n>         test -s err &&\n>         (\n> @@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '\n>\n>  '\n>\n> +test_expect_success GPGSM 'setup X.509 signed commit' '\n> +\n> +       git checkout -b x509-signing main &&\n> +       test_config gpg.format x509 &&\n> +       test_config user.signingkey $GIT_COMMITTER_EMAIL &&\n> +       echo \"X.509 content\" >file &&\n> +       git add file &&\n> +       git commit -S -m \"X.509 signed commit\" &&\n> +       X509_COMMIT=$(git rev-parse HEAD) &&\n> +       git checkout main\n> +\n> +'\n> +\n> +test_expect_success GPGSM 'round-trip X.509 signed commit' '\n> +\n> +       git fast-export --signed-commits=verbatim x509-signing >output &&\n> +       test_grep -E \"^gpgsig $GIT_DEFAULT_HASH x509\" output &&\n> +       (\n> +               cd new &&\n> +               git fast-import &&\n> +               git cat-file commit refs/heads/x509-signing >actual &&\n> +               grep \"^gpgsig\" actual &&\n> +               IMPORTED=$(git rev-parse refs/heads/x509-signing) &&\n> +               test $X509_COMMIT = $IMPORTED\n> +       ) <output\n> +\n> +'\n> +\n> +test_expect_success GPGSSH 'setup SSH signed commit' '\n> +\n> +       git checkout -b ssh-signing main &&\n> +       test_config gpg.format ssh &&\n> +       test_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n> +       echo \"SSH content\" >file &&\n> +       git add file &&\n> +       git commit -S -m \"SSH signed commit\" &&\n> +       SSH_COMMIT=$(git rev-parse HEAD) &&\n> +       git checkout main\n> +\n> +'\n> +\n> +test_expect_success GPGSSH 'round-trip SSH signed commit' '\n> +\n> +       git fast-export --signed-commits=verbatim ssh-signing >output &&\n> +       test_grep -E \"^gpgsig $GIT_DEFAULT_HASH ssh\" output &&\n> +       (\n> +               cd new &&\n> +               git fast-import &&\n> +               git cat-file commit refs/heads/ssh-signing >actual &&\n> +               grep \"^gpgsig\" actual &&\n> +               IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&\n> +               test $SSH_COMMIT = $IMPORTED\n> +       ) <output\n> +\n> +'\n> +\n>  test_expect_success 'setup submodule' '\n>\n>         test_config_global protocol.file.allow always &&\n> @@ -905,4 +961,46 @@ test_expect_success 'fast-export handles --end-of-options' '\n>         test_cmp expect actual\n>  '\n>\n> +test_expect_success GPG 'setup a commit with dual 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> +       echo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&\n> +       test_grep -E \"^gpgsig \" out &&\n> +       test_grep -E \"^gpgsig-sha256 \" out\n> +'\n> +\n> +test_expect_success GPG 'export and import of doubly signed commit' '\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> +\n> +       (\n> +               cd new &&\n> +               git fast-import &&\n> +               git cat-file commit refs/heads/dual-signed >actual &&\n> +               test_grep -E \"^gpgsig \" actual &&\n> +               test_grep -E \"^gpgsig-sha256 \" actual &&\n> +               IMPORTED=$(git rev-parse refs/heads/dual-signed) &&\n> +               if test \"$GIT_DEFAULT_HASH\" = \"sha1\"\n> +               then\n> +                       test $SHA1_B = $IMPORTED\n> +               else\n> +                       test $SHA256_B = $IMPORTED\n> +               fi\n> +       ) <output\n\nThis last bit seems a bit fragile; could the redirection of output to\nthe stdin of `git fast-import` be made specific to that one line\ninstead of to the whole range of commands?\n\nOtherwise, I very much appreciate the work to create a testcase with\nboth signature types on a single commit.\n"},{"id":"521592","messageId":"xmqqqzyqqlh3.fsf@gitster.g","threadId":"63342","inReplyTo":"CABPp-BF6OvH8oh=jG_8fWoC5gW+9E+wx=uDEk1uerJTOva5isg@mail.gmail.com","subject":"Re: [PATCH v5] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-09T00:03:20Z","receivedAt":"2025-07-09T00:03:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> +       if (!space)\n>> +               die(\"Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', \"\n>> +                   \"got 'gpgsig %s'\", args);\n>> +       *space++ = '\\0';\n>> +\n>> +       sig->hash_algo = args;\n>> +       sig->sig_format = space;\n>\n> Really minor nitpick, but it might be clearer to pre-increment space\n> here than to increment it above.\n\nFWIW, I find what Christian wrote easier to follow.  We find a \" \"\nin the buffer, point it with a pointer and NUL-terminate the\nsubstring.  We know we want to further process bytes that follow, so\nthe pointer is post-incremented after the NUL-termination.  The next\nuser of that pointer relies on the fact that the previous user\nconcluded its use with that post-increment.\n\nIf 'space' variable were named more genericly, like '*cp', it would\nhave been perfect.  Perhaps only the first half of the code was\nwritten first, and it looked for a space, so the name was chosen,\nbut then later ...\n\n>> +\n>> +       /* Remove any trailing newline from format */\n>> +       space = strchr(sig->sig_format, '\\n');\n\n... it is used to point at a LF X-<, at which time the author could\nhave renamed it to keep readers' sanity ;-)\n\n>> +       if (space)\n>> +               *space = '\\0';\n\nI also wonder what should (not \"does\", as I can see that the code\ndoes not do anything) happen if we do not find the LF we were\nlooking for.  Is the caller of this function so loosely written that\nit may or may not guarantee that the data it calls this function\nwith is properly terminated?\n\n>> +test_expect_success GPG 'export and import of doubly signed commit' '\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>> +\n>> +       (\n>> +               cd new &&\n>> +               git fast-import &&\n>> +               git cat-file commit refs/heads/dual-signed >actual &&\n>> +               test_grep -E \"^gpgsig \" actual &&\n>> +               test_grep -E \"^gpgsig-sha256 \" actual &&\n>> +               IMPORTED=$(git rev-parse refs/heads/dual-signed) &&\n>> +               if test \"$GIT_DEFAULT_HASH\" = \"sha1\"\n>> +               then\n>> +                       test $SHA1_B = $IMPORTED\n>> +               else\n>> +                       test $SHA256_B = $IMPORTED\n>> +               fi\n>> +       ) <output\n>\n> This last bit seems a bit fragile; could the redirection of output to\n> the stdin of `git fast-import` be made specific to that one line\n> instead of to the whole range of commands?\n>\n> Otherwise, I very much appreciate the work to create a testcase with\n> both signature types on a single commit.\n\nYup, thanks, both of you.  It seems that we are getting closer to\nthe finish line?\n\nThanks.\n"},{"id":"521594","messageId":"CABPp-BHb-yK1E0h4K9rJ1dk3HC6urv6b16birVv-L1pMsqs3Fg@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqqzyqqlh3.fsf@gitster.g","subject":"Re: [PATCH v5] fast-(import|export): improve on commit signature output format","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-07-09T00:10:57Z","receivedAt":"2025-07-09T00:11:09Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Jul 8, 2025 at 5:03 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n[...]\n> Yup, thanks, both of you.  It seems that we are getting closer to\n> the finish line?\n\nI think so; a few small touch-ups and I think this one should be good to go.\n"},{"id":"521597","messageId":"CAP8UFD1mgKT0AFuoYfisHMinP6KEDahcXCwiK6-wRFBKKymfsQ@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqbjpuwsbm.fsf@gitster.g","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-09T00:19:06Z","receivedAt":"2025-07-09T00:19:19Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Tue, Jul 8, 2025 at 6:38 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > Also if a contributor comes back with improved patches that try to\n> > follow closely what a reviewer suggested, then I think it can (and\n> > should) make a reviewer feel like they have really been heard better\n> > than just a hollow reply right away followed later by less well\n> > thought out patches.\n>\n> That is kind of \"better late than never\".  I would expect better\n> than that from more senior prominent contributors ;-)\n>\n> And I totally agree with you that reviews often deserve very well\n> reasoned responses, which take time to prepare; a response that\n> comes as spinal reflex without much thought is often not very\n> useful.\n>\n> It really depends on the definition of \"fast\" in \"fast response\".\n>\n> If we need a week to come up with a newer iteration,\n\nThe issue is that whatever the time we could set as a norm, like \"a\nweek\" here or 2 or 3 days, or one month, or whatever, we actually\ndon't know what happens in the life of contributors. Maybe some have\nhealth issues, maybe some work only a few days per week, maybe some\noare working on their free time and don't have much free time, maybe\nthey are asked to work a lot by their company on other internal urgent\nthings, maybe they have to take care of dependent people in their\nfamily, etc... So setting any norm here that everyone should try to\nrespect might just not work for some people.\n\nFor example there was a former Outreachy intern who continued working\nfor about 2 years on their project after the internship was over. She\nwas a young mother so didn't have much time to work on Git and would\ncome back to the mailing list only every few months with new patches\nand replies to reviewers. What would we have gained exactly by\nimposing a norm on her?\n\n> it would be\n> fair to expect that we can say something like \"I agree, I'll fix\",\n> \"I am not convinced because ...\", \"I am skeptical but let me first\n> see how it pans out\", etc. by day #2 or #3, wouldn't it?  Upon\n> recieving a response at the same time or soon after an updated\n> iteration was sent, especially when the response is \"no, I do not\n> think so\", what is the reviewer expected to do?  Saying \"you may not\n> think so but here is another point that may make you reconsider\"\n> would be too late, so it would actively discourage continued\n> discussion.\n>\n> > It doesn't mean that I think oldtimers should have some kind of\n> > privilege, and yeah they should also try to give a good example. But\n> > we should allow people to not always behave in a very formatted way.\n>\n> Old timers learn from experience how other old timers operate ;-)\n> and I have learned to ignore the usual signal when anticipating what\n> your next iteration may look like (in other words, interim responses\n> or lack thereof is usually a good signal for most developers, but\n> not for you---you tend to come back with your next iteration without\n> much interim interactions).  But other contributors shouldn't be\n> forced to. That is what we need some community norm for.\n>\n> >> On our team's handbook page [1] we have the following couple of bullet\n> >> points regarding how to respond to reviews:\n> >\n> > Yeah, I think they are likely to be good for newcomers.\n>\n> The handbook here is gitlab's team handbook, and it may not apply to\n> open source Git development community, but \"this rule applies only\n> to newcomers, I am above that rule\" is the same thing as saying\n> \"oldtimers like me should have some kind of privilege\".  I do not\n> know what to think about this and what you said above.\n\nI think it's fair to say that oldtimers are less likely to disappear\ntomorrow or to not follow up on reviewer feedback. So arguments like\n\"replying fast makes reviewers confident that they have been heard\"\nare just less true for oldtimers than for newcomers.\n\nAnother argument is that oldtimers are more likely to burn out or have\nmental health issues related to their Git work than newcomers. Adding\na norm that would put pressure on them to work more, or at times they\nwould prefer to do other things, significantly increases the burnout\nand mental health risks for them.\n\nSo putting pressure on oldtimers to follow a norm that is less\nrelevant for them but puts them at greater risk is just a bad idea in\nmy opinion.\n"},{"id":"521643","messageId":"CAP8UFD2qpFTy0jtrKk4iQ1C+7dZDV7Abqrqz3J6qx2T4Jy0ESA@mail.gmail.com","threadId":"63342","inReplyTo":"CABPp-BF6OvH8oh=jG_8fWoC5gW+9E+wx=uDEk1uerJTOva5isg@mail.gmail.com","subject":"Re: [PATCH v5] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-09T10:15:49Z","receivedAt":"2025-07-09T10:16:04Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, Jul 9, 2025 at 1:08 AM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Tue, Jul 8, 2025 at 2:18 AM Christian Couder\n> <christian.couder@gmail.com> wrote:\n\n> > diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\n> > index 250d866652..89dec1108f 100644\n> > --- a/Documentation/git-fast-import.adoc\n> > +++ b/Documentation/git-fast-import.adoc\n> > @@ -445,7 +445,7 @@ one).\n> >         original-oid?\n> >         ('author' (SP <name>)? SP LT <email> GT SP <when> LF)?\n> >         'committer' (SP <name>)? SP LT <email> GT SP <when> LF\n> > -       ('gpgsig' SP <alg> LF data)?\n> > +       ('gpgsig' SP <algo> SP <format> LF data)?\n> >         ('encoding' SP <encoding> LF)?\n> >         data\n> >         ('from' SP <commit-ish> LF)?\n> > @@ -518,13 +518,37 @@ their syntax.\n> >  ^^^^^^^^\n> >\n> >  The optional `gpgsig` command is used to include a PGP/GPG signature\n> > -that signs the commit data.\n> > +or other cryptographic signature that signs the commit data.\n> >\n> > -Here <alg> specifies which hashing algorithm is used for this\n> > -signature, either `sha1` or `sha256`.\n> > +....\n> > +       'gpgsig' SP <git-hash-algo> SP <signature-format> LF\n> > +       data\n>\n> Should the `data` be moved to the line above, just to make it clear\n> it's associated with it?  (Similar to the first line you changed in\n> git-fast-import.adoc?)\n\nYeah, I have changed it in my current version.\n\n> > +....\n> > +\n> > +The `gpgsig` command takes two arguments:\n> > +\n> > +* `<git-hash-algo>` specifies which Git object format this signature\n> > +  applies to, either `sha1` or `sha256`. This allows to know which\n> > +  representation of the commit was signed (the SHA-1 or the SHA-256\n> > +  version) which helps with both signature verification and\n> > +  interoperability between repos with different hash functions.\n>\n> Should there also be a note added that fast-import is limited on what\n> signatures it can verify if extensions.compatObjectFormat is not set?\n\nIt cannot yet verify signatures, but yeah it's probably a good idea to\nsay that setting this config option might help with verifying\nsignatures when it will be able to do it. See below.\n\n> > +* `<signature-format>` specifies the type of signature, such as\n> > +  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for\n> > +  tools that process the stream, so they don't have to parse the ASCII\n> > +  armor to identify the signature type.\n> > +\n> > +A commit may have at most one signature for the SHA-1 object format\n> > +(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n> > +(stored in the \"gpgsig-sha256\" header).\n> > +\n> > +See below for a detailed description of the `data` command which\n> > +contains the raw signature data.\n> > +\n> > +Signatures are not yet checked in the current implementation though.\n>\n> ...or maybe that extra note could be added as a parenthetical comment\n> here for now?\n\nYeah, in my current version, there is:\n\n\"Signatures are not yet checked in the current implementation\nthough. (Already setting the `extensions.compatObjectFormat`\nconfiguration option might help with verifying both SHA-1 and SHA-256\nobject format signatures when it will be implemented.)\"\n\n[...]\n\n\n> > +test_expect_success GPG 'export and import of doubly signed commit' '\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> > +\n> > +       (\n> > +               cd new &&\n> > +               git fast-import &&\n> > +               git cat-file commit refs/heads/dual-signed >actual &&\n> > +               test_grep -E \"^gpgsig \" actual &&\n> > +               test_grep -E \"^gpgsig-sha256 \" actual &&\n> > +               IMPORTED=$(git rev-parse refs/heads/dual-signed) &&\n> > +               if test \"$GIT_DEFAULT_HASH\" = \"sha1\"\n> > +               then\n> > +                       test $SHA1_B = $IMPORTED\n> > +               else\n> > +                       test $SHA256_B = $IMPORTED\n> > +               fi\n> > +       ) <output\n>\n> This last bit seems a bit fragile; could the redirection of output to\n> the stdin of `git fast-import` be made specific to that one line\n> instead of to the whole range of commands?\n\nI used the same style as many other tests in this file. For example\nthere are already:\n\ntest_expect_success GPG 'signed-commits=verbatim' '\n\n    git fast-export --signed-commits=verbatim --reencode=no\ncommit-signing >output &&\n    grep \"^gpgsig sha\" output &&\n    grep \"encoding ISO-8859-1\" output &&\n    (\n        cd new &&\n        git fast-import &&\n        STRIPPED=$(git rev-parse --verify refs/heads/commit-signing) &&\n        test $COMMIT_SIGNING = $STRIPPED\n    ) <output\n\n'\n\ntest_expect_success GPG 'signed-commits=warn-verbatim' '\n\n    git fast-export --signed-commits=warn-verbatim --reencode=no\ncommit-signing >output 2>err &&\n    grep \"^gpgsig sha\" output &&\n    grep \"encoding ISO-8859-1\" output &&\n    test -s err &&\n    (\n        cd new &&\n        git fast-import &&\n        STRIPPED=$(git rev-parse --verify refs/heads/commit-signing) &&\n        test $COMMIT_SIGNING = $STRIPPED\n    ) <output\n\n'\n\netc.\n\nSo to keep a consistent style in the tests, I would likely have to add\na preparatory commit that changes the style of all these existing\ntests. Not sure it's worth it at this point.\n\n> Otherwise, I very much appreciate the work to create a testcase with\n> both signature types on a single commit.\n\nThanks for the kind words and for your reviews!\n"},{"id":"521644","messageId":"CAP8UFD3h0TcO7zRgOOTBTLjRW9XWD2YMJwQZf42HNS9=UU-Kjg@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqqzyqqlh3.fsf@gitster.g","subject":"Re: [PATCH v5] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-09T10:18:28Z","receivedAt":"2025-07-09T10:18:41Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Wed, Jul 9, 2025 at 2:03 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n\n> > Really minor nitpick, but it might be clearer to pre-increment space\n> > here than to increment it above.\n>\n> FWIW, I find what Christian wrote easier to follow.  We find a \" \"\n> in the buffer, point it with a pointer and NUL-terminate the\n> substring.  We know we want to further process bytes that follow, so\n> the pointer is post-incremented after the NUL-termination.  The next\n> user of that pointer relies on the fact that the previous user\n> concluded its use with that post-increment.\n>\n> If 'space' variable were named more genericly, like '*cp', it would\n> have been perfect. Perhaps only the first half of the code was\n> written first, and it looked for a space, so the name was chosen,\n> but then later ...\n>\n> >> +\n> >> +       /* Remove any trailing newline from format */\n> >> +       space = strchr(sig->sig_format, '\\n');\n>\n> ... it is used to point at a LF X-<, at which time the author could\n> have renamed it to keep readers' sanity ;-)\n\nYeah, right, I couldn't think of a good name to rename it, so I\npostponed changing the name, and then forgot about it.\n\n> >> +       if (space)\n> >> +               *space = '\\0';\n>\n> I also wonder what should (not \"does\", as I can see that the code\n> does not do anything) happen if we do not find the LF we were\n> looking for.  Is the caller of this function so loosely written that\n> it may or may not guarantee that the data it calls this function\n> with is properly terminated?\n\nThis function is passed a pointer to 'command_buf.buf' after \"gpgsig \"\nwas skipped from it. 'read_next_command()' uses 'strbuf_getline_lf()'\nto put each line in 'command_buf.buf'. And yeah 'strbuf_getline_lf()'\nshould remove a LF from the end of 'command_buf.buf' if any.\n\nSo we don't need the code to remove any trailing LF. I have removed it\nin my current version. This means that we can keep using \"space\" as\nthe variable name, as it is now only related to the space between the\narguments.\n\n> > Otherwise, I very much appreciate the work to create a testcase with\n> > both signature types on a single commit.\n>\n> Yup, thanks, both of you.  It seems that we are getting closer to\n> the finish line?\n\nYeah, I plan to send a v6 soon, maybe later today with the changes\ndiscussed here and in my reply to Elijah.\n\nThanks.\n"},{"id":"521675","messageId":"20250709141253.623563-1-christian.couder@gmail.com","threadId":"63342","inReplyTo":"20250708091738.4072857-1-christian.couder@gmail.com","subject":"[PATCH v6] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-09T14:12:53Z","receivedAt":"2025-07-09T14:13:17Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for\nsigned-commits, 2025-03-10), added support for signed commits to\nfast-export and fast-import.\n\nWhen a signed commit is processed, fast-export can output either\n\"gpgsig sha1\" or \"gpgsig sha256\" depending on whether the signed\ncommit uses the SHA-1 or SHA-256 Git object format.\n\nHowever, this implementation has a number of limitations:\n\n  - the output format was not properly described in the documentation,\n  - the output format is not very informative as it doesn't even say\n    if the signature is an OpenPGP, an SSH, or an X509 signature,\n  - the implementation doesn't support having both one signature on\n    the SHA-1 object and one on the SHA-256 object.\n\nLet's improve on these limitations by improving fast-export and\nfast-import so that:\n\n  - all the signatures are exported,\n  - at most one signature on the SHA-1 object and one on the SHA-256\n    are imported,\n  - if there is more than one signature on the SHA-1 object or on\n    the SHA-256 object, fast-import emits a warning for each\n    additional signature,\n  - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n    where <git-hash-algo> is the Git object format as before, and\n    <signature-format> is the signature type (\"openpgp\", \"x509\",\n    \"ssh\" or \"unknown\"),\n  - the output is properly documented.\n\nAbout the output format:\n\n  - <git-hash-algo> allows to know which representation of the commit\n    was signed (the SHA-1 or the SHA-256 version) which helps with\n    both signature verification and interoperability between repos\n    with different hash functions,\n\n  - <signature-format> helps tools that process the fast-export\n    stream, so they don't have to parse the ASCII armor to identify\n    the signature type.\n\nIt could be even better to be able to import more than one signature\non the SHA-1 object and on the SHA-256 object, but other parts of\nGit don't handle that well for now, so this is left for future\nimprovements.\n\nHelped-by: brian m. carlson <sandals@crustytoothpaste.net>\nHelped-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Christian Couder <chriscool@tuxfamily.org>\n---\n\nThis v6 is very similar to v5. There are only a few small changes:\n\n  - In \"git-fast-import.adoc\", on a synopsys of the 'gpgsig' command,\n    'data' has been moved to the same line as the previous parts of\n    the synopsys.\n\n  - In \"git-fast-import.adoc\", it is suggested that setting the\n    `extensions.compatObjectFormat` config option might help in the\n    future with verifying signatures.\n\n  - In \"builtin/fast-import.c\", \"or\" has been added in a comment that\n    lists possible values for the signature format.\n\n  - In \"builtin/fast-import.c\", some useless code that checked for a\n    possible LF at the end of the 'gpgsig' command arguments has been\n    removed.\n\nThanks to brian, Elijah and Junio who commented on the previous\nversions.\n\nCI tests:\n\nhttps://github.com/chriscool/git/actions/runs/16165147132\n\nAll the tests passed.\n\nRange-diff with v5:\n\n1:  5791617d7d ! 1:  6e07895ea1 fast-(import|export): improve on commit signature output format\n    @@ Documentation/git-fast-import.adoc: their syntax.\n     -Here <alg> specifies which hashing algorithm is used for this\n     -signature, either `sha1` or `sha256`.\n     +....\n    -+\t'gpgsig' SP <git-hash-algo> SP <signature-format> LF\n    -+\tdata\n    ++\t'gpgsig' SP <git-hash-algo> SP <signature-format> LF data\n     +....\n     +\n     +The `gpgsig` command takes two arguments:\n    @@ Documentation/git-fast-import.adoc: their syntax.\n     +See below for a detailed description of the `data` command which\n     +contains the raw signature data.\n     +\n    -+Signatures are not yet checked in the current implementation though.\n    ++Signatures are not yet checked in the current implementation\n    ++though. (Already setting the `extensions.compatObjectFormat`\n    ++configuration option might help with verifying both SHA-1 and SHA-256\n    ++object format signatures when it will be implemented.)\n      \n     -NOTE: This is highly experimental and the format of the data stream may\n     -change in the future without compatibility guarantees.\n    @@ builtin/fast-import.c: static struct hash_list *parse_merge(unsigned int *count)\n      \n     +struct signature_data {\n     +\tchar *hash_algo;      /* \"sha1\" or \"sha256\" */\n    -+\tchar *sig_format;     /* \"openpgp\", \"x509\", \"ssh\", \"unknown\" */\n    ++\tchar *sig_format;     /* \"openpgp\", \"x509\", \"ssh\", or \"unknown\" */\n     +\tstruct strbuf data;   /* The actual signature data */\n     +};\n     +\n    @@ builtin/fast-import.c: static struct hash_list *parse_merge(unsigned int *count)\n     +\tif (!space)\n     +\t\tdie(\"Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', \"\n     +\t\t    \"got 'gpgsig %s'\", args);\n    -+\t*space++ = '\\0';\n    ++\t*space = '\\0';\n     +\n     +\tsig->hash_algo = args;\n    -+\tsig->sig_format = space;\n    -+\n    -+\t/* Remove any trailing newline from format */\n    -+\tspace = strchr(sig->sig_format, '\\n');\n    -+\tif (space)\n    -+\t\t*space = '\\0';\n    ++\tsig->sig_format = space + 1;\n     +\n     +\t/* Validate hash algorithm */\n     +\tif (strcmp(sig->hash_algo, \"sha1\") &&\n\n\n Documentation/git-fast-export.adoc |  17 +++++\n Documentation/git-fast-import.adoc |  38 ++++++++--\n builtin/fast-export.c              |  62 ++++++++++++----\n builtin/fast-import.c              | 113 +++++++++++++++++++++++------\n gpg-interface.c                    |  12 +++\n gpg-interface.h                    |  12 +++\n t/t9350-fast-export.sh             | 102 +++++++++++++++++++++++++-\n 7 files changed, 312 insertions(+), 44 deletions(-)\n\ndiff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc\nindex 43bbb4f63c..297b57bb2e 100644\n--- a/Documentation/git-fast-export.adoc\n+++ b/Documentation/git-fast-export.adoc\n@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.\n \tis the same as how earlier versions of this command without\n \tthis option behaved.\n +\n+When exported, a signature starts with:\n++\n+gpgsig <git-hash-algo> <signature-format>\n++\n+where <git-hash-algo> is the Git object hash so either \"sha1\" or\n+\"sha256\", and <signature-format> is the signature type, so \"openpgp\",\n+\"x509\", \"ssh\" or \"unknown\".\n++\n+For example, an OpenPGP signature on a SHA-1 commit starts with\n+`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit\n+starts with `gpgsig sha256 ssh`.\n++\n+While all the signatures of a commit are exported, an importer may\n+choose to accept only some of them. For example\n+linkgit:git-fast-import[1] currently stores at most one signature per\n+Git hash algorithm in each commit.\n++\n NOTE: This is highly experimental and the format of the data stream may\n change in the future without compatibility guarantees.\n \ndiff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc\nindex 250d866652..d232784200 100644\n--- a/Documentation/git-fast-import.adoc\n+++ b/Documentation/git-fast-import.adoc\n@@ -445,7 +445,7 @@ one).\n \toriginal-oid?\n \t('author' (SP <name>)? SP LT <email> GT SP <when> LF)?\n \t'committer' (SP <name>)? SP LT <email> GT SP <when> LF\n-\t('gpgsig' SP <alg> LF data)?\n+\t('gpgsig' SP <algo> SP <format> LF data)?\n \t('encoding' SP <encoding> LF)?\n \tdata\n \t('from' SP <commit-ish> LF)?\n@@ -518,13 +518,39 @@ their syntax.\n ^^^^^^^^\n \n The optional `gpgsig` command is used to include a PGP/GPG signature\n-that signs the commit data.\n+or other cryptographic signature that signs the commit data.\n \n-Here <alg> specifies which hashing algorithm is used for this\n-signature, either `sha1` or `sha256`.\n+....\n+\t'gpgsig' SP <git-hash-algo> SP <signature-format> LF data\n+....\n+\n+The `gpgsig` command takes two arguments:\n+\n+* `<git-hash-algo>` specifies which Git object format this signature\n+  applies to, either `sha1` or `sha256`. This allows to know which\n+  representation of the commit was signed (the SHA-1 or the SHA-256\n+  version) which helps with both signature verification and\n+  interoperability between repos with different hash functions.\n+\n+* `<signature-format>` specifies the type of signature, such as\n+  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for\n+  tools that process the stream, so they don't have to parse the ASCII\n+  armor to identify the signature type.\n+\n+A commit may have at most one signature for the SHA-1 object format\n+(stored in the \"gpgsig\" header) and one for the SHA-256 object format\n+(stored in the \"gpgsig-sha256\" header).\n+\n+See below for a detailed description of the `data` command which\n+contains the raw signature data.\n+\n+Signatures are not yet checked in the current implementation\n+though. (Already setting the `extensions.compatObjectFormat`\n+configuration option might help with verifying both SHA-1 and SHA-256\n+object format signatures when it will be implemented.)\n \n-NOTE: This is highly experimental and the format of the data stream may\n-change in the future without compatibility guarantees.\n+NOTE: This is highly experimental and the format of the `gpgsig`\n+command may change in the future without compatibility guarantees.\n \n `encoding`\n ^^^^^^^^^^\ndiff --git a/builtin/fast-export.c b/builtin/fast-export.c\nindex fcf6b00d5f..7b4e6a6e41 100644\n--- a/builtin/fast-export.c\n+++ b/builtin/fast-export.c\n@@ -29,6 +29,7 @@\n #include \"quote.h\"\n #include \"remote.h\"\n #include \"blob.h\"\n+#include \"gpg-interface.h\"\n \n static const char *const fast_export_usage[] = {\n \tN_(\"git fast-export [<rev-list-opts>]\"),\n@@ -652,6 +653,38 @@ static const char *find_commit_multiline_header(const char *msg,\n \treturn strbuf_detach(&val, NULL);\n }\n \n+static void print_signature(const char *signature, const char *object_hash)\n+{\n+\tif (!signature)\n+\t\treturn;\n+\n+\tprintf(\"gpgsig %s %s\\ndata %u\\n%s\\n\",\n+\t       object_hash,\n+\t       get_signature_format(signature),\n+\t       (unsigned)strlen(signature),\n+\t       signature);\n+}\n+\n+static const char *append_signatures_for_header(struct string_list *signatures,\n+\t\t\t\t\t\tconst char *pos,\n+\t\t\t\t\t\tconst char *header,\n+\t\t\t\t\t\tconst char *object_hash)\n+{\n+\tconst char *signature;\n+\tconst char *start = pos;\n+\tconst char *end = pos;\n+\n+\twhile ((signature = find_commit_multiline_header(start + 1,\n+\t\t\t\t\t\t\t header,\n+\t\t\t\t\t\t\t &end))) {\n+\t\tstring_list_append(signatures, signature)->util = (void *)object_hash;\n+\t\tfree((char *)signature);\n+\t\tstart = end;\n+\t}\n+\n+\treturn end;\n+}\n+\n static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t\t\t  struct string_list *paths_of_changed_objects)\n {\n@@ -660,7 +693,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \tconst char *author, *author_end, *committer, *committer_end;\n \tconst char *encoding = NULL;\n \tsize_t encoding_len;\n-\tconst char *signature_alg = NULL, *signature = NULL;\n+\tstruct string_list signatures = STRING_LIST_INIT_DUP;\n \tconst char *message;\n \tchar *reencoded = NULL;\n \tstruct commit_list *p;\n@@ -700,10 +733,11 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \t}\n \n \tif (*commit_buffer_cursor == '\\n') {\n-\t\tif ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig\", &commit_buffer_cursor)))\n-\t\t\tsignature_alg = \"sha1\";\n-\t\telse if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, \"gpgsig-sha256\", &commit_buffer_cursor)))\n-\t\t\tsignature_alg = \"sha256\";\n+\t\tconst char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n+\t\t\t\t\t\t\t\t      \"gpgsig\", \"sha1\");\n+\t\tconst char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,\n+\t\t\t\t\t\t\t\t\t\"gpgsig-sha256\", \"sha256\");\n+\t\tcommit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;\n \t}\n \n \tmessage = strstr(commit_buffer_cursor, \"\\n\\n\");\n@@ -769,30 +803,30 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,\n \tprintf(\"%.*s\\n%.*s\\n\",\n \t       (int)(author_end - author), author,\n \t       (int)(committer_end - committer), committer);\n-\tif (signature) {\n+\tif (signatures.nr) {\n \t\tswitch (signed_commit_mode) {\n \t\tcase SIGN_ABORT:\n \t\t\tdie(\"encountered signed commit %s; use \"\n \t\t\t    \"--signed-commits=<mode> to handle it\",\n \t\t\t    oid_to_hex(&commit->object.oid));\n \t\tcase SIGN_WARN_VERBATIM:\n-\t\t\twarning(\"exporting signed commit %s\",\n-\t\t\t\toid_to_hex(&commit->object.oid));\n+\t\t\twarning(\"exporting %\"PRIuMAX\" signature(s) for commit %s\",\n+\t\t\t\t(uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));\n \t\t\t/* fallthru */\n \t\tcase SIGN_VERBATIM:\n-\t\t\tprintf(\"gpgsig %s\\ndata %u\\n%s\",\n-\t\t\t       signature_alg,\n-\t\t\t       (unsigned)strlen(signature),\n-\t\t\t       signature);\n+\t\t\tfor (size_t i = 0; i < signatures.nr; i++) {\n+\t\t\t\tstruct string_list_item *item = &signatures.items[i];\n+\t\t\t\tprint_signature(item->string, item->util);\n+\t\t\t}\n \t\t\tbreak;\n \t\tcase SIGN_WARN_STRIP:\n-\t\t\twarning(\"stripping signature from commit %s\",\n+\t\t\twarning(\"stripping signature(s) from commit %s\",\n \t\t\t\toid_to_hex(&commit->object.oid));\n \t\t\t/* fallthru */\n \t\tcase SIGN_STRIP:\n \t\t\tbreak;\n \t\t}\n-\t\tfree((char *)signature);\n+\t\tstring_list_clear(&signatures, 0);\n \t}\n \tif (!reencoded && encoding)\n \t\tprintf(\"encoding %.*s\\n\", (int)encoding_len, encoding);\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex b2839c5f43..b51a17b95a 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -29,6 +29,7 @@\n #include \"commit-reach.h\"\n #include \"khash.h\"\n #include \"date.h\"\n+#include \"gpg-interface.h\"\n \n #define PACK_ID_BITS 16\n #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)\n@@ -2718,15 +2719,82 @@ static struct hash_list *parse_merge(unsigned int *count)\n \treturn list;\n }\n \n+struct signature_data {\n+\tchar *hash_algo;      /* \"sha1\" or \"sha256\" */\n+\tchar *sig_format;     /* \"openpgp\", \"x509\", \"ssh\", or \"unknown\" */\n+\tstruct strbuf data;   /* The actual signature data */\n+};\n+\n+static void parse_one_signature(struct signature_data *sig, const char *v)\n+{\n+\tchar *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n+\tchar *space = strchr(args, ' ');\n+\n+\tif (!space)\n+\t\tdie(\"Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', \"\n+\t\t    \"got 'gpgsig %s'\", args);\n+\t*space = '\\0';\n+\n+\tsig->hash_algo = args;\n+\tsig->sig_format = space + 1;\n+\n+\t/* Validate hash algorithm */\n+\tif (strcmp(sig->hash_algo, \"sha1\") &&\n+\t    strcmp(sig->hash_algo, \"sha256\"))\n+\t\tdie(\"Unknown git hash algorithm in gpgsig: '%s'\", sig->hash_algo);\n+\n+\t/* Validate signature format */\n+\tif (!valid_signature_format(sig->sig_format))\n+\t\tdie(\"Invalid signature format in gpgsig: '%s'\", sig->sig_format);\n+\tif (!strcmp(sig->sig_format, \"unknown\"))\n+\t\twarning(\"'unknown' signature format in gpgsig\");\n+\n+\t/* Read signature data */\n+\tread_next_command();\n+\tparse_data(&sig->data, 0, NULL);\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+{\n+\tstruct string_list siglines = STRING_LIST_INIT_NODUP;\n+\n+\tif (!sig->hash_algo)\n+\t\treturn;\n+\n+\tstrbuf_addstr(commit_data, header);\n+\tstring_list_split_in_place(&siglines, sig->data.buf, \"\\n\", -1);\n+\tstrbuf_add_separated_string_list(commit_data, \"\\n \", &siglines);\n+\tstrbuf_addch(commit_data, '\\n');\n+\tstring_list_clear(&siglines, 1);\n+\tstrbuf_release(&sig->data);\n+\tfree(sig->hash_algo);\n+}\n+\n+static void store_signature(struct signature_data *stored_sig,\n+\t\t\t    struct signature_data *new_sig,\n+\t\t\t    const char *hash_type)\n+{\n+\tif (stored_sig->hash_algo) {\n+\t\twarning(\"multiple %s signatures found, \"\n+\t\t\t\"ignoring additional signature\",\n+\t\t\thash_type);\n+\t\tstrbuf_release(&new_sig->data);\n+\t\tfree(new_sig->hash_algo);\n+\t} else {\n+\t\t*stored_sig = *new_sig;\n+\t}\n+}\n+\n static void parse_new_commit(const char *arg)\n {\n-\tstatic struct strbuf sig = STRBUF_INIT;\n \tstatic struct strbuf msg = STRBUF_INIT;\n-\tstruct string_list siglines = STRING_LIST_INIT_NODUP;\n+\tstruct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };\n+\tstruct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };\n \tstruct branch *b;\n \tchar *author = NULL;\n \tchar *committer = NULL;\n-\tchar *sig_alg = NULL;\n \tchar *encoding = NULL;\n \tstruct hash_list *merge_list = NULL;\n \tunsigned int merge_count;\n@@ -2750,13 +2818,23 @@ static void parse_new_commit(const char *arg)\n \t}\n \tif (!committer)\n \t\tdie(\"Expected committer but didn't get one\");\n-\tif (skip_prefix(command_buf.buf, \"gpgsig \", &v)) {\n-\t\tsig_alg = xstrdup(v);\n-\t\tread_next_command();\n-\t\tparse_data(&sig, 0, NULL);\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+\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\tread_next_command();\n-\t} else\n-\t\tstrbuf_setlen(&sig, 0);\n+\t}\n+\n \tif (skip_prefix(command_buf.buf, \"encoding \", &v)) {\n \t\tencoding = xstrdup(v);\n \t\tread_next_command();\n@@ -2830,23 +2908,14 @@ static void parse_new_commit(const char *arg)\n \t\tstrbuf_addf(&new_data,\n \t\t\t\"encoding %s\\n\",\n \t\t\tencoding);\n-\tif (sig_alg) {\n-\t\tif (!strcmp(sig_alg, \"sha1\"))\n-\t\t\tstrbuf_addstr(&new_data, \"gpgsig \");\n-\t\telse if (!strcmp(sig_alg, \"sha256\"))\n-\t\t\tstrbuf_addstr(&new_data, \"gpgsig-sha256 \");\n-\t\telse\n-\t\t\tdie(\"Expected gpgsig algorithm sha1 or sha256, got %s\", sig_alg);\n-\t\tstring_list_split_in_place(&siglines, sig.buf, \"\\n\", -1);\n-\t\tstrbuf_add_separated_string_list(&new_data, \"\\n \", &siglines);\n-\t\tstrbuf_addch(&new_data, '\\n');\n-\t}\n+\n+\tadd_gpgsig_to_commit(&new_data, \"gpgsig \", &sig_sha1);\n+\tadd_gpgsig_to_commit(&new_data, \"gpgsig-sha256 \", &sig_sha256);\n+\n \tstrbuf_addch(&new_data, '\\n');\n \tstrbuf_addbuf(&new_data, &msg);\n-\tstring_list_clear(&siglines, 1);\n \tfree(author);\n \tfree(committer);\n-\tfree(sig_alg);\n \tfree(encoding);\n \n \tif (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))\ndiff --git a/gpg-interface.c b/gpg-interface.c\nindex 0896458de5..6f2d87475f 100644\n--- a/gpg-interface.c\n+++ b/gpg-interface.c\n@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)\n \treturn NULL;\n }\n \n+const char *get_signature_format(const char *buf)\n+{\n+\tstruct gpg_format *format = get_format_by_sig(buf);\n+\treturn format ? format->name : \"unknown\";\n+}\n+\n+int valid_signature_format(const char *format)\n+{\n+       return (!!get_format_by_name(format) ||\n+\t       !strcmp(format, \"unknown\"));\n+}\n+\n void signature_check_clear(struct signature_check *sigc)\n {\n \tFREE_AND_NULL(sigc->payload);\ndiff --git a/gpg-interface.h b/gpg-interface.h\nindex e09f12e8d0..60ddf8bbfa 100644\n--- a/gpg-interface.h\n+++ b/gpg-interface.h\n@@ -47,6 +47,18 @@ struct signature_check {\n \n void signature_check_clear(struct signature_check *sigc);\n \n+/*\n+ * Return the format of the signature (like \"openpgp\", \"x509\", \"ssh\"\n+ * or \"unknown\").\n+ */\n+const char *get_signature_format(const char *buf);\n+\n+/*\n+ * Is the signature format valid (like \"openpgp\", \"x509\", \"ssh\" or\n+ * \"unknown\")\n+ */\n+int valid_signature_format(const char *format);\n+\n /*\n  * Look at a GPG signed tag object.  If such a signature exists, store it in\n  * signature and the signed content in payload.  Return 1 if a signature was\ndiff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh\nindex 76619765fc..46700dbc40 100755\n--- a/t/t9350-fast-export.sh\n+++ b/t/t9350-fast-export.sh\n@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '\n test_expect_success GPG 'signed-commits=verbatim' '\n \n \tgit fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&\n-\tgrep \"^gpgsig sha\" output &&\n+\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \t(\n \t\tcd new &&\n@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '\n test_expect_success GPG 'signed-commits=warn-verbatim' '\n \n \tgit fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&\n-\tgrep \"^gpgsig sha\" output &&\n+\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH openpgp\" output &&\n \tgrep \"encoding ISO-8859-1\" output &&\n \ttest -s err &&\n \t(\n@@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '\n \n '\n \n+test_expect_success GPGSM 'setup X.509 signed commit' '\n+\n+\tgit checkout -b x509-signing main &&\n+\ttest_config gpg.format x509 &&\n+\ttest_config user.signingkey $GIT_COMMITTER_EMAIL &&\n+\techo \"X.509 content\" >file &&\n+\tgit add file &&\n+\tgit commit -S -m \"X.509 signed commit\" &&\n+\tX509_COMMIT=$(git rev-parse HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSM 'round-trip X.509 signed commit' '\n+\n+\tgit fast-export --signed-commits=verbatim x509-signing >output &&\n+\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH x509\" output &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/x509-signing >actual &&\n+\t\tgrep \"^gpgsig\" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/x509-signing) &&\n+\t\ttest $X509_COMMIT = $IMPORTED\n+\t) <output\n+\n+'\n+\n+test_expect_success GPGSSH 'setup SSH signed commit' '\n+\n+\tgit checkout -b ssh-signing main &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\techo \"SSH content\" >file &&\n+\tgit add file &&\n+\tgit commit -S -m \"SSH signed commit\" &&\n+\tSSH_COMMIT=$(git rev-parse HEAD) &&\n+\tgit checkout main\n+\n+'\n+\n+test_expect_success GPGSSH 'round-trip SSH signed commit' '\n+\n+\tgit fast-export --signed-commits=verbatim ssh-signing >output &&\n+\ttest_grep -E \"^gpgsig $GIT_DEFAULT_HASH ssh\" output &&\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/ssh-signing >actual &&\n+\t\tgrep \"^gpgsig\" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/ssh-signing) &&\n+\t\ttest $SSH_COMMIT = $IMPORTED\n+\t) <output\n+\n+'\n+\n test_expect_success 'setup submodule' '\n \n \ttest_config_global protocol.file.allow always &&\n@@ -905,4 +961,46 @@ test_expect_success 'fast-export handles --end-of-options' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success GPG 'setup a commit with dual 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+\techo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&\n+\ttest_grep -E \"^gpgsig \" out &&\n+\ttest_grep -E \"^gpgsig-sha256 \" out\n+'\n+\n+test_expect_success GPG 'export and import of doubly signed commit' '\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+\n+\t(\n+\t\tcd new &&\n+\t\tgit fast-import &&\n+\t\tgit cat-file commit refs/heads/dual-signed >actual &&\n+\t\ttest_grep -E \"^gpgsig \" actual &&\n+\t\ttest_grep -E \"^gpgsig-sha256 \" actual &&\n+\t\tIMPORTED=$(git rev-parse refs/heads/dual-signed) &&\n+\t\tif test \"$GIT_DEFAULT_HASH\" = \"sha1\"\n+\t\tthen\n+\t\t\ttest $SHA1_B = $IMPORTED\n+\t\telse\n+\t\t\ttest $SHA256_B = $IMPORTED\n+\t\tfi\n+\t) <output\n+'\n+\n test_done\n-- \n2.50.0.174.g6e07895ea1\n\n"},{"id":"521692","messageId":"xmqq34b5pebr.fsf@gitster.g","threadId":"63342","inReplyTo":"CAP8UFD1mgKT0AFuoYfisHMinP6KEDahcXCwiK6-wRFBKKymfsQ@mail.gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-09T15:35:20Z","receivedAt":"2025-07-09T15:35:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n>> And I totally agree with you that reviews often deserve very well\n>> reasoned responses, which take time to prepare; a response that\n>> comes as spinal reflex without much thought is often not very\n>> useful.\n>>\n>> It really depends on the definition of \"fast\" in \"fast response\".\n>>\n>> If we need a week to come up with a newer iteration,\n>\n> The issue is that whatever the time we could set as a norm, like \"a\n> week\" here or 2 or 3 days, or one month, or whatever,\n\nYeah, topic sizes varies.\n\nHistorically, summer is a slower season and these messages I sent\nare primarily for me to keep track of topics on flight.  \"I am on\nvacation for a few more weeks so expect response time longer than\nusual\" would have been a perfectly fine response.\n\nNot even acknowledging receipt of review comments was what I\nprimarily saw as a communication gap.\n"},{"id":"521722","messageId":"xmqqple99ctv.fsf@gitster.g","threadId":"63342","inReplyTo":"20250709141253.623563-1-christian.couder@gmail.com","subject":"Re: [PATCH v6] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-09T23:14:20Z","receivedAt":"2025-07-09T23:14:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> +static void parse_one_signature(struct signature_data *sig, const char *v)\n> +{\n> +\tchar *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */\n> +\tchar *space = strchr(args, ' ');\n> +\n> +\tif (!space)\n> +\t\tdie(\"Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', \"\n> +\t\t    \"got 'gpgsig %s'\", args);\n> +\t*space = '\\0';\n> +\n> +\tsig->hash_algo = args;\n> +\tsig->sig_format = space + 1;\n\nThis is minor, but as I already said in the discussion of the\nprevious round, let me remind readers.\n\nI think \"*space++ = '\\0'\" followed by \"->sig_format = space\", as you\nwrote originally, was easier to follow.  If I were doing this 6th\niteration, I would have kept that part of the code around here, but\nwould have renamed \"space\" to a more generic \"cp\" (very often used\nin this codebase to stand for a character pointer).\n\nWill replace and requeue (unless you have v7 before my tomorrow's\nintegration cycle, in which case this iteration may be skipped).\n\nThanks.\n"},{"id":"521739","messageId":"aG94-0A6Qz35ekFh@pks.im","threadId":"63342","inReplyTo":"CAP8UFD1mgKT0AFuoYfisHMinP6KEDahcXCwiK6-wRFBKKymfsQ@mail.gmail.com","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-07-10T08:25:31Z","receivedAt":"2025-07-10T08:25:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 09, 2025 at 02:19:06AM +0200, Christian Couder wrote:\n> On Tue, Jul 8, 2025 at 6:38 PM Junio C Hamano <gitster@pobox.com> wrote:\n> > Christian Couder <christian.couder@gmail.com> writes:\n> >\n> > > Also if a contributor comes back with improved patches that try to\n> > > follow closely what a reviewer suggested, then I think it can (and\n> > > should) make a reviewer feel like they have really been heard better\n> > > than just a hollow reply right away followed later by less well\n> > > thought out patches.\n> >\n> > That is kind of \"better late than never\".  I would expect better\n> > than that from more senior prominent contributors ;-)\n> >\n> > And I totally agree with you that reviews often deserve very well\n> > reasoned responses, which take time to prepare; a response that\n> > comes as spinal reflex without much thought is often not very\n> > useful.\n> >\n> > It really depends on the definition of \"fast\" in \"fast response\".\n> >\n> > If we need a week to come up with a newer iteration,\n> \n> The issue is that whatever the time we could set as a norm, like \"a\n> week\" here or 2 or 3 days, or one month, or whatever, we actually\n> don't know what happens in the life of contributors. Maybe some have\n> health issues, maybe some work only a few days per week, maybe some\n> oare working on their free time and don't have much free time, maybe\n> they are asked to work a lot by their company on other internal urgent\n> things, maybe they have to take care of dependent people in their\n> family, etc... So setting any norm here that everyone should try to\n> respect might just not work for some people.\n> \n> For example there was a former Outreachy intern who continued working\n> for about 2 years on their project after the internship was over. She\n> was a young mother so didn't have much time to work on Git and would\n> come back to the mailing list only every few months with new patches\n> and replies to reviewers. What would we have gained exactly by\n> imposing a norm on her?\n\nThere are always going to be exceptions, that is of course true. But I\nalso think that long-time contributors that are employed to work on Git\nare somewhat special and don't (typically) fall into the mentioned\ngroups. From my perspective, it's especially this group of people that\nshould lead by example and encourage others to behave in a way that is\ngood for the overall Git community. And leading by example in this\ncontext also means that they should encourage healthy discussions.\n\nThat of course doesn't mean that such people should always respond\nwithin an hour, or even within a day. We all have to context switch, and\ncontext switches are costly, so it's entirely reasonable to try and\nminimize them. But outside of any special circumstances (vacations,\nhealth or similar) I think it should be possible to engage in such\ndiscussions within a small handful of days.\n\nIn any case, there of course is a distinction between people employed to\nwork on Git and those that do it in their own free time. The expectation\nthat is extended towards people who work on Git is way higher than the\nexpectation extended towards people who don't.\n\n> > it would be\n> > fair to expect that we can say something like \"I agree, I'll fix\",\n> > \"I am not convinced because ...\", \"I am skeptical but let me first\n> > see how it pans out\", etc. by day #2 or #3, wouldn't it?  Upon\n> > recieving a response at the same time or soon after an updated\n> > iteration was sent, especially when the response is \"no, I do not\n> > think so\", what is the reviewer expected to do?  Saying \"you may not\n> > think so but here is another point that may make you reconsider\"\n> > would be too late, so it would actively discourage continued\n> > discussion.\n> >\n> > > It doesn't mean that I think oldtimers should have some kind of\n> > > privilege, and yeah they should also try to give a good example. But\n> > > we should allow people to not always behave in a very formatted way.\n> >\n> > Old timers learn from experience how other old timers operate ;-)\n> > and I have learned to ignore the usual signal when anticipating what\n> > your next iteration may look like (in other words, interim responses\n> > or lack thereof is usually a good signal for most developers, but\n> > not for you---you tend to come back with your next iteration without\n> > much interim interactions).  But other contributors shouldn't be\n> > forced to. That is what we need some community norm for.\n> >\n> > >> On our team's handbook page [1] we have the following couple of bullet\n> > >> points regarding how to respond to reviews:\n> > >\n> > > Yeah, I think they are likely to be good for newcomers.\n> >\n> > The handbook here is gitlab's team handbook, and it may not apply to\n> > open source Git development community, but \"this rule applies only\n> > to newcomers, I am above that rule\" is the same thing as saying\n> > \"oldtimers like me should have some kind of privilege\".  I do not\n> > know what to think about this and what you said above.\n> \n> I think it's fair to say that oldtimers are less likely to disappear\n> tomorrow or to not follow up on reviewer feedback. So arguments like\n> \"replying fast makes reviewers confident that they have been heard\"\n> are just less true for oldtimers than for newcomers.\n\nI think these are two different things. It's probably true that you get\nsome privileges by being an old timer. But I think it's more in the\nsense of \"You get to tackle bigger things that may not be done in a\nsingle patch series, and we trust you to not just disappear\".\n\nBut with that privilege also comes responsibility. It's those old timers\nthat newcomers look to, so they need to lead by example.\n\n> Another argument is that oldtimers are more likely to burn out or have\n> mental health issues related to their Git work than newcomers. Adding\n> a norm that would put pressure on them to work more, or at times they\n> would prefer to do other things, significantly increases the burnout\n> and mental health risks for them.\n> \n> So putting pressure on oldtimers to follow a norm that is less\n> relevant for them but puts them at greater risk is just a bad idea in\n> my opinion.\n\nThis is of course something we must avoid. Nobody is being helped in\ncase people burn out. There needs to be a middle ground that works for\neveryone.\n\nPatrick\n"},{"id":"521755","messageId":"CAP8UFD2+n3jV4AGWQAD-J9wJ-szuxwOeZybyJvF4eeYvpH0+Fw@mail.gmail.com","threadId":"63342","inReplyTo":"aG94-0A6Qz35ekFh@pks.im","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-10T15:29:11Z","receivedAt":"2025-07-10T15:29:26Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Thu, Jul 10, 2025 at 2:01 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Wed, Jul 09, 2025 at 02:19:06AM +0200, Christian Couder wrote:\n> > On Tue, Jul 8, 2025 at 6:38 PM Junio C Hamano <gitster@pobox.com> wrote:\n\n> > > If we need a week to come up with a newer iteration,\n> >\n> > The issue is that whatever the time we could set as a norm, like \"a\n> > week\" here or 2 or 3 days, or one month, or whatever, we actually\n> > don't know what happens in the life of contributors. Maybe some have\n> > health issues, maybe some work only a few days per week, maybe some\n> > oare working on their free time and don't have much free time, maybe\n> > they are asked to work a lot by their company on other internal urgent\n> > things, maybe they have to take care of dependent people in their\n> > family, etc... So setting any norm here that everyone should try to\n> > respect might just not work for some people.\n> >\n> > For example there was a former Outreachy intern who continued working\n> > for about 2 years on their project after the internship was over. She\n> > was a young mother so didn't have much time to work on Git and would\n> > come back to the mailing list only every few months with new patches\n> > and replies to reviewers. What would we have gained exactly by\n> > imposing a norm on her?\n>\n> There are always going to be exceptions, that is of course true.\n\nOk, so how are we going to manage those exceptions? Are we going to\nask people publicly why they take a long time to reply, and expect\nthat they are going to tell everyone things that might be quite\npersonal?\n\nHow will they feel if they are asked repeatedly by different\nreviewers, in the case some reviewers didn't notice that other\nreviewers have already asked?\n\n> But I\n> also think that long-time contributors that are employed to work on Git\n> are somewhat special and don't (typically) fall into the mentioned\n> groups.\n\nThey could fall into similar groups. They could have a child they take\ncare of, they could have health issues (including burnout) or they\ncould just have a few bad days sometimes. They could also stop being\nemployed to work on Git and still want to contribute.\n\nAre we going to have a public list of people who are allowed to reply\nlate, so that every reviewer can check if it's Ok for the contributor\nto reply late?\n\n> From my perspective, it's especially this group of people that\n> should lead by example and encourage others to behave in a way that is\n> good for the overall Git community.\n\nSo you think it would be good for the community if people start to\nkeep count of how fast others reply and annoy them when they don't\nreply fast enough? In my opinion, insisting on this is just a bad idea\nthat could backfire in many ways.\n\nI am not saying that it wouldn't be good if people in general tried to\nreply soon to reviewers when they receive feedback. I am saying that\nit's not worth the possible costs to insist on this small aspect of\nwhat makes reviewers happy.\n\n> And leading by example in this\n> context also means that they should encourage healthy discussions.\n\nWhy is it not healthy when contributors reply when they think it's\nbest for them to reply? It seems to me that, on the contrary, it's not\nhealthy to put pressure on them to reply when they might not think\nit's best for them to reply.\n\nResponding soon is in my opinion a very small part of what makes\ndiscussions healthy. There is also the tone, the thankfulness, the\nconstructiveness, the technical accuracy of the information shared,\nthe work that might have been done to try solutions, the willingness\nto share knowledge, the effort to try to understand and act on what\nothers say, and so on.\n\nReducing the quality or healthiness of discussions to just how fast a\nperson replies, is just not a good metric. It's like measuring how\nproductive a person is by counting the lines of code produced.\n\n> That of course doesn't mean that such people should always respond\n> within an hour, or even within a day. We all have to context switch, and\n> context switches are costly, so it's entirely reasonable to try and\n> minimize them. But outside of any special circumstances (vacations,\n> health or similar) I think it should be possible to engage in such\n> discussions within a small handful of days.\n\nYou could also say \"it should be possible for most people outside any\nspecial circumstances to 'produce' 20 lines of tested code per working\nday\". Would that make it worth measuring and annoying people in case\nthe measure is not up to the norm?\n\n> In any case, there of course is a distinction between people employed to\n> work on Git and those that do it in their own free time. The expectation\n> that is extended towards people who work on Git is way higher than the\n> expectation extended towards people who don't.\n\nWhat kind of expectation? Do you mean that the resulting code after\nreview, or even before review, should be way better too? Even when the\nperson employed might be a junior programmer while there are very\nexperienced contributors working on Git in their free time? And if for\nexample someone retires from work and then starts contributing to Git\n10 hours per day every day of the year, then should people employed to\nwork on Git try to match that amount of working hours? If someone\nworking on Git in their free time has an especially good mastery of\nthe English language and starts to write awesome commit messages that\nare on average 100 lines long, then are we going to expect people\nemployed to work on Git especially those who are not native speakers\nto do as well in this regard?\n\nPeople are just different in many different ways, and what you might\nvery well expect trivially from some, might be very difficult for\nothers.\n\n> > I think it's fair to say that oldtimers are less likely to disappear\n> > tomorrow or to not follow up on reviewer feedback. So arguments like\n> > \"replying fast makes reviewers confident that they have been heard\"\n> > are just less true for oldtimers than for newcomers.\n>\n> I think these are two different things. It's probably true that you get\n> some privileges by being an old timer. But I think it's more in the\n> sense of \"You get to tackle bigger things that may not be done in a\n> single patch series, and we trust you to not just disappear\".\n>\n> But with that privilege also comes responsibility. It's those old timers\n> that newcomers look to, so they need to lead by example.\n\nPeople often are good in some ways and bad in others. You cannot\nexpect all old timers to be perfect examples in every way. And that\nshould be fine.\n\n> > Another argument is that oldtimers are more likely to burn out or have\n> > mental health issues related to their Git work than newcomers. Adding\n> > a norm that would put pressure on them to work more, or at times they\n> > would prefer to do other things, significantly increases the burnout\n> > and mental health risks for them.\n> >\n> > So putting pressure on oldtimers to follow a norm that is less\n> > relevant for them but puts them at greater risk is just a bad idea in\n> > my opinion.\n>\n> This is of course something we must avoid. Nobody is being helped in\n> case people burn out. There needs to be a middle ground that works for\n> everyone.\n\nMy opinion is that in cases like this, there is no norm that works for everyone.\n"},{"id":"521757","messageId":"xmqqldow83ih.fsf@gitster.g","threadId":"63342","inReplyTo":"aG94-0A6Qz35ekFh@pks.im","subject":"Re: [PATCH v4] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-10T15:33:10Z","receivedAt":"2025-07-10T15:33:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> There are always going to be exceptions, that is of course true. But I\n> also think that long-time contributors that are employed to work on Git\n> are somewhat special and don't (typically) fall into the mentioned\n> groups. From my perspective, it's especially this group of people that\n> should lead by example and encourage others to behave in a way that is\n> good for the overall Git community. And leading by example in this\n> context also means that they should encourage healthy discussions.\n\nThanks.  \n\nI do not have very much to add to what you already have said.\n"},{"id":"521921","messageId":"CABPp-BHM5afgiUf7GsTPWmrf_tm6mWnvHWMKiZPxApJzN-U8gg@mail.gmail.com","threadId":"63342","inReplyTo":"20250709141253.623563-1-christian.couder@gmail.com","subject":"Re: [PATCH v6] fast-(import|export): improve on commit signature output format","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-07-14T21:07:14Z","receivedAt":"2025-07-14T21:07:27Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Sorry for the delay; noticed this in what's cooking and wanted to\ndouble check whether I missed anything, and decided to add a comment\nhere...\n\nOn Wed, Jul 9, 2025 at 7:13 AM Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n[...]\n> Let's improve on these limitations by improving fast-export and\n> fast-import so that:\n>\n>   - all the signatures are exported,\n>   - at most one signature on the SHA-1 object and one on the SHA-256\n>     are imported,\n>   - if there is more than one signature on the SHA-1 object or on\n>     the SHA-256 object, fast-import emits a warning for each\n>     additional signature,\n>   - the output format is \"gpgsig <git-hash-algo> <signature-format>\",\n>     where <git-hash-algo> is the Git object format as before, and\n>     <signature-format> is the signature type (\"openpgp\", \"x509\",\n>     \"ssh\" or \"unknown\"),\n>   - the output is properly documented.\n>\n[...]\n>\n> +test_expect_success GPGSM 'setup X.509 signed commit' '\n> +\n> +       git checkout -b x509-signing main &&\n> +       test_config gpg.format x509 &&\n> +       test_config user.signingkey $GIT_COMMITTER_EMAIL &&\n> +       echo \"X.509 content\" >file &&\n> +       git add file &&\n> +       git commit -S -m \"X.509 signed commit\" &&\n> +       X509_COMMIT=$(git rev-parse HEAD) &&\n> +       git checkout main\n> +\n> +'\n> +\n> +test_expect_success GPGSM 'round-trip X.509 signed commit' '\n> +\n> +       git fast-export --signed-commits=verbatim x509-signing >output &&\n> +       test_grep -E \"^gpgsig $GIT_DEFAULT_HASH x509\" output &&\n> +       (\n> +               cd new &&\n> +               git fast-import &&\n> +               git cat-file commit refs/heads/x509-signing >actual &&\n> +               grep \"^gpgsig\" actual &&\n> +               IMPORTED=$(git rev-parse refs/heads/x509-signing) &&\n> +               test $X509_COMMIT = $IMPORTED\n> +       ) <output\n> +\n[...]\n> +\n> +test_expect_success GPGSSH 'round-trip SSH signed commit' '\n> +\n> +       git fast-export --signed-commits=verbatim ssh-signing >output &&\n> +       test_grep -E \"^gpgsig $GIT_DEFAULT_HASH ssh\" output &&\n> +       (\n> +               cd new &&\n> +               git fast-import &&\n> +               git cat-file commit refs/heads/ssh-signing >actual &&\n> +               grep \"^gpgsig\" actual &&\n> +               IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&\n> +               test $SSH_COMMIT = $IMPORTED\n> +       ) <output\n> +\n> +'\n> +\n\nOverall, the patch looks great now.  There's just one little nit-pick\nleft; I'm still not a fan of tests of the form\n\n  (\n    cd dir &&\n    git fast-import\n    ...lots of other commands...\n  ) <output\n\nbecause I think the \"<output\" should really be moved to the \"git\nfast-import\" line since it's only meant to be used there.\n\nThis series adds 2 such tests.  You did point out in the discussion on\nv5 that the testsuite already uses this idiom and you wanted to match\nexisting style.  (Though there were only 2 tests previously that used\nit, and you already modified both as part of this patch.)\n\nHowever...we've been through enough rounds and this is really just a\nnit-pick; I can submit a follow-on patch later to clean up the four\ntests and see if others agree with me that this is an eyesore, or if\nI'm just weird.\n\nI think it's good to merge down.\n"},{"id":"521923","messageId":"xmqqikjulb5d.fsf@gitster.g","threadId":"63342","inReplyTo":"CABPp-BHM5afgiUf7GsTPWmrf_tm6mWnvHWMKiZPxApJzN-U8gg@mail.gmail.com","subject":"Re: [PATCH v6] fast-(import|export): improve on commit signature output format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-14T21:23:26Z","receivedAt":"2025-07-14T21:23:29Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> Overall, the patch looks great now.  There's just one little nit-pick\n> left; I'm still not a fan of tests of the form\n>\n>   (\n>     cd dir &&\n>     git fast-import\n>     ...lots of other commands...\n>   ) <output\n>\n> because I think the \"<output\" should really be moved to the \"git\n> fast-import\" line since it's only meant to be used there.\n>\n> This series adds 2 such tests.  You did point out in the discussion on\n> v5 that the testsuite already uses this idiom and you wanted to match\n> existing style.  (Though there were only 2 tests previously that used\n> it, and you already modified both as part of this patch.)\n>\n> However...we've been through enough rounds and this is really just a\n> nit-pick; I can submit a follow-on patch later to clean up the four\n> tests and see if others agree with me that this is an eyesore, or if\n> I'm just weird.\n\nFWIW, I think it makes sense to ensure that the \"output\" is consumed\nonly by the intended command.  And \"there are already two cases\"\nwould not work very well as an excuse to add two more to make the\ncodebase even worse.\n\n> I think it's good to merge down.\n\nOK.  As long as somebody promises that the result will be cleaned up\nsoon later, I am OK with that.\n\nThanks.\n"},{"id":"522755","messageId":"CAP8UFD37JWpWoyMfpP1i6454ib14Jbg33=J=KgVBF54xp-d4hg@mail.gmail.com","threadId":"63342","inReplyTo":"xmqqikjulb5d.fsf@gitster.g","subject":"Re: [PATCH v6] fast-(import|export): improve on commit signature output format","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2025-07-25T16:11:41Z","receivedAt":"2025-07-25T16:11:55Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Mon, Jul 14, 2025 at 11:23 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> > Overall, the patch looks great now.  There's just one little nit-pick\n> > left; I'm still not a fan of tests of the form\n> >\n> >   (\n> >     cd dir &&\n> >     git fast-import\n> >     ...lots of other commands...\n> >   ) <output\n> >\n> > because I think the \"<output\" should really be moved to the \"git\n> > fast-import\" line since it's only meant to be used there.\n> >\n> > This series adds 2 such tests.  You did point out in the discussion on\n> > v5 that the testsuite already uses this idiom and you wanted to match\n> > existing style.  (Though there were only 2 tests previously that used\n> > it, and you already modified both as part of this patch.)\n> >\n> > However...we've been through enough rounds and this is really just a\n> > nit-pick; I can submit a follow-on patch later to clean up the four\n> > tests and see if others agree with me that this is an eyesore, or if\n> > I'm just weird.\n>\n> FWIW, I think it makes sense to ensure that the \"output\" is consumed\n> only by the intended command.  And \"there are already two cases\"\n> would not work very well as an excuse to add two more to make the\n> codebase even worse.\n\nI have just sent the following patch to fix all the instances of this\nin t9350-fast-export.sh:\n\nhttps://lore.kernel.org/git/20250725160536.2909011-1-christian.couder@gmail.com/\n\n> > I think it's good to merge down.\n>\n> OK.  As long as somebody promises that the result will be cleaned up\n> soon later, I am OK with that.\n\nThanks both for reviewing the previous patch.\n"}]}