git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH] fast-(import|export): improve on the signature algorithm name
@ 2025-04-24 20:39 Christian Couder
  2025-04-24 21:19 ` Junio C Hamano
                   ` (4 more replies)
  0 siblings, 5 replies; 65+ messages in thread
From: Christian Couder @ 2025-04-24 20:39 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	Johannes Schindelin, Christian Couder, Christian Couder

A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
signed-commits, 2025-03-10), added support for signed commits.

However, when processing signatures `git fast-export` outputs "gpgsig
sha1" not just when it encounters an OpenPGP SHA-1 signature, but also
when it encounters an SSH or X.509 signature. This is not very
informative to say the least, and this might prevent tools that process
the output from easily and properly handling signatures.

Let's improve on that by reusing the existing code from
"gpg-interface.{c,h}" to detect the signature algorithm, and then put
the signature algorithm name (like "openpgp", "x509" or "ssh") instead
of "sha1" in the output. If we can't detect the signature algorithm we
will use "unknown". It might be a signature added by an external tool
and we should likely keep it.

Similarly on the `git fast-import` side, let's use the existing code
from "gpg-interface.{c,h}" to check if a signature algorithm name is
valid. In case of an "unknown" signature algorithm name, we will warn
but still keep it. Future work might implement several options to let
users deal with it in different ways, and might implement checking
known signatures too.

Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---

This is a follow up from cc/signed-fast-export-import that was merged
by 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)
and introduced the support for signed commits.

The format that this series implemented was lacking a bit, so the goal
with this patch is to improve it and handle signed commits a bit more
consistently in the code base. It also shows in the tests and in our
documentation that SSH and X.509 signatures are supported.

 Documentation/git-fast-export.adoc |  5 +++
 Documentation/git-fast-import.adoc | 15 +++++++-
 builtin/fast-export.c              |  8 ++--
 builtin/fast-import.c              | 14 ++++---
 gpg-interface.c                    | 11 ++++++
 gpg-interface.h                    | 10 +++++
 t/t9350-fast-export.sh             | 60 +++++++++++++++++++++++++++++-
 7 files changed, 112 insertions(+), 11 deletions(-)

diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
index 413a527496..d03aeca781 100644
--- a/Documentation/git-fast-export.adoc
+++ b/Documentation/git-fast-export.adoc
@@ -54,6 +54,11 @@ of tools that call 'git fast-export' but do not yet support
 '--signed-commits', you may set the environment variable
 'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default
 from 'abort' to 'warn-strip'.
++
+When exported, signature starts with "gpgsig <alg>" where <alg> is the
+signature algorithm name as identified by Git (e.g. "openpgp", "x509",
+"ssh", or "sha256" for SHA-256 OpenPGP signatures), or "unknown" for
+signatures that can't be identified.
 
 --tag-of-filtered-object=(abort|drop|rewrite)::
 	Specify how to handle tags whose tagged object is filtered out.
diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
index 7b107f5e8e..50b6d2cc1d 100644
--- a/Documentation/git-fast-import.adoc
+++ b/Documentation/git-fast-import.adoc
@@ -521,7 +521,20 @@ The optional `gpgsig` command is used to include a PGP/GPG signature
 that signs the commit data.
 
 Here <alg> specifies which hashing algorithm is used for this
-signature, either `sha1` or `sha256`.
+signature. Current valid values are:
+
+* "openpgp" for SHA-1 OpenPGP signatures,
+
+* "sha256" for SHA-256 OpenPGP signatures,
+
+* "x509" for X.509 (GPGSM) signatures,
+
+* "ssh", for SSH signatures,
+
+* "unknown" for signatures that can't be identified (a warning is
+  emitted).
+
+Signatures are not yet checked in the current implementation though.
 
 `encoding`
 ^^^^^^^^^^
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index 170126d41a..d00f02dc74 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -29,6 +29,7 @@
 #include "quote.h"
 #include "remote.h"
 #include "blob.h"
+#include "gpg-interface.h"
 
 static const char *fast_export_usage[] = {
 	N_("git fast-export [<rev-list-opts>]"),
@@ -700,9 +701,10 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	}
 
 	if (*commit_buffer_cursor == '\n') {
-		if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
-			signature_alg = "sha1";
-		else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
+		if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor))) {
+			const char *name = get_signature_name(signature);
+			signature_alg = name ? name : "unknown";
+		} else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
 			signature_alg = "sha256";
 	}
 
diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index 63880b595c..59e991a03c 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -29,6 +29,7 @@
 #include "commit-reach.h"
 #include "khash.h"
 #include "date.h"
+#include "gpg-interface.h"
 
 #define PACK_ID_BITS 16
 #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
@@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)
 			"encoding %s\n",
 			encoding);
 	if (sig_alg) {
-		if (!strcmp(sig_alg, "sha1"))
-			strbuf_addstr(&new_data, "gpgsig ");
-		else if (!strcmp(sig_alg, "sha256"))
+		if (!strcmp(sig_alg, "sha256"))
 			strbuf_addstr(&new_data, "gpgsig-sha256 ");
-		else
-			die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
+		else if (valid_signature_name(sig_alg))
+			strbuf_addstr(&new_data, "gpgsig ");
+		else if (!strcmp(sig_alg, "unknown")) {
+			warning("Unknown gpgsig algorithm name!");
+			strbuf_addstr(&new_data, "gpgsig ");
+		} else
+			die("Invalid gpgsig algorithm name, got '%s'", sig_alg);
 		string_list_split_in_place(&siglines, sig.buf, "\n", -1);
 		strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
 		strbuf_addch(&new_data, '\n');
diff --git a/gpg-interface.c b/gpg-interface.c
index 0896458de5..dc6ea904d0 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -144,6 +144,17 @@ static struct gpg_format *get_format_by_sig(const char *sig)
 	return NULL;
 }
 
+const char *get_signature_name(const char *buf)
+{
+	struct gpg_format *format = get_format_by_sig(buf);
+	return format ? format->name : NULL;
+}
+
+int valid_signature_name(const char *name)
+{
+	return (get_format_by_name(name) != NULL);
+}
+
 void signature_check_clear(struct signature_check *sigc)
 {
 	FREE_AND_NULL(sigc->payload);
diff --git a/gpg-interface.h b/gpg-interface.h
index e09f12e8d0..332707facc 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -47,6 +47,16 @@ struct signature_check {
 
 void signature_check_clear(struct signature_check *sigc);
 
+/*
+ * Return the name of the signature (like "openpgp", "x509" or "ssh").
+ */
+const char *get_signature_name(const char *buf);
+
+/*
+ * Is the signature name valid (like "openpgp", "x509" or "ssh").
+ */
+int valid_signature_name(const char *name);
+
 /*
  * Look at a GPG signed tag object.  If such a signature exists, store it in
  * signature and the signed content in payload.  Return 1 if a signature was
diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
index dda9e7c3e7..2e2c83d153 100755
--- a/t/t9350-fast-export.sh
+++ b/t/t9350-fast-export.sh
@@ -326,7 +326,7 @@ test_expect_success GPG 'signed-commits=abort' '
 test_expect_success GPG 'signed-commits=verbatim' '
 
 	git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
-	grep "^gpgsig sha" output &&
+	grep "^gpgsig openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	(
 		cd new &&
@@ -340,7 +340,7 @@ test_expect_success GPG 'signed-commits=verbatim' '
 test_expect_success GPG 'signed-commits=warn-verbatim' '
 
 	git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
-	grep "^gpgsig sha" output &&
+	grep "^gpgsig openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	test -s err &&
 	(
@@ -381,6 +381,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '
 
 '
 
+test_expect_success GPGSM 'setup x509 signed commit' '
+
+	git checkout -b x509-signing main &&
+	test_config gpg.format x509 &&
+	test_config user.signingkey $GIT_COMMITTER_EMAIL &&
+	echo "x509 content" >file_for_x509 &&
+	git add file_for_x509 &&
+	git commit -S -m "X.509 signed commit" &&
+	X509_COMMIT=$(git rev-parse --verify HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSM 'x509 signature identified' '
+
+	git fast-export --signed-commits=verbatim --reencode=no x509-signing >output 2>err &&
+	grep "^gpgsig x509" output &&
+	test ! -s err &&
+	(
+		cd new &&
+		git fast-import &&
+		STRIPPED=$(git rev-parse --verify refs/heads/x509-signing) &&
+		test $X509_COMMIT = $STRIPPED
+	) <output &&
+	test_might_fail git update-ref -d refs/heads/x509-signing
+
+'
+
+test_expect_success GPGSSH 'setup ssh signed commit' '
+
+	git checkout -b ssh-signing main &&
+	test_config gpg.format ssh &&
+	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
+	echo "ssh content" >file_for_ssh &&
+	git add file_for_ssh &&
+	git commit -S -m "SSH signed commit" &&
+	SSH_COMMIT=$(git rev-parse --verify HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSSH 'ssh signature identified' '
+
+	git fast-export --signed-commits=verbatim --reencode=no ssh-signing >output 2>err &&
+	grep "^gpgsig ssh" output &&
+	test ! -s err &&
+	(
+		cd new &&
+		git fast-import &&
+		STRIPPED=$(git rev-parse --verify refs/heads/ssh-signing) &&
+		test "$SSH_COMMIT" = "$STRIPPED"
+	) <output &&
+	test_might_fail git update-ref -d refs/heads/ssh-signing
+
+'
+
 test_expect_success 'setup submodule' '
 
 	test_config_global protocol.file.allow always &&
-- 
2.49.0.392.g2fa1c74b07


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 20:39 [PATCH] fast-(import|export): improve on the signature algorithm name Christian Couder
@ 2025-04-24 21:19 ` Junio C Hamano
  2025-04-24 21:59   ` Elijah Newren
  2025-05-26 10:34   ` Christian Couder
  2025-04-24 21:41 ` Elijah Newren
                   ` (3 subsequent siblings)
  4 siblings, 2 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-04-24 21:19 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
> signed-commits, 2025-03-10), added support for signed commits.
>
> However, when processing signatures `git fast-export` outputs "gpgsig
> sha1" not just when it encounters an OpenPGP SHA-1 signature, but also
> when it encounters an SSH or X.509 signature. This is not very
> informative to say the least, and this might prevent tools that process
> the output from easily and properly handling signatures.
>
> Let's improve on that by reusing the existing code from
> "gpg-interface.{c,h}" to detect the signature algorithm, and then put
> the signature algorithm name (like "openpgp", "x509" or "ssh") instead
> of "sha1" in the output. If we can't detect the signature algorithm we
> will use "unknown". It might be a signature added by an external tool
> and we should likely keep it.
>
> Similarly on the `git fast-import` side, let's use the existing code
> from "gpg-interface.{c,h}" to check if a signature algorithm name is
> valid. In case of an "unknown" signature algorithm name, we will warn
> but still keep it. Future work might implement several options to let
> users deal with it in different ways, and might implement checking
> known signatures too.
>
> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
> ---
>
> This is a follow up from cc/signed-fast-export-import that was merged
> by 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)
> and introduced the support for signed commits.
>
> The format that this series implemented was lacking a bit, so the goal
> with this patch is to improve it and handle signed commits a bit more
> consistently in the code base. It also shows in the tests and in our
> documentation that SSH and X.509 signatures are supported.

Thanks.

It is a bit surprising and slightly sad that nobody bothered to
report/complain about the brokenness until the original author
follows up one month later X-<.  Nobody but the original author is
using this feature?  I would have expected that use of signed
commits were of high demand and many more people were actively
interested in the topic.

>  '--signed-commits', you may set the environment variable
>  'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default
>  from 'abort' to 'warn-strip'.
> ++
> +When exported, signature starts with "gpgsig <alg>" where <alg> is the
> +signature algorithm name as identified by Git (e.g. "openpgp", "x509",
> +"ssh", or "sha256" for SHA-256 OpenPGP signatures), or "unknown" for
> +signatures that can't be identified.

Nice to see these enumerated.  As we are not opening the choices of
"algorithms" up to end-users by allowing custom signature routines
to be plugged in, configured, or hooked into the system, it may make
sense to make it clear that we will keep a canonical and exhausitve
list here, by saying "one of these:" followed by a bulleted list, 
instead of a parenthesized examples (e.g.), like you did in the
other documentation below.  Or perhaps refer to the other document
from here so that we do not have to keep two lists in sync?

> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
> index 7b107f5e8e..50b6d2cc1d 100644
> --- a/Documentation/git-fast-import.adoc
> +++ b/Documentation/git-fast-import.adoc
> @@ -521,7 +521,20 @@ The optional `gpgsig` command is used to include a PGP/GPG signature
>  that signs the commit data.
>  
>  Here <alg> specifies which hashing algorithm is used for this
> -signature, either `sha1` or `sha256`.
> +signature. Current valid values are:
> +
> +* "openpgp" for SHA-1 OpenPGP signatures,
> +
> +* "sha256" for SHA-256 OpenPGP signatures,
> +
> +* "x509" for X.509 (GPGSM) signatures,
> +
> +* "ssh", for SSH signatures,
> +
> +* "unknown" for signatures that can't be identified (a warning is
> +  emitted).
> +
> +Signatures are not yet checked in the current implementation though.

Excellent.

> diff --git a/builtin/fast-export.c b/builtin/fast-export.c
> index 170126d41a..d00f02dc74 100644
> --- a/builtin/fast-export.c
> +++ b/builtin/fast-export.c
> @@ -29,6 +29,7 @@
>  #include "quote.h"
>  #include "remote.h"
>  #include "blob.h"
> +#include "gpg-interface.h"
>  
>  static const char *fast_export_usage[] = {
>  	N_("git fast-export [<rev-list-opts>]"),
> @@ -700,9 +701,10 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
>  	}
>  
>  	if (*commit_buffer_cursor == '\n') {
> -		if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
> -			signature_alg = "sha1";
> -		else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
> +		if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor))) {
> +			const char *name = get_signature_name(signature);
> +			signature_alg = name ? name : "unknown";
> +		} else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
>  			signature_alg = "sha256";

The original is bad enough but can we do something to these overly
long lines?

>  	}
>  
> diff --git a/builtin/fast-import.c b/builtin/fast-import.c
> index 63880b595c..59e991a03c 100644
> --- a/builtin/fast-import.c
> +++ b/builtin/fast-import.c
> @@ -29,6 +29,7 @@
>  #include "commit-reach.h"
>  #include "khash.h"
>  #include "date.h"
> +#include "gpg-interface.h"
>  
>  #define PACK_ID_BITS 16
>  #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
> @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)
>  			"encoding %s\n",
>  			encoding);
>  	if (sig_alg) {
> -		if (!strcmp(sig_alg, "sha1"))
> -			strbuf_addstr(&new_data, "gpgsig ");
> -		else if (!strcmp(sig_alg, "sha256"))
> +		if (!strcmp(sig_alg, "sha256"))
>  			strbuf_addstr(&new_data, "gpgsig-sha256 ");
> -		else
> -			die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
> +		else if (valid_signature_name(sig_alg))
> +			strbuf_addstr(&new_data, "gpgsig ");
> +		else if (!strcmp(sig_alg, "unknown")) {
> +			warning("Unknown gpgsig algorithm name!");
> +			strbuf_addstr(&new_data, "gpgsig ");
> +		} else
> +			die("Invalid gpgsig algorithm name, got '%s'", sig_alg);

Hmph, we used to have special cases for sha1 and sha256 but now we
can handle sha1 with a more generic "valid_signature_name()" logic?
And yet we need to still special case sha256?  Not that I trust the
old code all that much and take deviations from the patterns in the
old code as a sign of something not right...

The fast-export stream produced by the code with d9cb0e6f
(fast-export, fast-import: add support for signed-commits,
2025-03-10) used to identify a signature algorithm "sha1", but this
new version of fast-import lost the support for it, and will barf
when seeing such an existing fast-export stream?  I am not sure what
is going on around this code.

I am not so worried about the other case, where the stream produced
by fast-export contained in this version may or may not be readable
by an older version of fast-import.

I am puzzled enough, so I'll stop here for now.

Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 20:39 [PATCH] fast-(import|export): improve on the signature algorithm name Christian Couder
  2025-04-24 21:19 ` Junio C Hamano
@ 2025-04-24 21:41 ` Elijah Newren
  2025-05-26 10:34   ` Christian Couder
  2025-04-24 22:05 ` brian m. carlson
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 65+ messages in thread
From: Elijah Newren @ 2025-04-24 21:41 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

On Thu, Apr 24, 2025 at 1:39 PM Christian Couder
<christian.couder@gmail.com> wrote:
>
> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
> signed-commits, 2025-03-10), added support for signed commits.
>
> However, when processing signatures `git fast-export` outputs "gpgsig
> sha1" not just when it encounters an OpenPGP SHA-1 signature, but also
> when it encounters an SSH or X.509 signature. This is not very
> informative to say the least, and this might prevent tools that process
> the output from easily and properly handling signatures.
>
> Let's improve on that by reusing the existing code from
> "gpg-interface.{c,h}" to detect the signature algorithm, and then put
> the signature algorithm name (like "openpgp", "x509" or "ssh") instead
> of "sha1" in the output. If we can't detect the signature algorithm we
> will use "unknown". It might be a signature added by an external tool
> and we should likely keep it.
>
> Similarly on the `git fast-import` side, let's use the existing code
> from "gpg-interface.{c,h}" to check if a signature algorithm name is
> valid. In case of an "unknown" signature algorithm name, we will warn
> but still keep it. Future work might implement several options to let
> users deal with it in different ways, and might implement checking
> known signatures too.

The last sentence is somewhat ambiguous about whether it is only about
the "unknown" case or whether the second half of the sentence was
switching tracks to discuss something else about the known cases.  Do
you perhaps mean something like "Future work might implement several
options to let users deal with an "unknown" signature algorithm, and
when we have a valid signature algorithm, we may be able to not only
verify the signature algorithm name but start verifying the signature
itself to ensure it is valid as well."  ?

> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
> ---
>
> This is a follow up from cc/signed-fast-export-import that was merged
> by 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)
> and introduced the support for signed commits.
>
> The format that this series implemented was lacking a bit, so the goal
> with this patch is to improve it and handle signed commits a bit more
> consistently in the code base. It also shows in the tests and in our
> documentation that SSH and X.509 signatures are supported.
>
>  Documentation/git-fast-export.adoc |  5 +++
>  Documentation/git-fast-import.adoc | 15 +++++++-
>  builtin/fast-export.c              |  8 ++--
>  builtin/fast-import.c              | 14 ++++---
>  gpg-interface.c                    | 11 ++++++
>  gpg-interface.h                    | 10 +++++
>  t/t9350-fast-export.sh             | 60 +++++++++++++++++++++++++++++-
>  7 files changed, 112 insertions(+), 11 deletions(-)
>
> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
> index 413a527496..d03aeca781 100644
> --- a/Documentation/git-fast-export.adoc
> +++ b/Documentation/git-fast-export.adoc
> @@ -54,6 +54,11 @@ of tools that call 'git fast-export' but do not yet support
>  '--signed-commits', you may set the environment variable
>  'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default
>  from 'abort' to 'warn-strip'.
> ++
> +When exported, signature starts with "gpgsig <alg>" where <alg> is the
> +signature algorithm name as identified by Git (e.g. "openpgp", "x509",
> +"ssh", or "sha256" for SHA-256 OpenPGP signatures), or "unknown" for
> +signatures that can't be identified.
>
>  --tag-of-filtered-object=(abort|drop|rewrite)::
>         Specify how to handle tags whose tagged object is filtered out.
> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
> index 7b107f5e8e..50b6d2cc1d 100644
> --- a/Documentation/git-fast-import.adoc
> +++ b/Documentation/git-fast-import.adoc
> @@ -521,7 +521,20 @@ The optional `gpgsig` command is used to include a PGP/GPG signature
>  that signs the commit data.
>
>  Here <alg> specifies which hashing algorithm is used for this
> -signature, either `sha1` or `sha256`.
> +signature. Current valid values are:
> +
> +* "openpgp" for SHA-1 OpenPGP signatures,
> +
> +* "sha256" for SHA-256 OpenPGP signatures,
> +
> +* "x509" for X.509 (GPGSM) signatures,
> +
> +* "ssh", for SSH signatures,
> +
> +* "unknown" for signatures that can't be identified (a warning is
> +  emitted).
> +
> +Signatures are not yet checked in the current implementation though.

Thanks for calling this out.

>  `encoding`
>  ^^^^^^^^^^
> diff --git a/builtin/fast-export.c b/builtin/fast-export.c
> index 170126d41a..d00f02dc74 100644
> --- a/builtin/fast-export.c
> +++ b/builtin/fast-export.c
> @@ -29,6 +29,7 @@
>  #include "quote.h"
>  #include "remote.h"
>  #include "blob.h"
> +#include "gpg-interface.h"
>
>  static const char *fast_export_usage[] = {
>         N_("git fast-export [<rev-list-opts>]"),
> @@ -700,9 +701,10 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
>         }
>
>         if (*commit_buffer_cursor == '\n') {
> -               if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
> -                       signature_alg = "sha1";
> -               else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
> +               if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor))) {
> +                       const char *name = get_signature_name(signature);
> +                       signature_alg = name ? name : "unknown";
> +               } else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
>                         signature_alg = "sha256";
>         }
>
> diff --git a/builtin/fast-import.c b/builtin/fast-import.c
> index 63880b595c..59e991a03c 100644
> --- a/builtin/fast-import.c
> +++ b/builtin/fast-import.c
> @@ -29,6 +29,7 @@
>  #include "commit-reach.h"
>  #include "khash.h"
>  #include "date.h"
> +#include "gpg-interface.h"
>
>  #define PACK_ID_BITS 16
>  #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
> @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)
>                         "encoding %s\n",
>                         encoding);
>         if (sig_alg) {
> -               if (!strcmp(sig_alg, "sha1"))
> -                       strbuf_addstr(&new_data, "gpgsig ");
> -               else if (!strcmp(sig_alg, "sha256"))
> +               if (!strcmp(sig_alg, "sha256"))
>                         strbuf_addstr(&new_data, "gpgsig-sha256 ");
> -               else
> -                       die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
> +               else if (valid_signature_name(sig_alg))
> +                       strbuf_addstr(&new_data, "gpgsig ");
> +               else if (!strcmp(sig_alg, "unknown")) {
> +                       warning("Unknown gpgsig algorithm name!");
> +                       strbuf_addstr(&new_data, "gpgsig ");
> +               } else
> +                       die("Invalid gpgsig algorithm name, got '%s'", sig_alg);
>                 string_list_split_in_place(&siglines, sig.buf, "\n", -1);
>                 strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
>                 strbuf_addch(&new_data, '\n');

I'm not very familiar with gpg and other signatures, and was stuck
trying to parse this logic when a review from Junio came in, and I
decided to read it since he often "thinks out loud" to see if that'd
explain it better.  Sadly, didn't help... ;-)  But I'll watch for any
follow-up response you add over there.

> diff --git a/gpg-interface.c b/gpg-interface.c
> index 0896458de5..dc6ea904d0 100644
> --- a/gpg-interface.c
> +++ b/gpg-interface.c
> @@ -144,6 +144,17 @@ static struct gpg_format *get_format_by_sig(const char *sig)
>         return NULL;
>  }
>
> +const char *get_signature_name(const char *buf)
> +{
> +       struct gpg_format *format = get_format_by_sig(buf);
> +       return format ? format->name : NULL;
> +}
> +
> +int valid_signature_name(const char *name)
> +{
> +       return (get_format_by_name(name) != NULL);
> +}
> +
>  void signature_check_clear(struct signature_check *sigc)
>  {
>         FREE_AND_NULL(sigc->payload);
> diff --git a/gpg-interface.h b/gpg-interface.h
> index e09f12e8d0..332707facc 100644
> --- a/gpg-interface.h
> +++ b/gpg-interface.h
> @@ -47,6 +47,16 @@ struct signature_check {
>
>  void signature_check_clear(struct signature_check *sigc);
>
> +/*
> + * Return the name of the signature (like "openpgp", "x509" or "ssh").
> + */
> +const char *get_signature_name(const char *buf);
> +
> +/*
> + * Is the signature name valid (like "openpgp", "x509" or "ssh").
> + */
> +int valid_signature_name(const char *name);
> +
>  /*
>   * Look at a GPG signed tag object.  If such a signature exists, store it in
>   * signature and the signed content in payload.  Return 1 if a signature was
> diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
> index dda9e7c3e7..2e2c83d153 100755
> --- a/t/t9350-fast-export.sh
> +++ b/t/t9350-fast-export.sh
> @@ -326,7 +326,7 @@ test_expect_success GPG 'signed-commits=abort' '
>  test_expect_success GPG 'signed-commits=verbatim' '
>
>         git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
> -       grep "^gpgsig sha" output &&
> +       grep "^gpgsig openpgp" output &&
>         grep "encoding ISO-8859-1" output &&
>         (
>                 cd new &&
> @@ -340,7 +340,7 @@ test_expect_success GPG 'signed-commits=verbatim' '
>  test_expect_success GPG 'signed-commits=warn-verbatim' '
>
>         git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
> -       grep "^gpgsig sha" output &&
> +       grep "^gpgsig openpgp" output &&
>         grep "encoding ISO-8859-1" output &&
>         test -s err &&
>         (
> @@ -381,6 +381,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '
>
>  '
>
> +test_expect_success GPGSM 'setup x509 signed commit' '
> +
> +       git checkout -b x509-signing main &&
> +       test_config gpg.format x509 &&
> +       test_config user.signingkey $GIT_COMMITTER_EMAIL &&
> +       echo "x509 content" >file_for_x509 &&
> +       git add file_for_x509 &&
> +       git commit -S -m "X.509 signed commit" &&
> +       X509_COMMIT=$(git rev-parse --verify HEAD) &&
> +       git checkout main
> +
> +'
> +
> +test_expect_success GPGSM 'x509 signature identified' '
> +
> +       git fast-export --signed-commits=verbatim --reencode=no x509-signing >output 2>err &&

Is --reencode=no important here or does this work with --reencode=yes
as well?  (I understand the default being --reencode=abort and fact
that you are reusing an example that used a specialized encoding means
you need to specify something, was just curious if this particular
value was important)

> +       grep "^gpgsig x509" output &&
> +       test ! -s err &&
> +       (
> +               cd new &&
> +               git fast-import &&
> +               STRIPPED=$(git rev-parse --verify refs/heads/x509-signing) &&
> +               test $X509_COMMIT = $STRIPPED

Ah, --reencode=no is critical for the test, but only because you are
trying to ensure you get the same commit back.  Should there also be a
test for when something is tweaked, such as the encoding, and whether
the signature is still found?

> +       ) <output &&
> +       test_might_fail git update-ref -d refs/heads/x509-signing
> +
> +'
> +
> +test_expect_success GPGSSH 'setup ssh signed commit' '
> +
> +       git checkout -b ssh-signing main &&
> +       test_config gpg.format ssh &&
> +       test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
> +       echo "ssh content" >file_for_ssh &&
> +       git add file_for_ssh &&
> +       git commit -S -m "SSH signed commit" &&
> +       SSH_COMMIT=$(git rev-parse --verify HEAD) &&
> +       git checkout main
> +
> +'
> +
> +test_expect_success GPGSSH 'ssh signature identified' '
> +
> +       git fast-export --signed-commits=verbatim --reencode=no ssh-signing >output 2>err &&

Out of curiosity, any particular reason to export the entire history
instead of just the new commit you just added (though you'd likely
want --reference-excluded-parents if you did that, which really ought
to be the default)?  Anyway...

> +       grep "^gpgsig ssh" output &&
> +       test ! -s err &&
> +       (
> +               cd new &&
> +               git fast-import &&
> +               STRIPPED=$(git rev-parse --verify refs/heads/ssh-signing) &&
> +               test "$SSH_COMMIT" = "$STRIPPED"

Looks like your two tests are for different signature types, but as
noted above, I'm kind of curious to see a test covering what happens
when the resulting commit doesn't exactly match the original but still
retains a signature.

> +       ) <output &&
> +       test_might_fail git update-ref -d refs/heads/ssh-signing
> +
> +'
> +
>  test_expect_success 'setup submodule' '
>
>         test_config_global protocol.file.allow always &&
> --
> 2.49.0.392.g2fa1c74b07

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 21:19 ` Junio C Hamano
@ 2025-04-24 21:59   ` Elijah Newren
  2025-04-24 22:58     ` Junio C Hamano
  2025-05-26 10:34   ` Christian Couder
  1 sibling, 1 reply; 65+ messages in thread
From: Elijah Newren @ 2025-04-24 21:59 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Christian Couder, git, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

On Thu, Apr 24, 2025 at 2:19 PM Junio C Hamano <gitster@pobox.com> wrote:
>
[...]
> >
> > This is a follow up from cc/signed-fast-export-import that was merged
> > by 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)
> > and introduced the support for signed commits.
> >
> > The format that this series implemented was lacking a bit, so the goal
> > with this patch is to improve it and handle signed commits a bit more
> > consistently in the code base. It also shows in the tests and in our
> > documentation that SSH and X.509 signatures are supported.
>
> Thanks.
>
> It is a bit surprising and slightly sad that nobody bothered to
> report/complain about the brokenness until the original author
> follows up one month later X-<.  Nobody but the original author is
> using this feature?  I would have expected that use of signed
> commits were of high demand and many more people were actively
> interested in the topic.

Signed commits may be high demand, but we do have the intersection of
signed commits, usage of fast-export/fast-import, and using a
development version of Git.  I suspect that intersection is somewhat
small; my guess is that signed commits tends to be associated with
large enterprisy things, development versions of git tend to be the
opposite, and fast-export/fast-import are by design infrequently used
tools for any given repo.

(Also, not sure if this helps, but the original author was Luke, not
Christian; Christian was the one who came by three years later to jump
in and polish up the patches.)

[...]
> > @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)
> >                       "encoding %s\n",
> >                       encoding);
> >       if (sig_alg) {
> > -             if (!strcmp(sig_alg, "sha1"))
> > -                     strbuf_addstr(&new_data, "gpgsig ");
> > -             else if (!strcmp(sig_alg, "sha256"))
> > +             if (!strcmp(sig_alg, "sha256"))
> >                       strbuf_addstr(&new_data, "gpgsig-sha256 ");
> > -             else
> > -                     die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
> > +             else if (valid_signature_name(sig_alg))
> > +                     strbuf_addstr(&new_data, "gpgsig ");
> > +             else if (!strcmp(sig_alg, "unknown")) {
> > +                     warning("Unknown gpgsig algorithm name!");
> > +                     strbuf_addstr(&new_data, "gpgsig ");
> > +             } else
> > +                     die("Invalid gpgsig algorithm name, got '%s'", sig_alg);
>
> Hmph, we used to have special cases for sha1 and sha256 but now we
> can handle sha1 with a more generic "valid_signature_name()" logic?
> And yet we need to still special case sha256?  Not that I trust the
> old code all that much and take deviations from the patterns in the
> old code as a sign of something not right...
>
> The fast-export stream produced by the code with d9cb0e6f
> (fast-export, fast-import: add support for signed-commits,
> 2025-03-10) used to identify a signature algorithm "sha1", but this
> new version of fast-import lost the support for it, and will barf
> when seeing such an existing fast-export stream?  I am not sure what
> is going on around this code.
>
> I am not so worried about the other case, where the stream produced
> by fast-export contained in this version may or may not be readable
> by an older version of fast-import.

I certainly can't answer anything here as I know little about
signatures, but your comment brought up a different question for me:
Given that d9cb0e6ff8b3 (fast-export, fast-import: add support for
signed-commits, 2025-03-10) isn't part of any release (not even a
release candidate), do we need to have backward compatibility with
that version?

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 20:39 [PATCH] fast-(import|export): improve on the signature algorithm name Christian Couder
  2025-04-24 21:19 ` Junio C Hamano
  2025-04-24 21:41 ` Elijah Newren
@ 2025-04-24 22:05 ` brian m. carlson
  2025-05-26 10:35   ` Christian Couder
  2025-04-24 23:25 ` Junio C Hamano
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
  4 siblings, 1 reply; 65+ messages in thread
From: brian m. carlson @ 2025-04-24 22:05 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	Johannes Schindelin, Christian Couder

[-- Attachment #1: Type: text/plain, Size: 1313 bytes --]

On 2025-04-24 at 20:39:04, Christian Couder wrote:
>  Here <alg> specifies which hashing algorithm is used for this
> -signature, either `sha1` or `sha256`.
> +signature. Current valid values are:
> +
> +* "openpgp" for SHA-1 OpenPGP signatures,
> +
> +* "sha256" for SHA-256 OpenPGP signatures,
> +
> +* "x509" for X.509 (GPGSM) signatures,
> +
> +* "ssh", for SSH signatures,
> +
> +* "unknown" for signatures that can't be identified (a warning is
> +  emitted).

I don't think this is a good set of options.  We can have SHA-1 or
SHA-256 options for any of the three.  If I create a SHA-256 commit and
sign it with SSH, then it couldn't be exported with this type.

It is even possible and valid to create a signature over the SHA-1
content of an object and sign it with one protocol, say, OpenPGP, and
then create a signature over the SHA-256 content of the object and sign
it with a different one, such as SSH.  Git does not natively support
this, but it is possible to do by hand.

These should be separate fields: one for the hash algorithm and one for
the protocol.  Alternatively, we can just keep the hash algorithm field
and parse the protocol by reading the first line, which will differ for
different protocols.
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 21:59   ` Elijah Newren
@ 2025-04-24 22:58     ` Junio C Hamano
  2025-05-26 10:35       ` Christian Couder
  0 siblings, 1 reply; 65+ messages in thread
From: Junio C Hamano @ 2025-04-24 22:58 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, git, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

Elijah Newren <newren@gmail.com> writes:

>> The fast-export stream produced by the code with d9cb0e6f
>> (fast-export, fast-import: add support for signed-commits,
>> 2025-03-10) used to identify a signature algorithm "sha1", but this
>> new version of fast-import lost the support for it, and will barf
>> when seeing such an existing fast-export stream?  I am not sure what
>> is going on around this code.
>>
>> I am not so worried about the other case, where the stream produced
>> by fast-export contained in this version may or may not be readable
>> by an older version of fast-import.
>
> I certainly can't answer anything here as I know little about
> signatures, but your comment brought up a different question for me:
> Given that d9cb0e6ff8b3 (fast-export, fast-import: add support for
> signed-commits, 2025-03-10) isn't part of any release (not even a
> release candidate), do we need to have backward compatibility with
> that version?

I think we will lose all the credibility if we said "that's not in
an official release, so we are free to break early adopters", once
something is in 'master'.  As some corp environment are know to run
'next' and indeed we do encourage more folks to do so so that we can
catch breakages before they escape to 'master', I actually am equally
worried about things in 'next'.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 20:39 [PATCH] fast-(import|export): improve on the signature algorithm name Christian Couder
                   ` (2 preceding siblings ...)
  2025-04-24 22:05 ` brian m. carlson
@ 2025-04-24 23:25 ` Junio C Hamano
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
  4 siblings, 0 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-04-24 23:25 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> This is a follow up from cc/signed-fast-export-import that was merged
> by 01d17c0530 (Merge branch 'cc/signed-fast-export-import', 2025-03-29)
> and introduced the support for signed commits.

I've looked at it and tried to test the result of merging it to
'seen' locally, but for now I'll eject it from 'seen' before pushing
today's integration result out, as the local tests seem to be
failing (and even though I haven't had time to positively identify
this patch is the culprit, as nothing else is affecting fast-export,
I'd need to move with an educated guess and then dig deeper later to
keep the other topics moving).

Test Summary Report
-------------------
t9350-fast-export.sh                             (Wstat: 256 (exited 1) Tests: 65 Failed: 11)
  Failed tests:  24-25, 29, 31, 33, 35-37, 56-58
  Non-zero exit status: 1
Files=1021, Tests=31986, 646 wallclock secs (16.20 usr  5.76 sys + 1053.65 cusr 8317.02 csys = 9392.63 CPU)
Result: FAIL
gmake[1]: *** [Makefile:78: prove] Error 1
gmake[1]: Leaving directory '/usr/local/google/home/jch/w/buildfarm/seen/t'
gmake: *** [Makefile:3287: test] Error 2

^ permalink raw reply	[flat|nested] 65+ messages in thread

* [PATCH v2 0/6] extract algo information from signatures
  2025-04-24 20:39 [PATCH] fast-(import|export): improve on the signature algorithm name Christian Couder
                   ` (3 preceding siblings ...)
  2025-04-24 23:25 ` Junio C Hamano
@ 2025-05-26 10:33 ` Christian Couder
  2025-05-26 10:33   ` [PATCH v2 1/6] gpg-interface: simplify ssh fingerprint parsing Christian Couder
                     ` (8 more replies)
  4 siblings, 9 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Around one month ago, I sent a patch that tried to improve on how `git
fast-export` handled SSH and X.509 commit signatures:

https://lore.kernel.org/git/20250424203904.909777-1-christian.couder@gmail.com/

This patch was showing a single string for the hash algorithm with the
following possible values:

* "openpgp" for SHA-1 OpenPGP signatures,

* "sha256" for SHA-256 OpenPGP signatures,

* "x509" for X.509 (GPGSM) signatures,

* "ssh", for SSH signatures,

* "unknown" for signatures that can't be identified (a warning is
  emitted).

brian m. carlson however replied that it would be better to show two
pieces of information instead of one: one for the hash algorithm and
one for the protocol.

I have tried to do that but there were a number of issues. First it
seems to be easier to extract information from signatures when
checking them. And if you check them, then it might be interesting to
show the result of the check.

Also for SSH signatures, it's difficult and not so informative to get
the hash algorithm. That's because the hash algorithm is often
specified by the key type (like "RSA", "ECDSA", "Ed25519", ...). For
example "Ed25519" has SHA-512 integrated into its design, and "ECDSA"
and "RSA" are typically used with SHA-256. So for SSH signatures it
seems better to just show the key type and not the hash algorithm.

In general I am not sure what users might want regarding commit
signatures when using fast-export. Some might not need much signature
information at all, and for them checking signatures might just slow
the export process for no benefit, while others might want more
signature information even at the expense of a slower export.

To address this, I decided to focus first on extracting the hash
algorithm from OpenPGP/X.509 signatures and the key type from SSH
signature when checking signatures.

To test that, I thought that it could be interesting to add a
`--summary` option to `verify-commit` that shows a concise, one-line
summary of the signature verification to standard output in the
`STATUS FORMAT ALGORITHM` format, where:

* STATUS is the result character (e.g., G, B, E, U, N, ...), similar
  as what the "%G?" pretty format specifier shows,

* FORMAT is the signature format (`openpgp`, `x509`, or `ssh`),

* ALGORITHM is the hash algorithm used for GPG/GPGSM signatures
  (e.g. `sha1`, `sha256`, ...), or the key type for SSH signatures
  (`RSA`, `ECDSA`, `ED25519`, ...).

If we can agree on a concise format output for signature checks, then
maybe this format will be a good format to be used in the `git
fast-export` output for users who are fine with signatures being
checked.

What do you think?

CI tests
--------

They have all passed:

https://github.com/chriscool/git/actions/runs/15248563563

Range diff compared to v1
-------------------------

No range diff as this series is a completely different approach to the
problem, and running range-diff shows completely different patches.

Christian Couder (6):
  gpg-interface: simplify ssh fingerprint parsing
  gpg-interface: use left shift to define GPG_VERIFY_*
  doc/verify-commit: update and improve the whole doc
  gpg-interface: extract hash algorithm from signature status output
  gpg-interface: extract SSH key type from signature status output
  verify-commit: add a --summary flag

 Documentation/git-verify-commit.adoc |  53 +++++++++++--
 builtin/verify-commit.c              |   4 +-
 gpg-interface.c                      | 111 ++++++++++++++++++++++++++-
 gpg-interface.h                      |  16 +++-
 t/t7510-signed-commit.sh             |  24 ++++++
 t/t7528-signed-commit-ssh.sh         |  28 +++++++
 6 files changed, 224 insertions(+), 12 deletions(-)

-- 
2.49.0.609.g63c55177e5


^ permalink raw reply	[flat|nested] 65+ messages in thread

* [PATCH v2 1/6] gpg-interface: simplify ssh fingerprint parsing
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
@ 2025-05-26 10:33   ` Christian Couder
  2025-05-26 10:33   ` [PATCH v2 2/6] gpg-interface: use left shift to define GPG_VERIFY_* Christian Couder
                     ` (7 subsequent siblings)
  8 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

In "gpg-interface.c", the 'parse_ssh_output()' function takes a
'struct signature_check *sigc' argument and populates many members of
this 'sigc' using information parsed from 'sigc->output' which
contains the ouput of an `ssh-keygen -Y ...` command that was used to
verify an SSH signature.

When it populates 'sigc->fingerprint' though, it uses
`xstrdup(strstr(line, "key ") + 4)` while `strstr(line, "key ")` has
already been computed a few lines above and is already available in
the `key` variable.

Let's simplify this.

Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 gpg-interface.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gpg-interface.c b/gpg-interface.c
index 0896458de5..e7af82d123 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -431,7 +431,7 @@ static void parse_ssh_output(struct signature_check *sigc)
 
 	key = strstr(line, "key ");
 	if (key) {
-		sigc->fingerprint = xstrdup(strstr(line, "key ") + 4);
+		sigc->fingerprint = xstrdup(key + 4);
 		sigc->key = xstrdup(sigc->fingerprint);
 	} else {
 		/*
-- 
2.49.0.609.g63c55177e5


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH v2 2/6] gpg-interface: use left shift to define GPG_VERIFY_*
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
  2025-05-26 10:33   ` [PATCH v2 1/6] gpg-interface: simplify ssh fingerprint parsing Christian Couder
@ 2025-05-26 10:33   ` Christian Couder
  2025-05-26 10:33   ` [PATCH v2 3/6] doc/verify-commit: update and improve the whole doc Christian Couder
                     ` (6 subsequent siblings)
  8 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

In "gpg-interface.h", the definitions of the GPG_VERIFY_* boolean flags
are currently using 1, 2 and 4 while we often prefer the bitwise left
shift operator, `<<`, for that purpose to make it clearer that they are
boolean.

Let's use the left shift operator here too. Let's also fix an indent
issue with "4" while at it.

Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 gpg-interface.h | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/gpg-interface.h b/gpg-interface.h
index e09f12e8d0..9a32dd6ce8 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -3,9 +3,9 @@
 
 struct strbuf;
 
-#define GPG_VERIFY_VERBOSE		1
-#define GPG_VERIFY_RAW			2
-#define GPG_VERIFY_OMIT_STATUS	4
+#define GPG_VERIFY_VERBOSE	(1<<0)
+#define GPG_VERIFY_RAW		(1<<1)
+#define GPG_VERIFY_OMIT_STATUS	(1<<2)
 
 enum signature_trust_level {
 	TRUST_UNDEFINED,
-- 
2.49.0.609.g63c55177e5


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH v2 3/6] doc/verify-commit: update and improve the whole doc
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
  2025-05-26 10:33   ` [PATCH v2 1/6] gpg-interface: simplify ssh fingerprint parsing Christian Couder
  2025-05-26 10:33   ` [PATCH v2 2/6] gpg-interface: use left shift to define GPG_VERIFY_* Christian Couder
@ 2025-05-26 10:33   ` Christian Couder
  2025-05-26 10:33   ` [PATCH v2 4/6] gpg-interface: extract hash algorithm from signature status output Christian Couder
                     ` (5 subsequent siblings)
  8 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

The documentation of the `git verify-commit` commands currently looks
very outdated and minimal. Especially it has the following issues:

  - It only talks about verifying GPG signatures while the command
    actually supports verifying other signatures like SSH ones.

  - It's not clear what the exit code of the command is.

  - It talks about the `<commit>...` arguments only as "SHA-1
    identifiers" while SHA-256 as well as any committish is actually
    supported.

Let's fix all those issues by updating and improving the whole
documentation.

Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-verify-commit.adoc | 36 ++++++++++++++++++++++++----
 1 file changed, 31 insertions(+), 5 deletions(-)

diff --git a/Documentation/git-verify-commit.adoc b/Documentation/git-verify-commit.adoc
index aee4c40eac..6a208a0c2a 100644
--- a/Documentation/git-verify-commit.adoc
+++ b/Documentation/git-verify-commit.adoc
@@ -3,7 +3,7 @@ git-verify-commit(1)
 
 NAME
 ----
-git-verify-commit - Check the GPG signature of commits
+git-verify-commit - Check the signature of commits
 
 SYNOPSIS
 --------
@@ -12,20 +12,46 @@ SYNOPSIS
 
 DESCRIPTION
 -----------
-Validates the GPG signature created by 'git commit -S'.
+Validates the cryptographic signature of commits. This is typically
+a GPG signature created by 'git commit -S', but other signature
+formats like SSH may also be verified depending on Git configuration
+(see linkgit:git-config[1] and the `gpg.format` option).
+
+By default, the command prints human-readable verification results to
+standard error.
+
+EXIT STATUS
+-----------
+If all the specified commits are successfully verified and their
+signatures are good and trusted according to the configured trust
+requirements, the command exits with 0.
+
+If any commit fails verification (e.g., due to a bad signature, a
+missing or untrusted key), if a specified object cannot be found or is
+not a commit object, or if another error occurs during verification,
+the command exits with a non-zero status.
 
 OPTIONS
 -------
 --raw::
-	Print the raw gpg status output to standard error instead of the normal
-	human-readable output.
+	Print the raw signature verification status output to standard
+	error instead of the normal human-readable output. The format
+	of this output is specific to the signature format being used.
 
 -v::
 --verbose::
 	Print the contents of the commit object before validating it.
 
 <commit>...::
-	SHA-1 identifiers of Git commit objects.
+	Commit objects to verify. Can be specified using any format
+	accepted by linkgit:git-rev-parse[1].
+
+SEE ALSO
+--------
+linkgit:git-commit[1],
+linkgit:git-config[1],
+linkgit:git-verify-tag[1],
+linkgit:git-log[1]
 
 GIT
 ---
-- 
2.49.0.609.g63c55177e5


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH v2 4/6] gpg-interface: extract hash algorithm from signature status output
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
                     ` (2 preceding siblings ...)
  2025-05-26 10:33   ` [PATCH v2 3/6] doc/verify-commit: update and improve the whole doc Christian Couder
@ 2025-05-26 10:33   ` Christian Couder
  2025-05-26 10:33   ` [PATCH v2 5/6] gpg-interface: extract SSH key type " Christian Couder
                     ` (4 subsequent siblings)
  8 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

When using GPG/GPGSM to verify OpenPGP/X.509 signatures, the
verification result (good/bad/etc.), signer, and key fingerprint are
extracted from the output, but not the specific hash algorithm (e.g.,
"sha1", "sha256") reported by GPG as having been used for the
signature itself.

Let's improve the `gpg-interface` parsing logic to capture this
information.

This information can be useful for Git commands or external tools
that process signature information. For example, it could be used
when displaying signature verification results to users or when
working with various signature formats in tools like fast-export and
fast-import.

GPG provides the hash algorithm ID used for the signature within
its machine-readable status output, specifically in the fields
following the `VALIDSIG` and `ERRSIG` keywords, as documented in
GnuPG's `doc/DETAILS`.

The implementation follows RFC 4880 (OpenPGP Message Format) section
9.4 for the mapping between hash algorithm IDs and their
corresponding names.

Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 gpg-interface.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++
 gpg-interface.h |  4 +++
 2 files changed, 78 insertions(+)

diff --git a/gpg-interface.c b/gpg-interface.c
index e7af82d123..15687ede43 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -153,6 +153,7 @@ void signature_check_clear(struct signature_check *sigc)
 	FREE_AND_NULL(sigc->key);
 	FREE_AND_NULL(sigc->fingerprint);
 	FREE_AND_NULL(sigc->primary_key_fingerprint);
+	FREE_AND_NULL(sigc->sig_algo);
 }
 
 /* An exclusive status -- only one of them can appear in output */
@@ -221,6 +222,65 @@ static int parse_gpg_trust_level(const char *level,
 	return 1;
 }
 
+/* See RFC 4880: OpenPGP Message Format, section 9.4. Hash Algorithms */
+static struct sigcheck_gpg_hash_algo {
+	const char *id;
+	const char *name;
+} sigcheck_gpg_hash_algo[] = {
+	{ "1", "md5" },       /* deprecated */
+	{ "2", "sha1" },      /* mandatory */
+	{ "3", "ripemd160" },
+	{ "8", "sha256" },
+	{ "9", "sha384" },
+	{ "10", "sha512" },
+	{ "11", "sha224" },
+};
+
+static const char *lookup_gpg_hash_algo(const char *algo_id)
+{
+	if (!algo_id)
+		return NULL;
+
+	for (size_t i = 0; i < ARRAY_SIZE(sigcheck_gpg_hash_algo); i++) {
+		if (!strcmp(sigcheck_gpg_hash_algo[i].id, algo_id))
+			return sigcheck_gpg_hash_algo[i].name;
+	}
+
+	return NULL;
+}
+
+static char *extract_gpg_hash_algo(const char *args_start,
+				   const char *line_end,
+				   int field_index)
+{
+	const char *p = args_start;
+	int current_field = 0;
+	char *result = NULL;
+
+	while (p < line_end && current_field < field_index) {
+		/* Skip to the end of the current field */
+		while (p < line_end && *p != ' ')
+			p++;
+		/* Skip spaces to get to the start of the next field */
+		while (p < line_end && *p == ' ') {
+			p++;
+			current_field++;
+		}
+	}
+
+	if (p < line_end && current_field == field_index) {
+		/* Found start of the target field */
+		const char *algo_id_end = strchrnul(p, ' ');
+		char *algo_id = xmemdupz(p, algo_id_end - p);
+		const char *hash_algo = lookup_gpg_hash_algo(algo_id);
+		if (hash_algo)
+			result = xstrdup(hash_algo);
+		free(algo_id);
+	}
+
+	return result;
+}
+
 static void parse_gpg_output(struct signature_check *sigc)
 {
 	const char *buf = sigc->gpg_status;
@@ -242,6 +302,18 @@ static void parse_gpg_output(struct signature_check *sigc)
 		/* Iterate over all search strings */
 		for (size_t i = 0; i < ARRAY_SIZE(sigcheck_gpg_status); i++) {
 			if (skip_prefix(line, sigcheck_gpg_status[i].check, &line)) {
+
+				/* Do we have hash algorithm? */
+				if (!sigc->sig_algo) {
+					const char *line_end = strchrnul(line, '\n');
+					if (!strcmp(sigcheck_gpg_status[i].check, "VALIDSIG "))
+						/* Hash algorithm is the 8th field in VALIDSIG */
+						sigc->sig_algo = extract_gpg_hash_algo(line, line_end, 7);
+					else if (!strcmp(sigcheck_gpg_status[i].check, "ERRSIG "))
+						/* Hash algorithm is the 3rd field in ERRSIG */
+						sigc->sig_algo = extract_gpg_hash_algo(line, line_end, 2);
+				}
+
 				/*
 				 * GOODSIG, BADSIG etc. can occur only once for
 				 * each signature.  Therefore, if we had more
@@ -323,6 +395,7 @@ static void parse_gpg_output(struct signature_check *sigc)
 			}
 		}
 	}
+
 	return;
 
 error:
@@ -332,6 +405,7 @@ static void parse_gpg_output(struct signature_check *sigc)
 	FREE_AND_NULL(sigc->fingerprint);
 	FREE_AND_NULL(sigc->signer);
 	FREE_AND_NULL(sigc->key);
+	FREE_AND_NULL(sigc->sig_algo);
 }
 
 static int verify_gpg_signed_buffer(struct signature_check *sigc,
diff --git a/gpg-interface.h b/gpg-interface.h
index 9a32dd6ce8..2b7701ca2c 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -42,6 +42,10 @@ struct signature_check {
 	char *key;
 	char *fingerprint;
 	char *primary_key_fingerprint;
+
+	/* hash algo for GPG/GPGSM, key type for SSH */
+	char *sig_algo;
+
 	enum signature_trust_level trust_level;
 };
 
-- 
2.49.0.609.g63c55177e5


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH v2 5/6] gpg-interface: extract SSH key type from signature status output
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
                     ` (3 preceding siblings ...)
  2025-05-26 10:33   ` [PATCH v2 4/6] gpg-interface: extract hash algorithm from signature status output Christian Couder
@ 2025-05-26 10:33   ` Christian Couder
  2025-05-26 10:33   ` [PATCH v2 6/6] verify-commit: add a --summary flag Christian Couder
                     ` (3 subsequent siblings)
  8 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

A previous commit extracted the hash algorithm from GPG/GPGSM signature
status output and stored it in a new 'sig_algo' member of 'struct
signature_check'.

For SSH signatures, it's more interesting and easier to extract the key
type (like "RSA", "ECDSA", "Ed25519", ...) rather than the hash
algorithm which often depends on the key type. For example "Ed25519"
has SHA-512 integrated into its design, and "ECDSA" and "RSA" are
typically used with SHA-256.

Let's improve the `gpg-interface` parsing logic to capture the SSH key
type when parsing the SSH signature status output.

Similarly as the hash algorithm for GPG/GPGSM signatures, this
information can be useful for Git commands or external tools that
process signature information. For example, it could be used when
displaying signature verification results to users or when working with
various signature formats in tools like fast-export and fast-import.

As they share a common usage, we also store the SSH key type in the new
'sig_algo' member of 'struct signature_check'.

Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 gpg-interface.c | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/gpg-interface.c b/gpg-interface.c
index 15687ede43..182e579769 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -456,11 +456,27 @@ static int verify_gpg_signed_buffer(struct signature_check *sigc,
 	return ret;
 }
 
+static char *extract_ssh_key_type(const char *type_start, const char *type_end)
+{
+	if (!type_end || type_end <= type_start)
+		return NULL;
+
+	/* Back up over any spaces before " key " */
+	while (type_end > type_start && *(type_end - 1) == ' ')
+		type_end--;
+
+	if (type_end <= type_start)
+		return NULL;
+
+	return xmemdupz(type_start, type_end - type_start);
+}
+
 static void parse_ssh_output(struct signature_check *sigc)
 {
 	const char *line, *principal, *search;
 	char *to_free;
 	char *key = NULL;
+	const char *after_last_with = NULL;
 
 	/*
 	 * ssh-keygen output should be:
@@ -485,8 +501,10 @@ static void parse_ssh_output(struct signature_check *sigc)
 		principal = line;
 		do {
 			search = strstr(line, " with ");
-			if (search)
+			if (search) {
 				line = search + 1;
+				after_last_with = search + 6;
+			}
 		} while (search != NULL);
 		if (line == principal)
 			goto cleanup;
@@ -499,6 +517,7 @@ static void parse_ssh_output(struct signature_check *sigc)
 		/* Valid signature, but key unknown */
 		sigc->result = 'G';
 		sigc->trust_level = TRUST_UNDEFINED;
+		after_last_with = line;
 	} else {
 		goto cleanup;
 	}
@@ -507,6 +526,9 @@ static void parse_ssh_output(struct signature_check *sigc)
 	if (key) {
 		sigc->fingerprint = xstrdup(key + 4);
 		sigc->key = xstrdup(sigc->fingerprint);
+
+		if (after_last_with)
+			sigc->sig_algo = extract_ssh_key_type(after_last_with, key);
 	} else {
 		/*
 		 * Output did not match what we expected
-- 
2.49.0.609.g63c55177e5


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH v2 6/6] verify-commit: add a --summary flag
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
                     ` (4 preceding siblings ...)
  2025-05-26 10:33   ` [PATCH v2 5/6] gpg-interface: extract SSH key type " Christian Couder
@ 2025-05-26 10:33   ` Christian Couder
  2025-05-26 16:03   ` [PATCH v2 0/6] extract algo information from signatures Elijah Newren
                     ` (2 subsequent siblings)
  8 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:33 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

The current outputs, with `--raw`, `--verbose` or by default, from
`git verify-commit` are all quite verbose and do not make it easy to
quickly assess signature status.

Let's add a new `--summary` option to `git verify-commit` that prints
a concise, one-line summary of the signature verification to standard
output.

This compact format is useful for scripts and tools that need to
quickly parse signature verification results, while still being
human-readable.

Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---
 Documentation/git-verify-commit.adoc | 17 ++++++++++++++++-
 builtin/verify-commit.c              |  4 +++-
 gpg-interface.c                      | 11 +++++++++++
 gpg-interface.h                      |  6 ++++++
 t/t7510-signed-commit.sh             | 24 ++++++++++++++++++++++++
 t/t7528-signed-commit-ssh.sh         | 28 ++++++++++++++++++++++++++++
 6 files changed, 88 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-verify-commit.adoc b/Documentation/git-verify-commit.adoc
index 6a208a0c2a..fb038ae0cf 100644
--- a/Documentation/git-verify-commit.adoc
+++ b/Documentation/git-verify-commit.adoc
@@ -8,7 +8,7 @@ git-verify-commit - Check the signature of commits
 SYNOPSIS
 --------
 [verse]
-'git verify-commit' [-v | --verbose] [--raw] <commit>...
+'git verify-commit' [-v | --verbose] [--raw] [--summary] <commit>...
 
 DESCRIPTION
 -----------
@@ -38,6 +38,21 @@ OPTIONS
 	error instead of the normal human-readable output. The format
 	of this output is specific to the signature format being used.
 
+--summary::
+	Print a one-line human-readable summary of the signature check
+	to standard output in the format: `STATUS FORMAT ALGORITHM`.
++
+STATUS is the result character (e.g., G, B, E, U, N, ...) shown by the
+"%G?" pretty format specifier. See the "Pretty Formats" section in
+linkgit:git-log[1].
++
+FORMAT indicates the signature format (`openpgp`, `x509`, or `ssh`) or
+`?` if unknown.
++
+ALGORITHM is the hash algorithm used for GPG/GPGSM signatures
+(e.g. `sha1`, `sha256`, ...), or the key type for SSH signatures
+(`RSA`, `ECDSA`, `ED25519`, ...), or `?` if unknown.
+
 -v::
 --verbose::
 	Print the contents of the commit object before validating it.
diff --git a/builtin/verify-commit.c b/builtin/verify-commit.c
index 5f749a30da..54b5b7d360 100644
--- a/builtin/verify-commit.c
+++ b/builtin/verify-commit.c
@@ -14,7 +14,7 @@
 #include "gpg-interface.h"
 
 static const char * const verify_commit_usage[] = {
-		N_("git verify-commit [-v | --verbose] [--raw] <commit>..."),
+		N_("git verify-commit [-v | --verbose] [--raw] [--summary] <commit>..."),
 		NULL
 };
 
@@ -27,6 +27,7 @@ static int run_gpg_verify(struct commit *commit, unsigned flags)
 
 	ret = check_commit_signature(commit, &signature_check);
 	print_signature_buffer(&signature_check, flags);
+	print_signature_summary(&signature_check, flags);
 
 	signature_check_clear(&signature_check);
 	return ret;
@@ -60,6 +61,7 @@ int cmd_verify_commit(int argc,
 	const struct option verify_commit_options[] = {
 		OPT__VERBOSE(&verbose, N_("print commit contents")),
 		OPT_BIT(0, "raw", &flags, N_("print raw gpg status output"), GPG_VERIFY_RAW),
+		OPT_BIT(0, "summary", &flags, N_("print concise signature verification summary"), GPG_VERIFY_SUMMARY),
 		OPT_END()
 	};
 
diff --git a/gpg-interface.c b/gpg-interface.c
index 182e579769..fc198715c4 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -153,6 +153,7 @@ void signature_check_clear(struct signature_check *sigc)
 	FREE_AND_NULL(sigc->key);
 	FREE_AND_NULL(sigc->fingerprint);
 	FREE_AND_NULL(sigc->primary_key_fingerprint);
+	FREE_AND_NULL(sigc->format_name);
 	FREE_AND_NULL(sigc->sig_algo);
 }
 
@@ -756,6 +757,8 @@ int check_signature(struct signature_check *sigc,
 	if (!fmt)
 		die(_("bad/incompatible signature '%s'"), signature);
 
+	sigc->format_name = xstrdup(fmt->name);
+
 	if (parse_payload_metadata(sigc))
 		return 1;
 
@@ -782,6 +785,14 @@ void print_signature_buffer(const struct signature_check *sigc, unsigned flags)
 		fputs(output, stderr);
 }
 
+void print_signature_summary(const struct signature_check *sigc, unsigned flags)
+{
+	if (flags & GPG_VERIFY_SUMMARY)
+		printf("%c %s %s\n", sigc->result,
+		       sigc->format_name ? sigc->format_name : "?",
+		       sigc->sig_algo ? sigc->sig_algo : "?");
+}
+
 size_t parse_signed_buffer(const char *buf, size_t size)
 {
 	size_t len = 0;
diff --git a/gpg-interface.h b/gpg-interface.h
index 2b7701ca2c..a9565757f6 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -6,6 +6,7 @@ struct strbuf;
 #define GPG_VERIFY_VERBOSE	(1<<0)
 #define GPG_VERIFY_RAW		(1<<1)
 #define GPG_VERIFY_OMIT_STATUS	(1<<2)
+#define GPG_VERIFY_SUMMARY	(1<<3)
 
 enum signature_trust_level {
 	TRUST_UNDEFINED,
@@ -43,6 +44,9 @@ struct signature_check {
 	char *fingerprint;
 	char *primary_key_fingerprint;
 
+	/* "openpgp", "x509", "ssh" */
+	char *format_name;
+
 	/* hash algo for GPG/GPGSM, key type for SSH */
 	char *sig_algo;
 
@@ -95,5 +99,7 @@ int check_signature(struct signature_check *sigc,
 		    const char *signature, size_t slen);
 void print_signature_buffer(const struct signature_check *sigc,
 			    unsigned flags);
+void print_signature_summary(const struct signature_check *sigc,
+			     unsigned flags);
 
 #endif
diff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh
index 39677e859a..47f40862f3 100755
--- a/t/t7510-signed-commit.sh
+++ b/t/t7510-signed-commit.sh
@@ -232,6 +232,30 @@ test_expect_success GPG2 'bare signature' '
 	test_cmp expect actual
 '
 
+test_expect_success GPG 'verify signatures with --summary' '
+	# GPG-signed commit
+	git verify-commit --summary sixth-signed >actual &&
+	test_grep "^G openpgp sha1" actual &&
+
+	# Non-signed commit
+	test_must_fail git verify-commit --summary seventh-unsigned >actual 2>&1 &&
+	test_grep "^N ? ?" actual &&
+
+	# Trusted signature with alternate key (hash used might depend on the OS)
+	git verify-commit --summary eighth-signed-alt >actual &&
+	test_grep -E "^G openpgp sha(256|512)" actual &&
+
+	# Bad signature
+	test_must_fail git verify-commit --summary $(cat forged1.commit) >actual 2>err &&
+	test_grep "^B openpgp ?" actual
+'
+
+test_expect_success GPG '--summary and --raw work together' '
+	git verify-commit --summary --raw sixth-signed >actual 2>err &&
+	test_grep "^G openpgp sha1" actual &&
+	test_grep "GOODSIG" err
+'
+
 test_expect_success GPG 'show good signature with custom format' '
 	cat >expect <<-\EOF &&
 	G
diff --git a/t/t7528-signed-commit-ssh.sh b/t/t7528-signed-commit-ssh.sh
index 065f780636..3d0e7d7859 100755
--- a/t/t7528-signed-commit-ssh.sh
+++ b/t/t7528-signed-commit-ssh.sh
@@ -277,6 +277,34 @@ test_expect_success GPGSSH 'detect fudged signature with NUL' '
 	! grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" actual2
 '
 
+test_expect_success GPGSSH 'verify-commit --summary outputs format and key type for SSH signatures' '
+	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+
+	# SSH-signed commit with ED25519 key
+	git verify-commit --summary sixth-signed >actual &&
+	test_grep "^G ssh ED25519" actual &&
+
+	# SSH-signed commit with ECDSA key
+	git verify-commit --summary thirteenth-signed-ecdsa >actual &&
+	test_grep "^G ssh ECDSA" actual &&
+
+	# Non-signed commit
+	test_must_fail git verify-commit --summary seventh-unsigned >actual 2>&1 &&
+	test_grep "^N ? ?" actual &&
+
+	# Bad signature
+	test_must_fail git verify-commit --summary $(cat forged1.commit) >actual 2>err &&
+	test_grep "^B ssh ?" actual
+'
+
+test_expect_success GPGSSH '--summary and --raw work together' '
+	test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
+
+	git verify-commit --summary --raw sixth-signed >actual 2>err &&
+	test_grep "^G ssh ED25519" actual &&
+	test_grep "${GPGSSH_GOOD_SIGNATURE_TRUSTED}" err
+'
+
 test_expect_success GPGSSH 'amending already signed commit' '
 	test_config gpg.format ssh &&
 	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
-- 
2.49.0.609.g63c55177e5


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 21:19 ` Junio C Hamano
  2025-04-24 21:59   ` Elijah Newren
@ 2025-05-26 10:34   ` Christian Couder
  1 sibling, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:34 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	Johannes Schindelin, Christian Couder

On Thu, Apr 24, 2025 at 11:19 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:

> > diff --git a/builtin/fast-import.c b/builtin/fast-import.c
> > index 63880b595c..59e991a03c 100644
> > --- a/builtin/fast-import.c
> > +++ b/builtin/fast-import.c
> > @@ -29,6 +29,7 @@
> >  #include "commit-reach.h"
> >  #include "khash.h"
> >  #include "date.h"
> > +#include "gpg-interface.h"
> >
> >  #define PACK_ID_BITS 16
> >  #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
> > @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)
> >                       "encoding %s\n",
> >                       encoding);
> >       if (sig_alg) {
> > -             if (!strcmp(sig_alg, "sha1"))
> > -                     strbuf_addstr(&new_data, "gpgsig ");
> > -             else if (!strcmp(sig_alg, "sha256"))
> > +             if (!strcmp(sig_alg, "sha256"))
> >                       strbuf_addstr(&new_data, "gpgsig-sha256 ");
> > -             else
> > -                     die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
> > +             else if (valid_signature_name(sig_alg))
> > +                     strbuf_addstr(&new_data, "gpgsig ");
> > +             else if (!strcmp(sig_alg, "unknown")) {
> > +                     warning("Unknown gpgsig algorithm name!");
> > +                     strbuf_addstr(&new_data, "gpgsig ");
> > +             } else
> > +                     die("Invalid gpgsig algorithm name, got '%s'", sig_alg);
>
> Hmph, we used to have special cases for sha1 and sha256 but now we
> can handle sha1 with a more generic "valid_signature_name()" logic?
> And yet we need to still special case sha256?  Not that I trust the
> old code all that much and take deviations from the patterns in the
> old code as a sign of something not right...
>
> The fast-export stream produced by the code with d9cb0e6f
> (fast-export, fast-import: add support for signed-commits,
> 2025-03-10) used to identify a signature algorithm "sha1", but this
> new version of fast-import lost the support for it, and will barf
> when seeing such an existing fast-export stream?  I am not sure what
> is going on around this code.
>
> I am not so worried about the other case, where the stream produced
> by fast-export contained in this version may or may not be readable
> by an older version of fast-import.
>
> I am puzzled enough, so I'll stop here for now.

I am also not sure about the best way to approach this, so I tried a
new approach in v2.

Anyway thanks for your comments! I will address them if we decide that
the approach in this patch is still worth pursuing.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 21:41 ` Elijah Newren
@ 2025-05-26 10:34   ` Christian Couder
  0 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:34 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

On Thu, Apr 24, 2025 at 11:41 PM Elijah Newren <newren@gmail.com> wrote:
>
> On Thu, Apr 24, 2025 at 1:39 PM Christian Couder
> <christian.couder@gmail.com> wrote:
> >
> > A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
> > signed-commits, 2025-03-10), added support for signed commits.
> >
> > However, when processing signatures `git fast-export` outputs "gpgsig
> > sha1" not just when it encounters an OpenPGP SHA-1 signature, but also
> > when it encounters an SSH or X.509 signature. This is not very
> > informative to say the least, and this might prevent tools that process
> > the output from easily and properly handling signatures.
> >
> > Let's improve on that by reusing the existing code from
> > "gpg-interface.{c,h}" to detect the signature algorithm, and then put
> > the signature algorithm name (like "openpgp", "x509" or "ssh") instead
> > of "sha1" in the output. If we can't detect the signature algorithm we
> > will use "unknown". It might be a signature added by an external tool
> > and we should likely keep it.
> >
> > Similarly on the `git fast-import` side, let's use the existing code
> > from "gpg-interface.{c,h}" to check if a signature algorithm name is
> > valid. In case of an "unknown" signature algorithm name, we will warn
> > but still keep it. Future work might implement several options to let
> > users deal with it in different ways, and might implement checking
> > known signatures too.
>
> The last sentence is somewhat ambiguous about whether it is only about
> the "unknown" case or whether the second half of the sentence was
> switching tracks to discuss something else about the known cases.

Yeah, the second half of the sentence was about switching tracks to
discuss other things we might do in the future.

> Do
> you perhaps mean something like "Future work might implement several
> options to let users deal with an "unknown" signature algorithm, and
> when we have a valid signature algorithm, we may be able to not only
> verify the signature algorithm name but start verifying the signature
> itself to ensure it is valid as well."  ?

Yeah, that was the idea. Thanks for spelling it in a better way than I did.

In v2, I have actually tried an approach based on verifying
signatures, and I'd be happy to know your opinion about the different
approaches.

Thanks!

[...]

> > +Signatures are not yet checked in the current implementation though.
>
> Thanks for calling this out.

[...]

> > @@ -2830,12 +2831,15 @@ static void parse_new_commit(const char *arg)
> >                         "encoding %s\n",
> >                         encoding);
> >         if (sig_alg) {
> > -               if (!strcmp(sig_alg, "sha1"))
> > -                       strbuf_addstr(&new_data, "gpgsig ");
> > -               else if (!strcmp(sig_alg, "sha256"))
> > +               if (!strcmp(sig_alg, "sha256"))
> >                         strbuf_addstr(&new_data, "gpgsig-sha256 ");
> > -               else
> > -                       die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
> > +               else if (valid_signature_name(sig_alg))
> > +                       strbuf_addstr(&new_data, "gpgsig ");
> > +               else if (!strcmp(sig_alg, "unknown")) {
> > +                       warning("Unknown gpgsig algorithm name!");
> > +                       strbuf_addstr(&new_data, "gpgsig ");
> > +               } else
> > +                       die("Invalid gpgsig algorithm name, got '%s'", sig_alg);
> >                 string_list_split_in_place(&siglines, sig.buf, "\n", -1);
> >                 strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
> >                 strbuf_addch(&new_data, '\n');
>
> I'm not very familiar with gpg and other signatures, and was stuck
> trying to parse this logic when a review from Junio came in, and I
> decided to read it since he often "thinks out loud" to see if that'd
> explain it better.  Sadly, didn't help... ;-)  But I'll watch for any
> follow-up response you add over there.

Yeah, if we want to continue in the direction of this patch, let's
discuss it over there.

> > @@ -381,6 +381,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '
> >
> >  '
> >
> > +test_expect_success GPGSM 'setup x509 signed commit' '
> > +
> > +       git checkout -b x509-signing main &&
> > +       test_config gpg.format x509 &&
> > +       test_config user.signingkey $GIT_COMMITTER_EMAIL &&
> > +       echo "x509 content" >file_for_x509 &&
> > +       git add file_for_x509 &&
> > +       git commit -S -m "X.509 signed commit" &&
> > +       X509_COMMIT=$(git rev-parse --verify HEAD) &&
> > +       git checkout main
> > +
> > +'
> > +
> > +test_expect_success GPGSM 'x509 signature identified' '
> > +
> > +       git fast-export --signed-commits=verbatim --reencode=no x509-signing >output 2>err &&
>
> Is --reencode=no important here or does this work with --reencode=yes
> as well?  (I understand the default being --reencode=abort and fact
> that you are reusing an example that used a specialized encoding means
> you need to specify something, was just curious if this particular
> value was important)
>
> > +       grep "^gpgsig x509" output &&
> > +       test ! -s err &&
> > +       (
> > +               cd new &&
> > +               git fast-import &&
> > +               STRIPPED=$(git rev-parse --verify refs/heads/x509-signing) &&
> > +               test $X509_COMMIT = $STRIPPED
>
> Ah, --reencode=no is critical for the test, but only because you are
> trying to ensure you get the same commit back.

Yeah, I think it's useful to check that we can get the same commit back.

> Should there also be a
> test for when something is tweaked, such as the encoding, and whether
> the signature is still found?

[...]

> > +test_expect_success GPGSSH 'ssh signature identified' '
> > +
> > +       git fast-export --signed-commits=verbatim --reencode=no ssh-signing >output 2>err &&
>
> Out of curiosity, any particular reason to export the entire history
> instead of just the new commit you just added (though you'd likely
> want --reference-excluded-parents if you did that, which really ought
> to be the default)?  Anyway...

Thanks for your review and all your comments!

As I am not sure which approach is best and I am trying a different
approach in v2, it might not be worth discussing some of the details
of the approach I took in this v1 anymore. So I will skip answering
some of your specific comments unless we decide to go back to that
approach later.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 22:05 ` brian m. carlson
@ 2025-05-26 10:35   ` Christian Couder
  0 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:35 UTC (permalink / raw)
  To: brian m. carlson, Christian Couder, git, Junio C Hamano,
	Patrick Steinhardt, Elijah Newren, Jeff King, Johannes Schindelin,
	Christian Couder

On Fri, Apr 25, 2025 at 12:05 AM brian m. carlson
<sandals@crustytoothpaste.net> wrote:
>
> On 2025-04-24 at 20:39:04, Christian Couder wrote:
> >  Here <alg> specifies which hashing algorithm is used for this
> > -signature, either `sha1` or `sha256`.
> > +signature. Current valid values are:
> > +
> > +* "openpgp" for SHA-1 OpenPGP signatures,
> > +
> > +* "sha256" for SHA-256 OpenPGP signatures,
> > +
> > +* "x509" for X.509 (GPGSM) signatures,
> > +
> > +* "ssh", for SSH signatures,
> > +
> > +* "unknown" for signatures that can't be identified (a warning is
> > +  emitted).
>
> I don't think this is a good set of options.  We can have SHA-1 or
> SHA-256 options for any of the three.  If I create a SHA-256 commit and
> sign it with SSH, then it couldn't be exported with this type.
>
> It is even possible and valid to create a signature over the SHA-1
> content of an object and sign it with one protocol, say, OpenPGP, and
> then create a signature over the SHA-256 content of the object and sign
> it with a different one, such as SSH.  Git does not natively support
> this, but it is possible to do by hand.
>
> These should be separate fields: one for the hash algorithm and one for
> the protocol.

Yeah, I agree that the set of options is not ideal and it would be
better if it was possible to get these two separate fields.

> Alternatively, we can just keep the hash algorithm field
> and parse the protocol by reading the first line, which will differ for
> different protocols.

I am not sure it's easy to get all the information without checking
the signature. I have tried a different approach based on checking the
signature in the v2.

Thanks for the review!

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-04-24 22:58     ` Junio C Hamano
@ 2025-05-26 10:35       ` Christian Couder
  2025-05-27 15:18         ` Junio C Hamano
  0 siblings, 1 reply; 65+ messages in thread
From: Christian Couder @ 2025-05-26 10:35 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Elijah Newren, git, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

On Fri, Apr 25, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> >> The fast-export stream produced by the code with d9cb0e6f
> >> (fast-export, fast-import: add support for signed-commits,
> >> 2025-03-10) used to identify a signature algorithm "sha1", but this
> >> new version of fast-import lost the support for it, and will barf
> >> when seeing such an existing fast-export stream?  I am not sure what
> >> is going on around this code.
> >>
> >> I am not so worried about the other case, where the stream produced
> >> by fast-export contained in this version may or may not be readable
> >> by an older version of fast-import.
> >
> > I certainly can't answer anything here as I know little about
> > signatures, but your comment brought up a different question for me:
> > Given that d9cb0e6ff8b3 (fast-export, fast-import: add support for
> > signed-commits, 2025-03-10) isn't part of any release (not even a
> > release candidate), do we need to have backward compatibility with
> > that version?
>
> I think we will lose all the credibility if we said "that's not in
> an official release, so we are free to break early adopters", once
> something is in 'master'.

I agree that we should have at least said in big letters that the
improved support for signed commits in fast-export/import is very
experimental and very likely to change in the future.

We could still do so. This could give us a bit of time and flexibility
until we agree on and implement something better and backward
compatible. (Hopefully the v2 will help us move forward.)

> As some corp environment are know to run
> 'next' and indeed we do encourage more folks to do so so that we can
> catch breakages before they escape to 'master', I actually am equally
> worried about things in 'next'.

Yeah, right.

On the other hand I think it's fair for us to experiment in some
areas, at least when we document that we are experimenting, which
means that running 'master' or 'next' and blindly using any feature we
have just added is not the right thing to do if people don't want to
be exposed to not just bugs but also some possible backward
incompatibilities in some areas.

So yeah, we should definitely have said that we are experimenting. Let
me know if you want me to prepare a patch to add such wordings on top
of d9cb0e6f (fast-export, fast-import: add support for signed-commits,
2025-03-10). Otherwise we will have to be backward compatible, which
is not ideal, but might not be a very big issue either.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v2 0/6] extract algo information from signatures
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
                     ` (5 preceding siblings ...)
  2025-05-26 10:33   ` [PATCH v2 6/6] verify-commit: add a --summary flag Christian Couder
@ 2025-05-26 16:03   ` Elijah Newren
  2025-06-19 13:38     ` Christian Couder
  2025-06-02 22:17   ` brian m. carlson
  2025-06-18 15:18   ` [PATCH v3] fast-(import|export): improve on commit signature output format Christian Couder
  8 siblings, 1 reply; 65+ messages in thread
From: Elijah Newren @ 2025-05-26 16:03 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin

On Mon, May 26, 2025 at 3:33 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> Around one month ago, I sent a patch that tried to improve on how `git
> fast-export` handled SSH and X.509 commit signatures:
>
> https://lore.kernel.org/git/20250424203904.909777-1-christian.couder@gmail.com/
>
> This patch was showing a single string for the hash algorithm with the
> following possible values:
>
> * "openpgp" for SHA-1 OpenPGP signatures,
>
> * "sha256" for SHA-256 OpenPGP signatures,
>
> * "x509" for X.509 (GPGSM) signatures,
>
> * "ssh", for SSH signatures,
>
> * "unknown" for signatures that can't be identified (a warning is
>   emitted).
>
> brian m. carlson however replied that it would be better to show two
> pieces of information instead of one: one for the hash algorithm and
> one for the protocol.
>
> I have tried to do that but there were a number of issues. First it
> seems to be easier to extract information from signatures when
> checking them. And if you check them, then it might be interesting to
> show the result of the check.
>
> Also for SSH signatures, it's difficult and not so informative to get
> the hash algorithm. That's because the hash algorithm is often
> specified by the key type (like "RSA", "ECDSA", "Ed25519", ...). For
> example "Ed25519" has SHA-512 integrated into its design, and "ECDSA"
> and "RSA" are typically used with SHA-256. So for SSH signatures it
> seems better to just show the key type and not the hash algorithm.
>
> In general I am not sure what users might want regarding commit
> signatures when using fast-export. Some might not need much signature
> information at all, and for them checking signatures might just slow
> the export process for no benefit, while others might want more
> signature information even at the expense of a slower export.

I'd like to propose that the following are the possible uses that
users might have regarding commit signatures with
fast-export/fast-import (if anyone has additional usecases, let me
know):

(A) Make fast-export include signatures, and make fast-import include
them unconditionally (even if invalid)
(B) Similar to (A), but make *fast-import* check them and either error
out or drop them if they become invalid
(C) Simliar to (B), but make *fast-import* re-sign the commit if they
become invalid
(D) Similar to (A), but make *fast-import* re-sign the commit even if
the signature would have been valid

Note that in the above, there might be additional processing between
when fast-export runs and when fast-import does (e.g. by filter-repo
or a similar tool, or even the user editing by hand).

> To address this, I decided to focus first on extracting the hash
> algorithm from OpenPGP/X.509 signatures and the key type from SSH
> signature when checking signatures.
>
> To test that, I thought that it could be interesting to add a
> `--summary` option to `verify-commit` that shows a concise, one-line
> summary of the signature verification to standard output in the
> `STATUS FORMAT ALGORITHM` format, where:
>
> * STATUS is the result character (e.g., G, B, E, U, N, ...), similar
>   as what the "%G?" pretty format specifier shows,
>
> * FORMAT is the signature format (`openpgp`, `x509`, or `ssh`),
>
> * ALGORITHM is the hash algorithm used for GPG/GPGSM signatures
>   (e.g. `sha1`, `sha256`, ...), or the key type for SSH signatures
>   (`RSA`, `ECDSA`, `ED25519`, ...).

This sounds like it might be a nice feature extension to the
verify-commit builtin.  I don't see how it helps implement signature
handling in fast-export/fast-import, though.

> If we can agree on a concise format output for signature checks, then
> maybe this format will be a good format to be used in the `git
> fast-export` output for users who are fine with signatures being
> checked.
>
> What do you think?

Maybe I'm missing something, but it seems to me that checking
signatures *in fast-export* would be a complete waste of time.  For
usecases (A) & (D), checking signatures at all is a waste of time.
For usecases (B) & (C), checking signatures in fast-export is
throwaway work because whether or not the signatures are valid at the
time fast-export runs, and even in the rare usecase where there is no
additional processing between fast-export and fast-import (such as by
filter-repo), the signatures would still need to be re-checked by
fast-import anyway.  (Note that a simple `git fast-export ... | git
fast-import` is *not* guaranteed to get the same commit hashes even
when there are no commit signatures; that only happens when the
history is sufficiently canonical).

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-05-26 10:35       ` Christian Couder
@ 2025-05-27 15:18         ` Junio C Hamano
  2025-05-28 17:29           ` Junio C Hamano
  0 siblings, 1 reply; 65+ messages in thread
From: Junio C Hamano @ 2025-05-27 15:18 UTC (permalink / raw)
  To: Christian Couder
  Cc: Elijah Newren, git, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> I agree that we should have at least said in big letters that the
> improved support for signed commits in fast-export/import is very
> experimental and very likely to change in the future.
>
> We could still do so. This could give us a bit of time and flexibility
> until we agree on and implement something better and backward
> compatible. (Hopefully the v2 will help us move forward.)

OK, as the next release is approaching, perhaps we do a bit of
documentation update to address that "we are experimenting" and
nothing else, and leave the v2 updates for the next cycle?

Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-05-27 15:18         ` Junio C Hamano
@ 2025-05-28 17:29           ` Junio C Hamano
  2025-05-28 20:06             ` Elijah Newren
  2025-06-02 15:56             ` Christian Couder
  0 siblings, 2 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-05-28 17:29 UTC (permalink / raw)
  To: Christian Couder, Luke Shumaker
  Cc: Elijah Newren, git, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

Junio C Hamano <gitster@pobox.com> writes:

> Christian Couder <christian.couder@gmail.com> writes:
>
>> I agree that we should have at least said in big letters that the
>> improved support for signed commits in fast-export/import is very
>> experimental and very likely to change in the future.
>>
>> We could still do so. This could give us a bit of time and flexibility
>> until we agree on and implement something better and backward
>> compatible. (Hopefully the v2 will help us move forward.)
>
> OK, as the next release is approaching, perhaps we do a bit of
> documentation update to address that "we are experimenting" and
> nothing else, and leave the v2 updates for the next cycle?

---- >8 ----
Subject: [PATCH] fast-export: --signed-commits is experimental

As the design of signature handling is still being discussed, it is
likely that the data stream produced by the code in Git 2.50 would
have to be changed in such a way that is not backward compatible.

Mark the feature as experimental and discourge its use for now.

Also flip the default on the generation side to "strip"; users of
existing versions would not have passed --signed-commits=strip and
will be broken by this change if the default is made to abort, and
will be encouraged by the error message to produce data stream with
future breakage guarantees by passing --signed-commits option.

As we tone down the default behaviour, we no longer need the
FAST_EXPORT_SIGNED_COMMITS_NOABORT environment variable, which was
not discoverable enough.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/RelNotes/2.50.0.adoc |  4 +++-
 Documentation/git-fast-export.adoc | 12 +++++-------
 Documentation/git-fast-import.adoc |  3 +++
 builtin/fast-export.c              |  7 +------
 t/t9350-fast-export.sh             | 20 ++++----------------
 5 files changed, 16 insertions(+), 30 deletions(-)

diff --git a/Documentation/RelNotes/2.50.0.adoc b/Documentation/RelNotes/2.50.0.adoc
index c6c34d1a1d..9a1cdf0dc0 100644
--- a/Documentation/RelNotes/2.50.0.adoc
+++ b/Documentation/RelNotes/2.50.0.adoc
@@ -100,7 +100,9 @@ Performance, Internal Implementation, Development Support etc.
  * "git fsck" becomes more careful when checking the refs.
 
  * "git fast-export | git fast-import" learns to deal with commit and
-   tag objects with embedded signatures a bit better.
+   tag objects with embedded signatures a bit better.  This is highly
+   experimental and the format of the data stream may change in the
+   future without compatibility guarantees.
 
  * The code paths to check whether a refname X is available (by seeing
    if another ref X/Y exists, etc.) have been optimized.
diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
index 413a527496..43bbb4f63c 100644
--- a/Documentation/git-fast-export.adoc
+++ b/Documentation/git-fast-export.adoc
@@ -46,14 +46,12 @@ resulting tag will have an invalid signature.
 
 --signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::
 	Specify how to handle signed commits.  Behaves exactly as
-	'--signed-tags', but for commits.  Default is 'abort'.
+	'--signed-tags', but for commits.  Default is 'strip', which
+	is the same as how earlier versions of this command without
+	this option behaved.
 +
-Earlier versions this command that did not have '--signed-commits'
-behaved as if '--signed-commits=strip'.  As an escape hatch for users
-of tools that call 'git fast-export' but do not yet support
-'--signed-commits', you may set the environment variable
-'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default
-from 'abort' to 'warn-strip'.
+NOTE: This is highly experimental and the format of the data stream may
+change in the future without compatibility guarantees.
 
 --tag-of-filtered-object=(abort|drop|rewrite)::
 	Specify how to handle tags whose tagged object is filtered out.
diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
index 7b107f5e8e..250d866652 100644
--- a/Documentation/git-fast-import.adoc
+++ b/Documentation/git-fast-import.adoc
@@ -523,6 +523,9 @@ that signs the commit data.
 Here <alg> specifies which hashing algorithm is used for this
 signature, either `sha1` or `sha256`.
 
+NOTE: This is highly experimental and the format of the data stream may
+change in the future without compatibility guarantees.
+
 `encoding`
 ^^^^^^^^^^
 The optional `encoding` command indicates the encoding of the commit
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index 37c01d6c6f..fcf6b00d5f 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -39,7 +39,7 @@ enum sign_mode { SIGN_ABORT, SIGN_VERBATIM, SIGN_STRIP, SIGN_WARN_VERBATIM, SIGN
 
 static int progress;
 static enum sign_mode signed_tag_mode = SIGN_ABORT;
-static enum sign_mode signed_commit_mode = SIGN_ABORT;
+static enum sign_mode signed_commit_mode = SIGN_STRIP;
 static enum tag_of_filtered_mode { TAG_FILTERING_ABORT, DROP, REWRITE } tag_of_filtered_mode = TAG_FILTERING_ABORT;
 static enum reencode_mode { REENCODE_ABORT, REENCODE_YES, REENCODE_NO } reencode_mode = REENCODE_ABORT;
 static int fake_missing_tagger;
@@ -1269,7 +1269,6 @@ int cmd_fast_export(int argc,
 		    const char *prefix,
 		    struct repository *repo UNUSED)
 {
-	const char *env_signed_commits_noabort;
 	struct rev_info revs;
 	struct commit *commit;
 	char *export_filename = NULL,
@@ -1327,10 +1326,6 @@ int cmd_fast_export(int argc,
 	if (argc == 1)
 		usage_with_options (fast_export_usage, options);
 
-	env_signed_commits_noabort = getenv("FAST_EXPORT_SIGNED_COMMITS_NOABORT");
-	if (env_signed_commits_noabort && *env_signed_commits_noabort)
-		signed_commit_mode = SIGN_WARN_STRIP;
-
 	/* we handle encodings */
 	git_config(git_default_config, NULL);
 
diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
index dda9e7c3e7..76619765fc 100755
--- a/t/t9350-fast-export.sh
+++ b/t/t9350-fast-export.sh
@@ -299,22 +299,10 @@ test_expect_success GPG 'set up signed commit' '
 
 '
 
-test_expect_success GPG 'signed-commits default' '
-
-	sane_unset FAST_EXPORT_SIGNED_COMMITS_NOABORT &&
-	test_must_fail git fast-export --reencode=no commit-signing &&
-
-	FAST_EXPORT_SIGNED_COMMITS_NOABORT=1 git fast-export --reencode=no commit-signing >output 2>err &&
-	! grep ^gpgsig output &&
-	grep "^encoding ISO-8859-1" output &&
-	test -s err &&
-	sed "s/commit-signing/commit-strip-signing/" output | (
-		cd new &&
-		git fast-import &&
-		STRIPPED=$(git rev-parse --verify refs/heads/commit-strip-signing) &&
-		test $COMMIT_SIGNING != $STRIPPED
-	)
-
+test_expect_success GPG 'signed-commits default is same as strip' '
+	git fast-export --reencode=no commit-signing >out1 2>err &&
+	git fast-export --reencode=no --signed-commits=strip commit-signing >out2 &&
+	test_cmp out1 out2
 '
 
 test_expect_success GPG 'signed-commits=abort' '
-- 
2.50.0-rc0-134-gb29a910c2a


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-05-28 17:29           ` Junio C Hamano
@ 2025-05-28 20:06             ` Elijah Newren
  2025-05-28 21:59               ` Junio C Hamano
  2025-06-02 15:56             ` Christian Couder
  1 sibling, 1 reply; 65+ messages in thread
From: Elijah Newren @ 2025-05-28 20:06 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Christian Couder, Luke Shumaker, git, Patrick Steinhardt,
	Jeff King, Johannes Schindelin, Christian Couder

On Wed, May 28, 2025 at 10:29 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Junio C Hamano <gitster@pobox.com> writes:
>
> > Christian Couder <christian.couder@gmail.com> writes:
> >
> >> I agree that we should have at least said in big letters that the
> >> improved support for signed commits in fast-export/import is very
> >> experimental and very likely to change in the future.
> >>
> >> We could still do so. This could give us a bit of time and flexibility
> >> until we agree on and implement something better and backward
> >> compatible. (Hopefully the v2 will help us move forward.)
> >
> > OK, as the next release is approaching, perhaps we do a bit of
> > documentation update to address that "we are experimenting" and
> > nothing else, and leave the v2 updates for the next cycle?
>
> ---- >8 ----
> Subject: [PATCH] fast-export: --signed-commits is experimental
>
> As the design of signature handling is still being discussed, it is
> likely that the data stream produced by the code in Git 2.50 would
> have to be changed in such a way that is not backward compatible.
>
> Mark the feature as experimental and discourge its use for now.

I think this is a very good thing to do.

minor nit: discourge -> discourage

> Also flip the default on the generation side to "strip"; users of
> existing versions would not have passed --signed-commits=strip and
> will be broken by this change if the default is made to abort, and
> will be encouraged by the error message to produce data stream with
> future breakage guarantees by passing --signed-commits option.

So...git-filter-repo runs fast-export and has limited flexibility
about which options it passes to fast-export under the hood, so this
change would save me from the patch I was planning to add to
filter-repo.  So that's evidence in support of your statement, but the
"will be broken" statement appears to me to be incongruent with past
deprecations and changes of default that we have gone through.  Often
when we have deprecated or changed an option our process was to first
produce an error and update documentation and wait a while, then go
and change the default after a sufficiently long time.  Here, we had
kind of stopped at just producing the error with no plans to take
another step.  If that was the route we took in the past, what makes
this considered a breakage and not the other changes we made?

(Just curious, I'm not against this change.)

> As we tone down the default behaviour, we no longer need the
> FAST_EXPORT_SIGNED_COMMITS_NOABORT environment variable, which was
> not discoverable enough.

Makes sense.
>
> Signed-off-by: Junio C Hamano <gitster@pobox.com>
> ---
>  Documentation/RelNotes/2.50.0.adoc |  4 +++-
>  Documentation/git-fast-export.adoc | 12 +++++-------
>  Documentation/git-fast-import.adoc |  3 +++
>  builtin/fast-export.c              |  7 +------
>  t/t9350-fast-export.sh             | 20 ++++----------------
>  5 files changed, 16 insertions(+), 30 deletions(-)
>
> diff --git a/Documentation/RelNotes/2.50.0.adoc b/Documentation/RelNotes/2.50.0.adoc
> index c6c34d1a1d..9a1cdf0dc0 100644
> --- a/Documentation/RelNotes/2.50.0.adoc
> +++ b/Documentation/RelNotes/2.50.0.adoc
> @@ -100,7 +100,9 @@ Performance, Internal Implementation, Development Support etc.
>   * "git fsck" becomes more careful when checking the refs.
>
>   * "git fast-export | git fast-import" learns to deal with commit and
> -   tag objects with embedded signatures a bit better.
> +   tag objects with embedded signatures a bit better.  This is highly
> +   experimental and the format of the data stream may change in the
> +   future without compatibility guarantees.
>
>   * The code paths to check whether a refname X is available (by seeing
>     if another ref X/Y exists, etc.) have been optimized.
> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
> index 413a527496..43bbb4f63c 100644
> --- a/Documentation/git-fast-export.adoc
> +++ b/Documentation/git-fast-export.adoc
> @@ -46,14 +46,12 @@ resulting tag will have an invalid signature.
>
>  --signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::
>         Specify how to handle signed commits.  Behaves exactly as
> -       '--signed-tags', but for commits.  Default is 'abort'.
> +       '--signed-tags', but for commits.  Default is 'strip', which
> +       is the same as how earlier versions of this command without
> +       this option behaved.
>  +
> -Earlier versions this command that did not have '--signed-commits'
> -behaved as if '--signed-commits=strip'.  As an escape hatch for users
> -of tools that call 'git fast-export' but do not yet support
> -'--signed-commits', you may set the environment variable
> -'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default
> -from 'abort' to 'warn-strip'.
> +NOTE: This is highly experimental and the format of the data stream may
> +change in the future without compatibility guarantees.
>
>  --tag-of-filtered-object=(abort|drop|rewrite)::
>         Specify how to handle tags whose tagged object is filtered out.
> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
> index 7b107f5e8e..250d866652 100644
> --- a/Documentation/git-fast-import.adoc
> +++ b/Documentation/git-fast-import.adoc
> @@ -523,6 +523,9 @@ that signs the commit data.
>  Here <alg> specifies which hashing algorithm is used for this
>  signature, either `sha1` or `sha256`.
>
> +NOTE: This is highly experimental and the format of the data stream may
> +change in the future without compatibility guarantees.
> +
>  `encoding`
>  ^^^^^^^^^^
>  The optional `encoding` command indicates the encoding of the commit
> diff --git a/builtin/fast-export.c b/builtin/fast-export.c
> index 37c01d6c6f..fcf6b00d5f 100644
> --- a/builtin/fast-export.c
> +++ b/builtin/fast-export.c
> @@ -39,7 +39,7 @@ enum sign_mode { SIGN_ABORT, SIGN_VERBATIM, SIGN_STRIP, SIGN_WARN_VERBATIM, SIGN
>
>  static int progress;
>  static enum sign_mode signed_tag_mode = SIGN_ABORT;
> -static enum sign_mode signed_commit_mode = SIGN_ABORT;
> +static enum sign_mode signed_commit_mode = SIGN_STRIP;
>  static enum tag_of_filtered_mode { TAG_FILTERING_ABORT, DROP, REWRITE } tag_of_filtered_mode = TAG_FILTERING_ABORT;
>  static enum reencode_mode { REENCODE_ABORT, REENCODE_YES, REENCODE_NO } reencode_mode = REENCODE_ABORT;
>  static int fake_missing_tagger;
> @@ -1269,7 +1269,6 @@ int cmd_fast_export(int argc,
>                     const char *prefix,
>                     struct repository *repo UNUSED)
>  {
> -       const char *env_signed_commits_noabort;
>         struct rev_info revs;
>         struct commit *commit;
>         char *export_filename = NULL,
> @@ -1327,10 +1326,6 @@ int cmd_fast_export(int argc,
>         if (argc == 1)
>                 usage_with_options (fast_export_usage, options);
>
> -       env_signed_commits_noabort = getenv("FAST_EXPORT_SIGNED_COMMITS_NOABORT");
> -       if (env_signed_commits_noabort && *env_signed_commits_noabort)
> -               signed_commit_mode = SIGN_WARN_STRIP;
> -
>         /* we handle encodings */
>         git_config(git_default_config, NULL);
>
> diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
> index dda9e7c3e7..76619765fc 100755
> --- a/t/t9350-fast-export.sh
> +++ b/t/t9350-fast-export.sh
> @@ -299,22 +299,10 @@ test_expect_success GPG 'set up signed commit' '
>
>  '
>
> -test_expect_success GPG 'signed-commits default' '
> -
> -       sane_unset FAST_EXPORT_SIGNED_COMMITS_NOABORT &&
> -       test_must_fail git fast-export --reencode=no commit-signing &&
> -
> -       FAST_EXPORT_SIGNED_COMMITS_NOABORT=1 git fast-export --reencode=no commit-signing >output 2>err &&
> -       ! grep ^gpgsig output &&
> -       grep "^encoding ISO-8859-1" output &&
> -       test -s err &&
> -       sed "s/commit-signing/commit-strip-signing/" output | (
> -               cd new &&
> -               git fast-import &&
> -               STRIPPED=$(git rev-parse --verify refs/heads/commit-strip-signing) &&
> -               test $COMMIT_SIGNING != $STRIPPED
> -       )
> -
> +test_expect_success GPG 'signed-commits default is same as strip' '
> +       git fast-export --reencode=no commit-signing >out1 2>err &&
> +       git fast-export --reencode=no --signed-commits=strip commit-signing >out2 &&
> +       test_cmp out1 out2
>  '
>
>  test_expect_success GPG 'signed-commits=abort' '
> --
> 2.50.0-rc0-134-gb29a910c2a

Looks good to me.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-05-28 20:06             ` Elijah Newren
@ 2025-05-28 21:59               ` Junio C Hamano
  2025-05-28 23:15                 ` Elijah Newren
  0 siblings, 1 reply; 65+ messages in thread
From: Junio C Hamano @ 2025-05-28 21:59 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, Luke Shumaker, git, Patrick Steinhardt,
	Jeff King, Johannes Schindelin, Christian Couder

Elijah Newren <newren@gmail.com> writes:

> Often
> when we have deprecated or changed an option our process was to first
> produce an error and update documentation and wait a while, then go
> and change the default after a sufficiently long time.  Here, we had
> kind of stopped at just producing the error with no plans to take
> another step.  If that was the route we took in the past, what makes
> this considered a breakage and not the other changes we made?
>
> (Just curious, I'm not against this change.)

What is wrong is the behaviour change in the original, which luckily
is not in any released versions (except for 2.50-rc0, which should
not count, as I think we should do this toning-down before -rc1).

We used to silently ignore and strip commit signatures and that has
always been the behaviour the existing users have relied upon; we
started requiring these existing users to either explicitly pass
--signed-c=strip or set an environmtne variable.  A new feature
should be opt-in to make the transition smoother, but the topic did
not follow that pattern.

I view this last-minute band-aid patch that flips the default back
to what it used to be as remedying that mistake in the original
series.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-05-28 21:59               ` Junio C Hamano
@ 2025-05-28 23:15                 ` Elijah Newren
  2025-05-29  3:14                   ` Junio C Hamano
  0 siblings, 1 reply; 65+ messages in thread
From: Elijah Newren @ 2025-05-28 23:15 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Christian Couder, Luke Shumaker, git, Patrick Steinhardt,
	Jeff King, Johannes Schindelin, Christian Couder

On Wed, May 28, 2025 at 2:59 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> > Often
> > when we have deprecated or changed an option our process was to first
> > produce an error and update documentation and wait a while, then go
> > and change the default after a sufficiently long time.  Here, we had
> > kind of stopped at just producing the error with no plans to take
> > another step.  If that was the route we took in the past, what makes
> > this considered a breakage and not the other changes we made?
> >
> > (Just curious, I'm not against this change.)
>
> What is wrong is the behaviour change in the original, which luckily
> is not in any released versions (except for 2.50-rc0, which should
> not count, as I think we should do this toning-down before -rc1).
>
> We used to silently ignore and strip commit signatures and that has
> always been the behaviour the existing users have relied upon; we
> started requiring these existing users to either explicitly pass
> --signed-c=strip or set an environmtne variable.  A new feature
> should be opt-in to make the transition smoother, but the topic did
> not follow that pattern.
>
> I view this last-minute band-aid patch that flips the default back
> to what it used to be as remedying that mistake in the original
> series.

An earlier version of the series did keep the stripping, but you were
the one who objected
(https://lore.kernel.org/git/xmqqfszbcazc.fsf@gitster.g/):

"""
Why deliberate inconsistency?  I am not sure "historically we did a
wrong thing" is a good reason (if we view that silently stripping
was a disservice to the users, aborting would be a bugfix).
"""

and later in the series
(https://lore.kernel.org/git/xmqqim44fyjj.fsf@gitster.g/), you added:

"""
Thanks.  The "filter-repo already gets bug reports from the users"
is a valuable input when deciding if it is reasonable to sell the
behaviour change as a bugfix to our users.
"""

Personally, I kind of think abort makes more sense as the default --
at some point.  So I'm curious if you've just changed your mind
completely from before and are against changing the default at all,
whether you think there's more steps that should be done before we
change the default, or whether this new flag feeling incomplete and
development related to it being slow moving means we should wait off
until we have a better picture of where it'll end up and leave the
defaults alone until we get there.  (I'm kind of leaning towards the
3rd of those, but am curious if I'm misaligned with your vision.)

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-05-28 23:15                 ` Elijah Newren
@ 2025-05-29  3:14                   ` Junio C Hamano
  2025-06-02 15:56                     ` Christian Couder
  0 siblings, 1 reply; 65+ messages in thread
From: Junio C Hamano @ 2025-05-29  3:14 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, Luke Shumaker, git, Patrick Steinhardt,
	Jeff King, Johannes Schindelin, Christian Couder

Elijah Newren <newren@gmail.com> writes:

> Personally, I kind of think abort makes more sense as the default --
> at some point.

Oh, of course.

> So I'm curious if you've just changed your mind
> completely from before and are against changing the default at all,

Yes, after seeing that the representation of the signature algorithm
and encapsulation format was not as well thought out as I thought it
would already be and its design still being discussed, I realized
that the new feature was way premature to have in the release.  At
some point, when things mature and we are reasonably sure we will
not have to make incompatible changes in the data stream, we might
need to switch, and the best default might turn out to be to refuse
to work unless the end-user makes an explicit choice, but as the
design of the feature stands now, I have a feeling that it is a bit
premature.  Certainly not ready for general consumption.

Of course, I could have just reverted the merge of the original
topic and give it a chance for a fresh restart the next cycle, but a
new feature clearly marked "highly experimental" would hopefully set
the end-user expectation straight, as long as the default is "do not
do anything different from before", which is the safest choice for a
feature whose design is still wobbly.


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-05-28 17:29           ` Junio C Hamano
  2025-05-28 20:06             ` Elijah Newren
@ 2025-06-02 15:56             ` Christian Couder
  2025-06-02 16:20               ` Junio C Hamano
  1 sibling, 1 reply; 65+ messages in thread
From: Christian Couder @ 2025-06-02 15:56 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Luke Shumaker, Elijah Newren, git, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

(Sorry for the late answer to this.)

On Wed, May 28, 2025 at 7:29 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Junio C Hamano <gitster@pobox.com> writes:
>
> > Christian Couder <christian.couder@gmail.com> writes:
> >
> >> I agree that we should have at least said in big letters that the
> >> improved support for signed commits in fast-export/import is very
> >> experimental and very likely to change in the future.
> >>
> >> We could still do so. This could give us a bit of time and flexibility
> >> until we agree on and implement something better and backward
> >> compatible. (Hopefully the v2 will help us move forward.)
> >
> > OK, as the next release is approaching, perhaps we do a bit of
> > documentation update to address that "we are experimenting" and
> > nothing else, and leave the v2 updates for the next cycle?

Thanks for this. I agree that it's the best approach.

> ---- >8 ----
> Subject: [PATCH] fast-export: --signed-commits is experimental
>
> As the design of signature handling is still being discussed, it is
> likely that the data stream produced by the code in Git 2.50 would
> have to be changed in such a way that is not backward compatible.
>
> Mark the feature as experimental and discourge its use for now.

Yeah, right.

> Also flip the default on the generation side to "strip"; users of
> existing versions would not have passed --signed-commits=strip and
> will be broken by this change if the default is made to abort, and
> will be encouraged by the error message to produce data stream with
> future breakage guarantees by passing --signed-commits option.
>
> As we tone down the default behaviour, we no longer need the
> FAST_EXPORT_SIGNED_COMMITS_NOABORT environment variable, which was
> not discoverable enough.
>
> Signed-off-by: Junio C Hamano <gitster@pobox.com>
> ---
>  Documentation/RelNotes/2.50.0.adoc |  4 +++-
>  Documentation/git-fast-export.adoc | 12 +++++-------
>  Documentation/git-fast-import.adoc |  3 +++
>  builtin/fast-export.c              |  7 +------
>  t/t9350-fast-export.sh             | 20 ++++----------------
>  5 files changed, 16 insertions(+), 30 deletions(-)
>
> diff --git a/Documentation/RelNotes/2.50.0.adoc b/Documentation/RelNotes/2.50.0.adoc
> index c6c34d1a1d..9a1cdf0dc0 100644
> --- a/Documentation/RelNotes/2.50.0.adoc
> +++ b/Documentation/RelNotes/2.50.0.adoc
> @@ -100,7 +100,9 @@ Performance, Internal Implementation, Development Support etc.
>   * "git fsck" becomes more careful when checking the refs.
>
>   * "git fast-export | git fast-import" learns to deal with commit and
> -   tag objects with embedded signatures a bit better.
> +   tag objects with embedded signatures a bit better.  This is highly
> +   experimental and the format of the data stream may change in the
> +   future without compatibility guarantees.
>
>   * The code paths to check whether a refname X is available (by seeing
>     if another ref X/Y exists, etc.) have been optimized.
> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
> index 413a527496..43bbb4f63c 100644
> --- a/Documentation/git-fast-export.adoc
> +++ b/Documentation/git-fast-export.adoc
> @@ -46,14 +46,12 @@ resulting tag will have an invalid signature.
>
>  --signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::
>         Specify how to handle signed commits.  Behaves exactly as
> -       '--signed-tags', but for commits.  Default is 'abort'.
> +       '--signed-tags', but for commits.  Default is 'strip', which
> +       is the same as how earlier versions of this command without
> +       this option behaved.
>  +
> -Earlier versions this command that did not have '--signed-commits'
> -behaved as if '--signed-commits=strip'.  As an escape hatch for users
> -of tools that call 'git fast-export' but do not yet support
> -'--signed-commits', you may set the environment variable
> -'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default
> -from 'abort' to 'warn-strip'.
> +NOTE: This is highly experimental and the format of the data stream may
> +change in the future without compatibility guarantees.

I wonder if it should say that the default is likely to change too?

>  --tag-of-filtered-object=(abort|drop|rewrite)::
>         Specify how to handle tags whose tagged object is filtered out.
> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
> index 7b107f5e8e..250d866652 100644
> --- a/Documentation/git-fast-import.adoc
> +++ b/Documentation/git-fast-import.adoc
> @@ -523,6 +523,9 @@ that signs the commit data.
>  Here <alg> specifies which hashing algorithm is used for this
>  signature, either `sha1` or `sha256`.
>
> +NOTE: This is highly experimental and the format of the data stream may
> +change in the future without compatibility guarantees.
> +
>  `encoding`
>  ^^^^^^^^^^
>  The optional `encoding` command indicates the encoding of the commit
> diff --git a/builtin/fast-export.c b/builtin/fast-export.c
> index 37c01d6c6f..fcf6b00d5f 100644
> --- a/builtin/fast-export.c
> +++ b/builtin/fast-export.c
> @@ -39,7 +39,7 @@ enum sign_mode { SIGN_ABORT, SIGN_VERBATIM, SIGN_STRIP, SIGN_WARN_VERBATIM, SIGN
>
>  static int progress;
>  static enum sign_mode signed_tag_mode = SIGN_ABORT;
> -static enum sign_mode signed_commit_mode = SIGN_ABORT;
> +static enum sign_mode signed_commit_mode = SIGN_STRIP;
>  static enum tag_of_filtered_mode { TAG_FILTERING_ABORT, DROP, REWRITE } tag_of_filtered_mode = TAG_FILTERING_ABORT;
>  static enum reencode_mode { REENCODE_ABORT, REENCODE_YES, REENCODE_NO } reencode_mode = REENCODE_ABORT;
>  static int fake_missing_tagger;
> @@ -1269,7 +1269,6 @@ int cmd_fast_export(int argc,
>                     const char *prefix,
>                     struct repository *repo UNUSED)
>  {
> -       const char *env_signed_commits_noabort;
>         struct rev_info revs;
>         struct commit *commit;
>         char *export_filename = NULL,
> @@ -1327,10 +1326,6 @@ int cmd_fast_export(int argc,
>         if (argc == 1)
>                 usage_with_options (fast_export_usage, options);
>
> -       env_signed_commits_noabort = getenv("FAST_EXPORT_SIGNED_COMMITS_NOABORT");
> -       if (env_signed_commits_noabort && *env_signed_commits_noabort)
> -               signed_commit_mode = SIGN_WARN_STRIP;
> -
>         /* we handle encodings */
>         git_config(git_default_config, NULL);
>
> diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
> index dda9e7c3e7..76619765fc 100755
> --- a/t/t9350-fast-export.sh
> +++ b/t/t9350-fast-export.sh
> @@ -299,22 +299,10 @@ test_expect_success GPG 'set up signed commit' '
>
>  '
>
> -test_expect_success GPG 'signed-commits default' '
> -
> -       sane_unset FAST_EXPORT_SIGNED_COMMITS_NOABORT &&
> -       test_must_fail git fast-export --reencode=no commit-signing &&
> -
> -       FAST_EXPORT_SIGNED_COMMITS_NOABORT=1 git fast-export --reencode=no commit-signing >output 2>err &&
> -       ! grep ^gpgsig output &&
> -       grep "^encoding ISO-8859-1" output &&
> -       test -s err &&
> -       sed "s/commit-signing/commit-strip-signing/" output | (
> -               cd new &&
> -               git fast-import &&
> -               STRIPPED=$(git rev-parse --verify refs/heads/commit-strip-signing) &&
> -               test $COMMIT_SIGNING != $STRIPPED
> -       )
> -
> +test_expect_success GPG 'signed-commits default is same as strip' '

Here also maybe we should say that the default could change in case
advanced users look at test cases to get hints at what is cast in
stone?

> +       git fast-export --reencode=no commit-signing >out1 2>err &&
> +       git fast-export --reencode=no --signed-commits=strip commit-signing >out2 &&
> +       test_cmp out1 out2
>  '

Otherwise the patch looks good to me.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-05-29  3:14                   ` Junio C Hamano
@ 2025-06-02 15:56                     ` Christian Couder
  0 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-06-02 15:56 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Elijah Newren, Luke Shumaker, git, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

On Thu, May 29, 2025 at 5:14 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> > Personally, I kind of think abort makes more sense as the default --
> > at some point.
>
> Oh, of course.
>
> > So I'm curious if you've just changed your mind
> > completely from before and are against changing the default at all,
>
> Yes, after seeing that the representation of the signature algorithm
> and encapsulation format was not as well thought out as I thought it
> would already be and its design still being discussed, I realized
> that the new feature was way premature to have in the release.  At
> some point, when things mature and we are reasonably sure we will
> not have to make incompatible changes in the data stream, we might
> need to switch, and the best default might turn out to be to refuse
> to work unless the end-user makes an explicit choice, but as the
> design of the feature stands now, I have a feeling that it is a bit
> premature.  Certainly not ready for general consumption.

Discouraging the use of the feature and saying it's highly
experimental should hint that the default might change in the future,
but maybe we should explicitly say so?

> Of course, I could have just reverted the merge of the original
> topic and give it a chance for a fresh restart the next cycle, but a
> new feature clearly marked "highly experimental" would hopefully set
> the end-user expectation straight, as long as the default is "do not
> do anything different from before", which is the safest choice for a
> feature whose design is still wobbly.

Yeah, I agree with this approach in general.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH] fast-(import|export): improve on the signature algorithm name
  2025-06-02 15:56             ` Christian Couder
@ 2025-06-02 16:20               ` Junio C Hamano
  0 siblings, 0 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-06-02 16:20 UTC (permalink / raw)
  To: Christian Couder
  Cc: Luke Shumaker, Elijah Newren, git, Patrick Steinhardt, Jeff King,
	Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

>> -Earlier versions this command that did not have '--signed-commits'
>> -behaved as if '--signed-commits=strip'.  As an escape hatch for users
>> -of tools that call 'git fast-export' but do not yet support
>> -'--signed-commits', you may set the environment variable
>> -'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default
>> -from 'abort' to 'warn-strip'.
>> +NOTE: This is highly experimental and the format of the data stream may
>> +change in the future without compatibility guarantees.
>
> I wonder if it should say that the default is likely to change too?

It is to be decided in the future and we have no plan to do so
before the feature loses "experimental" label, no?  Those who are
opting into a highly experimental feature would know that already.

>> +test_expect_success GPG 'signed-commits default is same as strip' '
>
> Here also maybe we should say that the default could change in case
> advanced users look at test cases to get hints at what is cast in
> stone?

The same answer applies.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v2 0/6] extract algo information from signatures
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
                     ` (6 preceding siblings ...)
  2025-05-26 16:03   ` [PATCH v2 0/6] extract algo information from signatures Elijah Newren
@ 2025-06-02 22:17   ` brian m. carlson
  2025-06-19 13:37     ` Christian Couder
  2025-06-18 15:18   ` [PATCH v3] fast-(import|export): improve on commit signature output format Christian Couder
  8 siblings, 1 reply; 65+ messages in thread
From: brian m. carlson @ 2025-06-02 22:17 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	Johannes Schindelin

[-- Attachment #1: Type: text/plain, Size: 1246 bytes --]

On 2025-05-26 at 10:33:08, Christian Couder wrote:
> Around one month ago, I sent a patch that tried to improve on how `git
> fast-export` handled SSH and X.509 commit signatures:
> 
> https://lore.kernel.org/git/20250424203904.909777-1-christian.couder@gmail.com/
> 
> This patch was showing a single string for the hash algorithm with the
> following possible values:
> 
> * "openpgp" for SHA-1 OpenPGP signatures,
> 
> * "sha256" for SHA-256 OpenPGP signatures,
> 
> * "x509" for X.509 (GPGSM) signatures,
> 
> * "ssh", for SSH signatures,
> 
> * "unknown" for signatures that can't be identified (a warning is
>   emitted).
> 
> brian m. carlson however replied that it would be better to show two
> pieces of information instead of one: one for the hash algorithm and
> one for the protocol.

Actually, what I was saying is that we should have one for the hash
algorithm that is used in the Git object.  I don't care about the hash
algorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's
signed with SHA-512 or SHA-256), but we can have multiple signatures in
a single commit such that there's both a SHA-1 signature and a SHA-256
signature.
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]

^ permalink raw reply	[flat|nested] 65+ messages in thread

* [PATCH v3] fast-(import|export): improve on commit signature output format
  2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
                     ` (7 preceding siblings ...)
  2025-06-02 22:17   ` brian m. carlson
@ 2025-06-18 15:18   ` Christian Couder
  2025-06-19 13:36     ` [PATCH v4] " Christian Couder
  8 siblings, 1 reply; 65+ messages in thread
From: Christian Couder @ 2025-06-18 15:18 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
signed-commits, 2025-03-10), added support for signed commits to
fast-export and fast-import.

When a signed commit is processed, fast-export can output either
"gpgsig sha1" or "gpgsig sha256" depending on whether the signed
commit uses the SHA-1 or SHA-256 Git object format.

However, this implementation has a number of limitations:

  - the output format was not properly described in the documentation,
  - the output format is not very informative as it doesn't even say
    if the signature is an OpenPGP, an SSH, or an X509 signature,
  - the implementation doesn't support having both one signature on
    the SHA-1 object and one on the SHA-256 object.

Let's improve on these limitations by improving fast-export and
fast-import so that:

  - both one signature on the SHA-1 object and one on the SHA-256
    object can be exported and imported,
  - if there is more than one signature on the SHA-1 object or on
    the SHA-256 object, a warning is emitted,
  - the output format is "gpgsig <git-hash-algo> <signature-format>",
    where <git-hash-algo> is the Git object format as before, and
    <signature-format> is the signature type ("openpgp", "x509",
    "ssh" or "unknown",
  - the output is properly documented.

Note that it could be even better to be able to export and import
more than one signature on the SHA-1 object and on the SHA-256
object, but other parts of Git don't handle that well for now, so
this is left for future improvements.

Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---

The v1 of this patch series simply replaced the generic "sha1" or
"sha256" in the 'gpgsig' command that `git fast-export` produces and
`git fast-import` consumes with the signature format (e.g., openpgp,
ssh).

In the v2, a different approach was tried in the direction of possibly
making `git fast-export` check signatures to be able to get more
information from them.

In this v3, the approach is more similar to v1, but takes into account
what was learned from the previous approaches. The signature format
(e.g., openpgp, ssh) is added to the 'gpgsig' command, while both a
SHA-1 and a SHA-256 signature from the same commit can be processed
and a warning are emitted for each additional signature with the same
hash type.

There are no tests in this v3 with both a SHA-1 and a SHA-256
signature on the same commit though, as I am not sure yet how to best
generate a commit with such signatures. Suggestions welcome!

As the patch is very different from both v1 and v2 no range-diff is
provided.

I have tried to launch CI tests but they seem blocked for 24 minutes at
the "config" stage.

Thanks to brian, Elijah and Junio who commented on the v1 and v2.

 Documentation/git-fast-export.adoc |  17 +++++
 Documentation/git-fast-import.adoc |  31 ++++++--
 builtin/fast-export.c              |  67 ++++++++++++++---
 builtin/fast-import.c              | 117 +++++++++++++++++++++++------
 gpg-interface.c                    |  12 +++
 gpg-interface.h                    |  12 +++
 t/t9350-fast-export.sh             |  60 ++++++++++++++-
 7 files changed, 274 insertions(+), 42 deletions(-)

diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
index 43bbb4f63c..64198f2186 100644
--- a/Documentation/git-fast-export.adoc
+++ b/Documentation/git-fast-export.adoc
@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.
 	is the same as how earlier versions of this command without
 	this option behaved.
 +
+When exported, a signature starts with:
++
+gpgsig <git-hash-algo> <signature-format>
++
+where <git-hash-algo> is the Git object hash so either "sha1" or
+"sha256", and <signature-format> is the signature type, so "openpgp",
+"x509", "ssh" or "unknown".
++
+For example, an OpenPGP signature on a SHA-1 commit starts with
+`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit
+starts with `gpgsig sha256 ssh`.
++
+Currently for a given commit, at most one signature for the SHA-1
+object and one signature for the SHA-256 object are exported, each
+with their respective <git-hash-algo> identifier. A warning is
+emitted for each additional signature found.
++
 NOTE: This is highly experimental and the format of the data stream may
 change in the future without compatibility guarantees.
 
diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
index 250d866652..db5e5c8da5 100644
--- a/Documentation/git-fast-import.adoc
+++ b/Documentation/git-fast-import.adoc
@@ -445,7 +445,7 @@ one).
 	original-oid?
 	('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
 	'committer' (SP <name>)? SP LT <email> GT SP <when> LF
-	('gpgsig' SP <alg> LF data)?
+	('gpgsig' SP <algo> SP <format> LF data)?
 	('encoding' SP <encoding> LF)?
 	data
 	('from' SP <commit-ish> LF)?
@@ -518,13 +518,32 @@ their syntax.
 ^^^^^^^^
 
 The optional `gpgsig` command is used to include a PGP/GPG signature
-that signs the commit data.
+or other cryptographic signature that signs the commit data.
 
-Here <alg> specifies which hashing algorithm is used for this
-signature, either `sha1` or `sha256`.
+....
+	'gpgsig' SP <git-hash-algo> SP <signature-format> LF
+	data
+....
+
+The `gpgsig` command takes two arguments:
+
+* `<git-hash-algo>` specifies which Git object format this signature
+  applies to, either `sha1` or `sha256`.
+
+* `<signature-format>` specifies the type of signature, such as
+  `openpgp`, `x509`, `ssh`, or `unknown`.
+
+A commit may have at most one signature for the SHA-1 object format
+(stored in the "gpgsig" header) and one for the SHA-256 object format
+(stored in the "gpgsig-sha256" header).
+
+See below for a detailed description of the `data` command which
+contains the raw signature data.
+
+Signatures are not yet checked in the current implementation though.
 
-NOTE: This is highly experimental and the format of the data stream may
-change in the future without compatibility guarantees.
+NOTE: This is highly experimental and the format of the `gpgsig`
+command may change in the future without compatibility guarantees.
 
 `encoding`
 ^^^^^^^^^^
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index fcf6b00d5f..332c036ee4 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -29,6 +29,7 @@
 #include "quote.h"
 #include "remote.h"
 #include "blob.h"
+#include "gpg-interface.h"
 
 static const char *const fast_export_usage[] = {
 	N_("git fast-export [<rev-list-opts>]"),
@@ -652,6 +653,30 @@ static const char *find_commit_multiline_header(const char *msg,
 	return strbuf_detach(&val, NULL);
 }
 
+static void print_signature(const char *signature, const char *object_hash)
+{
+	if (!signature)
+		return;
+
+	printf("gpgsig %s %s\ndata %u\n%s",
+	       object_hash,
+	       get_signature_format(signature),
+	       (unsigned)strlen(signature),
+	       signature);
+}
+
+static void warn_on_extra_sig(const char **pos, struct commit *commit, int is_sha1)
+{
+	const char *header = is_sha1 ? "gpgsig" : "gpgsig-sha256";
+	const char *extra_sig = find_commit_multiline_header(*pos + 1, header, pos);
+	if (extra_sig) {
+		const char *hash = is_sha1 ? "SHA-1" : "SHA-256";
+		warning("more than one %s signature found on commit %s, using only the first one",
+			hash, oid_to_hex(&commit->object.oid));
+		free((char *)extra_sig);
+	}
+}
+
 static void handle_commit(struct commit *commit, struct rev_info *rev,
 			  struct string_list *paths_of_changed_objects)
 {
@@ -660,7 +685,8 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	const char *author, *author_end, *committer, *committer_end;
 	const char *encoding = NULL;
 	size_t encoding_len;
-	const char *signature_alg = NULL, *signature = NULL;
+	const char *sig_sha1 = NULL;
+	const char *sig_sha256 = NULL;
 	const char *message;
 	char *reencoded = NULL;
 	struct commit_list *p;
@@ -700,10 +726,28 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	}
 
 	if (*commit_buffer_cursor == '\n') {
-		if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
-			signature_alg = "sha1";
-		else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
-			signature_alg = "sha256";
+		const char *sig_cursor = commit_buffer_cursor;
+		const char *after_sha1 = commit_buffer_cursor;
+		const char *after_sha256 = commit_buffer_cursor;
+
+		/*
+		 * Find the first signature for each hash algorithm.
+		 * The searches must start from the same position.
+		 */
+		sig_sha1 = find_commit_multiline_header(sig_cursor + 1,
+							"gpgsig",
+							&after_sha1);
+		sig_sha256 = find_commit_multiline_header(sig_cursor + 1,
+							  "gpgsig-sha256",
+							  &after_sha256);
+
+		/* Warn on any additional signatures, as they will be ignored. */
+		if (sig_sha1)
+			warn_on_extra_sig(&after_sha1, commit, 1);
+		if (sig_sha256)
+			warn_on_extra_sig(&after_sha256, commit, 0);
+
+		commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
 	}
 
 	message = strstr(commit_buffer_cursor, "\n\n");
@@ -769,7 +813,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	printf("%.*s\n%.*s\n",
 	       (int)(author_end - author), author,
 	       (int)(committer_end - committer), committer);
-	if (signature) {
+	if (sig_sha1 || sig_sha256) {
 		switch (signed_commit_mode) {
 		case SIGN_ABORT:
 			die("encountered signed commit %s; use "
@@ -780,19 +824,18 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 				oid_to_hex(&commit->object.oid));
 			/* fallthru */
 		case SIGN_VERBATIM:
-			printf("gpgsig %s\ndata %u\n%s",
-			       signature_alg,
-			       (unsigned)strlen(signature),
-			       signature);
+			print_signature(sig_sha1, "sha1");
+			print_signature(sig_sha256, "sha256");
 			break;
 		case SIGN_WARN_STRIP:
-			warning("stripping signature from commit %s",
+			warning("stripping signature(s) from commit %s",
 				oid_to_hex(&commit->object.oid));
 			/* fallthru */
 		case SIGN_STRIP:
 			break;
 		}
-		free((char *)signature);
+		free((char *)sig_sha1);
+		free((char *)sig_sha256);
 	}
 	if (!reencoded && encoding)
 		printf("encoding %.*s\n", (int)encoding_len, encoding);
diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index b2839c5f43..48ce8ebb77 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -29,6 +29,7 @@
 #include "commit-reach.h"
 #include "khash.h"
 #include "date.h"
+#include "gpg-interface.h"
 
 #define PACK_ID_BITS 16
 #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
@@ -2718,15 +2719,86 @@ static struct hash_list *parse_merge(unsigned int *count)
 	return list;
 }
 
+struct signature_data {
+	char *hash_algo;      /* "sha1" or "sha256" */
+	char *sig_format;     /* "openpgp", "x509", "ssh", "unknown" */
+	struct strbuf data;   /* The actual signature data */
+};
+
+static void parse_one_signature(struct signature_data *sig, const char *v)
+{
+	char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */
+	char *space = strchr(args, ' ');
+
+	if (!space)
+		die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
+		    "got 'gpgsig %s'", args);
+	*space++ = '\0';
+
+	sig->hash_algo = args;
+	sig->sig_format = space;
+
+	/* Remove any trailing newline from format */
+	space = strchr(sig->sig_format, '\n');
+	if (space)
+		*space = '\0';
+
+	/* Validate hash algorithm */
+	if (strcmp(sig->hash_algo, "sha1") &&
+	    strcmp(sig->hash_algo, "sha256"))
+		die("Unknown git hash algorithm in gpgsig: '%s'", sig->hash_algo);
+
+	/* Validate signature format */
+	if (!valid_signature_format(sig->sig_format))
+		die("Invalid signature format in gpgsig: '%s'", sig->sig_format);
+	if (!strcmp(sig->sig_format, "unknown"))
+		warning("'unknown' signature format in gpgsig");
+
+	/* Read signature data */
+	read_next_command();
+	parse_data(&sig->data, 0, NULL);
+}
+
+static void add_gpgsig_to_commit(struct strbuf *commit_data,
+				 const char *header,
+				 struct signature_data *sig)
+{
+	struct string_list siglines = STRING_LIST_INIT_NODUP;
+
+	if (!sig->hash_algo)
+		return;
+
+	strbuf_addstr(commit_data, header);
+	string_list_split_in_place(&siglines, sig->data.buf, "\n", -1);
+	strbuf_add_separated_string_list(commit_data, "\n ", &siglines);
+	strbuf_addch(commit_data, '\n');
+	string_list_clear(&siglines, 1);
+	strbuf_release(&sig->data);
+	free(sig->hash_algo);
+}
+
+static void store_signature(struct signature_data *stored_sig,
+			    struct signature_data *new_sig,
+			    const char *hash_type)
+{
+	if (stored_sig->hash_algo) {
+		warning("Multiple %s signatures found, ignoring additional signature",
+			hash_type);
+		strbuf_release(&new_sig->data);
+		free(new_sig->hash_algo);
+	} else {
+		*stored_sig = *new_sig;
+	}
+}
+
 static void parse_new_commit(const char *arg)
 {
-	static struct strbuf sig = STRBUF_INIT;
 	static struct strbuf msg = STRBUF_INIT;
-	struct string_list siglines = STRING_LIST_INIT_NODUP;
+	struct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };
+	struct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };
 	struct branch *b;
 	char *author = NULL;
 	char *committer = NULL;
-	char *sig_alg = NULL;
 	char *encoding = NULL;
 	struct hash_list *merge_list = NULL;
 	unsigned int merge_count;
@@ -2750,13 +2822,23 @@ static void parse_new_commit(const char *arg)
 	}
 	if (!committer)
 		die("Expected committer but didn't get one");
-	if (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
-		sig_alg = xstrdup(v);
-		read_next_command();
-		parse_data(&sig, 0, NULL);
+
+	/* Process signatures (up to 2: one "sha1" and one "sha256") */
+	while (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
+		struct signature_data sig = { NULL, NULL, STRBUF_INIT };
+
+		parse_one_signature(&sig, v);
+
+		if (!strcmp(sig.hash_algo, "sha1"))
+			store_signature(&sig_sha1, &sig, "SHA-1");
+		else if (!strcmp(sig.hash_algo, "sha256"))
+			store_signature(&sig_sha256, &sig, "SHA-256");
+		else
+			BUG("parse_one_signature() returned unknown hash algo");
+
 		read_next_command();
-	} else
-		strbuf_setlen(&sig, 0);
+	}
+
 	if (skip_prefix(command_buf.buf, "encoding ", &v)) {
 		encoding = xstrdup(v);
 		read_next_command();
@@ -2830,23 +2912,14 @@ static void parse_new_commit(const char *arg)
 		strbuf_addf(&new_data,
 			"encoding %s\n",
 			encoding);
-	if (sig_alg) {
-		if (!strcmp(sig_alg, "sha1"))
-			strbuf_addstr(&new_data, "gpgsig ");
-		else if (!strcmp(sig_alg, "sha256"))
-			strbuf_addstr(&new_data, "gpgsig-sha256 ");
-		else
-			die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
-		string_list_split_in_place(&siglines, sig.buf, "\n", -1);
-		strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
-		strbuf_addch(&new_data, '\n');
-	}
+
+	add_gpgsig_to_commit(&new_data, "gpgsig ", &sig_sha1);
+	add_gpgsig_to_commit(&new_data, "gpgsig-sha256 ", &sig_sha256);
+
 	strbuf_addch(&new_data, '\n');
 	strbuf_addbuf(&new_data, &msg);
-	string_list_clear(&siglines, 1);
 	free(author);
 	free(committer);
-	free(sig_alg);
 	free(encoding);
 
 	if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))
diff --git a/gpg-interface.c b/gpg-interface.c
index 0896458de5..6f2d87475f 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)
 	return NULL;
 }
 
+const char *get_signature_format(const char *buf)
+{
+	struct gpg_format *format = get_format_by_sig(buf);
+	return format ? format->name : "unknown";
+}
+
+int valid_signature_format(const char *format)
+{
+       return (!!get_format_by_name(format) ||
+	       !strcmp(format, "unknown"));
+}
+
 void signature_check_clear(struct signature_check *sigc)
 {
 	FREE_AND_NULL(sigc->payload);
diff --git a/gpg-interface.h b/gpg-interface.h
index e09f12e8d0..60ddf8bbfa 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -47,6 +47,18 @@ struct signature_check {
 
 void signature_check_clear(struct signature_check *sigc);
 
+/*
+ * Return the format of the signature (like "openpgp", "x509", "ssh"
+ * or "unknown").
+ */
+const char *get_signature_format(const char *buf);
+
+/*
+ * Is the signature format valid (like "openpgp", "x509", "ssh" or
+ * "unknown")
+ */
+int valid_signature_format(const char *format);
+
 /*
  * Look at a GPG signed tag object.  If such a signature exists, store it in
  * signature and the signed content in payload.  Return 1 if a signature was
diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
index 76619765fc..74dd23cb77 100755
--- a/t/t9350-fast-export.sh
+++ b/t/t9350-fast-export.sh
@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '
 test_expect_success GPG 'signed-commits=verbatim' '
 
 	git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
-	grep "^gpgsig sha" output &&
+	test_grep -E "^gpgsig sha(1|256) openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	(
 		cd new &&
@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '
 test_expect_success GPG 'signed-commits=warn-verbatim' '
 
 	git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
-	grep "^gpgsig sha" output &&
+	test_grep -E "^gpgsig sha(1|256) openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	test -s err &&
 	(
@@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '
 
 '
 
+test_expect_success GPGSM 'setup X.509 signed commit' '
+
+	git checkout -b x509-signing main &&
+	test_config gpg.format x509 &&
+	test_config user.signingkey $GIT_COMMITTER_EMAIL &&
+	echo "X.509 content" >file &&
+	git add file &&
+	git commit -S -m "X.509 signed commit" &&
+	X509_COMMIT=$(git rev-parse HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSM 'round-trip X.509 signed commit' '
+
+	git fast-export --signed-commits=verbatim x509-signing >output &&
+	grep "^gpgsig sha1 x509" output &&
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/x509-signing >actual &&
+		grep "^gpgsig " actual &&
+		IMPORTED=$(git rev-parse refs/heads/x509-signing) &&
+		test $X509_COMMIT = $IMPORTED
+	) <output
+
+'
+
+test_expect_success GPGSSH 'setup SSH signed commit' '
+
+	git checkout -b ssh-signing main &&
+	test_config gpg.format ssh &&
+	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
+	echo "SSH content" >file &&
+	git add file &&
+	git commit -S -m "SSH signed commit" &&
+	SSH_COMMIT=$(git rev-parse HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSSH 'round-trip SSH signed commit' '
+
+	git fast-export --signed-commits=verbatim ssh-signing >output &&
+	grep "^gpgsig sha1 ssh" output &&
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/ssh-signing >actual &&
+		grep "^gpgsig " actual &&
+		IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&
+		test $SSH_COMMIT = $IMPORTED
+	) <output
+
+'
+
 test_expect_success 'setup submodule' '
 
 	test_config_global protocol.file.allow always &&
-- 
2.50.0.2.g875523421d


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-18 15:18   ` [PATCH v3] fast-(import|export): improve on commit signature output format Christian Couder
@ 2025-06-19 13:36     ` Christian Couder
  2025-06-19 14:55       ` Junio C Hamano
                         ` (3 more replies)
  0 siblings, 4 replies; 65+ messages in thread
From: Christian Couder @ 2025-06-19 13:36 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
signed-commits, 2025-03-10), added support for signed commits to
fast-export and fast-import.

When a signed commit is processed, fast-export can output either
"gpgsig sha1" or "gpgsig sha256" depending on whether the signed
commit uses the SHA-1 or SHA-256 Git object format.

However, this implementation has a number of limitations:

  - the output format was not properly described in the documentation,
  - the output format is not very informative as it doesn't even say
    if the signature is an OpenPGP, an SSH, or an X509 signature,
  - the implementation doesn't support having both one signature on
    the SHA-1 object and one on the SHA-256 object.

Let's improve on these limitations by improving fast-export and
fast-import so that:

  - both one signature on the SHA-1 object and one on the SHA-256
    object can be exported and imported,
  - if there is more than one signature on the SHA-1 object or on
    the SHA-256 object, a warning is emitted,
  - the output format is "gpgsig <git-hash-algo> <signature-format>",
    where <git-hash-algo> is the Git object format as before, and
    <signature-format> is the signature type ("openpgp", "x509",
    "ssh" or "unknown",
  - the output is properly documented.

Note that it could be even better to be able to export and import
more than one signature on the SHA-1 object and on the SHA-256
object, but other parts of Git don't handle that well for now, so
this is left for future improvements.

Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---

This v4 is just about fixing a few bugs in the tests using the SHA-256
object format compared to the v3. (I had issues with CI tests on v3,
so I sent it without waiting for the results.)

The v1 of this patch series simply replaced the generic "sha1" or
"sha256" in the 'gpgsig' command that `git fast-export` produces and
`git fast-import` consumes with the signature format (e.g., openpgp,
ssh).

In the v2, a different approach was tried in the direction of possibly
making `git fast-export` check signatures to be able to get more
information from them.

In this v4 and in v3, the approach is more similar to v1, but takes
into account what was learned from the previous approaches. The
signature format (e.g., openpgp, ssh) is added to the 'gpgsig'
command, while both a SHA-1 and a SHA-256 signature from the same
commit can be processed and a warning are emitted for each additional
signature with the same hash type.

There are no tests in this v4 and in v3 with both a SHA-1 and a
SHA-256 signature on the same commit though, as I am not sure yet how
to best generate a commit with such signatures. Suggestions welcome!

Thanks to brian, Elijah and Junio who commented on the v1 and v2.

Range-diff with v3:

1:  d28cf238fb ! 1:  b3d8b13c0e fast-(import|export): improve on commit signature output format
    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '
     +test_expect_success GPGSM 'round-trip X.509 signed commit' '
     +
     +  git fast-export --signed-commits=verbatim x509-signing >output &&
    -+  grep "^gpgsig sha1 x509" output &&
    ++  test_grep -E "^gpgsig sha(1|256) x509" output &&
     +  (
     +          cd new &&
     +          git fast-import &&
     +          git cat-file commit refs/heads/x509-signing >actual &&
    -+          grep "^gpgsig " actual &&
    ++          grep "^gpgsig" actual &&
     +          IMPORTED=$(git rev-parse refs/heads/x509-signing) &&
     +          test $X509_COMMIT = $IMPORTED
     +  ) <output
    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '
     +test_expect_success GPGSSH 'round-trip SSH signed commit' '
     +
     +  git fast-export --signed-commits=verbatim ssh-signing >output &&
    -+  grep "^gpgsig sha1 ssh" output &&
    ++  test_grep -E "^gpgsig sha(1|256) ssh" output &&
     +  (
     +          cd new &&
     +          git fast-import &&
     +          git cat-file commit refs/heads/ssh-signing >actual &&
    -+          grep "^gpgsig " actual &&
    ++          grep "^gpgsig" actual &&
     +          IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&
     +          test $SSH_COMMIT = $IMPORTED
     +  ) <output


CI tests:

They have all passed:

https://github.com/chriscool/git/actions/runs/15758360209


 Documentation/git-fast-export.adoc |  17 +++++
 Documentation/git-fast-import.adoc |  31 ++++++--
 builtin/fast-export.c              |  67 ++++++++++++++---
 builtin/fast-import.c              | 117 +++++++++++++++++++++++------
 gpg-interface.c                    |  12 +++
 gpg-interface.h                    |  12 +++
 t/t9350-fast-export.sh             |  60 ++++++++++++++-
 7 files changed, 274 insertions(+), 42 deletions(-)

diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
index 43bbb4f63c..64198f2186 100644
--- a/Documentation/git-fast-export.adoc
+++ b/Documentation/git-fast-export.adoc
@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.
 	is the same as how earlier versions of this command without
 	this option behaved.
 +
+When exported, a signature starts with:
++
+gpgsig <git-hash-algo> <signature-format>
++
+where <git-hash-algo> is the Git object hash so either "sha1" or
+"sha256", and <signature-format> is the signature type, so "openpgp",
+"x509", "ssh" or "unknown".
++
+For example, an OpenPGP signature on a SHA-1 commit starts with
+`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit
+starts with `gpgsig sha256 ssh`.
++
+Currently for a given commit, at most one signature for the SHA-1
+object and one signature for the SHA-256 object are exported, each
+with their respective <git-hash-algo> identifier. A warning is
+emitted for each additional signature found.
++
 NOTE: This is highly experimental and the format of the data stream may
 change in the future without compatibility guarantees.
 
diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
index 250d866652..db5e5c8da5 100644
--- a/Documentation/git-fast-import.adoc
+++ b/Documentation/git-fast-import.adoc
@@ -445,7 +445,7 @@ one).
 	original-oid?
 	('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
 	'committer' (SP <name>)? SP LT <email> GT SP <when> LF
-	('gpgsig' SP <alg> LF data)?
+	('gpgsig' SP <algo> SP <format> LF data)?
 	('encoding' SP <encoding> LF)?
 	data
 	('from' SP <commit-ish> LF)?
@@ -518,13 +518,32 @@ their syntax.
 ^^^^^^^^
 
 The optional `gpgsig` command is used to include a PGP/GPG signature
-that signs the commit data.
+or other cryptographic signature that signs the commit data.
 
-Here <alg> specifies which hashing algorithm is used for this
-signature, either `sha1` or `sha256`.
+....
+	'gpgsig' SP <git-hash-algo> SP <signature-format> LF
+	data
+....
+
+The `gpgsig` command takes two arguments:
+
+* `<git-hash-algo>` specifies which Git object format this signature
+  applies to, either `sha1` or `sha256`.
+
+* `<signature-format>` specifies the type of signature, such as
+  `openpgp`, `x509`, `ssh`, or `unknown`.
+
+A commit may have at most one signature for the SHA-1 object format
+(stored in the "gpgsig" header) and one for the SHA-256 object format
+(stored in the "gpgsig-sha256" header).
+
+See below for a detailed description of the `data` command which
+contains the raw signature data.
+
+Signatures are not yet checked in the current implementation though.
 
-NOTE: This is highly experimental and the format of the data stream may
-change in the future without compatibility guarantees.
+NOTE: This is highly experimental and the format of the `gpgsig`
+command may change in the future without compatibility guarantees.
 
 `encoding`
 ^^^^^^^^^^
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index fcf6b00d5f..332c036ee4 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -29,6 +29,7 @@
 #include "quote.h"
 #include "remote.h"
 #include "blob.h"
+#include "gpg-interface.h"
 
 static const char *const fast_export_usage[] = {
 	N_("git fast-export [<rev-list-opts>]"),
@@ -652,6 +653,30 @@ static const char *find_commit_multiline_header(const char *msg,
 	return strbuf_detach(&val, NULL);
 }
 
+static void print_signature(const char *signature, const char *object_hash)
+{
+	if (!signature)
+		return;
+
+	printf("gpgsig %s %s\ndata %u\n%s",
+	       object_hash,
+	       get_signature_format(signature),
+	       (unsigned)strlen(signature),
+	       signature);
+}
+
+static void warn_on_extra_sig(const char **pos, struct commit *commit, int is_sha1)
+{
+	const char *header = is_sha1 ? "gpgsig" : "gpgsig-sha256";
+	const char *extra_sig = find_commit_multiline_header(*pos + 1, header, pos);
+	if (extra_sig) {
+		const char *hash = is_sha1 ? "SHA-1" : "SHA-256";
+		warning("more than one %s signature found on commit %s, using only the first one",
+			hash, oid_to_hex(&commit->object.oid));
+		free((char *)extra_sig);
+	}
+}
+
 static void handle_commit(struct commit *commit, struct rev_info *rev,
 			  struct string_list *paths_of_changed_objects)
 {
@@ -660,7 +685,8 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	const char *author, *author_end, *committer, *committer_end;
 	const char *encoding = NULL;
 	size_t encoding_len;
-	const char *signature_alg = NULL, *signature = NULL;
+	const char *sig_sha1 = NULL;
+	const char *sig_sha256 = NULL;
 	const char *message;
 	char *reencoded = NULL;
 	struct commit_list *p;
@@ -700,10 +726,28 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	}
 
 	if (*commit_buffer_cursor == '\n') {
-		if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
-			signature_alg = "sha1";
-		else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
-			signature_alg = "sha256";
+		const char *sig_cursor = commit_buffer_cursor;
+		const char *after_sha1 = commit_buffer_cursor;
+		const char *after_sha256 = commit_buffer_cursor;
+
+		/*
+		 * Find the first signature for each hash algorithm.
+		 * The searches must start from the same position.
+		 */
+		sig_sha1 = find_commit_multiline_header(sig_cursor + 1,
+							"gpgsig",
+							&after_sha1);
+		sig_sha256 = find_commit_multiline_header(sig_cursor + 1,
+							  "gpgsig-sha256",
+							  &after_sha256);
+
+		/* Warn on any additional signatures, as they will be ignored. */
+		if (sig_sha1)
+			warn_on_extra_sig(&after_sha1, commit, 1);
+		if (sig_sha256)
+			warn_on_extra_sig(&after_sha256, commit, 0);
+
+		commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
 	}
 
 	message = strstr(commit_buffer_cursor, "\n\n");
@@ -769,7 +813,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	printf("%.*s\n%.*s\n",
 	       (int)(author_end - author), author,
 	       (int)(committer_end - committer), committer);
-	if (signature) {
+	if (sig_sha1 || sig_sha256) {
 		switch (signed_commit_mode) {
 		case SIGN_ABORT:
 			die("encountered signed commit %s; use "
@@ -780,19 +824,18 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 				oid_to_hex(&commit->object.oid));
 			/* fallthru */
 		case SIGN_VERBATIM:
-			printf("gpgsig %s\ndata %u\n%s",
-			       signature_alg,
-			       (unsigned)strlen(signature),
-			       signature);
+			print_signature(sig_sha1, "sha1");
+			print_signature(sig_sha256, "sha256");
 			break;
 		case SIGN_WARN_STRIP:
-			warning("stripping signature from commit %s",
+			warning("stripping signature(s) from commit %s",
 				oid_to_hex(&commit->object.oid));
 			/* fallthru */
 		case SIGN_STRIP:
 			break;
 		}
-		free((char *)signature);
+		free((char *)sig_sha1);
+		free((char *)sig_sha256);
 	}
 	if (!reencoded && encoding)
 		printf("encoding %.*s\n", (int)encoding_len, encoding);
diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index b2839c5f43..48ce8ebb77 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -29,6 +29,7 @@
 #include "commit-reach.h"
 #include "khash.h"
 #include "date.h"
+#include "gpg-interface.h"
 
 #define PACK_ID_BITS 16
 #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
@@ -2718,15 +2719,86 @@ static struct hash_list *parse_merge(unsigned int *count)
 	return list;
 }
 
+struct signature_data {
+	char *hash_algo;      /* "sha1" or "sha256" */
+	char *sig_format;     /* "openpgp", "x509", "ssh", "unknown" */
+	struct strbuf data;   /* The actual signature data */
+};
+
+static void parse_one_signature(struct signature_data *sig, const char *v)
+{
+	char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */
+	char *space = strchr(args, ' ');
+
+	if (!space)
+		die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
+		    "got 'gpgsig %s'", args);
+	*space++ = '\0';
+
+	sig->hash_algo = args;
+	sig->sig_format = space;
+
+	/* Remove any trailing newline from format */
+	space = strchr(sig->sig_format, '\n');
+	if (space)
+		*space = '\0';
+
+	/* Validate hash algorithm */
+	if (strcmp(sig->hash_algo, "sha1") &&
+	    strcmp(sig->hash_algo, "sha256"))
+		die("Unknown git hash algorithm in gpgsig: '%s'", sig->hash_algo);
+
+	/* Validate signature format */
+	if (!valid_signature_format(sig->sig_format))
+		die("Invalid signature format in gpgsig: '%s'", sig->sig_format);
+	if (!strcmp(sig->sig_format, "unknown"))
+		warning("'unknown' signature format in gpgsig");
+
+	/* Read signature data */
+	read_next_command();
+	parse_data(&sig->data, 0, NULL);
+}
+
+static void add_gpgsig_to_commit(struct strbuf *commit_data,
+				 const char *header,
+				 struct signature_data *sig)
+{
+	struct string_list siglines = STRING_LIST_INIT_NODUP;
+
+	if (!sig->hash_algo)
+		return;
+
+	strbuf_addstr(commit_data, header);
+	string_list_split_in_place(&siglines, sig->data.buf, "\n", -1);
+	strbuf_add_separated_string_list(commit_data, "\n ", &siglines);
+	strbuf_addch(commit_data, '\n');
+	string_list_clear(&siglines, 1);
+	strbuf_release(&sig->data);
+	free(sig->hash_algo);
+}
+
+static void store_signature(struct signature_data *stored_sig,
+			    struct signature_data *new_sig,
+			    const char *hash_type)
+{
+	if (stored_sig->hash_algo) {
+		warning("Multiple %s signatures found, ignoring additional signature",
+			hash_type);
+		strbuf_release(&new_sig->data);
+		free(new_sig->hash_algo);
+	} else {
+		*stored_sig = *new_sig;
+	}
+}
+
 static void parse_new_commit(const char *arg)
 {
-	static struct strbuf sig = STRBUF_INIT;
 	static struct strbuf msg = STRBUF_INIT;
-	struct string_list siglines = STRING_LIST_INIT_NODUP;
+	struct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };
+	struct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };
 	struct branch *b;
 	char *author = NULL;
 	char *committer = NULL;
-	char *sig_alg = NULL;
 	char *encoding = NULL;
 	struct hash_list *merge_list = NULL;
 	unsigned int merge_count;
@@ -2750,13 +2822,23 @@ static void parse_new_commit(const char *arg)
 	}
 	if (!committer)
 		die("Expected committer but didn't get one");
-	if (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
-		sig_alg = xstrdup(v);
-		read_next_command();
-		parse_data(&sig, 0, NULL);
+
+	/* Process signatures (up to 2: one "sha1" and one "sha256") */
+	while (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
+		struct signature_data sig = { NULL, NULL, STRBUF_INIT };
+
+		parse_one_signature(&sig, v);
+
+		if (!strcmp(sig.hash_algo, "sha1"))
+			store_signature(&sig_sha1, &sig, "SHA-1");
+		else if (!strcmp(sig.hash_algo, "sha256"))
+			store_signature(&sig_sha256, &sig, "SHA-256");
+		else
+			BUG("parse_one_signature() returned unknown hash algo");
+
 		read_next_command();
-	} else
-		strbuf_setlen(&sig, 0);
+	}
+
 	if (skip_prefix(command_buf.buf, "encoding ", &v)) {
 		encoding = xstrdup(v);
 		read_next_command();
@@ -2830,23 +2912,14 @@ static void parse_new_commit(const char *arg)
 		strbuf_addf(&new_data,
 			"encoding %s\n",
 			encoding);
-	if (sig_alg) {
-		if (!strcmp(sig_alg, "sha1"))
-			strbuf_addstr(&new_data, "gpgsig ");
-		else if (!strcmp(sig_alg, "sha256"))
-			strbuf_addstr(&new_data, "gpgsig-sha256 ");
-		else
-			die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
-		string_list_split_in_place(&siglines, sig.buf, "\n", -1);
-		strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
-		strbuf_addch(&new_data, '\n');
-	}
+
+	add_gpgsig_to_commit(&new_data, "gpgsig ", &sig_sha1);
+	add_gpgsig_to_commit(&new_data, "gpgsig-sha256 ", &sig_sha256);
+
 	strbuf_addch(&new_data, '\n');
 	strbuf_addbuf(&new_data, &msg);
-	string_list_clear(&siglines, 1);
 	free(author);
 	free(committer);
-	free(sig_alg);
 	free(encoding);
 
 	if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))
diff --git a/gpg-interface.c b/gpg-interface.c
index 0896458de5..6f2d87475f 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)
 	return NULL;
 }
 
+const char *get_signature_format(const char *buf)
+{
+	struct gpg_format *format = get_format_by_sig(buf);
+	return format ? format->name : "unknown";
+}
+
+int valid_signature_format(const char *format)
+{
+       return (!!get_format_by_name(format) ||
+	       !strcmp(format, "unknown"));
+}
+
 void signature_check_clear(struct signature_check *sigc)
 {
 	FREE_AND_NULL(sigc->payload);
diff --git a/gpg-interface.h b/gpg-interface.h
index e09f12e8d0..60ddf8bbfa 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -47,6 +47,18 @@ struct signature_check {
 
 void signature_check_clear(struct signature_check *sigc);
 
+/*
+ * Return the format of the signature (like "openpgp", "x509", "ssh"
+ * or "unknown").
+ */
+const char *get_signature_format(const char *buf);
+
+/*
+ * Is the signature format valid (like "openpgp", "x509", "ssh" or
+ * "unknown")
+ */
+int valid_signature_format(const char *format);
+
 /*
  * Look at a GPG signed tag object.  If such a signature exists, store it in
  * signature and the signed content in payload.  Return 1 if a signature was
diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
index 76619765fc..3559a33c72 100755
--- a/t/t9350-fast-export.sh
+++ b/t/t9350-fast-export.sh
@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '
 test_expect_success GPG 'signed-commits=verbatim' '
 
 	git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
-	grep "^gpgsig sha" output &&
+	test_grep -E "^gpgsig sha(1|256) openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	(
 		cd new &&
@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '
 test_expect_success GPG 'signed-commits=warn-verbatim' '
 
 	git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
-	grep "^gpgsig sha" output &&
+	test_grep -E "^gpgsig sha(1|256) openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	test -s err &&
 	(
@@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '
 
 '
 
+test_expect_success GPGSM 'setup X.509 signed commit' '
+
+	git checkout -b x509-signing main &&
+	test_config gpg.format x509 &&
+	test_config user.signingkey $GIT_COMMITTER_EMAIL &&
+	echo "X.509 content" >file &&
+	git add file &&
+	git commit -S -m "X.509 signed commit" &&
+	X509_COMMIT=$(git rev-parse HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSM 'round-trip X.509 signed commit' '
+
+	git fast-export --signed-commits=verbatim x509-signing >output &&
+	test_grep -E "^gpgsig sha(1|256) x509" output &&
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/x509-signing >actual &&
+		grep "^gpgsig" actual &&
+		IMPORTED=$(git rev-parse refs/heads/x509-signing) &&
+		test $X509_COMMIT = $IMPORTED
+	) <output
+
+'
+
+test_expect_success GPGSSH 'setup SSH signed commit' '
+
+	git checkout -b ssh-signing main &&
+	test_config gpg.format ssh &&
+	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
+	echo "SSH content" >file &&
+	git add file &&
+	git commit -S -m "SSH signed commit" &&
+	SSH_COMMIT=$(git rev-parse HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSSH 'round-trip SSH signed commit' '
+
+	git fast-export --signed-commits=verbatim ssh-signing >output &&
+	test_grep -E "^gpgsig sha(1|256) ssh" output &&
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/ssh-signing >actual &&
+		grep "^gpgsig" actual &&
+		IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&
+		test $SSH_COMMIT = $IMPORTED
+	) <output
+
+'
+
 test_expect_success 'setup submodule' '
 
 	test_config_global protocol.file.allow always &&
-- 
2.50.0.2.g875523421d


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* Re: [PATCH v2 0/6] extract algo information from signatures
  2025-06-02 22:17   ` brian m. carlson
@ 2025-06-19 13:37     ` Christian Couder
  0 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-06-19 13:37 UTC (permalink / raw)
  To: brian m. carlson, Christian Couder, git, Junio C Hamano,
	Patrick Steinhardt, Elijah Newren, Jeff King, Johannes Schindelin

On Tue, Jun 3, 2025 at 12:17 AM brian m. carlson
<sandals@crustytoothpaste.net> wrote:
>
> On 2025-05-26 at 10:33:08, Christian Couder wrote:

> > brian m. carlson however replied that it would be better to show two
> > pieces of information instead of one: one for the hash algorithm and
> > one for the protocol.
>
> Actually, what I was saying is that we should have one for the hash
> algorithm that is used in the Git object.  I don't care about the hash
> algorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's
> signed with SHA-512 or SHA-256), but we can have multiple signatures in
> a single commit such that there's both a SHA-1 signature and a SHA-256
> signature.

Thanks for clarifying. I think that's what I implemented in v3 and v4.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v2 0/6] extract algo information from signatures
  2025-05-26 16:03   ` [PATCH v2 0/6] extract algo information from signatures Elijah Newren
@ 2025-06-19 13:38     ` Christian Couder
  0 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-06-19 13:38 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin

On Mon, May 26, 2025 at 6:03 PM Elijah Newren <newren@gmail.com> wrote:
>
> On Mon, May 26, 2025 at 3:33 AM Christian Couder
> <christian.couder@gmail.com> wrote:

> I'd like to propose that the following are the possible uses that
> users might have regarding commit signatures with
> fast-export/fast-import (if anyone has additional usecases, let me
> know):
>
> (A) Make fast-export include signatures, and make fast-import include
> them unconditionally (even if invalid)
> (B) Similar to (A), but make *fast-import* check them and either error
> out or drop them if they become invalid
> (C) Simliar to (B), but make *fast-import* re-sign the commit if they
> become invalid
> (D) Similar to (A), but make *fast-import* re-sign the commit even if
> the signature would have been valid
>
> Note that in the above, there might be additional processing between
> when fast-export runs and when fast-import does (e.g. by filter-repo
> or a similar tool, or even the user editing by hand).

I agree that they are likely to be the most important use cases, and I
am fine with working on these use cases.

> > To address this, I decided to focus first on extracting the hash
> > algorithm from OpenPGP/X.509 signatures and the key type from SSH
> > signature when checking signatures.
> >
> > To test that, I thought that it could be interesting to add a
> > `--summary` option to `verify-commit` that shows a concise, one-line
> > summary of the signature verification to standard output in the
> > `STATUS FORMAT ALGORITHM` format, where:
> >
> > * STATUS is the result character (e.g., G, B, E, U, N, ...), similar
> >   as what the "%G?" pretty format specifier shows,
> >
> > * FORMAT is the signature format (`openpgp`, `x509`, or `ssh`),
> >
> > * ALGORITHM is the hash algorithm used for GPG/GPGSM signatures
> >   (e.g. `sha1`, `sha256`, ...), or the key type for SSH signatures
> >   (`RSA`, `ECDSA`, `ED25519`, ...).
>
> This sounds like it might be a nice feature extension to the
> verify-commit builtin.  I don't see how it helps implement signature
> handling in fast-export/fast-import, though.

Fair enough. In the v3 and v4, I changed the approach and dropped all of this.

> > If we can agree on a concise format output for signature checks, then
> > maybe this format will be a good format to be used in the `git
> > fast-export` output for users who are fine with signatures being
> > checked.
> >
> > What do you think?
>
> Maybe I'm missing something, but it seems to me that checking
> signatures *in fast-export* would be a complete waste of time.  For
> usecases (A) & (D), checking signatures at all is a waste of time.
> For usecases (B) & (C), checking signatures in fast-export is
> throwaway work because whether or not the signatures are valid at the
> time fast-export runs, and even in the rare usecase where there is no
> additional processing between fast-export and fast-import (such as by
> filter-repo), the signatures would still need to be re-checked by
> fast-import anyway.  (Note that a simple `git fast-export ... | git
> fast-import` is *not* guaranteed to get the same commit hashes even
> when there are no commit signatures; that only happens when the
> history is sufficiently canonical).

Yeah, right. In v3 and v4, I dropped this in favor of something
simpler similar to what was in the v1 patch, and after that I plan to
work on checking signatures in fast-import soon. Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-19 13:36     ` [PATCH v4] " Christian Couder
@ 2025-06-19 14:55       ` Junio C Hamano
  2025-07-08  9:16         ` Christian Couder
  2025-06-19 21:44       ` Elijah Newren
                         ` (2 subsequent siblings)
  3 siblings, 1 reply; 65+ messages in thread
From: Junio C Hamano @ 2025-06-19 14:55 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> This v4 is just about fixing a few bugs in the tests using the SHA-256
> object format compared to the v3. (I had issues with CI tests on v3,
> so I sent it without waiting for the results.)

Thanks.

I am not sure if "I am happy is either 1 or 256" is what you really
want, though.  The test presumably knows what algorithm is being
used during its run, so wouldn't you want to say more like "I know I
used sha256, and I expect seeing sha256, ah, I see sha256 and even
better I see no sha1, so I am very happy"?

> There are no tests in this v4 and in v3 with both a SHA-1 and a
> SHA-256 signature on the same commit though, as I am not sure yet how
> to best generate a commit with such signatures. Suggestions welcome!

Good point to fill potential gaps.  If we had such a commit, then
would these tests say "I know I want both 1 and 256, and I do see
one instance each of 1 and 256, so I am happy"?


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-19 13:36     ` [PATCH v4] " Christian Couder
  2025-06-19 14:55       ` Junio C Hamano
@ 2025-06-19 21:44       ` Elijah Newren
  2025-06-20 16:12         ` Christian Couder
  2025-07-07 22:58       ` Junio C Hamano
  2025-07-08  9:17       ` [PATCH v5] " Christian Couder
  3 siblings, 1 reply; 65+ messages in thread
From: Elijah Newren @ 2025-06-19 21:44 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Thu, Jun 19, 2025 at 6:36 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
> signed-commits, 2025-03-10), added support for signed commits to
> fast-export and fast-import.
>
> When a signed commit is processed, fast-export can output either
> "gpgsig sha1" or "gpgsig sha256" depending on whether the signed
> commit uses the SHA-1 or SHA-256 Git object format.
>
> However, this implementation has a number of limitations:
>
>   - the output format was not properly described in the documentation,

Thanks for working on fixing this.

>   - the output format is not very informative as it doesn't even say
>     if the signature is an OpenPGP, an SSH, or an X509 signature,

Why would it need to say what type of signature it is?  Don't the
ascii armor lines have e.g. "----BEGIN PGP SIGNATURE----" and "----END
PGP SIGNATURE----" around it, which fast-import can read as well as
fast-export?  Is the idea that we strip those lines and now need to
replace the information we lost?

>   - the implementation doesn't support having both one signature on
>     the SHA-1 object and one on the SHA-256 object.

Not sure I understand this; more questions around this later.

> Let's improve on these limitations by improving fast-export and
> fast-import so that:
>
>   - both one signature on the SHA-1 object and one on the SHA-256
>     object can be exported and imported,
>   - if there is more than one signature on the SHA-1 object or on
>     the SHA-256 object, a warning is emitted,
>   - the output format is "gpgsig <git-hash-algo> <signature-format>",
>     where <git-hash-algo> is the Git object format as before, and
>     <signature-format> is the signature type ("openpgp", "x509",
>     "ssh" or "unknown",
>   - the output is properly documented.

Perhaps this was discussed in an earlier round, but if so I either
forgot or missed it.  What value does <git-hash-algo> and
<signature-format> provide?  How are they intended to be used?

Is the <signature-format> merely self-inflicted pain from stripping
the ascii armor lines?  If so, would it make more sense to just
include those armor lines as-is in the fast-export stream and let
fast-import process it?  Then we wouldn't have to introduce all the
outputting and parsing of this new <signature-format> field and worry
about the new special "unknown" status.

Is the <git-hash-algo> due to the fact that we have separate `gpgsig`
and `gpgsig-sha256` commit headers and we want to use that information
to avoid writing these headers to the wrong-sized objects (and/or to
avoid checking whether the signature is valid on the wrong-sized
objects)?  If so, could that be spelled out in the docs as well,
especially since it appears that the intent of these headers is left
unimplemented due to not changing fast-import to do anything with
them?

And if <git-hash-algo>'s purpose is to ensure they are only used when
writing same-sized object as what was exported, then...isn't that a
bug?  This series was started because people wanted to be able to do
things like keeping signature even when they are no longer valid or
resigning commits that have a no longer commit signature (among other
uses), but that would mean that if someone exports a sha1 repository
and imports it as sha256, we don't want to ignore the fact that the
sha1 commit was signed for those usecases.

If, however, the <git-hash-algo>'s purpose is merely as a performance
optimization that fast-import can employ in the cases where it checks
for signatures being valid, so it can avoid checking when it know the
hash size isn't even the same, then it could make sense.

But, short of that performance optimization, it's unclear to me
whether we'd lose anything by simply exporting "gpgsig <contents of
signature header from original object, including the armor lines>",
and dropping the <git-hash-algo> and <signature-format> lines
entirely.  Am I missing something?  (I may well be; I don't know much
about signing stuff beyond the very basics, and don't mess with signed
commits or tags much myself.)

[...]
> There are no tests in this v4 and in v3 with both a SHA-1 and a
> SHA-256 signature on the same commit though, as I am not sure yet how
> to best generate a commit with such signatures. Suggestions welcome!

If no suggestions are forthcoming, it feels odd to implement this with
no tests.  Would it make sense to leave it out until we know how to
test it?  (More questions on this below...)

[...]
> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
> index 43bbb4f63c..64198f2186 100644
> --- a/Documentation/git-fast-export.adoc
> +++ b/Documentation/git-fast-export.adoc
> @@ -50,6 +50,23 @@ resulting tag will have an invalid signature.
>         is the same as how earlier versions of this command without
>         this option behaved.
>  +
> +When exported, a signature starts with:
> ++
> +gpgsig <git-hash-algo> <signature-format>
> ++
> +where <git-hash-algo> is the Git object hash so either "sha1" or
> +"sha256", and <signature-format> is the signature type, so "openpgp",
> +"x509", "ssh" or "unknown".
> ++
> +For example, an OpenPGP signature on a SHA-1 commit starts with
> +`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit
> +starts with `gpgsig sha256 ssh`.

I had a number of comments/questions on this above already.

> ++
> +Currently for a given commit, at most one signature for the SHA-1
> +object and one signature for the SHA-256 object are exported, each
> +with their respective <git-hash-algo> identifier.

Wait..does this mean fast-export is obligated to walk over both all
sha1 commits and all "equivalent" sha256 commits when exporting a
repo?  I thought most operations on the repo would walk over only one
or the other; walking over both seems to be against the spirit of the
"fast" in "fast-export".  Am I missing something?  (Possibly related
question: Does "git log" bother walking over both, or does it only
walk over one?)  Even if this really is wanted by some users,
shouldn't they manually request it rather than making exports slow for
everyone else by default?

> +A warning is
> +emitted for each additional signature found.

Why?  This seems odd to me.  Why not merely export them all, and let
fast-import throw warnings or errors if it sees more than one and is
not yet prepared to handle multiple signatures?

> ++
>  NOTE: This is highly experimental and the format of the data stream may
>  change in the future without compatibility guarantees.
>
> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
> index 250d866652..db5e5c8da5 100644
> --- a/Documentation/git-fast-import.adoc
> +++ b/Documentation/git-fast-import.adoc
> @@ -445,7 +445,7 @@ one).
>         original-oid?
>         ('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
>         'committer' (SP <name>)? SP LT <email> GT SP <when> LF
> -       ('gpgsig' SP <alg> LF data)?
> +       ('gpgsig' SP <algo> SP <format> LF data)?
>         ('encoding' SP <encoding> LF)?
>         data
>         ('from' SP <commit-ish> LF)?
> @@ -518,13 +518,32 @@ their syntax.
>  ^^^^^^^^
>
>  The optional `gpgsig` command is used to include a PGP/GPG signature
> -that signs the commit data.
> +or other cryptographic signature that signs the commit data.

Good catch.

>
> -Here <alg> specifies which hashing algorithm is used for this
> -signature, either `sha1` or `sha256`.
> +....
> +       'gpgsig' SP <git-hash-algo> SP <signature-format> LF
> +       data
> +....
> +
> +The `gpgsig` command takes two arguments:
> +
> +* `<git-hash-algo>` specifies which Git object format this signature
> +  applies to, either `sha1` or `sha256`.
> +
> +* `<signature-format>` specifies the type of signature, such as
> +  `openpgp`, `x509`, `ssh`, or `unknown`.
> +
> +A commit may have at most one signature for the SHA-1 object format
> +(stored in the "gpgsig" header) and one for the SHA-256 object format
> +(stored in the "gpgsig-sha256" header).

Why?  Does this mean hg-fast-export or hg-fast-import (or a
jj-fast-export or jj-fast-import) wouldn't be allowed to specify
multiple signatures?  The fast-export and fast-import streams are
often used for interoperation with other VCSes, but as far as I can
tell, you're encoding a restriction on what's allowed that isn't an
actual problem for git but just a not-yet-implemented state.  If I've
understood correctly that the restriction is merely due to the current
implementation, perhaps the wording could be changed to not list it as
an encoding restriction, but as a current fast-import limitation?

Also, I'm slightly uncomfortable with "the SHA-1 object format" and
"the SHA-256 object format" because of the fact that these tools are
used for interoperability with similarly-named tools from other VCSes.
I think it'd be better to just treat them equally as "here was/were
some signature(s) on the object in the original repo; importers can
choose what to do with them".

> +
> +See below for a detailed description of the `data` command which
> +contains the raw signature data.
> +
> +Signatures are not yet checked in the current implementation though.

...but what does happen with those signatures?  Dropped?  Kept as-is?
Can we just spell this out a bit more clearly?  e.g.

"Signatures are not checked in the current implementation; they are
used as-is, which may mean the signatures are invalid in the imported
repository."

> -NOTE: This is highly experimental and the format of the data stream may
> -change in the future without compatibility guarantees.
> +NOTE: This is highly experimental and the format of the `gpgsig`
> +command may change in the future without compatibility guarantees.

Good clarification.


I briefly skimmed the implementation and test files, and didn't see
any problems...but I think it probably makes more sense to get aligned
on the goals of the format and how these fields are meant to be used
before diving into those details closer.

Thanks for working on this topic.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-19 21:44       ` Elijah Newren
@ 2025-06-20 16:12         ` Christian Couder
  2025-06-20 19:20           ` Junio C Hamano
  2025-06-26 19:11           ` Elijah Newren
  0 siblings, 2 replies; 65+ messages in thread
From: Christian Couder @ 2025-06-20 16:12 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Thu, Jun 19, 2025 at 11:44 PM Elijah Newren <newren@gmail.com> wrote:
>
> On Thu, Jun 19, 2025 at 6:36 AM Christian Couder
> <christian.couder@gmail.com> wrote:

> > However, this implementation has a number of limitations:
> >
> >   - the output format was not properly described in the documentation,
>
> Thanks for working on fixing this.
>
> >   - the output format is not very informative as it doesn't even say
> >     if the signature is an OpenPGP, an SSH, or an X509 signature,
>
> Why would it need to say what type of signature it is?  Don't the
> ascii armor lines have e.g. "----BEGIN PGP SIGNATURE----" and "----END
> PGP SIGNATURE----" around it, which fast-import can read as well as
> fast-export?  Is the idea that we strip those lines and now need to
> replace the information we lost?

In https://lore.kernel.org/git/aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net/
brian said:

"These should be separate fields: one for the hash algorithm and one for
the protocol.  Alternatively, we can just keep the hash algorithm field
and parse the protocol by reading the first line, which will differ for
different protocols."

It would have been nice if you had then said that you prefer not to
have the protocol.

My opinion was that it was better for tools processing fast-export
output to have the protocol as they have to parse the "gppsig ..."
line anyway. So it should be easier for them than to parse the ascii
armor lines.

> >   - the implementation doesn't support having both one signature on
> >     the SHA-1 object and one on the SHA-256 object.
>
> Not sure I understand this; more questions around this later.

In https://lore.kernel.org/git/aD4i7YhUnT5Kgew-@tapette.crustytoothpaste.net/
brian said:

"Actually, what I was saying is that we should have one for the hash
algorithm that is used in the Git object.  I don't care about the hash
algorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's
signed with SHA-512 or SHA-256), but we can have multiple signatures in
a single commit such that there's both a SHA-1 signature and a SHA-256
signature."

so I implemented the possibility that there's both a SHA-1 signature
and a SHA-256 signature in a single commit.

If you disagreed with what brian suggested, it would have been nice to
reply to brian then.

> > Let's improve on these limitations by improving fast-export and
> > fast-import so that:
> >
> >   - both one signature on the SHA-1 object and one on the SHA-256
> >     object can be exported and imported,
> >   - if there is more than one signature on the SHA-1 object or on
> >     the SHA-256 object, a warning is emitted,
> >   - the output format is "gpgsig <git-hash-algo> <signature-format>",
> >     where <git-hash-algo> is the Git object format as before, and
> >     <signature-format> is the signature type ("openpgp", "x509",
> >     "ssh" or "unknown",
> >   - the output is properly documented.
>
> Perhaps this was discussed in an earlier round, but if so I either
> forgot or missed it.  What value does <git-hash-algo> and
> <signature-format> provide?  How are they intended to be used?

My opinion on this is that in cases like this we might not always know
what could be useful for tools or users in general, so it might be
better to provide more information that can be easily discarded if not
useful rather than not enough information.

brian seemed to say that <git-hash-algo> and <signature-format> are
important so I just prefered to have both, especially as they are not
costly to get.

> Is the <signature-format> merely self-inflicted pain from stripping
> the ascii armor lines?  If so, would it make more sense to just
> include those armor lines as-is in the fast-export stream and let
> fast-import process it?

The ascii armor lines are kept in the fast-export stream, but
fast-export's ouput is supposed to be processed somehow sometimes
before being fed back to fast-import, so I think we should make it
easy for tools or people processing the output to get signature
information.

>  Then we wouldn't have to introduce all the
> outputting and parsing of this new <signature-format> field and worry
> about the new special "unknown" status.

I think it's a trade-off. If there is a consensus that it's very
unlikely to help anyone, I am fine with removing it.

> Is the <git-hash-algo> due to the fact that we have separate `gpgsig`
> and `gpgsig-sha256` commit headers and we want to use that information
> to avoid writing these headers to the wrong-sized objects (and/or to
> avoid checking whether the signature is valid on the wrong-sized
> objects)?

Yeah, I think it could help with that, but brian might better answer that.

> If so, could that be spelled out in the docs as well,
> especially since it appears that the intent of these headers is left
> unimplemented due to not changing fast-import to do anything with
> them?

fast-import puts back the `gpgsig` or `gpgsig-sha256` headers
depending on <git-hash-algo>, so it's useful at least for that. I will
improve the docs about it.

> And if <git-hash-algo>'s purpose is to ensure they are only used when
> writing same-sized object as what was exported, then...isn't that a
> bug?

I am not sure I understand what would be the bug.

> This series was started because people wanted to be able to do
> things like keeping signature even when they are no longer valid or
> resigning commits that have a no longer commit signature (among other
> uses), but that would mean that if someone exports a sha1 repository
> and imports it as sha256, we don't want to ignore the fact that the
> sha1 commit was signed for those usecases.
>
> If, however, the <git-hash-algo>'s purpose is merely as a performance
> optimization that fast-import can employ in the cases where it checks
> for signatures being valid, so it can avoid checking when it know the
> hash size isn't even the same, then it could make sense.

I think it could be used for that, but I'd like brian's opinion about this.

> But, short of that performance optimization, it's unclear to me
> whether we'd lose anything by simply exporting "gpgsig <contents of
> signature header from original object, including the armor lines>",
> and dropping the <git-hash-algo> and <signature-format> lines
> entirely.  Am I missing something?  (I may well be; I don't know much
> about signing stuff beyond the very basics, and don't mess with signed
> commits or tags much myself.)

I don't know much about signing either, that's why I have tried to
rely on brian's suggestion on this.

> [...]
> > There are no tests in this v4 and in v3 with both a SHA-1 and a
> > SHA-256 signature on the same commit though, as I am not sure yet how
> > to best generate a commit with such signatures. Suggestions welcome!
>
> If no suggestions are forthcoming, it feels odd to implement this with
> no tests.  Would it make sense to leave it out until we know how to
> test it?  (More questions on this below...)

I had hoped that implementing this and then asking how best to test
with both a SHA-1 and a SHA-256 signature on the same commit would be
better to move things forward.

> > +Currently for a given commit, at most one signature for the SHA-1
> > +object and one signature for the SHA-256 object are exported, each
> > +with their respective <git-hash-algo> identifier.
>
> Wait..does this mean fast-export is obligated to walk over both all
> sha1 commits and all "equivalent" sha256 commits when exporting a
> repo?  I thought most operations on the repo would walk over only one
> or the other; walking over both seems to be against the spirit of the
> "fast" in "fast-export".  Am I missing something?  (Possibly related
> question: Does "git log" bother walking over both, or does it only
> walk over one?)  Even if this really is wanted by some users,
> shouldn't they manually request it rather than making exports slow for
> everyone else by default?

No fast-export doesn't walk over both the sha1 commits and all
"equivalent" sha256 commits, it can just process at most 2 signatures
for a given commit, one with the `gpgsig` header and one with the
`gpgsig-sha256` header. I will try to reword the doc to make it
clearer.

> > +A warning is
> > +emitted for each additional signature found.
>
> Why?  This seems odd to me.  Why not merely export them all, and let
> fast-import throw warnings or errors if it sees more than one and is
> not yet prepared to handle multiple signatures?

I am not against implementing both export and import support for any
number of signatures, but it seems that Git in general doesn't support
more than one `gpgsig` and one `gpgsig-sha256` signature well, and
that's also what brian suggested supporting. It's also better than
just 1 signature max which is the current state.

> > -Here <alg> specifies which hashing algorithm is used for this
> > -signature, either `sha1` or `sha256`.
> > +....
> > +       'gpgsig' SP <git-hash-algo> SP <signature-format> LF
> > +       data
> > +....
> > +
> > +The `gpgsig` command takes two arguments:
> > +
> > +* `<git-hash-algo>` specifies which Git object format this signature
> > +  applies to, either `sha1` or `sha256`.
> > +
> > +* `<signature-format>` specifies the type of signature, such as
> > +  `openpgp`, `x509`, `ssh`, or `unknown`.
> > +
> > +A commit may have at most one signature for the SHA-1 object format
> > +(stored in the "gpgsig" header) and one for the SHA-256 object format
> > +(stored in the "gpgsig-sha256" header).
>
> Why?  Does this mean hg-fast-export or hg-fast-import (or a
> jj-fast-export or jj-fast-import) wouldn't be allowed to specify
> multiple signatures?  The fast-export and fast-import streams are
> often used for interoperation with other VCSes, but as far as I can
> tell, you're encoding a restriction on what's allowed that isn't an
> actual problem for git but just a not-yet-implemented state.

I would be fine with rewording this to say that the current
implementation allows at most one signature for the SHA-1 object
format and one for the SHA-256 object format to be imported. But as
far as I understand additional signatures on top of that might not be
very well supported by Git in general, so maybe I should mention that
too?

> If I've
> understood correctly that the restriction is merely due to the current
> implementation, perhaps the wording could be changed to not list it as
> an encoding restriction, but as a current fast-import limitation?

If that current fast-import limitation was lifted, we should probably
still say that additional signatures might not be very well supported
by Git in general.

> Also, I'm slightly uncomfortable with "the SHA-1 object format" and
> "the SHA-256 object format" because of the fact that these tools are
> used for interoperability with similarly-named tools from other VCSes.
> I think it'd be better to just treat them equally as "here was/were
> some signature(s) on the object in the original repo; importers can
> choose what to do with them".

I don't think giving that information prevents tools from other VCSes
from doing what they want with the signatures.

> > +See below for a detailed description of the `data` command which
> > +contains the raw signature data.
> > +
> > +Signatures are not yet checked in the current implementation though.
>
> ...but what does happen with those signatures?  Dropped?  Kept as-is?

This section (about the `gpgsig` command) starts with:

"The optional `gpgsig` command is used to include a PGP/GPG signature
that signs the commit data."

so yeah they are kept as-is.

> Can we just spell this out a bit more clearly?  e.g.
>
> "Signatures are not checked in the current implementation; they are
> used as-is, which may mean the signatures are invalid in the imported
> repository."

It feels a bit redundant but, yeah, the part about possibly invalid
signatures might clarify things, so I am fine with spelling it out
like this. Thanks.

[...]

> I briefly skimmed the implementation and test files, and didn't see
> any problems...but I think it probably makes more sense to get aligned
> on the goals of the format and how these fields are meant to be used
> before diving into those details closer.

Yeah, I would have been happy if we could have been aligned with the
goals of the format and the fields earlier, but better late than
never.

> Thanks for working on this topic.

Thanks for reviewing.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-20 16:12         ` Christian Couder
@ 2025-06-20 19:20           ` Junio C Hamano
  2025-07-08  9:16             ` Christian Couder
  2025-06-26 19:11           ` Elijah Newren
  1 sibling, 1 reply; 65+ messages in thread
From: Junio C Hamano @ 2025-06-20 19:20 UTC (permalink / raw)
  To: Christian Couder
  Cc: Elijah Newren, git, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

tldr; 2 brief requests.  Please

 - Be gentle to people and expect that it is normal for them to be
   off of the list for a few weeks (or even more), not able to give
   a timely comments;

 - Fully stand behind your own patch (unless it is an RFC), even if
   some of the idea came from elsewhere.

>> Why would it need to say what type of signature it is?  Don't the
>> ascii armor lines have e.g. "----BEGIN PGP SIGNATURE----" and "----END
>> PGP SIGNATURE----" around it, which fast-import can read as well as
>> fast-export?  Is the idea that we strip those lines and now need to
>> replace the information we lost?
>
> In https://lore.kernel.org/git/aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net/
> brian said:
>
> "These should be separate fields: one for the hash algorithm and one for
> the protocol.  Alternatively, we can just keep the hash algorithm field
> and parse the protocol by reading the first line, which will differ for
> different protocols."
>
> It would have been nice if you had then said that you prefer not to
> have the protocol.

Let's remember to be gentle for those who give varluable feedbacks
but may not be always on this list.  A late comment on a topic that
has not hit 'next' is much better than a late comment after the
topic hits 'next', or no comment at all.

Also, even if the idea came from somebody else, if you agreed to the
idea and made it part of your submission, then it would be better to
explain it in your own words, in the most appropriate way to answer
the question asked (e.g. the original from Brian and the question by
Elijah may have stress on different aspect of the problem).

> My opinion was that it was better for tools processing fast-export
> output to have the protocol as they have to parse the "gppsig ..."
> line anyway. So it should be easier for them than to parse the ascii
> armor lines.

And if you do not agree with Brian's, perhaps discuss a bit more to
(1) either convince yourself that Brian's idea is better and rewrite
your code to adopt the idea, or (2) explain the reason why your "the
importer reads and parses anyway" is better design and stick to it.

> ...
> Yeah, I would have been happy if we could have been aligned with the
> goals of the format and the fields earlier, but better late than
> never.
>
>> Thanks for working on this topic.
>
> Thanks for reviewing.

Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-20 16:12         ` Christian Couder
  2025-06-20 19:20           ` Junio C Hamano
@ 2025-06-26 19:11           ` Elijah Newren
  2025-07-08  9:16             ` Christian Couder
  1 sibling, 1 reply; 65+ messages in thread
From: Elijah Newren @ 2025-06-26 19:11 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Fri, Jun 20, 2025 at 9:12 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> On Thu, Jun 19, 2025 at 11:44 PM Elijah Newren <newren@gmail.com> wrote:
> >
> > On Thu, Jun 19, 2025 at 6:36 AM Christian Couder
> > <christian.couder@gmail.com> wrote:
>
> > > However, this implementation has a number of limitations:
> > >
> > >   - the output format was not properly described in the documentation,
> >
> > Thanks for working on fixing this.
> >
> > >   - the output format is not very informative as it doesn't even say
> > >     if the signature is an OpenPGP, an SSH, or an X509 signature,
> >
> > Why would it need to say what type of signature it is?  Don't the
> > ascii armor lines have e.g. "----BEGIN PGP SIGNATURE----" and "----END
> > PGP SIGNATURE----" around it, which fast-import can read as well as
> > fast-export?  Is the idea that we strip those lines and now need to
> > replace the information we lost?
>
> In https://lore.kernel.org/git/aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net/
> brian said:
>
> "These should be separate fields: one for the hash algorithm and one for
> the protocol.  Alternatively, we can just keep the hash algorithm field
> and parse the protocol by reading the first line, which will differ for
> different protocols."
>
> It would have been nice if you had then said that you prefer not to
> have the protocol.
>
> My opinion was that it was better for tools processing fast-export
> output to have the protocol as they have to parse the "gppsig ..."
> line anyway. So it should be easier for them than to parse the ascii
> armor lines.
>
> > >   - the implementation doesn't support having both one signature on
> > >     the SHA-1 object and one on the SHA-256 object.
> >
> > Not sure I understand this; more questions around this later.
>
> In https://lore.kernel.org/git/aD4i7YhUnT5Kgew-@tapette.crustytoothpaste.net/
> brian said:
>
> "Actually, what I was saying is that we should have one for the hash
> algorithm that is used in the Git object.  I don't care about the hash
> algorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's
> signed with SHA-512 or SHA-256), but we can have multiple signatures in
> a single commit such that there's both a SHA-1 signature and a SHA-256
> signature."
>
> so I implemented the possibility that there's both a SHA-1 signature
> and a SHA-256 signature in a single commit.
>
> If you disagreed with what brian suggested, it would have been nice to
> reply to brian then.

Sorry about that.  I hadn't read the whole thread; further, I was
previously trying to avoid the details of signatures beyond the basics
and hoping to leave those details to others, but between v2 and some
words you had in this version plus deciding to just take a closer look
while on vacation (as I was last week), I decided to try to look a bit
closer.

Turns out, though, that despite my attempts to read a bit more
documentation and code in the project, I still had a fundamental
misunderstanding last week when I was responding.  I had been assuming
that the gpgsig headers would appear on sha1-commits and gpgsig-sha256
headers would appear on sha256 commits.  I didn't understand that both
types of headers can appear on both types of commits (so that, for
example, gpgsig-sha256 would be use the sha256-commit as the source
and sign that data but write the result to a sha1-commit). I talked
with brian this morning to ask a few questions and he clarified this
for me.

(Side note: It might be nice to clarify this in
Documentation/technical/hash-function-transition.adoc; that document
did not dispel this confusion when I read it last week.)

> > > Let's improve on these limitations by improving fast-export and
> > > fast-import so that:
> > >
> > >   - both one signature on the SHA-1 object and one on the SHA-256
> > >     object can be exported and imported,

And the part I didn't understand the first time around is that e.g.
the SHA-1 commit can include both the signatures on the SHA-1 object
and on the SHA-256 object.  (Similarly, the SHA-256 commit can include
both signatures as well.)

> > >   - if there is more than one signature on the SHA-1 object or on
> > >     the SHA-256 object, a warning is emitted,

This is now ambiguous to me.  more than one signature using the SHA-1
as source for signing, or more than one signature recorded within the
SHA-1 commit?  I think you mean the former.

> > >   - the output format is "gpgsig <git-hash-algo> <signature-format>",
> > >     where <git-hash-algo> is the Git object format as before, and
> > >     <signature-format> is the signature type ("openpgp", "x509",
> > >     "ssh" or "unknown",
> > >   - the output is properly documented.
> >
> > Perhaps this was discussed in an earlier round, but if so I either
> > forgot or missed it.  What value does <git-hash-algo> and
> > <signature-format> provide?  How are they intended to be used?
>
> My opinion on this is that in cases like this we might not always know
> what could be useful for tools or users in general, so it might be
> better to provide more information that can be easily discarded if not
> useful rather than not enough information.
>
> brian seemed to say that <git-hash-algo> and <signature-format> are
> important so I just prefered to have both, especially as they are not
> costly to get.

So, with my new understanding, <git-hash-algo> is necessary because if
you only had the gpg signature on a commit object, you don't know what
it was a signature of -- it might be a signature of that commit
object, or it might be a signature of it's "compatibility" object
created with a different git hashing algorithm.  So, fast-import --
regardless of whether it is writing into a sha1 repo or a sha256 repo
-- won't be able to know how to check the validity of the gpg
signature unless it knows whether to check the signature of the sha1
commit or the sha256 commit.  And if the repository that fast-import
is writing to isn't writing compatibility objects for interoperation
at all, then it won't be able to check any compatibility signatures at
all; it'll only be able to check signatures of the actual object it
wants to write.

> > Is the <signature-format> merely self-inflicted pain from stripping
> > the ascii armor lines?  If so, would it make more sense to just
> > include those armor lines as-is in the fast-export stream and let
> > fast-import process it?
>
> The ascii armor lines are kept in the fast-export stream, but
> fast-export's ouput is supposed to be processed somehow sometimes
> before being fed back to fast-import, so I think we should make it
> easy for tools or people processing the output to get signature
> information.

Ah, just for convenience of the importer/processor; fair enough.

> > Is the <git-hash-algo> due to the fact that we have separate `gpgsig`
> > and `gpgsig-sha256` commit headers and we want to use that information
> > to avoid writing these headers to the wrong-sized objects (and/or to
> > avoid checking whether the signature is valid on the wrong-sized
> > objects)?
>
> Yeah, I think it could help with that, but brian might better answer that.

I think the answer is actually no, this isn't at all what these are
about.  These are about knowing how to check the validity of the
signatures, because the signatures aren't necessarily associated with
the object they were read from; they could be a signature of its
compatibility object instead.

> > If so, could that be spelled out in the docs as well,
> > especially since it appears that the intent of these headers is left
> > unimplemented due to not changing fast-import to do anything with
> > them?
>
> fast-import puts back the `gpgsig` or `gpgsig-sha256` headers
> depending on <git-hash-algo>, so it's useful at least for that. I will
> improve the docs about it.
>
> > And if <git-hash-algo>'s purpose is to ensure they are only used when
> > writing same-sized object as what was exported, then...isn't that a
> > bug?
>
> I am not sure I understand what would be the bug.

It's not relevant given my misunderstanding, but to explain what I was
thinking last week (note that the first two bullets are my mistake):

  * gpgsig headers only occur on sha1 commits and are the signatures
of the sha1 commit sans the gpgsig header
  * gpgsig-sha256 headers are similar with respect to sha256 commits
  * we were using <git-hash-algo> as an optimization to only sign
commits in fast-import if the repository we were writing to was using
the same hash function as the repo we were writing from

In such a case, that'd be a bug for anyone that wanted a
'resign-commits-that-were-previously-signed-if-the-signature-becomes-invalid'.
Although a signature of a sha256 commit clearly won't verify if we
exported a sha256 repo and imported it as a sha1 repo and tried to
verify that signature using a sha1 commit, that doesn't mean we should
just ignore the fact that the sha256 commit had a signature.  The user
clearly expressed the intent to resign any commits that had a
signature before but where the signature is no longer valid.

> > This series was started because people wanted to be able to do
> > things like keeping signature even when they are no longer valid or
> > resigning commits that have a no longer commit signature (among other
> > uses), but that would mean that if someone exports a sha1 repository
> > and imports it as sha256, we don't want to ignore the fact that the
> > sha1 commit was signed for those usecases.
> >
> > If, however, the <git-hash-algo>'s purpose is merely as a performance
> > optimization that fast-import can employ in the cases where it checks
> > for signatures being valid, so it can avoid checking when it know the
> > hash size isn't even the same, then it could make sense.
>
> I think it could be used for that, but I'd like brian's opinion about this.

No, I was just wrong; it can't be used as such a performance
optimization.  A signature of a sha256 commit stored in a sha1 commit
(or a signature of a sha1 commit stored in a sha256 commit) isn't
accidental, it's part of the intentional design of the hash-function
transition.

But, it might mean that the rules for what to do with signatures in
fast-import need to be more complex than what I previously
wrote...which I'll comment on a bit below.

> > Wait..does this mean fast-export is obligated to walk over both all
> > sha1 commits and all "equivalent" sha256 commits when exporting a
> > repo?  I thought most operations on the repo would walk over only one
> > or the other; walking over both seems to be against the spirit of the
> > "fast" in "fast-export".  Am I missing something?  (Possibly related
> > question: Does "git log" bother walking over both, or does it only
> > walk over one?)  Even if this really is wanted by some users,
> > shouldn't they manually request it rather than making exports slow for
> > everyone else by default?
>
> No fast-export doesn't walk over both the sha1 commits and all
> "equivalent" sha256 commits, it can just process at most 2 signatures
> for a given commit, one with the `gpgsig` header and one with the
> `gpgsig-sha256` header. I will try to reword the doc to make it
> clearer.

Right, but fast-import might need to *write* both sha1 commits and
sha256 commits (or at least write one of them and keep a mapping
between the two in memory) in order to correctly verify and process
both gpgsig headers...or at least, it will be unable to verify
signatures of compatibility objects if people do not have the
appropriate extensions.compatObjectFormat config setting set within
the repository they are importing to.

This actually expands the usecases I mentioned previously a bit.
Those usecases were:

(A) Make fast-export include signatures, and make fast-import include
them unconditionally (even if invalid)
(B) Similar to (A), but make *fast-import* check them and either error
out or drop them if they become invalid
(C) Simliar to (B), but make *fast-import* re-sign the commit if they
become invalid
(D) Similar to (A), but make *fast-import* re-sign the commit even if
the signature would have been valid

Cases (B) and (C) might either need special care or need to bifurcate
into additional options given that we can have two signatures on a
commit.  "Validity of gpg signature stored in the commit" now becomes
"Validity of gpg signatureS stored in the commit".  Whereas before we
only considered the cases of "no valid signatures" and "all valid
signatures", we also need to worry about the case where one signature
is valid and the other is either known to be invalid or simply cannot
be checked because this repo isn't set up to write compatibility
objects.

> > > +A warning is
> > > +emitted for each additional signature found.
> >
> > Why?  This seems odd to me.  Why not merely export them all, and let
> > fast-import throw warnings or errors if it sees more than one and is
> > not yet prepared to handle multiple signatures?
>
> I am not against implementing both export and import support for any
> number of signatures, but it seems that Git in general doesn't support
> more than one `gpgsig` and one `gpgsig-sha256` signature well, and
> that's also what brian suggested supporting. It's also better than
> just 1 signature max which is the current state.
[...]
> > If I've
> > understood correctly that the restriction is merely due to the current
> > implementation, perhaps the wording could be changed to not list it as
> > an encoding restriction, but as a current fast-import limitation?
>
> If that current fast-import limitation was lifted, we should probably
> still say that additional signatures might not be very well supported
> by Git in general.

Ah!  I thought you were saying that fast-import specifically didn't
have good support for more than one signature (and I was assuming the
rest of Git did) and were using that as the reason to avoid handling
more than 1.

Sorry about that, I'm following you on this point now.

> > Also, I'm slightly uncomfortable with "the SHA-1 object format" and
> > "the SHA-256 object format" because of the fact that these tools are
> > used for interoperability with similarly-named tools from other VCSes.
> > I think it'd be better to just treat them equally as "here was/were
> > some signature(s) on the object in the original repo; importers can
> > choose what to do with them".
>
> I don't think giving that information prevents tools from other VCSes
> from doing what they want with the signatures.

Yeah, sorry, this was fallout from my misunderstanding about commits
having signatures from their compatibility object.  That's just kind
of fundamental to this hash-function transition, so ignore what I
wrote here.

> [...]
>
> > I briefly skimmed the implementation and test files, and didn't see
> > any problems...but I think it probably makes more sense to get aligned
> > on the goals of the format and how these fields are meant to be used
> > before diving into those details closer.
>
> Yeah, I would have been happy if we could have been aligned with the
> goals of the format and the fields earlier, but better late than
> never.

I think the proposal for the fields in this version make sense now,
but it might have been easier to get alignment if (1) we could get
real testcases of the multiple signature case, and (2) we could see
the actual code for fast-import to deal with the signatures (trying to
guess the needs of the importer and the meanings of the fields we
export is supposed to be is harder when their usage has been left
unimplemented).  That said, I also understand you wanting to avoid
implementing too much and throwing things away if we disagreed on
early decisions.

Anyway, I'm on board with these new fields and their purpose, though
I'm still curious if we start diverging when we run across surprises
as we dig further into the implementation.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-19 13:36     ` [PATCH v4] " Christian Couder
  2025-06-19 14:55       ` Junio C Hamano
  2025-06-19 21:44       ` Elijah Newren
@ 2025-07-07 22:58       ` Junio C Hamano
  2025-07-08  3:35         ` Christian Couder
  2025-07-08  9:17       ` [PATCH v5] " Christian Couder
  3 siblings, 1 reply; 65+ messages in thread
From: Junio C Hamano @ 2025-07-07 22:58 UTC (permalink / raw)
  To: git
  Cc: Christian Couder, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> This v4 is just about fixing a few bugs in the tests using the SHA-256
> object format compared to the v3. (I had issues with CI tests on v3,
> so I sent it without waiting for the results.)

We haven't heard much after a few comments were posted on this
latest round, since Elijah's
<20250619133630.727274-1-christian.couder@gmail.com>; I understand
that it would be the author's turn to respond (the response does not
necessarily have to be with an updated iteration).  If so, let me
mark the topic as Stalled in the draft of the latest issue of the
"What's cooking" report.

Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-07 22:58       ` Junio C Hamano
@ 2025-07-08  3:35         ` Christian Couder
  2025-07-08  5:03           ` Junio C Hamano
  0 siblings, 1 reply; 65+ messages in thread
From: Christian Couder @ 2025-07-08  3:35 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Tue, Jul 8, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > This v4 is just about fixing a few bugs in the tests using the SHA-256
> > object format compared to the v3. (I had issues with CI tests on v3,
> > so I sent it without waiting for the results.)
>
> We haven't heard much after a few comments were posted on this
> latest round, since Elijah's
> <20250619133630.727274-1-christian.couder@gmail.com>; I understand
> that it would be the author's turn to respond (the response does not
> necessarily have to be with an updated iteration).  If so, let me
> mark the topic as Stalled in the draft of the latest issue of the
> "What's cooking" report.

I will hopefully send a v5 later today.

Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-08  3:35         ` Christian Couder
@ 2025-07-08  5:03           ` Junio C Hamano
  2025-07-08  6:38             ` Patrick Steinhardt
  2025-07-08 10:17             ` Christian Couder
  0 siblings, 2 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-07-08  5:03 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> On Tue, Jul 8, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:
>>
>> Christian Couder <christian.couder@gmail.com> writes:
>>
>> > This v4 is just about fixing a few bugs in the tests using the SHA-256
>> > object format compared to the v3. (I had issues with CI tests on v3,
>> > so I sent it without waiting for the results.)
>>
>> We haven't heard much after a few comments were posted on this
>> latest round, since Elijah's
>> <20250619133630.727274-1-christian.couder@gmail.com>; I understand
>> that it would be the author's turn to respond (the response does not
>> necessarily have to be with an updated iteration).  If so, let me
>> mark the topic as Stalled in the draft of the latest issue of the
>> "What's cooking" report.
>
> I will hopefully send a v5 later today.

Thanks.

By the way, I noticed that you often do not respond to reviews until
the last minute, at the same time as when you send your next
iteration, or even soon after doing so.

That is quite different from how other contributors operate, i.e.
respond and engage in discussions triggered by the reviews, and
after people involved in discussion got an (even rough) idea of what
the right next step would be, if not a total consensus, send the
next iteration.

I do not know which style is more efficient form of cooperation, but
it somewhat makes my job harder, if I do not hear much _heartbeats_
after I see review comments on the list.  I do not mind waiting for
seeing the next round for quite a while---after all, any substantial
(re)work takes time.  And responding to reviews may need thinking
things through carefully, which may take some time, so I would not
demand an immediate response, either.  But it would be nearly
impossible to feel the current status of such a topic---a few review
comments are seen, the author goes silent for a while, we cannot
tell if the author is working on a new iteration or where the author
and reviewers agree and disagree.

Also a review response that comes at the same time or immediately
after a new iteration is already sent out makes it look like the
author is refusing to continue discussion and reviewers are not
welcome to make follow-up suggestions during such a discussion.

Instead, the next iteration comes as a fait accompli, and makes it
less useful to continue the review discussion on the previous round
by responding to such a late response.


^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-08  5:03           ` Junio C Hamano
@ 2025-07-08  6:38             ` Patrick Steinhardt
  2025-07-08 11:08               ` Christian Couder
  2025-07-08 10:17             ` Christian Couder
  1 sibling, 1 reply; 65+ messages in thread
From: Patrick Steinhardt @ 2025-07-08  6:38 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Christian Couder, git, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Mon, Jul 07, 2025 at 10:03:12PM -0700, Junio C Hamano wrote:
> Christian Couder <christian.couder@gmail.com> writes:
> 
> > On Tue, Jul 8, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:
> >>
> >> Christian Couder <christian.couder@gmail.com> writes:
> >>
> >> > This v4 is just about fixing a few bugs in the tests using the SHA-256
> >> > object format compared to the v3. (I had issues with CI tests on v3,
> >> > so I sent it without waiting for the results.)
> >>
> >> We haven't heard much after a few comments were posted on this
> >> latest round, since Elijah's
> >> <20250619133630.727274-1-christian.couder@gmail.com>; I understand
> >> that it would be the author's turn to respond (the response does not
> >> necessarily have to be with an updated iteration).  If so, let me
> >> mark the topic as Stalled in the draft of the latest issue of the
> >> "What's cooking" report.
> >
> > I will hopefully send a v5 later today.
> 
> Thanks.
> 
> By the way, I noticed that you often do not respond to reviews until
> the last minute, at the same time as when you send your next
> iteration, or even soon after doing so.
> 
> That is quite different from how other contributors operate, i.e.
> respond and engage in discussions triggered by the reviews, and
> after people involved in discussion got an (even rough) idea of what
> the right next step would be, if not a total consensus, send the
> next iteration.
> 
> I do not know which style is more efficient form of cooperation, but
> it somewhat makes my job harder, if I do not hear much _heartbeats_
> after I see review comments on the list.  I do not mind waiting for
> seeing the next round for quite a while---after all, any substantial
> (re)work takes time.  And responding to reviews may need thinking
> things through carefully, which may take some time, so I would not
> demand an immediate response, either.  But it would be nearly
> impossible to feel the current status of such a topic---a few review
> comments are seen, the author goes silent for a while, we cannot
> tell if the author is working on a new iteration or where the author
> and reviewers agree and disagree.
> 
> Also a review response that comes at the same time or immediately
> after a new iteration is already sent out makes it look like the
> author is refusing to continue discussion and reviewers are not
> welcome to make follow-up suggestions during such a discussion.
> 
> Instead, the next iteration comes as a fait accompli, and makes it
> less useful to continue the review discussion on the previous round
> by responding to such a late response.

I agree with your points. Overall, a fast response cycle is key to good
collaboration from my point of view. I think it not only makes your life
as a maintainer easier, but it also makes the reviewer feel like they
are being heard and is the prerequisite for good discussion.

On our team's handbook page [1] we have the following couple of bullet
points regarding how to respond to reviews:

  * Respond to feedback that you have received as fast as possible. A
    fast exchange is a prerequisite for a fruitful discussion and
    ensures that you keep momentum.

  * On the other hand, it shouldn’t be necessary to respond to every
    small typo correction in case you will send out the next version
    soon anyway. If it will take a while before you send the next
    version though it is nice to acknowledge nits, but mention that you
    will hold off sending a new version of the series until you got more
    feedback.

  * Consider the viewpoint of the other person and be ready to disagree and
    commit. Do not ignore feedback that you have received, as that will lead
    to frustration and a decreased likelihood for that person to review your
    future patch series.

Patrick

[1]: https://handbook.gitlab.com/handbook/engineering/infrastructure-platforms/data-access/git/#iteration

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-19 14:55       ` Junio C Hamano
@ 2025-07-08  9:16         ` Christian Couder
  0 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-08  9:16 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Thu, Jun 19, 2025 at 4:55 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > This v4 is just about fixing a few bugs in the tests using the SHA-256
> > object format compared to the v3. (I had issues with CI tests on v3,
> > so I sent it without waiting for the results.)
>
> Thanks.
>
> I am not sure if "I am happy is either 1 or 256" is what you really
> want, though.  The test presumably knows what algorithm is being
> used during its run, so wouldn't you want to say more like "I know I
> used sha256, and I expect seeing sha256, ah, I see sha256 and even
> better I see no sha1, so I am very happy"?

Yeah, I agree it's better to have tests say things like "I know I used
sha256, and I expect to see sha256". So in v5 tests now use:

  test_grep -E "^gpgsig $GIT_DEFAULT_HASH x509" output

as "$GIT_DEFAULT_HASH" should be either "sha1" or "sha256" depending
on the current hash.

I am not sure "and even better I see no sha1" is worth it then though,
so I haven't added that.

> > There are no tests in this v4 and in v3 with both a SHA-1 and a
> > SHA-256 signature on the same commit though, as I am not sure yet how
> > to best generate a commit with such signatures. Suggestions welcome!
>
> Good point to fill potential gaps.  If we had such a commit, then
> would these tests say "I know I want both 1 and 256, and I do see
> one instance each of 1 and 256, so I am happy"?

There is a test with such a commit in the v5 I am about to send and
yeah it checks that there is one instance of 1 and 256.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-20 19:20           ` Junio C Hamano
@ 2025-07-08  9:16             ` Christian Couder
  0 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-08  9:16 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Elijah Newren, git, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Fri, Jun 20, 2025 at 9:20 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> tldr; 2 brief requests.  Please
>
>  - Be gentle to people and expect that it is normal for them to be
>    off of the list for a few weeks (or even more), not able to give
>    a timely comments;
>
>  - Fully stand behind your own patch (unless it is an RFC), even if
>    some of the idea came from elsewhere.

Ok I will keep these in mind.

> >> Why would it need to say what type of signature it is?  Don't the
> >> ascii armor lines have e.g. "----BEGIN PGP SIGNATURE----" and "----END
> >> PGP SIGNATURE----" around it, which fast-import can read as well as
> >> fast-export?  Is the idea that we strip those lines and now need to
> >> replace the information we lost?
> >
> > In https://lore.kernel.org/git/aAq1nvcPRlIPal5l@tapette.crustytoothpaste.net/
> > brian said:
> >
> > "These should be separate fields: one for the hash algorithm and one for
> > the protocol.  Alternatively, we can just keep the hash algorithm field
> > and parse the protocol by reading the first line, which will differ for
> > different protocols."
> >
> > It would have been nice if you had then said that you prefer not to
> > have the protocol.
>
> Let's remember to be gentle for those who give varluable feedbacks
> but may not be always on this list.  A late comment on a topic that
> has not hit 'next' is much better than a late comment after the
> topic hits 'next', or no comment at all.

I agree that the comment was valuable, sorry if it appeared otherwise.

> Also, even if the idea came from somebody else, if you agreed to the
> idea and made it part of your submission, then it would be better to
> explain it in your own words, in the most appropriate way to answer
> the question asked (e.g. the original from Brian and the question by
> Elijah may have stress on different aspect of the problem).
>
> > My opinion was that it was better for tools processing fast-export
> > output to have the protocol as they have to parse the "gppsig ..."
> > line anyway. So it should be easier for them than to parse the ascii
> > armor lines.
>
> And if you do not agree with Brian's, perhaps discuss a bit more to
> (1) either convince yourself that Brian's idea is better and rewrite
> your code to adopt the idea, or (2) explain the reason why your "the
> importer reads and parses anyway" is better design and stick to it.

I actually agreed with brian about "These should be separate fields:
one for the hash algorithm and one for the protocol." Anyway it seems
that we all agree now on this.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-06-26 19:11           ` Elijah Newren
@ 2025-07-08  9:16             ` Christian Couder
  0 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-08  9:16 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Thu, Jun 26, 2025 at 9:12 PM Elijah Newren <newren@gmail.com> wrote:
>
> On Fri, Jun 20, 2025 at 9:12 AM Christian Couder
> <christian.couder@gmail.com> wrote:

> > In https://lore.kernel.org/git/aD4i7YhUnT5Kgew-@tapette.crustytoothpaste.net/
> > brian said:
> >
> > "Actually, what I was saying is that we should have one for the hash
> > algorithm that is used in the Git object.  I don't care about the hash
> > algorithm used in OpenPGP, X.509, or OpenSSH (that is, whether it's
> > signed with SHA-512 or SHA-256), but we can have multiple signatures in
> > a single commit such that there's both a SHA-1 signature and a SHA-256
> > signature."
> >
> > so I implemented the possibility that there's both a SHA-1 signature
> > and a SHA-256 signature in a single commit.
> >
> > If you disagreed with what brian suggested, it would have been nice to
> > reply to brian then.
>
> Sorry about that.  I hadn't read the whole thread; further, I was
> previously trying to avoid the details of signatures beyond the basics
> and hoping to leave those details to others, but between v2 and some
> words you had in this version plus deciding to just take a closer look
> while on vacation (as I was last week), I decided to try to look a bit
> closer.

Thanks for taking a closer look.

> Turns out, though, that despite my attempts to read a bit more
> documentation and code in the project, I still had a fundamental
> misunderstanding last week when I was responding.  I had been assuming
> that the gpgsig headers would appear on sha1-commits and gpgsig-sha256
> headers would appear on sha256 commits.  I didn't understand that both
> types of headers can appear on both types of commits (so that, for
> example, gpgsig-sha256 would be use the sha256-commit as the source
> and sign that data but write the result to a sha1-commit). I talked
> with brian this morning to ask a few questions and he clarified this
> for me.
>
> (Side note: It might be nice to clarify this in
> Documentation/technical/hash-function-transition.adoc; that document
> did not dispel this confusion when I read it last week.)

I agree that it would help to improve that document. I think it's a
separate topic though.

> And the part I didn't understand the first time around is that e.g.
> the SHA-1 commit can include both the signatures on the SHA-1 object
> and on the SHA-256 object.  (Similarly, the SHA-256 commit can include
> both signatures as well.)
>
> > > >   - if there is more than one signature on the SHA-1 object or on
> > > >     the SHA-256 object, a warning is emitted,
>
> This is now ambiguous to me.  more than one signature using the SHA-1
> as source for signing, or more than one signature recorded within the
> SHA-1 commit?  I think you mean the former.

Yes, it's the former. I am not sure there is a simple and short way to
disambiguate these meanings.

> > My opinion on this is that in cases like this we might not always know
> > what could be useful for tools or users in general, so it might be
> > better to provide more information that can be easily discarded if not
> > useful rather than not enough information.
> >
> > brian seemed to say that <git-hash-algo> and <signature-format> are
> > important so I just prefered to have both, especially as they are not
> > costly to get.
>
> So, with my new understanding, <git-hash-algo> is necessary because if
> you only had the gpg signature on a commit object, you don't know what
> it was a signature of -- it might be a signature of that commit
> object, or it might be a signature of it's "compatibility" object
> created with a different git hashing algorithm.

I agree that it's important for this reason.

> So, fast-import --
> regardless of whether it is writing into a sha1 repo or a sha256 repo
> -- won't be able to know how to check the validity of the gpg
> signature unless it knows whether to check the signature of the sha1
> commit or the sha256 commit. And if the repository that fast-import> is writing to isn't writing compatibility objects for interoperation
> at all, then it won't be able to check any compatibility signatures at
> all; it'll only be able to check signatures of the actual object it
> wants to write.

Yeah, right.

> > > Is the <signature-format> merely self-inflicted pain from stripping
> > > the ascii armor lines?  If so, would it make more sense to just
> > > include those armor lines as-is in the fast-export stream and let
> > > fast-import process it?
> >
> > The ascii armor lines are kept in the fast-export stream, but
> > fast-export's ouput is supposed to be processed somehow sometimes
> > before being fed back to fast-import, so I think we should make it
> > easy for tools or people processing the output to get signature
> > information.
>
> Ah, just for convenience of the importer/processor; fair enough.

Yeah, right.

> > > Is the <git-hash-algo> due to the fact that we have separate `gpgsig`
> > > and `gpgsig-sha256` commit headers and we want to use that information
> > > to avoid writing these headers to the wrong-sized objects (and/or to
> > > avoid checking whether the signature is valid on the wrong-sized
> > > objects)?
> >
> > Yeah, I think it could help with that, but brian might better answer that.
>
> I think the answer is actually no, this isn't at all what these are
> about.  These are about knowing how to check the validity of the
> signatures, because the signatures aren't necessarily associated with
> the object they were read from; they could be a signature of its
> compatibility object instead.

First I am now not sure what "wrong-sized objects" meant in your
previous sentence. I thought it meant "object using SHA-1 vs object
using SHA-256". In that case, the <git-hash-algo> could indeed help
check the signature on the right object.

Also git fast-import has to recreate the `gpgsig` and `gpgsig-sha256`
commit headers when importing signatures, so <git-hash-algo> can help
it with that too.

> > > If so, could that be spelled out in the docs as well,
> > > especially since it appears that the intent of these headers is left
> > > unimplemented due to not changing fast-import to do anything with
> > > them?
> >
> > fast-import puts back the `gpgsig` or `gpgsig-sha256` headers
> > depending on <git-hash-algo>, so it's useful at least for that. I will
> > improve the docs about it.

In v5, I have improved the fast-import documentation with the following:

* `<git-hash-algo>` specifies which Git object format this signature
  applies to, either `sha1` or `sha256`. This allows to know which
  representation of the commit was signed (the SHA-1 or the SHA-256
  version) which helps with both signature verification and
  interoperability between repos with different hash functions.

* `<signature-format>` specifies the type of signature, such as
  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for
  tools that process the stream, so they don't have to parse the ASCII
  armor to identify the signature type.

The commit message is improved too.

> > > And if <git-hash-algo>'s purpose is to ensure they are only used when
> > > writing same-sized object as what was exported, then...isn't that a
> > > bug?
> >
> > I am not sure I understand what would be the bug.
>
> It's not relevant given my misunderstanding, but to explain what I was
> thinking last week (note that the first two bullets are my mistake):
>
>   * gpgsig headers only occur on sha1 commits and are the signatures
> of the sha1 commit sans the gpgsig header
>   * gpgsig-sha256 headers are similar with respect to sha256 commits
>   * we were using <git-hash-algo> as an optimization to only sign
> commits in fast-import if the repository we were writing to was using
> the same hash function as the repo we were writing from
>
> In such a case, that'd be a bug for anyone that wanted a
> 'resign-commits-that-were-previously-signed-if-the-signature-becomes-invalid'.
> Although a signature of a sha256 commit clearly won't verify if we
> exported a sha256 repo and imported it as a sha1 repo and tried to
> verify that signature using a sha1 commit, that doesn't mean we should
> just ignore the fact that the sha256 commit had a signature.  The user
> clearly expressed the intent to resign any commits that had a
> signature before but where the signature is no longer valid.

I see.

> > > This series was started because people wanted to be able to do
> > > things like keeping signature even when they are no longer valid or
> > > resigning commits that have a no longer commit signature (among other
> > > uses), but that would mean that if someone exports a sha1 repository
> > > and imports it as sha256, we don't want to ignore the fact that the
> > > sha1 commit was signed for those usecases.
> > >
> > > If, however, the <git-hash-algo>'s purpose is merely as a performance
> > > optimization that fast-import can employ in the cases where it checks
> > > for signatures being valid, so it can avoid checking when it know the
> > > hash size isn't even the same, then it could make sense.
> >
> > I think it could be used for that, but I'd like brian's opinion about this.
>
> No, I was just wrong; it can't be used as such a performance
> optimization.  A signature of a sha256 commit stored in a sha1 commit
> (or a signature of a sha1 commit stored in a sha256 commit) isn't
> accidental, it's part of the intentional design of the hash-function
> transition.
>
> But, it might mean that the rules for what to do with signatures in
> fast-import need to be more complex than what I previously
> wrote...which I'll comment on a bit below.

Ok.

> > > Wait..does this mean fast-export is obligated to walk over both all
> > > sha1 commits and all "equivalent" sha256 commits when exporting a
> > > repo?  I thought most operations on the repo would walk over only one
> > > or the other; walking over both seems to be against the spirit of the
> > > "fast" in "fast-export".  Am I missing something?  (Possibly related
> > > question: Does "git log" bother walking over both, or does it only
> > > walk over one?)  Even if this really is wanted by some users,
> > > shouldn't they manually request it rather than making exports slow for
> > > everyone else by default?
> >
> > No fast-export doesn't walk over both the sha1 commits and all
> > "equivalent" sha256 commits, it can just process at most 2 signatures
> > for a given commit, one with the `gpgsig` header and one with the
> > `gpgsig-sha256` header. I will try to reword the doc to make it
> > clearer.

In v5 all the signatures are exported, so the doc is now:

"While all the signatures of a commit are exported, an importer may
choose to accept only some of them. For example
linkgit:git-fast-import[1] currently stores at most one signature per
Git hash algorithm in each commit."

I hope it avoids misunderstandings.

> Right, but fast-import might need to *write* both sha1 commits and
> sha256 commits (or at least write one of them and keep a mapping
> between the two in memory) in order to correctly verify and process
> both gpgsig headers...or at least, it will be unable to verify
> signatures of compatibility objects if people do not have the
> appropriate extensions.compatObjectFormat config setting set within
> the repository they are importing to.

Yeah, it seems to me that when extensions.compatObjectFormat is set,
then a compatibility mapping between objects can be used. I don't
think using it should be part of this patch though. When I will work
on making it possible for fast-import to check signatures, I will try
to use it to write both sha1 commits and sha256 commits, and check
both signatures.

> This actually expands the usecases I mentioned previously a bit.
> Those usecases were:
>
> (A) Make fast-export include signatures, and make fast-import include
> them unconditionally (even if invalid)
> (B) Similar to (A), but make *fast-import* check them and either error
> out or drop them if they become invalid
> (C) Simliar to (B), but make *fast-import* re-sign the commit if they
> become invalid
> (D) Similar to (A), but make *fast-import* re-sign the commit even if
> the signature would have been valid
>
> Cases (B) and (C) might either need special care or need to bifurcate
> into additional options given that we can have two signatures on a
> commit.  "Validity of gpg signature stored in the commit" now becomes
> "Validity of gpg signatureS stored in the commit".  Whereas before we
> only considered the cases of "no valid signatures" and "all valid
> signatures", we also need to worry about the case where one signature
> is valid and the other is either known to be invalid or simply cannot
> be checked because this repo isn't set up to write compatibility
> objects.

Yeah, I agree that there might be new cases to consider. I don't think
this affects this patch though as long as it allows one signature for
each hash to be exported and imported which is the case.

> > Yeah, I would have been happy if we could have been aligned with the
> > goals of the format and the fields earlier, but better late than
> > never.
>
> I think the proposal for the fields in this version make sense now,
> but it might have been easier to get alignment if (1) we could get
> real testcases of the multiple signature case,

There is such a multiple signature test case in v5.

> and (2) we could see
> the actual code for fast-import to deal with the signatures (trying to
> guess the needs of the importer and the meanings of the fields we
> export is supposed to be is harder when their usage has been left
> unimplemented).  That said, I also understand you wanting to avoid
> implementing too much and throwing things away if we disagreed on
> early decisions.

Yeah, I prefer to move forward step by step on this.

> Anyway, I'm on board with these new fields and their purpose, though
> I'm still curious if we start diverging when we run across surprises
> as we dig further into the implementation.

As we have clearly warned that the current implementation is
experimental, I think we should have more flexibility to adapt the
formats and behaviors if we run across surprises in the next steps.

Thanks for your thoughts and reviews.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* [PATCH v5] fast-(import|export): improve on commit signature output format
  2025-06-19 13:36     ` [PATCH v4] " Christian Couder
                         ` (2 preceding siblings ...)
  2025-07-07 22:58       ` Junio C Hamano
@ 2025-07-08  9:17       ` Christian Couder
  2025-07-08 21:58         ` Junio C Hamano
                           ` (2 more replies)
  3 siblings, 3 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-08  9:17 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
signed-commits, 2025-03-10), added support for signed commits to
fast-export and fast-import.

When a signed commit is processed, fast-export can output either
"gpgsig sha1" or "gpgsig sha256" depending on whether the signed
commit uses the SHA-1 or SHA-256 Git object format.

However, this implementation has a number of limitations:

  - the output format was not properly described in the documentation,
  - the output format is not very informative as it doesn't even say
    if the signature is an OpenPGP, an SSH, or an X509 signature,
  - the implementation doesn't support having both one signature on
    the SHA-1 object and one on the SHA-256 object.

Let's improve on these limitations by improving fast-export and
fast-import so that:

  - all the signatures are exported,
  - at most one signature on the SHA-1 object and one on the SHA-256
    are imported,
  - if there is more than one signature on the SHA-1 object or on
    the SHA-256 object, fast-import emits a warning for each
    additional signature,
  - the output format is "gpgsig <git-hash-algo> <signature-format>",
    where <git-hash-algo> is the Git object format as before, and
    <signature-format> is the signature type ("openpgp", "x509",
    "ssh" or "unknown"),
  - the output is properly documented.

About the output format:

  - <git-hash-algo> allows to know which representation of the commit
    was signed (the SHA-1 or the SHA-256 version) which helps with
    both signature verification and interoperability between repos
    with different hash functions,

  - <signature-format> helps tools that process the fast-export
    stream, so they don't have to parse the ASCII armor to identify
    the signature type.

It could be even better to be able to import more than one signature
on the SHA-1 object and on the SHA-256 object, but other parts of
Git don't handle that well for now, so this is left for future
improvements.

Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---

This v5 is similar in spirit to v4, especially the format of the
"gpgsig ..." command didn't change, but there are a number of
improvements:

  - All the signatures are now exported. On the import side, still at
    most one signature for SHA-1 and one for SHA-256 are imported (and
    a warning is still emitted for additional signatures) though.

  - The code makes sure each signature ends with a LF character. While
    this is not mandatory, it is encouraged and makes the output more
    human readable and 'grep'able.

  - A test with both a SHA-1 and a SHA-256 signature on the same
    commit has been added.

  - Some tests check that either "sha1" or "sha256" is in the "gpgsig
    ..."  command instead of matching "sha(1|256)".

  - The format of the "gpgsig ..." command is better explained both in
    the commit message and in the fast-import documentation.

  - There are some typo fixes, lines wrapped, and a few other such
    small changes.

Thanks to brian, Elijah and Junio who commented on the v1 and v2.

CI tests:

They have all passed:

https://github.com/chriscool/git/actions/runs/16136587558

Range-diff with v4:

1:  b3d8b13c0e ! 1:  5791617d7d fast-(import|export): improve on commit signature output format
    @@ Commit message
         Let's improve on these limitations by improving fast-export and
         fast-import so that:
     
    -      - both one signature on the SHA-1 object and one on the SHA-256
    -        object can be exported and imported,
    +      - all the signatures are exported,
    +      - at most one signature on the SHA-1 object and one on the SHA-256
    +        are imported,
           - if there is more than one signature on the SHA-1 object or on
    -        the SHA-256 object, a warning is emitted,
    +        the SHA-256 object, fast-import emits a warning for each
    +        additional signature,
           - the output format is "gpgsig <git-hash-algo> <signature-format>",
             where <git-hash-algo> is the Git object format as before, and
             <signature-format> is the signature type ("openpgp", "x509",
    -        "ssh" or "unknown",
    +        "ssh" or "unknown"),
           - the output is properly documented.
     
    -    Note that it could be even better to be able to export and import
    -    more than one signature on the SHA-1 object and on the SHA-256
    -    object, but other parts of Git don't handle that well for now, so
    -    this is left for future improvements.
    +    About the output format:
    +
    +      - <git-hash-algo> allows to know which representation of the commit
    +        was signed (the SHA-1 or the SHA-256 version) which helps with
    +        both signature verification and interoperability between repos
    +        with different hash functions,
    +
    +      - <signature-format> helps tools that process the fast-export
    +        stream, so they don't have to parse the ASCII armor to identify
    +        the signature type.
    +
    +    It could be even better to be able to import more than one signature
    +    on the SHA-1 object and on the SHA-256 object, but other parts of
    +    Git don't handle that well for now, so this is left for future
    +    improvements.
     
         Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
         Helped-by: Elijah Newren <newren@gmail.com>
    @@ Documentation/git-fast-export.adoc: resulting tag will have an invalid signature
     +`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit
     +starts with `gpgsig sha256 ssh`.
     ++
    -+Currently for a given commit, at most one signature for the SHA-1
    -+object and one signature for the SHA-256 object are exported, each
    -+with their respective <git-hash-algo> identifier. A warning is
    -+emitted for each additional signature found.
    ++While all the signatures of a commit are exported, an importer may
    ++choose to accept only some of them. For example
    ++linkgit:git-fast-import[1] currently stores at most one signature per
    ++Git hash algorithm in each commit.
     ++
      NOTE: This is highly experimental and the format of the data stream may
      change in the future without compatibility guarantees.
    @@ Documentation/git-fast-import.adoc: their syntax.
     +The `gpgsig` command takes two arguments:
     +
     +* `<git-hash-algo>` specifies which Git object format this signature
    -+  applies to, either `sha1` or `sha256`.
    ++  applies to, either `sha1` or `sha256`. This allows to know which
    ++  representation of the commit was signed (the SHA-1 or the SHA-256
    ++  version) which helps with both signature verification and
    ++  interoperability between repos with different hash functions.
     +
     +* `<signature-format>` specifies the type of signature, such as
    -+  `openpgp`, `x509`, `ssh`, or `unknown`.
    ++  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for
    ++  tools that process the stream, so they don't have to parse the ASCII
    ++  armor to identify the signature type.
     +
     +A commit may have at most one signature for the SHA-1 object format
     +(stored in the "gpgsig" header) and one for the SHA-256 object format
    @@ builtin/fast-export.c: static const char *find_commit_multiline_header(const cha
     +	if (!signature)
     +		return;
     +
    -+	printf("gpgsig %s %s\ndata %u\n%s",
    ++	printf("gpgsig %s %s\ndata %u\n%s\n",
     +	       object_hash,
     +	       get_signature_format(signature),
     +	       (unsigned)strlen(signature),
     +	       signature);
     +}
     +
    -+static void warn_on_extra_sig(const char **pos, struct commit *commit, int is_sha1)
    ++static const char *append_signatures_for_header(struct string_list *signatures,
    ++						const char *pos,
    ++						const char *header,
    ++						const char *object_hash)
     +{
    -+	const char *header = is_sha1 ? "gpgsig" : "gpgsig-sha256";
    -+	const char *extra_sig = find_commit_multiline_header(*pos + 1, header, pos);
    -+	if (extra_sig) {
    -+		const char *hash = is_sha1 ? "SHA-1" : "SHA-256";
    -+		warning("more than one %s signature found on commit %s, using only the first one",
    -+			hash, oid_to_hex(&commit->object.oid));
    -+		free((char *)extra_sig);
    ++	const char *signature;
    ++	const char *start = pos;
    ++	const char *end = pos;
    ++
    ++	while ((signature = find_commit_multiline_header(start + 1,
    ++							 header,
    ++							 &end))) {
    ++		string_list_append(signatures, signature)->util = (void *)object_hash;
    ++		free((char *)signature);
    ++		start = end;
     +	}
    ++
    ++	return end;
     +}
     +
      static void handle_commit(struct commit *commit, struct rev_info *rev,
    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r
      	const char *encoding = NULL;
      	size_t encoding_len;
     -	const char *signature_alg = NULL, *signature = NULL;
    -+	const char *sig_sha1 = NULL;
    -+	const char *sig_sha256 = NULL;
    ++	struct string_list signatures = STRING_LIST_INIT_DUP;
      	const char *message;
      	char *reencoded = NULL;
      	struct commit_list *p;
    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r
     -			signature_alg = "sha1";
     -		else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
     -			signature_alg = "sha256";
    -+		const char *sig_cursor = commit_buffer_cursor;
    -+		const char *after_sha1 = commit_buffer_cursor;
    -+		const char *after_sha256 = commit_buffer_cursor;
    -+
    -+		/*
    -+		 * Find the first signature for each hash algorithm.
    -+		 * The searches must start from the same position.
    -+		 */
    -+		sig_sha1 = find_commit_multiline_header(sig_cursor + 1,
    -+							"gpgsig",
    -+							&after_sha1);
    -+		sig_sha256 = find_commit_multiline_header(sig_cursor + 1,
    -+							  "gpgsig-sha256",
    -+							  &after_sha256);
    -+
    -+		/* Warn on any additional signatures, as they will be ignored. */
    -+		if (sig_sha1)
    -+			warn_on_extra_sig(&after_sha1, commit, 1);
    -+		if (sig_sha256)
    -+			warn_on_extra_sig(&after_sha256, commit, 0);
    -+
    ++		const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,
    ++								      "gpgsig", "sha1");
    ++		const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,
    ++									"gpgsig-sha256", "sha256");
     +		commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
      	}
      
    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r
      	       (int)(author_end - author), author,
      	       (int)(committer_end - committer), committer);
     -	if (signature) {
    -+	if (sig_sha1 || sig_sha256) {
    ++	if (signatures.nr) {
      		switch (signed_commit_mode) {
      		case SIGN_ABORT:
      			die("encountered signed commit %s; use "
    -@@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct rev_info *rev,
    - 				oid_to_hex(&commit->object.oid));
    + 			    "--signed-commits=<mode> to handle it",
    + 			    oid_to_hex(&commit->object.oid));
    + 		case SIGN_WARN_VERBATIM:
    +-			warning("exporting signed commit %s",
    +-				oid_to_hex(&commit->object.oid));
    ++			warning("exporting %"PRIuMAX" signature(s) for commit %s",
    ++				(uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));
      			/* fallthru */
      		case SIGN_VERBATIM:
     -			printf("gpgsig %s\ndata %u\n%s",
     -			       signature_alg,
     -			       (unsigned)strlen(signature),
     -			       signature);
    -+			print_signature(sig_sha1, "sha1");
    -+			print_signature(sig_sha256, "sha256");
    ++			for (size_t i = 0; i < signatures.nr; i++) {
    ++				struct string_list_item *item = &signatures.items[i];
    ++				print_signature(item->string, item->util);
    ++			}
      			break;
      		case SIGN_WARN_STRIP:
     -			warning("stripping signature from commit %s",
    @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r
      			break;
      		}
     -		free((char *)signature);
    -+		free((char *)sig_sha1);
    -+		free((char *)sig_sha256);
    ++		string_list_clear(&signatures, 0);
      	}
      	if (!reencoded && encoding)
      		printf("encoding %.*s\n", (int)encoding_len, encoding);
    @@ builtin/fast-import.c: static struct hash_list *parse_merge(unsigned int *count)
     +			    const char *hash_type)
     +{
     +	if (stored_sig->hash_algo) {
    -+		warning("Multiple %s signatures found, ignoring additional signature",
    ++		warning("multiple %s signatures found, "
    ++			"ignoring additional signature",
     +			hash_type);
     +		strbuf_release(&new_sig->data);
     +		free(new_sig->hash_algo);
    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=abort' '
      
      	git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
     -	grep "^gpgsig sha" output &&
    -+	test_grep -E "^gpgsig sha(1|256) openpgp" output &&
    ++	test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
      	grep "encoding ISO-8859-1" output &&
      	(
      		cd new &&
    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=verbatim' '
      
      	git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
     -	grep "^gpgsig sha" output &&
    -+	test_grep -E "^gpgsig sha(1|256) openpgp" output &&
    ++	test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
      	grep "encoding ISO-8859-1" output &&
      	test -s err &&
      	(
    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '
     +test_expect_success GPGSM 'round-trip X.509 signed commit' '
     +
     +	git fast-export --signed-commits=verbatim x509-signing >output &&
    -+	test_grep -E "^gpgsig sha(1|256) x509" output &&
    ++	test_grep -E "^gpgsig $GIT_DEFAULT_HASH x509" output &&
     +	(
     +		cd new &&
     +		git fast-import &&
    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '
     +test_expect_success GPGSSH 'round-trip SSH signed commit' '
     +
     +	git fast-export --signed-commits=verbatim ssh-signing >output &&
    -+	test_grep -E "^gpgsig sha(1|256) ssh" output &&
    ++	test_grep -E "^gpgsig $GIT_DEFAULT_HASH ssh" output &&
     +	(
     +		cd new &&
     +		git fast-import &&
    @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '
      test_expect_success 'setup submodule' '
      
      	test_config_global protocol.file.allow always &&
    +@@ t/t9350-fast-export.sh: test_expect_success 'fast-export handles --end-of-options' '
    + 	test_cmp expect actual
    + '
    + 
    ++test_expect_success GPG 'setup a commit with dual signatures on its SHA-1 and SHA-256 formats' '
    ++	# Create a signed SHA-256 commit
    ++	git init --object-format=sha256 explicit-sha256 &&
    ++	git -C explicit-sha256 config extensions.compatObjectFormat sha1 &&
    ++	git -C explicit-sha256 checkout -b dual-signed &&
    ++	test_commit -C explicit-sha256 A &&
    ++	echo B >explicit-sha256/B &&
    ++	git -C explicit-sha256 add B &&
    ++	test_tick &&
    ++	git -C explicit-sha256 commit -S -m "signed" B &&
    ++	SHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&
    ++
    ++	# Create the corresponding SHA-1 commit
    ++	SHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&
    ++
    ++	# Check that the resulting SHA-1 commit has both signatures
    ++	echo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&
    ++	test_grep -E "^gpgsig " out &&
    ++	test_grep -E "^gpgsig-sha256 " out
    ++'
    ++
    ++test_expect_success GPG 'export and import of doubly signed commit' '
    ++	git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&
    ++	test_grep -E "^gpgsig sha1 openpgp" output &&
    ++	test_grep -E "^gpgsig sha256 openpgp" output &&
    ++
    ++	(
    ++		cd new &&
    ++		git fast-import &&
    ++		git cat-file commit refs/heads/dual-signed >actual &&
    ++		test_grep -E "^gpgsig " actual &&
    ++		test_grep -E "^gpgsig-sha256 " actual &&
    ++		IMPORTED=$(git rev-parse refs/heads/dual-signed) &&
    ++		if test "$GIT_DEFAULT_HASH" = "sha1"
    ++		then
    ++			test $SHA1_B = $IMPORTED
    ++		else
    ++			test $SHA256_B = $IMPORTED
    ++		fi
    ++	) <output
    ++'
    ++
    + test_done


 Documentation/git-fast-export.adoc |  17 +++++
 Documentation/git-fast-import.adoc |  36 +++++++--
 builtin/fast-export.c              |  62 +++++++++++----
 builtin/fast-import.c              | 118 +++++++++++++++++++++++------
 gpg-interface.c                    |  12 +++
 gpg-interface.h                    |  12 +++
 t/t9350-fast-export.sh             | 102 ++++++++++++++++++++++++-
 7 files changed, 315 insertions(+), 44 deletions(-)

diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
index 43bbb4f63c..297b57bb2e 100644
--- a/Documentation/git-fast-export.adoc
+++ b/Documentation/git-fast-export.adoc
@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.
 	is the same as how earlier versions of this command without
 	this option behaved.
 +
+When exported, a signature starts with:
++
+gpgsig <git-hash-algo> <signature-format>
++
+where <git-hash-algo> is the Git object hash so either "sha1" or
+"sha256", and <signature-format> is the signature type, so "openpgp",
+"x509", "ssh" or "unknown".
++
+For example, an OpenPGP signature on a SHA-1 commit starts with
+`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit
+starts with `gpgsig sha256 ssh`.
++
+While all the signatures of a commit are exported, an importer may
+choose to accept only some of them. For example
+linkgit:git-fast-import[1] currently stores at most one signature per
+Git hash algorithm in each commit.
++
 NOTE: This is highly experimental and the format of the data stream may
 change in the future without compatibility guarantees.
 
diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
index 250d866652..89dec1108f 100644
--- a/Documentation/git-fast-import.adoc
+++ b/Documentation/git-fast-import.adoc
@@ -445,7 +445,7 @@ one).
 	original-oid?
 	('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
 	'committer' (SP <name>)? SP LT <email> GT SP <when> LF
-	('gpgsig' SP <alg> LF data)?
+	('gpgsig' SP <algo> SP <format> LF data)?
 	('encoding' SP <encoding> LF)?
 	data
 	('from' SP <commit-ish> LF)?
@@ -518,13 +518,37 @@ their syntax.
 ^^^^^^^^
 
 The optional `gpgsig` command is used to include a PGP/GPG signature
-that signs the commit data.
+or other cryptographic signature that signs the commit data.
 
-Here <alg> specifies which hashing algorithm is used for this
-signature, either `sha1` or `sha256`.
+....
+	'gpgsig' SP <git-hash-algo> SP <signature-format> LF
+	data
+....
+
+The `gpgsig` command takes two arguments:
+
+* `<git-hash-algo>` specifies which Git object format this signature
+  applies to, either `sha1` or `sha256`. This allows to know which
+  representation of the commit was signed (the SHA-1 or the SHA-256
+  version) which helps with both signature verification and
+  interoperability between repos with different hash functions.
+
+* `<signature-format>` specifies the type of signature, such as
+  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for
+  tools that process the stream, so they don't have to parse the ASCII
+  armor to identify the signature type.
+
+A commit may have at most one signature for the SHA-1 object format
+(stored in the "gpgsig" header) and one for the SHA-256 object format
+(stored in the "gpgsig-sha256" header).
+
+See below for a detailed description of the `data` command which
+contains the raw signature data.
+
+Signatures are not yet checked in the current implementation though.
 
-NOTE: This is highly experimental and the format of the data stream may
-change in the future without compatibility guarantees.
+NOTE: This is highly experimental and the format of the `gpgsig`
+command may change in the future without compatibility guarantees.
 
 `encoding`
 ^^^^^^^^^^
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index fcf6b00d5f..7b4e6a6e41 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -29,6 +29,7 @@
 #include "quote.h"
 #include "remote.h"
 #include "blob.h"
+#include "gpg-interface.h"
 
 static const char *const fast_export_usage[] = {
 	N_("git fast-export [<rev-list-opts>]"),
@@ -652,6 +653,38 @@ static const char *find_commit_multiline_header(const char *msg,
 	return strbuf_detach(&val, NULL);
 }
 
+static void print_signature(const char *signature, const char *object_hash)
+{
+	if (!signature)
+		return;
+
+	printf("gpgsig %s %s\ndata %u\n%s\n",
+	       object_hash,
+	       get_signature_format(signature),
+	       (unsigned)strlen(signature),
+	       signature);
+}
+
+static const char *append_signatures_for_header(struct string_list *signatures,
+						const char *pos,
+						const char *header,
+						const char *object_hash)
+{
+	const char *signature;
+	const char *start = pos;
+	const char *end = pos;
+
+	while ((signature = find_commit_multiline_header(start + 1,
+							 header,
+							 &end))) {
+		string_list_append(signatures, signature)->util = (void *)object_hash;
+		free((char *)signature);
+		start = end;
+	}
+
+	return end;
+}
+
 static void handle_commit(struct commit *commit, struct rev_info *rev,
 			  struct string_list *paths_of_changed_objects)
 {
@@ -660,7 +693,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	const char *author, *author_end, *committer, *committer_end;
 	const char *encoding = NULL;
 	size_t encoding_len;
-	const char *signature_alg = NULL, *signature = NULL;
+	struct string_list signatures = STRING_LIST_INIT_DUP;
 	const char *message;
 	char *reencoded = NULL;
 	struct commit_list *p;
@@ -700,10 +733,11 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	}
 
 	if (*commit_buffer_cursor == '\n') {
-		if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
-			signature_alg = "sha1";
-		else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
-			signature_alg = "sha256";
+		const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,
+								      "gpgsig", "sha1");
+		const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,
+									"gpgsig-sha256", "sha256");
+		commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
 	}
 
 	message = strstr(commit_buffer_cursor, "\n\n");
@@ -769,30 +803,30 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	printf("%.*s\n%.*s\n",
 	       (int)(author_end - author), author,
 	       (int)(committer_end - committer), committer);
-	if (signature) {
+	if (signatures.nr) {
 		switch (signed_commit_mode) {
 		case SIGN_ABORT:
 			die("encountered signed commit %s; use "
 			    "--signed-commits=<mode> to handle it",
 			    oid_to_hex(&commit->object.oid));
 		case SIGN_WARN_VERBATIM:
-			warning("exporting signed commit %s",
-				oid_to_hex(&commit->object.oid));
+			warning("exporting %"PRIuMAX" signature(s) for commit %s",
+				(uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));
 			/* fallthru */
 		case SIGN_VERBATIM:
-			printf("gpgsig %s\ndata %u\n%s",
-			       signature_alg,
-			       (unsigned)strlen(signature),
-			       signature);
+			for (size_t i = 0; i < signatures.nr; i++) {
+				struct string_list_item *item = &signatures.items[i];
+				print_signature(item->string, item->util);
+			}
 			break;
 		case SIGN_WARN_STRIP:
-			warning("stripping signature from commit %s",
+			warning("stripping signature(s) from commit %s",
 				oid_to_hex(&commit->object.oid));
 			/* fallthru */
 		case SIGN_STRIP:
 			break;
 		}
-		free((char *)signature);
+		string_list_clear(&signatures, 0);
 	}
 	if (!reencoded && encoding)
 		printf("encoding %.*s\n", (int)encoding_len, encoding);
diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index b2839c5f43..332073d0f6 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -29,6 +29,7 @@
 #include "commit-reach.h"
 #include "khash.h"
 #include "date.h"
+#include "gpg-interface.h"
 
 #define PACK_ID_BITS 16
 #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
@@ -2718,15 +2719,87 @@ static struct hash_list *parse_merge(unsigned int *count)
 	return list;
 }
 
+struct signature_data {
+	char *hash_algo;      /* "sha1" or "sha256" */
+	char *sig_format;     /* "openpgp", "x509", "ssh", "unknown" */
+	struct strbuf data;   /* The actual signature data */
+};
+
+static void parse_one_signature(struct signature_data *sig, const char *v)
+{
+	char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */
+	char *space = strchr(args, ' ');
+
+	if (!space)
+		die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
+		    "got 'gpgsig %s'", args);
+	*space++ = '\0';
+
+	sig->hash_algo = args;
+	sig->sig_format = space;
+
+	/* Remove any trailing newline from format */
+	space = strchr(sig->sig_format, '\n');
+	if (space)
+		*space = '\0';
+
+	/* Validate hash algorithm */
+	if (strcmp(sig->hash_algo, "sha1") &&
+	    strcmp(sig->hash_algo, "sha256"))
+		die("Unknown git hash algorithm in gpgsig: '%s'", sig->hash_algo);
+
+	/* Validate signature format */
+	if (!valid_signature_format(sig->sig_format))
+		die("Invalid signature format in gpgsig: '%s'", sig->sig_format);
+	if (!strcmp(sig->sig_format, "unknown"))
+		warning("'unknown' signature format in gpgsig");
+
+	/* Read signature data */
+	read_next_command();
+	parse_data(&sig->data, 0, NULL);
+}
+
+static void add_gpgsig_to_commit(struct strbuf *commit_data,
+				 const char *header,
+				 struct signature_data *sig)
+{
+	struct string_list siglines = STRING_LIST_INIT_NODUP;
+
+	if (!sig->hash_algo)
+		return;
+
+	strbuf_addstr(commit_data, header);
+	string_list_split_in_place(&siglines, sig->data.buf, "\n", -1);
+	strbuf_add_separated_string_list(commit_data, "\n ", &siglines);
+	strbuf_addch(commit_data, '\n');
+	string_list_clear(&siglines, 1);
+	strbuf_release(&sig->data);
+	free(sig->hash_algo);
+}
+
+static void store_signature(struct signature_data *stored_sig,
+			    struct signature_data *new_sig,
+			    const char *hash_type)
+{
+	if (stored_sig->hash_algo) {
+		warning("multiple %s signatures found, "
+			"ignoring additional signature",
+			hash_type);
+		strbuf_release(&new_sig->data);
+		free(new_sig->hash_algo);
+	} else {
+		*stored_sig = *new_sig;
+	}
+}
+
 static void parse_new_commit(const char *arg)
 {
-	static struct strbuf sig = STRBUF_INIT;
 	static struct strbuf msg = STRBUF_INIT;
-	struct string_list siglines = STRING_LIST_INIT_NODUP;
+	struct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };
+	struct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };
 	struct branch *b;
 	char *author = NULL;
 	char *committer = NULL;
-	char *sig_alg = NULL;
 	char *encoding = NULL;
 	struct hash_list *merge_list = NULL;
 	unsigned int merge_count;
@@ -2750,13 +2823,23 @@ static void parse_new_commit(const char *arg)
 	}
 	if (!committer)
 		die("Expected committer but didn't get one");
-	if (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
-		sig_alg = xstrdup(v);
-		read_next_command();
-		parse_data(&sig, 0, NULL);
+
+	/* Process signatures (up to 2: one "sha1" and one "sha256") */
+	while (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
+		struct signature_data sig = { NULL, NULL, STRBUF_INIT };
+
+		parse_one_signature(&sig, v);
+
+		if (!strcmp(sig.hash_algo, "sha1"))
+			store_signature(&sig_sha1, &sig, "SHA-1");
+		else if (!strcmp(sig.hash_algo, "sha256"))
+			store_signature(&sig_sha256, &sig, "SHA-256");
+		else
+			BUG("parse_one_signature() returned unknown hash algo");
+
 		read_next_command();
-	} else
-		strbuf_setlen(&sig, 0);
+	}
+
 	if (skip_prefix(command_buf.buf, "encoding ", &v)) {
 		encoding = xstrdup(v);
 		read_next_command();
@@ -2830,23 +2913,14 @@ static void parse_new_commit(const char *arg)
 		strbuf_addf(&new_data,
 			"encoding %s\n",
 			encoding);
-	if (sig_alg) {
-		if (!strcmp(sig_alg, "sha1"))
-			strbuf_addstr(&new_data, "gpgsig ");
-		else if (!strcmp(sig_alg, "sha256"))
-			strbuf_addstr(&new_data, "gpgsig-sha256 ");
-		else
-			die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
-		string_list_split_in_place(&siglines, sig.buf, "\n", -1);
-		strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
-		strbuf_addch(&new_data, '\n');
-	}
+
+	add_gpgsig_to_commit(&new_data, "gpgsig ", &sig_sha1);
+	add_gpgsig_to_commit(&new_data, "gpgsig-sha256 ", &sig_sha256);
+
 	strbuf_addch(&new_data, '\n');
 	strbuf_addbuf(&new_data, &msg);
-	string_list_clear(&siglines, 1);
 	free(author);
 	free(committer);
-	free(sig_alg);
 	free(encoding);
 
 	if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))
diff --git a/gpg-interface.c b/gpg-interface.c
index 0896458de5..6f2d87475f 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)
 	return NULL;
 }
 
+const char *get_signature_format(const char *buf)
+{
+	struct gpg_format *format = get_format_by_sig(buf);
+	return format ? format->name : "unknown";
+}
+
+int valid_signature_format(const char *format)
+{
+       return (!!get_format_by_name(format) ||
+	       !strcmp(format, "unknown"));
+}
+
 void signature_check_clear(struct signature_check *sigc)
 {
 	FREE_AND_NULL(sigc->payload);
diff --git a/gpg-interface.h b/gpg-interface.h
index e09f12e8d0..60ddf8bbfa 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -47,6 +47,18 @@ struct signature_check {
 
 void signature_check_clear(struct signature_check *sigc);
 
+/*
+ * Return the format of the signature (like "openpgp", "x509", "ssh"
+ * or "unknown").
+ */
+const char *get_signature_format(const char *buf);
+
+/*
+ * Is the signature format valid (like "openpgp", "x509", "ssh" or
+ * "unknown")
+ */
+int valid_signature_format(const char *format);
+
 /*
  * Look at a GPG signed tag object.  If such a signature exists, store it in
  * signature and the signed content in payload.  Return 1 if a signature was
diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
index 76619765fc..46700dbc40 100755
--- a/t/t9350-fast-export.sh
+++ b/t/t9350-fast-export.sh
@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '
 test_expect_success GPG 'signed-commits=verbatim' '
 
 	git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
-	grep "^gpgsig sha" output &&
+	test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	(
 		cd new &&
@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '
 test_expect_success GPG 'signed-commits=warn-verbatim' '
 
 	git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
-	grep "^gpgsig sha" output &&
+	test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	test -s err &&
 	(
@@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '
 
 '
 
+test_expect_success GPGSM 'setup X.509 signed commit' '
+
+	git checkout -b x509-signing main &&
+	test_config gpg.format x509 &&
+	test_config user.signingkey $GIT_COMMITTER_EMAIL &&
+	echo "X.509 content" >file &&
+	git add file &&
+	git commit -S -m "X.509 signed commit" &&
+	X509_COMMIT=$(git rev-parse HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSM 'round-trip X.509 signed commit' '
+
+	git fast-export --signed-commits=verbatim x509-signing >output &&
+	test_grep -E "^gpgsig $GIT_DEFAULT_HASH x509" output &&
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/x509-signing >actual &&
+		grep "^gpgsig" actual &&
+		IMPORTED=$(git rev-parse refs/heads/x509-signing) &&
+		test $X509_COMMIT = $IMPORTED
+	) <output
+
+'
+
+test_expect_success GPGSSH 'setup SSH signed commit' '
+
+	git checkout -b ssh-signing main &&
+	test_config gpg.format ssh &&
+	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
+	echo "SSH content" >file &&
+	git add file &&
+	git commit -S -m "SSH signed commit" &&
+	SSH_COMMIT=$(git rev-parse HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSSH 'round-trip SSH signed commit' '
+
+	git fast-export --signed-commits=verbatim ssh-signing >output &&
+	test_grep -E "^gpgsig $GIT_DEFAULT_HASH ssh" output &&
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/ssh-signing >actual &&
+		grep "^gpgsig" actual &&
+		IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&
+		test $SSH_COMMIT = $IMPORTED
+	) <output
+
+'
+
 test_expect_success 'setup submodule' '
 
 	test_config_global protocol.file.allow always &&
@@ -905,4 +961,46 @@ test_expect_success 'fast-export handles --end-of-options' '
 	test_cmp expect actual
 '
 
+test_expect_success GPG 'setup a commit with dual signatures on its SHA-1 and SHA-256 formats' '
+	# Create a signed SHA-256 commit
+	git init --object-format=sha256 explicit-sha256 &&
+	git -C explicit-sha256 config extensions.compatObjectFormat sha1 &&
+	git -C explicit-sha256 checkout -b dual-signed &&
+	test_commit -C explicit-sha256 A &&
+	echo B >explicit-sha256/B &&
+	git -C explicit-sha256 add B &&
+	test_tick &&
+	git -C explicit-sha256 commit -S -m "signed" B &&
+	SHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&
+
+	# Create the corresponding SHA-1 commit
+	SHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&
+
+	# Check that the resulting SHA-1 commit has both signatures
+	echo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&
+	test_grep -E "^gpgsig " out &&
+	test_grep -E "^gpgsig-sha256 " out
+'
+
+test_expect_success GPG 'export and import of doubly signed commit' '
+	git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&
+	test_grep -E "^gpgsig sha1 openpgp" output &&
+	test_grep -E "^gpgsig sha256 openpgp" output &&
+
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/dual-signed >actual &&
+		test_grep -E "^gpgsig " actual &&
+		test_grep -E "^gpgsig-sha256 " actual &&
+		IMPORTED=$(git rev-parse refs/heads/dual-signed) &&
+		if test "$GIT_DEFAULT_HASH" = "sha1"
+		then
+			test $SHA1_B = $IMPORTED
+		else
+			test $SHA256_B = $IMPORTED
+		fi
+	) <output
+'
+
 test_done
-- 
2.50.0.174.gcf836c1ec7


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-08  5:03           ` Junio C Hamano
  2025-07-08  6:38             ` Patrick Steinhardt
@ 2025-07-08 10:17             ` Christian Couder
  1 sibling, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-08 10:17 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Tue, Jul 8, 2025 at 7:03 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > On Tue, Jul 8, 2025 at 12:58 AM Junio C Hamano <gitster@pobox.com> wrote:

> >> We haven't heard much after a few comments were posted on this
> >> latest round, since Elijah's
> >> <20250619133630.727274-1-christian.couder@gmail.com>; I understand
> >> that it would be the author's turn to respond (the response does not
> >> necessarily have to be with an updated iteration).  If so, let me
> >> mark the topic as Stalled in the draft of the latest issue of the
> >> "What's cooking" report.
> >
> > I will hopefully send a v5 later today.

I just sent it and replied to the pending reviews about v4.

> Thanks.
>
> By the way, I noticed that you often do not respond to reviews until
> the last minute, at the same time as when you send your next
> iteration, or even soon after doing so.

Yeah, right.

> That is quite different from how other contributors operate, i.e.
> respond and engage in discussions triggered by the reviews, and
> after people involved in discussion got an (even rough) idea of what
> the right next step would be, if not a total consensus, send the
> next iteration.
>
> I do not know which style is more efficient form of cooperation, but
> it somewhat makes my job harder, if I do not hear much _heartbeats_
> after I see review comments on the list.  I do not mind waiting for
> seeing the next round for quite a while---after all, any substantial
> (re)work takes time.  And responding to reviews may need thinking
> things through carefully, which may take some time, so I would not
> demand an immediate response, either.  But it would be nearly
> impossible to feel the current status of such a topic---a few review
> comments are seen, the author goes silent for a while, we cannot
> tell if the author is working on a new iteration or where the author
> and reviewers agree and disagree.

Sorry if it makes your job harder.

When I work on a number of different things, I alternate between
topics. Just after I send a new version of some series to the mailing
list, I usually start working on a different topic. These days for
example I alternate between this topic and the promisor-remote
capability topic. So it seems to me that if I were to respond to
reviews right away, I could be switching topics all the time if there
are discussions happening on several topics I work on.

I know I still have to switch often anyway between topics because I
might be pinged internally at GitLab about some issues or because
someone I mentor asks me a question privately, etc. And maybe for you
or others switching topics often is not an issue, but when topics are
quite complex I feel it makes it much harder for me to focus on what I
am doing. I don't think I am the only one in this case by the way.

> Also a review response that comes at the same time or immediately
> after a new iteration is already sent out makes it look like the
> author is refusing to continue discussion and reviewers are not
> welcome to make follow-up suggestions during such a discussion.

Sorry if it looks like this. I am not refusing any discussion or
follow up suggestions. As you say above, responding to reviews may
need thinking and often working to try things out, and often it seems
to me that I cannot really reply properly if I haven't worked enough
to try some ideas.

Let me take for example the v5 I just sent. It's only by researching
and trying different ideas without knowing if they would work that I
found (after a long time) a way to write a proper test with both a
SHA-1 and a SHA-256 signature on the same commit. It was the same for
using "$GIT_DEFAULT_HASH" instead of "sha(1|256)" in the tests.

So yeah, I could have replied early with "I will do it in v5." or "I
will try to do it in v5." or "Ok" or "I will think about it." to most
suggestions I got, but what would have really been the value of a
response with mostly those kinds of sentences in it?

> Instead, the next iteration comes as a fait accompli,

Even if I had replied with mostly "I will do it in v5." or "I will try
to do it in v5.", etc, to many suggestions, I could still have found
or decided for some reasons to actually implement something else and
use those reasons to justify it. Would it have been less of a fait
accompli?

> and makes it
> less useful to continue the review discussion on the previous round
> by responding to such a late response.

In my opinion the discussion can continue with more useful and higher
quality information, as I have worked significantly to think through
and try to implement the suggestions that were made or to research and
then often implement other solutions. Yeah, it doesn't continue on the
previous round, but hopefully the new round is better, so ...

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-08  6:38             ` Patrick Steinhardt
@ 2025-07-08 11:08               ` Christian Couder
  2025-07-08 16:38                 ` Junio C Hamano
  0 siblings, 1 reply; 65+ messages in thread
From: Christian Couder @ 2025-07-08 11:08 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: Junio C Hamano, git, Elijah Newren, Jeff King, brian m . carlson,
	Johannes Schindelin, Christian Couder

On Tue, Jul 8, 2025 at 8:38 AM Patrick Steinhardt <ps@pks.im> wrote:

> I agree with your points. Overall, a fast response cycle is key to good
> collaboration from my point of view. I think it not only makes your life
> as a maintainer easier, but it also makes the reviewer feel like they
> are being heard and is the prerequisite for good discussion.

I don't agree with "is the prerequisite for good discussion".  I think
it's perfectly possible to have good long running discussions even
when people's replies are delayed. And people can have vacations or
weekends or private issues or work on other things which can delay
some replies. And when people work significantly for a long time on a
topic before replying, I really think it can increase the quality of
the discussion.

Also if a contributor comes back with improved patches that try to
follow closely what a reviewer suggested, then I think it can (and
should) make a reviewer feel like they have really been heard better
than just a hollow reply right away followed later by less well
thought out patches.

This doesn't mean that I think there is no value in a fast response
cycle. But I think it depends a lot on the circumstances.

Yeah, for someone new in the community I think it can often help a
lot. And I encourage the contributors I mentor to respond soon.

When someone has been part of the community for a long time, I think
it's different though. It can happen, but it's much less likely that
they are going to disappear tomorrow or otherwise not follow up on
feedback they got from reviewers.

It doesn't mean that I think oldtimers should have some kind of
privilege, and yeah they should also try to give a good example. But
we should allow people to not always behave in a very formatted way.
As I mentioned above and in my reply to Junio, many things outside Git
development can happen, and people can behave differently, have
different preferences in the way they work, and especially not like or
want to switch topics very often for example to keep a better mental
focus on what they are currently doing.

> On our team's handbook page [1] we have the following couple of bullet
> points regarding how to respond to reviews:

Yeah, I think they are likely to be good for newcomers.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-08 11:08               ` Christian Couder
@ 2025-07-08 16:38                 ` Junio C Hamano
  2025-07-09  0:19                   ` Christian Couder
  0 siblings, 1 reply; 65+ messages in thread
From: Junio C Hamano @ 2025-07-08 16:38 UTC (permalink / raw)
  To: Christian Couder
  Cc: Patrick Steinhardt, git, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> Also if a contributor comes back with improved patches that try to
> follow closely what a reviewer suggested, then I think it can (and
> should) make a reviewer feel like they have really been heard better
> than just a hollow reply right away followed later by less well
> thought out patches.

That is kind of "better late than never".  I would expect better
than that from more senior prominent contributors ;-)

And I totally agree with you that reviews often deserve very well
reasoned responses, which take time to prepare; a response that
comes as spinal reflex without much thought is often not very
useful.

It really depends on the definition of "fast" in "fast response".

If we need a week to come up with a newer iteration, it would be
fair to expect that we can say something like "I agree, I'll fix",
"I am not convinced because ...", "I am skeptical but let me first
see how it pans out", etc. by day #2 or #3, wouldn't it?  Upon
recieving a response at the same time or soon after an updated
iteration was sent, especially when the response is "no, I do not
think so", what is the reviewer expected to do?  Saying "you may not
think so but here is another point that may make you reconsider"
would be too late, so it would actively discourage continued
discussion.

> It doesn't mean that I think oldtimers should have some kind of
> privilege, and yeah they should also try to give a good example. But
> we should allow people to not always behave in a very formatted way.

Old timers learn from experience how other old timers operate ;-)
and I have learned to ignore the usual signal when anticipating what
your next iteration may look like (in other words, interim responses
or lack thereof is usually a good signal for most developers, but
not for you---you tend to come back with your next iteration without
much interim interactions).  But other contributors shouldn't be
forced to.  That is what we need some community norm for.

>> On our team's handbook page [1] we have the following couple of bullet
>> points regarding how to respond to reviews:
>
> Yeah, I think they are likely to be good for newcomers.

The handbook here is gitlab's team handbook, and it may not apply to
open source Git development community, but "this rule applies only
to newcomers, I am above that rule" is the same thing as saying
"oldtimers like me should have some kind of privilege".  I do not
know what to think about this and what you said above.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v5] fast-(import|export): improve on commit signature output format
  2025-07-08  9:17       ` [PATCH v5] " Christian Couder
@ 2025-07-08 21:58         ` Junio C Hamano
  2025-07-08 23:08         ` Elijah Newren
  2025-07-09 14:12         ` [PATCH v6] " Christian Couder
  2 siblings, 0 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-07-08 21:58 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
> signed-commits, 2025-03-10), added support for signed commits to
> fast-export and fast-import.
> ...
> It could be even better to be able to import more than one signature
> on the SHA-1 object and on the SHA-256 object, but other parts of
> Git don't handle that well for now, so this is left for future
> improvements.
>
> Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
> Helped-by: Elijah Newren <newren@gmail.com>
> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
> ---
>
> This v5 is similar in spirit to v4, especially the format of the
> "gpgsig ..." command didn't change, but there are a number of
> improvements:
>
>   - All the signatures are now exported. On the import side, still at
>     most one signature for SHA-1 and one for SHA-256 are imported (and
>     a warning is still emitted for additional signatures) though.
>
>   - The code makes sure each signature ends with a LF character. While
>     this is not mandatory, it is encouraged and makes the output more
>     human readable and 'grep'able.
>
>   - A test with both a SHA-1 and a SHA-256 signature on the same
>     commit has been added.
>
>   - Some tests check that either "sha1" or "sha256" is in the "gpgsig
>     ..."  command instead of matching "sha(1|256)".
>
>   - The format of the "gpgsig ..." command is better explained both in
>     the commit message and in the fast-import documentation.
>
>   - There are some typo fixes, lines wrapped, and a few other such
>     small changes.

Thanks.  Will queue.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v5] fast-(import|export): improve on commit signature output format
  2025-07-08  9:17       ` [PATCH v5] " Christian Couder
  2025-07-08 21:58         ` Junio C Hamano
@ 2025-07-08 23:08         ` Elijah Newren
  2025-07-09  0:03           ` Junio C Hamano
  2025-07-09 10:15           ` Christian Couder
  2025-07-09 14:12         ` [PATCH v6] " Christian Couder
  2 siblings, 2 replies; 65+ messages in thread
From: Elijah Newren @ 2025-07-08 23:08 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Tue, Jul 8, 2025 at 2:18 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
> A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
> signed-commits, 2025-03-10), added support for signed commits to
> fast-export and fast-import.
>
> When a signed commit is processed, fast-export can output either
> "gpgsig sha1" or "gpgsig sha256" depending on whether the signed
> commit uses the SHA-1 or SHA-256 Git object format.
>
> However, this implementation has a number of limitations:
>
>   - the output format was not properly described in the documentation,
>   - the output format is not very informative as it doesn't even say
>     if the signature is an OpenPGP, an SSH, or an X509 signature,
>   - the implementation doesn't support having both one signature on
>     the SHA-1 object and one on the SHA-256 object.
>
> Let's improve on these limitations by improving fast-export and
> fast-import so that:
>
>   - all the signatures are exported,
>   - at most one signature on the SHA-1 object and one on the SHA-256
>     are imported,
>   - if there is more than one signature on the SHA-1 object or on
>     the SHA-256 object, fast-import emits a warning for each
>     additional signature,
>   - the output format is "gpgsig <git-hash-algo> <signature-format>",
>     where <git-hash-algo> is the Git object format as before, and
>     <signature-format> is the signature type ("openpgp", "x509",
>     "ssh" or "unknown"),
>   - the output is properly documented.
>
> About the output format:
>
>   - <git-hash-algo> allows to know which representation of the commit
>     was signed (the SHA-1 or the SHA-256 version) which helps with
>     both signature verification and interoperability between repos
>     with different hash functions,
>
>   - <signature-format> helps tools that process the fast-export
>     stream, so they don't have to parse the ASCII armor to identify
>     the signature type.
>
> It could be even better to be able to import more than one signature
> on the SHA-1 object and on the SHA-256 object, but other parts of
> Git don't handle that well for now, so this is left for future
> improvements.
>
> Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
> Helped-by: Elijah Newren <newren@gmail.com>
> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
> ---
>
> This v5 is similar in spirit to v4, especially the format of the
> "gpgsig ..." command didn't change, but there are a number of
> improvements:
>
>   - All the signatures are now exported. On the import side, still at
>     most one signature for SHA-1 and one for SHA-256 are imported (and
>     a warning is still emitted for additional signatures) though.
>
>   - The code makes sure each signature ends with a LF character. While
>     this is not mandatory, it is encouraged and makes the output more
>     human readable and 'grep'able.
>
>   - A test with both a SHA-1 and a SHA-256 signature on the same
>     commit has been added.
>
>   - Some tests check that either "sha1" or "sha256" is in the "gpgsig
>     ..."  command instead of matching "sha(1|256)".
>
>   - The format of the "gpgsig ..." command is better explained both in
>     the commit message and in the fast-import documentation.
>
>   - There are some typo fixes, lines wrapped, and a few other such
>     small changes.
>
> Thanks to brian, Elijah and Junio who commented on the v1 and v2.
>
> CI tests:
>
> They have all passed:
>
> https://github.com/chriscool/git/actions/runs/16136587558
>
> Range-diff with v4:
>
> 1:  b3d8b13c0e ! 1:  5791617d7d fast-(import|export): improve on commit signature output format
>     @@ Commit message
>          Let's improve on these limitations by improving fast-export and
>          fast-import so that:
>
>     -      - both one signature on the SHA-1 object and one on the SHA-256
>     -        object can be exported and imported,
>     +      - all the signatures are exported,
>     +      - at most one signature on the SHA-1 object and one on the SHA-256
>     +        are imported,
>            - if there is more than one signature on the SHA-1 object or on
>     -        the SHA-256 object, a warning is emitted,
>     +        the SHA-256 object, fast-import emits a warning for each
>     +        additional signature,
>            - the output format is "gpgsig <git-hash-algo> <signature-format>",
>              where <git-hash-algo> is the Git object format as before, and
>              <signature-format> is the signature type ("openpgp", "x509",
>     -        "ssh" or "unknown",
>     +        "ssh" or "unknown"),
>            - the output is properly documented.
>
>     -    Note that it could be even better to be able to export and import
>     -    more than one signature on the SHA-1 object and on the SHA-256
>     -    object, but other parts of Git don't handle that well for now, so
>     -    this is left for future improvements.
>     +    About the output format:
>     +
>     +      - <git-hash-algo> allows to know which representation of the commit
>     +        was signed (the SHA-1 or the SHA-256 version) which helps with
>     +        both signature verification and interoperability between repos
>     +        with different hash functions,
>     +
>     +      - <signature-format> helps tools that process the fast-export
>     +        stream, so they don't have to parse the ASCII armor to identify
>     +        the signature type.
>     +
>     +    It could be even better to be able to import more than one signature
>     +    on the SHA-1 object and on the SHA-256 object, but other parts of
>     +    Git don't handle that well for now, so this is left for future
>     +    improvements.
>
>          Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
>          Helped-by: Elijah Newren <newren@gmail.com>
>     @@ Documentation/git-fast-export.adoc: resulting tag will have an invalid signature
>      +`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit
>      +starts with `gpgsig sha256 ssh`.
>      ++
>     -+Currently for a given commit, at most one signature for the SHA-1
>     -+object and one signature for the SHA-256 object are exported, each
>     -+with their respective <git-hash-algo> identifier. A warning is
>     -+emitted for each additional signature found.
>     ++While all the signatures of a commit are exported, an importer may
>     ++choose to accept only some of them. For example
>     ++linkgit:git-fast-import[1] currently stores at most one signature per
>     ++Git hash algorithm in each commit.
>      ++
>       NOTE: This is highly experimental and the format of the data stream may
>       change in the future without compatibility guarantees.
>     @@ Documentation/git-fast-import.adoc: their syntax.
>      +The `gpgsig` command takes two arguments:
>      +
>      +* `<git-hash-algo>` specifies which Git object format this signature
>     -+  applies to, either `sha1` or `sha256`.
>     ++  applies to, either `sha1` or `sha256`. This allows to know which
>     ++  representation of the commit was signed (the SHA-1 or the SHA-256
>     ++  version) which helps with both signature verification and
>     ++  interoperability between repos with different hash functions.
>      +
>      +* `<signature-format>` specifies the type of signature, such as
>     -+  `openpgp`, `x509`, `ssh`, or `unknown`.
>     ++  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for
>     ++  tools that process the stream, so they don't have to parse the ASCII
>     ++  armor to identify the signature type.
>      +
>      +A commit may have at most one signature for the SHA-1 object format
>      +(stored in the "gpgsig" header) and one for the SHA-256 object format
>     @@ builtin/fast-export.c: static const char *find_commit_multiline_header(const cha
>      +  if (!signature)
>      +          return;
>      +
>     -+  printf("gpgsig %s %s\ndata %u\n%s",
>     ++  printf("gpgsig %s %s\ndata %u\n%s\n",
>      +         object_hash,
>      +         get_signature_format(signature),
>      +         (unsigned)strlen(signature),
>      +         signature);
>      +}
>      +
>     -+static void warn_on_extra_sig(const char **pos, struct commit *commit, int is_sha1)
>     ++static const char *append_signatures_for_header(struct string_list *signatures,
>     ++                                          const char *pos,
>     ++                                          const char *header,
>     ++                                          const char *object_hash)
>      +{
>     -+  const char *header = is_sha1 ? "gpgsig" : "gpgsig-sha256";
>     -+  const char *extra_sig = find_commit_multiline_header(*pos + 1, header, pos);
>     -+  if (extra_sig) {
>     -+          const char *hash = is_sha1 ? "SHA-1" : "SHA-256";
>     -+          warning("more than one %s signature found on commit %s, using only the first one",
>     -+                  hash, oid_to_hex(&commit->object.oid));
>     -+          free((char *)extra_sig);
>     ++  const char *signature;
>     ++  const char *start = pos;
>     ++  const char *end = pos;
>     ++
>     ++  while ((signature = find_commit_multiline_header(start + 1,
>     ++                                                   header,
>     ++                                                   &end))) {
>     ++          string_list_append(signatures, signature)->util = (void *)object_hash;
>     ++          free((char *)signature);
>     ++          start = end;
>      +  }
>     ++
>     ++  return end;
>      +}
>      +
>       static void handle_commit(struct commit *commit, struct rev_info *rev,
>     @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r
>         const char *encoding = NULL;
>         size_t encoding_len;
>      -  const char *signature_alg = NULL, *signature = NULL;
>     -+  const char *sig_sha1 = NULL;
>     -+  const char *sig_sha256 = NULL;
>     ++  struct string_list signatures = STRING_LIST_INIT_DUP;
>         const char *message;
>         char *reencoded = NULL;
>         struct commit_list *p;
>     @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r
>      -                  signature_alg = "sha1";
>      -          else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
>      -                  signature_alg = "sha256";
>     -+          const char *sig_cursor = commit_buffer_cursor;
>     -+          const char *after_sha1 = commit_buffer_cursor;
>     -+          const char *after_sha256 = commit_buffer_cursor;
>     -+
>     -+          /*
>     -+           * Find the first signature for each hash algorithm.
>     -+           * The searches must start from the same position.
>     -+           */
>     -+          sig_sha1 = find_commit_multiline_header(sig_cursor + 1,
>     -+                                                  "gpgsig",
>     -+                                                  &after_sha1);
>     -+          sig_sha256 = find_commit_multiline_header(sig_cursor + 1,
>     -+                                                    "gpgsig-sha256",
>     -+                                                    &after_sha256);
>     -+
>     -+          /* Warn on any additional signatures, as they will be ignored. */
>     -+          if (sig_sha1)
>     -+                  warn_on_extra_sig(&after_sha1, commit, 1);
>     -+          if (sig_sha256)
>     -+                  warn_on_extra_sig(&after_sha256, commit, 0);
>     -+
>     ++          const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,
>     ++                                                                "gpgsig", "sha1");
>     ++          const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,
>     ++                                                                  "gpgsig-sha256", "sha256");
>      +          commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
>         }
>
>     @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r
>                (int)(author_end - author), author,
>                (int)(committer_end - committer), committer);
>      -  if (signature) {
>     -+  if (sig_sha1 || sig_sha256) {
>     ++  if (signatures.nr) {
>                 switch (signed_commit_mode) {
>                 case SIGN_ABORT:
>                         die("encountered signed commit %s; use "
>     -@@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct rev_info *rev,
>     -                           oid_to_hex(&commit->object.oid));
>     +                       "--signed-commits=<mode> to handle it",
>     +                       oid_to_hex(&commit->object.oid));
>     +           case SIGN_WARN_VERBATIM:
>     +-                  warning("exporting signed commit %s",
>     +-                          oid_to_hex(&commit->object.oid));
>     ++                  warning("exporting %"PRIuMAX" signature(s) for commit %s",
>     ++                          (uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));
>                         /* fallthru */
>                 case SIGN_VERBATIM:
>      -                  printf("gpgsig %s\ndata %u\n%s",
>      -                         signature_alg,
>      -                         (unsigned)strlen(signature),
>      -                         signature);
>     -+                  print_signature(sig_sha1, "sha1");
>     -+                  print_signature(sig_sha256, "sha256");
>     ++                  for (size_t i = 0; i < signatures.nr; i++) {
>     ++                          struct string_list_item *item = &signatures.items[i];
>     ++                          print_signature(item->string, item->util);
>     ++                  }
>                         break;
>                 case SIGN_WARN_STRIP:
>      -                  warning("stripping signature from commit %s",
>     @@ builtin/fast-export.c: static void handle_commit(struct commit *commit, struct r
>                         break;
>                 }
>      -          free((char *)signature);
>     -+          free((char *)sig_sha1);
>     -+          free((char *)sig_sha256);
>     ++          string_list_clear(&signatures, 0);
>         }
>         if (!reencoded && encoding)
>                 printf("encoding %.*s\n", (int)encoding_len, encoding);
>     @@ builtin/fast-import.c: static struct hash_list *parse_merge(unsigned int *count)
>      +                      const char *hash_type)
>      +{
>      +  if (stored_sig->hash_algo) {
>     -+          warning("Multiple %s signatures found, ignoring additional signature",
>     ++          warning("multiple %s signatures found, "
>     ++                  "ignoring additional signature",
>      +                  hash_type);
>      +          strbuf_release(&new_sig->data);
>      +          free(new_sig->hash_algo);
>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=abort' '
>
>         git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
>      -  grep "^gpgsig sha" output &&
>     -+  test_grep -E "^gpgsig sha(1|256) openpgp" output &&
>     ++  test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
>         grep "encoding ISO-8859-1" output &&
>         (
>                 cd new &&
>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=verbatim' '
>
>         git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
>      -  grep "^gpgsig sha" output &&
>     -+  test_grep -E "^gpgsig sha(1|256) openpgp" output &&
>     ++  test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
>         grep "encoding ISO-8859-1" output &&
>         test -s err &&
>         (
>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '
>      +test_expect_success GPGSM 'round-trip X.509 signed commit' '
>      +
>      +  git fast-export --signed-commits=verbatim x509-signing >output &&
>     -+  test_grep -E "^gpgsig sha(1|256) x509" output &&
>     ++  test_grep -E "^gpgsig $GIT_DEFAULT_HASH x509" output &&
>      +  (
>      +          cd new &&
>      +          git fast-import &&
>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '
>      +test_expect_success GPGSSH 'round-trip SSH signed commit' '
>      +
>      +  git fast-export --signed-commits=verbatim ssh-signing >output &&
>     -+  test_grep -E "^gpgsig sha(1|256) ssh" output &&
>     ++  test_grep -E "^gpgsig $GIT_DEFAULT_HASH ssh" output &&
>      +  (
>      +          cd new &&
>      +          git fast-import &&
>     @@ t/t9350-fast-export.sh: test_expect_success GPG 'signed-commits=warn-strip' '
>       test_expect_success 'setup submodule' '
>
>         test_config_global protocol.file.allow always &&
>     +@@ t/t9350-fast-export.sh: test_expect_success 'fast-export handles --end-of-options' '
>     +   test_cmp expect actual
>     + '
>     +
>     ++test_expect_success GPG 'setup a commit with dual signatures on its SHA-1 and SHA-256 formats' '
>     ++  # Create a signed SHA-256 commit
>     ++  git init --object-format=sha256 explicit-sha256 &&
>     ++  git -C explicit-sha256 config extensions.compatObjectFormat sha1 &&
>     ++  git -C explicit-sha256 checkout -b dual-signed &&
>     ++  test_commit -C explicit-sha256 A &&
>     ++  echo B >explicit-sha256/B &&
>     ++  git -C explicit-sha256 add B &&
>     ++  test_tick &&
>     ++  git -C explicit-sha256 commit -S -m "signed" B &&
>     ++  SHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&
>     ++
>     ++  # Create the corresponding SHA-1 commit
>     ++  SHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&
>     ++
>     ++  # Check that the resulting SHA-1 commit has both signatures
>     ++  echo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&
>     ++  test_grep -E "^gpgsig " out &&
>     ++  test_grep -E "^gpgsig-sha256 " out
>     ++'
>     ++
>     ++test_expect_success GPG 'export and import of doubly signed commit' '
>     ++  git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&
>     ++  test_grep -E "^gpgsig sha1 openpgp" output &&
>     ++  test_grep -E "^gpgsig sha256 openpgp" output &&
>     ++
>     ++  (
>     ++          cd new &&
>     ++          git fast-import &&
>     ++          git cat-file commit refs/heads/dual-signed >actual &&
>     ++          test_grep -E "^gpgsig " actual &&
>     ++          test_grep -E "^gpgsig-sha256 " actual &&
>     ++          IMPORTED=$(git rev-parse refs/heads/dual-signed) &&
>     ++          if test "$GIT_DEFAULT_HASH" = "sha1"
>     ++          then
>     ++                  test $SHA1_B = $IMPORTED
>     ++          else
>     ++                  test $SHA256_B = $IMPORTED
>     ++          fi
>     ++  ) <output
>     ++'
>     ++
>     + test_done
>
>
>  Documentation/git-fast-export.adoc |  17 +++++
>  Documentation/git-fast-import.adoc |  36 +++++++--
>  builtin/fast-export.c              |  62 +++++++++++----
>  builtin/fast-import.c              | 118 +++++++++++++++++++++++------
>  gpg-interface.c                    |  12 +++
>  gpg-interface.h                    |  12 +++
>  t/t9350-fast-export.sh             | 102 ++++++++++++++++++++++++-
>  7 files changed, 315 insertions(+), 44 deletions(-)
>
> diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
> index 43bbb4f63c..297b57bb2e 100644
> --- a/Documentation/git-fast-export.adoc
> +++ b/Documentation/git-fast-export.adoc
> @@ -50,6 +50,23 @@ resulting tag will have an invalid signature.
>         is the same as how earlier versions of this command without
>         this option behaved.
>  +
> +When exported, a signature starts with:
> ++
> +gpgsig <git-hash-algo> <signature-format>
> ++
> +where <git-hash-algo> is the Git object hash so either "sha1" or
> +"sha256", and <signature-format> is the signature type, so "openpgp",
> +"x509", "ssh" or "unknown".
> ++
> +For example, an OpenPGP signature on a SHA-1 commit starts with
> +`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit
> +starts with `gpgsig sha256 ssh`.
> ++
> +While all the signatures of a commit are exported, an importer may
> +choose to accept only some of them. For example
> +linkgit:git-fast-import[1] currently stores at most one signature per
> +Git hash algorithm in each commit.
> ++
>  NOTE: This is highly experimental and the format of the data stream may
>  change in the future without compatibility guarantees.
>
> diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
> index 250d866652..89dec1108f 100644
> --- a/Documentation/git-fast-import.adoc
> +++ b/Documentation/git-fast-import.adoc
> @@ -445,7 +445,7 @@ one).
>         original-oid?
>         ('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
>         'committer' (SP <name>)? SP LT <email> GT SP <when> LF
> -       ('gpgsig' SP <alg> LF data)?
> +       ('gpgsig' SP <algo> SP <format> LF data)?
>         ('encoding' SP <encoding> LF)?
>         data
>         ('from' SP <commit-ish> LF)?
> @@ -518,13 +518,37 @@ their syntax.
>  ^^^^^^^^
>
>  The optional `gpgsig` command is used to include a PGP/GPG signature
> -that signs the commit data.
> +or other cryptographic signature that signs the commit data.
>
> -Here <alg> specifies which hashing algorithm is used for this
> -signature, either `sha1` or `sha256`.
> +....
> +       'gpgsig' SP <git-hash-algo> SP <signature-format> LF
> +       data

Should the `data` be moved to the line above, just to make it clear
it's associated with it?  (Similar to the first line you changed in
git-fast-import.adoc?)

> +....
> +
> +The `gpgsig` command takes two arguments:
> +
> +* `<git-hash-algo>` specifies which Git object format this signature
> +  applies to, either `sha1` or `sha256`. This allows to know which
> +  representation of the commit was signed (the SHA-1 or the SHA-256
> +  version) which helps with both signature verification and
> +  interoperability between repos with different hash functions.

Should there also be a note added that fast-import is limited on what
signatures it can verify if extensions.compatObjectFormat is not set?

> +
> +* `<signature-format>` specifies the type of signature, such as
> +  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for
> +  tools that process the stream, so they don't have to parse the ASCII
> +  armor to identify the signature type.
> +
> +A commit may have at most one signature for the SHA-1 object format
> +(stored in the "gpgsig" header) and one for the SHA-256 object format
> +(stored in the "gpgsig-sha256" header).
> +
> +See below for a detailed description of the `data` command which
> +contains the raw signature data.
> +
> +Signatures are not yet checked in the current implementation though.

...or maybe that extra note could be added as a parenthetical comment
here for now?

> -NOTE: This is highly experimental and the format of the data stream may
> -change in the future without compatibility guarantees.
> +NOTE: This is highly experimental and the format of the `gpgsig`
> +command may change in the future without compatibility guarantees.
>
>  `encoding`
>  ^^^^^^^^^^
> diff --git a/builtin/fast-export.c b/builtin/fast-export.c
> index fcf6b00d5f..7b4e6a6e41 100644
> --- a/builtin/fast-export.c
> +++ b/builtin/fast-export.c
> @@ -29,6 +29,7 @@
>  #include "quote.h"
>  #include "remote.h"
>  #include "blob.h"
> +#include "gpg-interface.h"
>
>  static const char *const fast_export_usage[] = {
>         N_("git fast-export [<rev-list-opts>]"),
> @@ -652,6 +653,38 @@ static const char *find_commit_multiline_header(const char *msg,
>         return strbuf_detach(&val, NULL);
>  }
>
> +static void print_signature(const char *signature, const char *object_hash)
> +{
> +       if (!signature)
> +               return;
> +
> +       printf("gpgsig %s %s\ndata %u\n%s\n",
> +              object_hash,
> +              get_signature_format(signature),
> +              (unsigned)strlen(signature),
> +              signature);
> +}
> +
> +static const char *append_signatures_for_header(struct string_list *signatures,
> +                                               const char *pos,
> +                                               const char *header,
> +                                               const char *object_hash)
> +{
> +       const char *signature;
> +       const char *start = pos;
> +       const char *end = pos;
> +
> +       while ((signature = find_commit_multiline_header(start + 1,
> +                                                        header,
> +                                                        &end))) {
> +               string_list_append(signatures, signature)->util = (void *)object_hash;
> +               free((char *)signature);
> +               start = end;
> +       }
> +
> +       return end;
> +}
> +
>  static void handle_commit(struct commit *commit, struct rev_info *rev,
>                           struct string_list *paths_of_changed_objects)
>  {
> @@ -660,7 +693,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
>         const char *author, *author_end, *committer, *committer_end;
>         const char *encoding = NULL;
>         size_t encoding_len;
> -       const char *signature_alg = NULL, *signature = NULL;
> +       struct string_list signatures = STRING_LIST_INIT_DUP;
>         const char *message;
>         char *reencoded = NULL;
>         struct commit_list *p;
> @@ -700,10 +733,11 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
>         }
>
>         if (*commit_buffer_cursor == '\n') {
> -               if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
> -                       signature_alg = "sha1";
> -               else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
> -                       signature_alg = "sha256";
> +               const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,
> +                                                                     "gpgsig", "sha1");
> +               const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,
> +                                                                       "gpgsig-sha256", "sha256");
> +               commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
>         }
>
>         message = strstr(commit_buffer_cursor, "\n\n");
> @@ -769,30 +803,30 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
>         printf("%.*s\n%.*s\n",
>                (int)(author_end - author), author,
>                (int)(committer_end - committer), committer);
> -       if (signature) {
> +       if (signatures.nr) {
>                 switch (signed_commit_mode) {
>                 case SIGN_ABORT:
>                         die("encountered signed commit %s; use "
>                             "--signed-commits=<mode> to handle it",
>                             oid_to_hex(&commit->object.oid));
>                 case SIGN_WARN_VERBATIM:
> -                       warning("exporting signed commit %s",
> -                               oid_to_hex(&commit->object.oid));
> +                       warning("exporting %"PRIuMAX" signature(s) for commit %s",
> +                               (uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));
>                         /* fallthru */
>                 case SIGN_VERBATIM:
> -                       printf("gpgsig %s\ndata %u\n%s",
> -                              signature_alg,
> -                              (unsigned)strlen(signature),
> -                              signature);
> +                       for (size_t i = 0; i < signatures.nr; i++) {
> +                               struct string_list_item *item = &signatures.items[i];
> +                               print_signature(item->string, item->util);
> +                       }
>                         break;
>                 case SIGN_WARN_STRIP:
> -                       warning("stripping signature from commit %s",
> +                       warning("stripping signature(s) from commit %s",

Nice catch; that's the kind of thing that could be easily overlooked.

>                                 oid_to_hex(&commit->object.oid));
>                         /* fallthru */
>                 case SIGN_STRIP:
>                         break;
>                 }
> -               free((char *)signature);
> +               string_list_clear(&signatures, 0);
>         }
>         if (!reencoded && encoding)
>                 printf("encoding %.*s\n", (int)encoding_len, encoding);
> diff --git a/builtin/fast-import.c b/builtin/fast-import.c
> index b2839c5f43..332073d0f6 100644
> --- a/builtin/fast-import.c
> +++ b/builtin/fast-import.c
> @@ -29,6 +29,7 @@
>  #include "commit-reach.h"
>  #include "khash.h"
>  #include "date.h"
> +#include "gpg-interface.h"
>
>  #define PACK_ID_BITS 16
>  #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
> @@ -2718,15 +2719,87 @@ static struct hash_list *parse_merge(unsigned int *count)
>         return list;
>  }
>
> +struct signature_data {
> +       char *hash_algo;      /* "sha1" or "sha256" */
> +       char *sig_format;     /* "openpgp", "x509", "ssh", "unknown" */
> +       struct strbuf data;   /* The actual signature data */
> +};
> +
> +static void parse_one_signature(struct signature_data *sig, const char *v)
> +{
> +       char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */
> +       char *space = strchr(args, ' ');
> +
> +       if (!space)
> +               die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
> +                   "got 'gpgsig %s'", args);
> +       *space++ = '\0';
> +
> +       sig->hash_algo = args;
> +       sig->sig_format = space;

Really minor nitpick, but it might be clearer to pre-increment space
here than to increment it above.

> +
> +       /* Remove any trailing newline from format */
> +       space = strchr(sig->sig_format, '\n');
> +       if (space)
> +               *space = '\0';
> +
> +       /* Validate hash algorithm */
> +       if (strcmp(sig->hash_algo, "sha1") &&
> +           strcmp(sig->hash_algo, "sha256"))
> +               die("Unknown git hash algorithm in gpgsig: '%s'", sig->hash_algo);
> +
> +       /* Validate signature format */
> +       if (!valid_signature_format(sig->sig_format))
> +               die("Invalid signature format in gpgsig: '%s'", sig->sig_format);
> +       if (!strcmp(sig->sig_format, "unknown"))
> +               warning("'unknown' signature format in gpgsig");
> +
> +       /* Read signature data */
> +       read_next_command();
> +       parse_data(&sig->data, 0, NULL);
> +}
> +
> +static void add_gpgsig_to_commit(struct strbuf *commit_data,
> +                                const char *header,
> +                                struct signature_data *sig)
> +{
> +       struct string_list siglines = STRING_LIST_INIT_NODUP;
> +
> +       if (!sig->hash_algo)
> +               return;
> +
> +       strbuf_addstr(commit_data, header);
> +       string_list_split_in_place(&siglines, sig->data.buf, "\n", -1);
> +       strbuf_add_separated_string_list(commit_data, "\n ", &siglines);
> +       strbuf_addch(commit_data, '\n');
> +       string_list_clear(&siglines, 1);
> +       strbuf_release(&sig->data);
> +       free(sig->hash_algo);
> +}
> +
> +static void store_signature(struct signature_data *stored_sig,
> +                           struct signature_data *new_sig,
> +                           const char *hash_type)
> +{
> +       if (stored_sig->hash_algo) {
> +               warning("multiple %s signatures found, "
> +                       "ignoring additional signature",
> +                       hash_type);
> +               strbuf_release(&new_sig->data);
> +               free(new_sig->hash_algo);
> +       } else {
> +               *stored_sig = *new_sig;
> +       }
> +}
> +
>  static void parse_new_commit(const char *arg)
>  {
> -       static struct strbuf sig = STRBUF_INIT;
>         static struct strbuf msg = STRBUF_INIT;
> -       struct string_list siglines = STRING_LIST_INIT_NODUP;
> +       struct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };
> +       struct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };
>         struct branch *b;
>         char *author = NULL;
>         char *committer = NULL;
> -       char *sig_alg = NULL;
>         char *encoding = NULL;
>         struct hash_list *merge_list = NULL;
>         unsigned int merge_count;
> @@ -2750,13 +2823,23 @@ static void parse_new_commit(const char *arg)
>         }
>         if (!committer)
>                 die("Expected committer but didn't get one");
> -       if (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
> -               sig_alg = xstrdup(v);
> -               read_next_command();
> -               parse_data(&sig, 0, NULL);
> +
> +       /* Process signatures (up to 2: one "sha1" and one "sha256") */
> +       while (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
> +               struct signature_data sig = { NULL, NULL, STRBUF_INIT };
> +
> +               parse_one_signature(&sig, v);
> +
> +               if (!strcmp(sig.hash_algo, "sha1"))
> +                       store_signature(&sig_sha1, &sig, "SHA-1");
> +               else if (!strcmp(sig.hash_algo, "sha256"))
> +                       store_signature(&sig_sha256, &sig, "SHA-256");
> +               else
> +                       BUG("parse_one_signature() returned unknown hash algo");
> +
>                 read_next_command();
> -       } else
> -               strbuf_setlen(&sig, 0);
> +       }
> +
>         if (skip_prefix(command_buf.buf, "encoding ", &v)) {
>                 encoding = xstrdup(v);
>                 read_next_command();
> @@ -2830,23 +2913,14 @@ static void parse_new_commit(const char *arg)
>                 strbuf_addf(&new_data,
>                         "encoding %s\n",
>                         encoding);
> -       if (sig_alg) {
> -               if (!strcmp(sig_alg, "sha1"))
> -                       strbuf_addstr(&new_data, "gpgsig ");
> -               else if (!strcmp(sig_alg, "sha256"))
> -                       strbuf_addstr(&new_data, "gpgsig-sha256 ");
> -               else
> -                       die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
> -               string_list_split_in_place(&siglines, sig.buf, "\n", -1);
> -               strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
> -               strbuf_addch(&new_data, '\n');
> -       }
> +
> +       add_gpgsig_to_commit(&new_data, "gpgsig ", &sig_sha1);
> +       add_gpgsig_to_commit(&new_data, "gpgsig-sha256 ", &sig_sha256);
> +
>         strbuf_addch(&new_data, '\n');
>         strbuf_addbuf(&new_data, &msg);
> -       string_list_clear(&siglines, 1);
>         free(author);
>         free(committer);
> -       free(sig_alg);
>         free(encoding);
>
>         if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))
> diff --git a/gpg-interface.c b/gpg-interface.c
> index 0896458de5..6f2d87475f 100644
> --- a/gpg-interface.c
> +++ b/gpg-interface.c
> @@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)
>         return NULL;
>  }
>
> +const char *get_signature_format(const char *buf)
> +{
> +       struct gpg_format *format = get_format_by_sig(buf);
> +       return format ? format->name : "unknown";
> +}
> +
> +int valid_signature_format(const char *format)
> +{
> +       return (!!get_format_by_name(format) ||
> +              !strcmp(format, "unknown"));
> +}
> +
>  void signature_check_clear(struct signature_check *sigc)
>  {
>         FREE_AND_NULL(sigc->payload);
> diff --git a/gpg-interface.h b/gpg-interface.h
> index e09f12e8d0..60ddf8bbfa 100644
> --- a/gpg-interface.h
> +++ b/gpg-interface.h
> @@ -47,6 +47,18 @@ struct signature_check {
>
>  void signature_check_clear(struct signature_check *sigc);
>
> +/*
> + * Return the format of the signature (like "openpgp", "x509", "ssh"
> + * or "unknown").
> + */
> +const char *get_signature_format(const char *buf);
> +
> +/*
> + * Is the signature format valid (like "openpgp", "x509", "ssh" or
> + * "unknown")
> + */
> +int valid_signature_format(const char *format);
> +
>  /*
>   * Look at a GPG signed tag object.  If such a signature exists, store it in
>   * signature and the signed content in payload.  Return 1 if a signature was
> diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
> index 76619765fc..46700dbc40 100755
> --- a/t/t9350-fast-export.sh
> +++ b/t/t9350-fast-export.sh
> @@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '
>  test_expect_success GPG 'signed-commits=verbatim' '
>
>         git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
> -       grep "^gpgsig sha" output &&
> +       test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
>         grep "encoding ISO-8859-1" output &&
>         (
>                 cd new &&
> @@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '
>  test_expect_success GPG 'signed-commits=warn-verbatim' '
>
>         git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
> -       grep "^gpgsig sha" output &&
> +       test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
>         grep "encoding ISO-8859-1" output &&
>         test -s err &&
>         (
> @@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '
>
>  '
>
> +test_expect_success GPGSM 'setup X.509 signed commit' '
> +
> +       git checkout -b x509-signing main &&
> +       test_config gpg.format x509 &&
> +       test_config user.signingkey $GIT_COMMITTER_EMAIL &&
> +       echo "X.509 content" >file &&
> +       git add file &&
> +       git commit -S -m "X.509 signed commit" &&
> +       X509_COMMIT=$(git rev-parse HEAD) &&
> +       git checkout main
> +
> +'
> +
> +test_expect_success GPGSM 'round-trip X.509 signed commit' '
> +
> +       git fast-export --signed-commits=verbatim x509-signing >output &&
> +       test_grep -E "^gpgsig $GIT_DEFAULT_HASH x509" output &&
> +       (
> +               cd new &&
> +               git fast-import &&
> +               git cat-file commit refs/heads/x509-signing >actual &&
> +               grep "^gpgsig" actual &&
> +               IMPORTED=$(git rev-parse refs/heads/x509-signing) &&
> +               test $X509_COMMIT = $IMPORTED
> +       ) <output
> +
> +'
> +
> +test_expect_success GPGSSH 'setup SSH signed commit' '
> +
> +       git checkout -b ssh-signing main &&
> +       test_config gpg.format ssh &&
> +       test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
> +       echo "SSH content" >file &&
> +       git add file &&
> +       git commit -S -m "SSH signed commit" &&
> +       SSH_COMMIT=$(git rev-parse HEAD) &&
> +       git checkout main
> +
> +'
> +
> +test_expect_success GPGSSH 'round-trip SSH signed commit' '
> +
> +       git fast-export --signed-commits=verbatim ssh-signing >output &&
> +       test_grep -E "^gpgsig $GIT_DEFAULT_HASH ssh" output &&
> +       (
> +               cd new &&
> +               git fast-import &&
> +               git cat-file commit refs/heads/ssh-signing >actual &&
> +               grep "^gpgsig" actual &&
> +               IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&
> +               test $SSH_COMMIT = $IMPORTED
> +       ) <output
> +
> +'
> +
>  test_expect_success 'setup submodule' '
>
>         test_config_global protocol.file.allow always &&
> @@ -905,4 +961,46 @@ test_expect_success 'fast-export handles --end-of-options' '
>         test_cmp expect actual
>  '
>
> +test_expect_success GPG 'setup a commit with dual signatures on its SHA-1 and SHA-256 formats' '
> +       # Create a signed SHA-256 commit
> +       git init --object-format=sha256 explicit-sha256 &&
> +       git -C explicit-sha256 config extensions.compatObjectFormat sha1 &&
> +       git -C explicit-sha256 checkout -b dual-signed &&
> +       test_commit -C explicit-sha256 A &&
> +       echo B >explicit-sha256/B &&
> +       git -C explicit-sha256 add B &&
> +       test_tick &&
> +       git -C explicit-sha256 commit -S -m "signed" B &&
> +       SHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&
> +
> +       # Create the corresponding SHA-1 commit
> +       SHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&
> +
> +       # Check that the resulting SHA-1 commit has both signatures
> +       echo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&
> +       test_grep -E "^gpgsig " out &&
> +       test_grep -E "^gpgsig-sha256 " out
> +'
> +
> +test_expect_success GPG 'export and import of doubly signed commit' '
> +       git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&
> +       test_grep -E "^gpgsig sha1 openpgp" output &&
> +       test_grep -E "^gpgsig sha256 openpgp" output &&
> +
> +       (
> +               cd new &&
> +               git fast-import &&
> +               git cat-file commit refs/heads/dual-signed >actual &&
> +               test_grep -E "^gpgsig " actual &&
> +               test_grep -E "^gpgsig-sha256 " actual &&
> +               IMPORTED=$(git rev-parse refs/heads/dual-signed) &&
> +               if test "$GIT_DEFAULT_HASH" = "sha1"
> +               then
> +                       test $SHA1_B = $IMPORTED
> +               else
> +                       test $SHA256_B = $IMPORTED
> +               fi
> +       ) <output

This last bit seems a bit fragile; could the redirection of output to
the stdin of `git fast-import` be made specific to that one line
instead of to the whole range of commands?

Otherwise, I very much appreciate the work to create a testcase with
both signature types on a single commit.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v5] fast-(import|export): improve on commit signature output format
  2025-07-08 23:08         ` Elijah Newren
@ 2025-07-09  0:03           ` Junio C Hamano
  2025-07-09  0:10             ` Elijah Newren
  2025-07-09 10:18             ` Christian Couder
  2025-07-09 10:15           ` Christian Couder
  1 sibling, 2 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-07-09  0:03 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, git, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Elijah Newren <newren@gmail.com> writes:

>> +       if (!space)
>> +               die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
>> +                   "got 'gpgsig %s'", args);
>> +       *space++ = '\0';
>> +
>> +       sig->hash_algo = args;
>> +       sig->sig_format = space;
>
> Really minor nitpick, but it might be clearer to pre-increment space
> here than to increment it above.

FWIW, I find what Christian wrote easier to follow.  We find a " "
in the buffer, point it with a pointer and NUL-terminate the
substring.  We know we want to further process bytes that follow, so
the pointer is post-incremented after the NUL-termination.  The next
user of that pointer relies on the fact that the previous user
concluded its use with that post-increment.

If 'space' variable were named more genericly, like '*cp', it would
have been perfect.  Perhaps only the first half of the code was
written first, and it looked for a space, so the name was chosen,
but then later ...

>> +
>> +       /* Remove any trailing newline from format */
>> +       space = strchr(sig->sig_format, '\n');

... it is used to point at a LF X-<, at which time the author could
have renamed it to keep readers' sanity ;-)

>> +       if (space)
>> +               *space = '\0';

I also wonder what should (not "does", as I can see that the code
does not do anything) happen if we do not find the LF we were
looking for.  Is the caller of this function so loosely written that
it may or may not guarantee that the data it calls this function
with is properly terminated?

>> +test_expect_success GPG 'export and import of doubly signed commit' '
>> +       git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&
>> +       test_grep -E "^gpgsig sha1 openpgp" output &&
>> +       test_grep -E "^gpgsig sha256 openpgp" output &&
>> +
>> +       (
>> +               cd new &&
>> +               git fast-import &&
>> +               git cat-file commit refs/heads/dual-signed >actual &&
>> +               test_grep -E "^gpgsig " actual &&
>> +               test_grep -E "^gpgsig-sha256 " actual &&
>> +               IMPORTED=$(git rev-parse refs/heads/dual-signed) &&
>> +               if test "$GIT_DEFAULT_HASH" = "sha1"
>> +               then
>> +                       test $SHA1_B = $IMPORTED
>> +               else
>> +                       test $SHA256_B = $IMPORTED
>> +               fi
>> +       ) <output
>
> This last bit seems a bit fragile; could the redirection of output to
> the stdin of `git fast-import` be made specific to that one line
> instead of to the whole range of commands?
>
> Otherwise, I very much appreciate the work to create a testcase with
> both signature types on a single commit.

Yup, thanks, both of you.  It seems that we are getting closer to
the finish line?

Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v5] fast-(import|export): improve on commit signature output format
  2025-07-09  0:03           ` Junio C Hamano
@ 2025-07-09  0:10             ` Elijah Newren
  2025-07-09 10:18             ` Christian Couder
  1 sibling, 0 replies; 65+ messages in thread
From: Elijah Newren @ 2025-07-09  0:10 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Christian Couder, git, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Tue, Jul 8, 2025 at 5:03 PM Junio C Hamano <gitster@pobox.com> wrote:
>
[...]
> Yup, thanks, both of you.  It seems that we are getting closer to
> the finish line?

I think so; a few small touch-ups and I think this one should be good to go.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-08 16:38                 ` Junio C Hamano
@ 2025-07-09  0:19                   ` Christian Couder
  2025-07-09 15:35                     ` Junio C Hamano
  2025-07-10  8:25                     ` Patrick Steinhardt
  0 siblings, 2 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-09  0:19 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Patrick Steinhardt, git, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Tue, Jul 8, 2025 at 6:38 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Christian Couder <christian.couder@gmail.com> writes:
>
> > Also if a contributor comes back with improved patches that try to
> > follow closely what a reviewer suggested, then I think it can (and
> > should) make a reviewer feel like they have really been heard better
> > than just a hollow reply right away followed later by less well
> > thought out patches.
>
> That is kind of "better late than never".  I would expect better
> than that from more senior prominent contributors ;-)
>
> And I totally agree with you that reviews often deserve very well
> reasoned responses, which take time to prepare; a response that
> comes as spinal reflex without much thought is often not very
> useful.
>
> It really depends on the definition of "fast" in "fast response".
>
> If we need a week to come up with a newer iteration,

The issue is that whatever the time we could set as a norm, like "a
week" here or 2 or 3 days, or one month, or whatever, we actually
don't know what happens in the life of contributors. Maybe some have
health issues, maybe some work only a few days per week, maybe some
oare working on their free time and don't have much free time, maybe
they are asked to work a lot by their company on other internal urgent
things, maybe they have to take care of dependent people in their
family, etc... So setting any norm here that everyone should try to
respect might just not work for some people.

For example there was a former Outreachy intern who continued working
for about 2 years on their project after the internship was over. She
was a young mother so didn't have much time to work on Git and would
come back to the mailing list only every few months with new patches
and replies to reviewers. What would we have gained exactly by
imposing a norm on her?

> it would be
> fair to expect that we can say something like "I agree, I'll fix",
> "I am not convinced because ...", "I am skeptical but let me first
> see how it pans out", etc. by day #2 or #3, wouldn't it?  Upon
> recieving a response at the same time or soon after an updated
> iteration was sent, especially when the response is "no, I do not
> think so", what is the reviewer expected to do?  Saying "you may not
> think so but here is another point that may make you reconsider"
> would be too late, so it would actively discourage continued
> discussion.
>
> > It doesn't mean that I think oldtimers should have some kind of
> > privilege, and yeah they should also try to give a good example. But
> > we should allow people to not always behave in a very formatted way.
>
> Old timers learn from experience how other old timers operate ;-)
> and I have learned to ignore the usual signal when anticipating what
> your next iteration may look like (in other words, interim responses
> or lack thereof is usually a good signal for most developers, but
> not for you---you tend to come back with your next iteration without
> much interim interactions).  But other contributors shouldn't be
> forced to. That is what we need some community norm for.
>
> >> On our team's handbook page [1] we have the following couple of bullet
> >> points regarding how to respond to reviews:
> >
> > Yeah, I think they are likely to be good for newcomers.
>
> The handbook here is gitlab's team handbook, and it may not apply to
> open source Git development community, but "this rule applies only
> to newcomers, I am above that rule" is the same thing as saying
> "oldtimers like me should have some kind of privilege".  I do not
> know what to think about this and what you said above.

I think it's fair to say that oldtimers are less likely to disappear
tomorrow or to not follow up on reviewer feedback. So arguments like
"replying fast makes reviewers confident that they have been heard"
are just less true for oldtimers than for newcomers.

Another argument is that oldtimers are more likely to burn out or have
mental health issues related to their Git work than newcomers. Adding
a norm that would put pressure on them to work more, or at times they
would prefer to do other things, significantly increases the burnout
and mental health risks for them.

So putting pressure on oldtimers to follow a norm that is less
relevant for them but puts them at greater risk is just a bad idea in
my opinion.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v5] fast-(import|export): improve on commit signature output format
  2025-07-08 23:08         ` Elijah Newren
  2025-07-09  0:03           ` Junio C Hamano
@ 2025-07-09 10:15           ` Christian Couder
  1 sibling, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-09 10:15 UTC (permalink / raw)
  To: Elijah Newren
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Wed, Jul 9, 2025 at 1:08 AM Elijah Newren <newren@gmail.com> wrote:
>
> On Tue, Jul 8, 2025 at 2:18 AM Christian Couder
> <christian.couder@gmail.com> wrote:

> > diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
> > index 250d866652..89dec1108f 100644
> > --- a/Documentation/git-fast-import.adoc
> > +++ b/Documentation/git-fast-import.adoc
> > @@ -445,7 +445,7 @@ one).
> >         original-oid?
> >         ('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
> >         'committer' (SP <name>)? SP LT <email> GT SP <when> LF
> > -       ('gpgsig' SP <alg> LF data)?
> > +       ('gpgsig' SP <algo> SP <format> LF data)?
> >         ('encoding' SP <encoding> LF)?
> >         data
> >         ('from' SP <commit-ish> LF)?
> > @@ -518,13 +518,37 @@ their syntax.
> >  ^^^^^^^^
> >
> >  The optional `gpgsig` command is used to include a PGP/GPG signature
> > -that signs the commit data.
> > +or other cryptographic signature that signs the commit data.
> >
> > -Here <alg> specifies which hashing algorithm is used for this
> > -signature, either `sha1` or `sha256`.
> > +....
> > +       'gpgsig' SP <git-hash-algo> SP <signature-format> LF
> > +       data
>
> Should the `data` be moved to the line above, just to make it clear
> it's associated with it?  (Similar to the first line you changed in
> git-fast-import.adoc?)

Yeah, I have changed it in my current version.

> > +....
> > +
> > +The `gpgsig` command takes two arguments:
> > +
> > +* `<git-hash-algo>` specifies which Git object format this signature
> > +  applies to, either `sha1` or `sha256`. This allows to know which
> > +  representation of the commit was signed (the SHA-1 or the SHA-256
> > +  version) which helps with both signature verification and
> > +  interoperability between repos with different hash functions.
>
> Should there also be a note added that fast-import is limited on what
> signatures it can verify if extensions.compatObjectFormat is not set?

It cannot yet verify signatures, but yeah it's probably a good idea to
say that setting this config option might help with verifying
signatures when it will be able to do it. See below.

> > +* `<signature-format>` specifies the type of signature, such as
> > +  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for
> > +  tools that process the stream, so they don't have to parse the ASCII
> > +  armor to identify the signature type.
> > +
> > +A commit may have at most one signature for the SHA-1 object format
> > +(stored in the "gpgsig" header) and one for the SHA-256 object format
> > +(stored in the "gpgsig-sha256" header).
> > +
> > +See below for a detailed description of the `data` command which
> > +contains the raw signature data.
> > +
> > +Signatures are not yet checked in the current implementation though.
>
> ...or maybe that extra note could be added as a parenthetical comment
> here for now?

Yeah, in my current version, there is:

"Signatures are not yet checked in the current implementation
though. (Already setting the `extensions.compatObjectFormat`
configuration option might help with verifying both SHA-1 and SHA-256
object format signatures when it will be implemented.)"

[...]


> > +test_expect_success GPG 'export and import of doubly signed commit' '
> > +       git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&
> > +       test_grep -E "^gpgsig sha1 openpgp" output &&
> > +       test_grep -E "^gpgsig sha256 openpgp" output &&
> > +
> > +       (
> > +               cd new &&
> > +               git fast-import &&
> > +               git cat-file commit refs/heads/dual-signed >actual &&
> > +               test_grep -E "^gpgsig " actual &&
> > +               test_grep -E "^gpgsig-sha256 " actual &&
> > +               IMPORTED=$(git rev-parse refs/heads/dual-signed) &&
> > +               if test "$GIT_DEFAULT_HASH" = "sha1"
> > +               then
> > +                       test $SHA1_B = $IMPORTED
> > +               else
> > +                       test $SHA256_B = $IMPORTED
> > +               fi
> > +       ) <output
>
> This last bit seems a bit fragile; could the redirection of output to
> the stdin of `git fast-import` be made specific to that one line
> instead of to the whole range of commands?

I used the same style as many other tests in this file. For example
there are already:

test_expect_success GPG 'signed-commits=verbatim' '

    git fast-export --signed-commits=verbatim --reencode=no
commit-signing >output &&
    grep "^gpgsig sha" output &&
    grep "encoding ISO-8859-1" output &&
    (
        cd new &&
        git fast-import &&
        STRIPPED=$(git rev-parse --verify refs/heads/commit-signing) &&
        test $COMMIT_SIGNING = $STRIPPED
    ) <output

'

test_expect_success GPG 'signed-commits=warn-verbatim' '

    git fast-export --signed-commits=warn-verbatim --reencode=no
commit-signing >output 2>err &&
    grep "^gpgsig sha" output &&
    grep "encoding ISO-8859-1" output &&
    test -s err &&
    (
        cd new &&
        git fast-import &&
        STRIPPED=$(git rev-parse --verify refs/heads/commit-signing) &&
        test $COMMIT_SIGNING = $STRIPPED
    ) <output

'

etc.

So to keep a consistent style in the tests, I would likely have to add
a preparatory commit that changes the style of all these existing
tests. Not sure it's worth it at this point.

> Otherwise, I very much appreciate the work to create a testcase with
> both signature types on a single commit.

Thanks for the kind words and for your reviews!

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v5] fast-(import|export): improve on commit signature output format
  2025-07-09  0:03           ` Junio C Hamano
  2025-07-09  0:10             ` Elijah Newren
@ 2025-07-09 10:18             ` Christian Couder
  1 sibling, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-09 10:18 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Elijah Newren, git, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Wed, Jul 9, 2025 at 2:03 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:

> > Really minor nitpick, but it might be clearer to pre-increment space
> > here than to increment it above.
>
> FWIW, I find what Christian wrote easier to follow.  We find a " "
> in the buffer, point it with a pointer and NUL-terminate the
> substring.  We know we want to further process bytes that follow, so
> the pointer is post-incremented after the NUL-termination.  The next
> user of that pointer relies on the fact that the previous user
> concluded its use with that post-increment.
>
> If 'space' variable were named more genericly, like '*cp', it would
> have been perfect. Perhaps only the first half of the code was
> written first, and it looked for a space, so the name was chosen,
> but then later ...
>
> >> +
> >> +       /* Remove any trailing newline from format */
> >> +       space = strchr(sig->sig_format, '\n');
>
> ... it is used to point at a LF X-<, at which time the author could
> have renamed it to keep readers' sanity ;-)

Yeah, right, I couldn't think of a good name to rename it, so I
postponed changing the name, and then forgot about it.

> >> +       if (space)
> >> +               *space = '\0';
>
> I also wonder what should (not "does", as I can see that the code
> does not do anything) happen if we do not find the LF we were
> looking for.  Is the caller of this function so loosely written that
> it may or may not guarantee that the data it calls this function
> with is properly terminated?

This function is passed a pointer to 'command_buf.buf' after "gpgsig "
was skipped from it. 'read_next_command()' uses 'strbuf_getline_lf()'
to put each line in 'command_buf.buf'. And yeah 'strbuf_getline_lf()'
should remove a LF from the end of 'command_buf.buf' if any.

So we don't need the code to remove any trailing LF. I have removed it
in my current version. This means that we can keep using "space" as
the variable name, as it is now only related to the space between the
arguments.

> > Otherwise, I very much appreciate the work to create a testcase with
> > both signature types on a single commit.
>
> Yup, thanks, both of you.  It seems that we are getting closer to
> the finish line?

Yeah, I plan to send a v6 soon, maybe later today with the changes
discussed here and in my reply to Elijah.

Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* [PATCH v6] fast-(import|export): improve on commit signature output format
  2025-07-08  9:17       ` [PATCH v5] " Christian Couder
  2025-07-08 21:58         ` Junio C Hamano
  2025-07-08 23:08         ` Elijah Newren
@ 2025-07-09 14:12         ` Christian Couder
  2025-07-09 23:14           ` Junio C Hamano
  2025-07-14 21:07           ` Elijah Newren
  2 siblings, 2 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-09 14:12 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder,
	Christian Couder

A recent commit, d9cb0e6ff8 (fast-export, fast-import: add support for
signed-commits, 2025-03-10), added support for signed commits to
fast-export and fast-import.

When a signed commit is processed, fast-export can output either
"gpgsig sha1" or "gpgsig sha256" depending on whether the signed
commit uses the SHA-1 or SHA-256 Git object format.

However, this implementation has a number of limitations:

  - the output format was not properly described in the documentation,
  - the output format is not very informative as it doesn't even say
    if the signature is an OpenPGP, an SSH, or an X509 signature,
  - the implementation doesn't support having both one signature on
    the SHA-1 object and one on the SHA-256 object.

Let's improve on these limitations by improving fast-export and
fast-import so that:

  - all the signatures are exported,
  - at most one signature on the SHA-1 object and one on the SHA-256
    are imported,
  - if there is more than one signature on the SHA-1 object or on
    the SHA-256 object, fast-import emits a warning for each
    additional signature,
  - the output format is "gpgsig <git-hash-algo> <signature-format>",
    where <git-hash-algo> is the Git object format as before, and
    <signature-format> is the signature type ("openpgp", "x509",
    "ssh" or "unknown"),
  - the output is properly documented.

About the output format:

  - <git-hash-algo> allows to know which representation of the commit
    was signed (the SHA-1 or the SHA-256 version) which helps with
    both signature verification and interoperability between repos
    with different hash functions,

  - <signature-format> helps tools that process the fast-export
    stream, so they don't have to parse the ASCII armor to identify
    the signature type.

It could be even better to be able to import more than one signature
on the SHA-1 object and on the SHA-256 object, but other parts of
Git don't handle that well for now, so this is left for future
improvements.

Helped-by: brian m. carlson <sandals@crustytoothpaste.net>
Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
---

This v6 is very similar to v5. There are only a few small changes:

  - In "git-fast-import.adoc", on a synopsys of the 'gpgsig' command,
    'data' has been moved to the same line as the previous parts of
    the synopsys.

  - In "git-fast-import.adoc", it is suggested that setting the
    `extensions.compatObjectFormat` config option might help in the
    future with verifying signatures.

  - In "builtin/fast-import.c", "or" has been added in a comment that
    lists possible values for the signature format.

  - In "builtin/fast-import.c", some useless code that checked for a
    possible LF at the end of the 'gpgsig' command arguments has been
    removed.

Thanks to brian, Elijah and Junio who commented on the previous
versions.

CI tests:

https://github.com/chriscool/git/actions/runs/16165147132

All the tests passed.

Range-diff with v5:

1:  5791617d7d ! 1:  6e07895ea1 fast-(import|export): improve on commit signature output format
    @@ Documentation/git-fast-import.adoc: their syntax.
     -Here <alg> specifies which hashing algorithm is used for this
     -signature, either `sha1` or `sha256`.
     +....
    -+	'gpgsig' SP <git-hash-algo> SP <signature-format> LF
    -+	data
    ++	'gpgsig' SP <git-hash-algo> SP <signature-format> LF data
     +....
     +
     +The `gpgsig` command takes two arguments:
    @@ Documentation/git-fast-import.adoc: their syntax.
     +See below for a detailed description of the `data` command which
     +contains the raw signature data.
     +
    -+Signatures are not yet checked in the current implementation though.
    ++Signatures are not yet checked in the current implementation
    ++though. (Already setting the `extensions.compatObjectFormat`
    ++configuration option might help with verifying both SHA-1 and SHA-256
    ++object format signatures when it will be implemented.)
      
     -NOTE: This is highly experimental and the format of the data stream may
     -change in the future without compatibility guarantees.
    @@ builtin/fast-import.c: static struct hash_list *parse_merge(unsigned int *count)
      
     +struct signature_data {
     +	char *hash_algo;      /* "sha1" or "sha256" */
    -+	char *sig_format;     /* "openpgp", "x509", "ssh", "unknown" */
    ++	char *sig_format;     /* "openpgp", "x509", "ssh", or "unknown" */
     +	struct strbuf data;   /* The actual signature data */
     +};
     +
    @@ builtin/fast-import.c: static struct hash_list *parse_merge(unsigned int *count)
     +	if (!space)
     +		die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
     +		    "got 'gpgsig %s'", args);
    -+	*space++ = '\0';
    ++	*space = '\0';
     +
     +	sig->hash_algo = args;
    -+	sig->sig_format = space;
    -+
    -+	/* Remove any trailing newline from format */
    -+	space = strchr(sig->sig_format, '\n');
    -+	if (space)
    -+		*space = '\0';
    ++	sig->sig_format = space + 1;
     +
     +	/* Validate hash algorithm */
     +	if (strcmp(sig->hash_algo, "sha1") &&


 Documentation/git-fast-export.adoc |  17 +++++
 Documentation/git-fast-import.adoc |  38 ++++++++--
 builtin/fast-export.c              |  62 ++++++++++++----
 builtin/fast-import.c              | 113 +++++++++++++++++++++++------
 gpg-interface.c                    |  12 +++
 gpg-interface.h                    |  12 +++
 t/t9350-fast-export.sh             | 102 +++++++++++++++++++++++++-
 7 files changed, 312 insertions(+), 44 deletions(-)

diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc
index 43bbb4f63c..297b57bb2e 100644
--- a/Documentation/git-fast-export.adoc
+++ b/Documentation/git-fast-export.adoc
@@ -50,6 +50,23 @@ resulting tag will have an invalid signature.
 	is the same as how earlier versions of this command without
 	this option behaved.
 +
+When exported, a signature starts with:
++
+gpgsig <git-hash-algo> <signature-format>
++
+where <git-hash-algo> is the Git object hash so either "sha1" or
+"sha256", and <signature-format> is the signature type, so "openpgp",
+"x509", "ssh" or "unknown".
++
+For example, an OpenPGP signature on a SHA-1 commit starts with
+`gpgsig sha1 openpgp`, while an SSH signature on a SHA-256 commit
+starts with `gpgsig sha256 ssh`.
++
+While all the signatures of a commit are exported, an importer may
+choose to accept only some of them. For example
+linkgit:git-fast-import[1] currently stores at most one signature per
+Git hash algorithm in each commit.
++
 NOTE: This is highly experimental and the format of the data stream may
 change in the future without compatibility guarantees.
 
diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc
index 250d866652..d232784200 100644
--- a/Documentation/git-fast-import.adoc
+++ b/Documentation/git-fast-import.adoc
@@ -445,7 +445,7 @@ one).
 	original-oid?
 	('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
 	'committer' (SP <name>)? SP LT <email> GT SP <when> LF
-	('gpgsig' SP <alg> LF data)?
+	('gpgsig' SP <algo> SP <format> LF data)?
 	('encoding' SP <encoding> LF)?
 	data
 	('from' SP <commit-ish> LF)?
@@ -518,13 +518,39 @@ their syntax.
 ^^^^^^^^
 
 The optional `gpgsig` command is used to include a PGP/GPG signature
-that signs the commit data.
+or other cryptographic signature that signs the commit data.
 
-Here <alg> specifies which hashing algorithm is used for this
-signature, either `sha1` or `sha256`.
+....
+	'gpgsig' SP <git-hash-algo> SP <signature-format> LF data
+....
+
+The `gpgsig` command takes two arguments:
+
+* `<git-hash-algo>` specifies which Git object format this signature
+  applies to, either `sha1` or `sha256`. This allows to know which
+  representation of the commit was signed (the SHA-1 or the SHA-256
+  version) which helps with both signature verification and
+  interoperability between repos with different hash functions.
+
+* `<signature-format>` specifies the type of signature, such as
+  `openpgp`, `x509`, `ssh`, or `unknown`. This is a convenience for
+  tools that process the stream, so they don't have to parse the ASCII
+  armor to identify the signature type.
+
+A commit may have at most one signature for the SHA-1 object format
+(stored in the "gpgsig" header) and one for the SHA-256 object format
+(stored in the "gpgsig-sha256" header).
+
+See below for a detailed description of the `data` command which
+contains the raw signature data.
+
+Signatures are not yet checked in the current implementation
+though. (Already setting the `extensions.compatObjectFormat`
+configuration option might help with verifying both SHA-1 and SHA-256
+object format signatures when it will be implemented.)
 
-NOTE: This is highly experimental and the format of the data stream may
-change in the future without compatibility guarantees.
+NOTE: This is highly experimental and the format of the `gpgsig`
+command may change in the future without compatibility guarantees.
 
 `encoding`
 ^^^^^^^^^^
diff --git a/builtin/fast-export.c b/builtin/fast-export.c
index fcf6b00d5f..7b4e6a6e41 100644
--- a/builtin/fast-export.c
+++ b/builtin/fast-export.c
@@ -29,6 +29,7 @@
 #include "quote.h"
 #include "remote.h"
 #include "blob.h"
+#include "gpg-interface.h"
 
 static const char *const fast_export_usage[] = {
 	N_("git fast-export [<rev-list-opts>]"),
@@ -652,6 +653,38 @@ static const char *find_commit_multiline_header(const char *msg,
 	return strbuf_detach(&val, NULL);
 }
 
+static void print_signature(const char *signature, const char *object_hash)
+{
+	if (!signature)
+		return;
+
+	printf("gpgsig %s %s\ndata %u\n%s\n",
+	       object_hash,
+	       get_signature_format(signature),
+	       (unsigned)strlen(signature),
+	       signature);
+}
+
+static const char *append_signatures_for_header(struct string_list *signatures,
+						const char *pos,
+						const char *header,
+						const char *object_hash)
+{
+	const char *signature;
+	const char *start = pos;
+	const char *end = pos;
+
+	while ((signature = find_commit_multiline_header(start + 1,
+							 header,
+							 &end))) {
+		string_list_append(signatures, signature)->util = (void *)object_hash;
+		free((char *)signature);
+		start = end;
+	}
+
+	return end;
+}
+
 static void handle_commit(struct commit *commit, struct rev_info *rev,
 			  struct string_list *paths_of_changed_objects)
 {
@@ -660,7 +693,7 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	const char *author, *author_end, *committer, *committer_end;
 	const char *encoding = NULL;
 	size_t encoding_len;
-	const char *signature_alg = NULL, *signature = NULL;
+	struct string_list signatures = STRING_LIST_INIT_DUP;
 	const char *message;
 	char *reencoded = NULL;
 	struct commit_list *p;
@@ -700,10 +733,11 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	}
 
 	if (*commit_buffer_cursor == '\n') {
-		if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
-			signature_alg = "sha1";
-		else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
-			signature_alg = "sha256";
+		const char *after_sha1 = append_signatures_for_header(&signatures, commit_buffer_cursor,
+								      "gpgsig", "sha1");
+		const char *after_sha256 = append_signatures_for_header(&signatures, commit_buffer_cursor,
+									"gpgsig-sha256", "sha256");
+		commit_buffer_cursor = (after_sha1 > after_sha256) ? after_sha1 : after_sha256;
 	}
 
 	message = strstr(commit_buffer_cursor, "\n\n");
@@ -769,30 +803,30 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
 	printf("%.*s\n%.*s\n",
 	       (int)(author_end - author), author,
 	       (int)(committer_end - committer), committer);
-	if (signature) {
+	if (signatures.nr) {
 		switch (signed_commit_mode) {
 		case SIGN_ABORT:
 			die("encountered signed commit %s; use "
 			    "--signed-commits=<mode> to handle it",
 			    oid_to_hex(&commit->object.oid));
 		case SIGN_WARN_VERBATIM:
-			warning("exporting signed commit %s",
-				oid_to_hex(&commit->object.oid));
+			warning("exporting %"PRIuMAX" signature(s) for commit %s",
+				(uintmax_t)signatures.nr, oid_to_hex(&commit->object.oid));
 			/* fallthru */
 		case SIGN_VERBATIM:
-			printf("gpgsig %s\ndata %u\n%s",
-			       signature_alg,
-			       (unsigned)strlen(signature),
-			       signature);
+			for (size_t i = 0; i < signatures.nr; i++) {
+				struct string_list_item *item = &signatures.items[i];
+				print_signature(item->string, item->util);
+			}
 			break;
 		case SIGN_WARN_STRIP:
-			warning("stripping signature from commit %s",
+			warning("stripping signature(s) from commit %s",
 				oid_to_hex(&commit->object.oid));
 			/* fallthru */
 		case SIGN_STRIP:
 			break;
 		}
-		free((char *)signature);
+		string_list_clear(&signatures, 0);
 	}
 	if (!reencoded && encoding)
 		printf("encoding %.*s\n", (int)encoding_len, encoding);
diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index b2839c5f43..b51a17b95a 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -29,6 +29,7 @@
 #include "commit-reach.h"
 #include "khash.h"
 #include "date.h"
+#include "gpg-interface.h"
 
 #define PACK_ID_BITS 16
 #define MAX_PACK_ID ((1<<PACK_ID_BITS)-1)
@@ -2718,15 +2719,82 @@ static struct hash_list *parse_merge(unsigned int *count)
 	return list;
 }
 
+struct signature_data {
+	char *hash_algo;      /* "sha1" or "sha256" */
+	char *sig_format;     /* "openpgp", "x509", "ssh", or "unknown" */
+	struct strbuf data;   /* The actual signature data */
+};
+
+static void parse_one_signature(struct signature_data *sig, const char *v)
+{
+	char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */
+	char *space = strchr(args, ' ');
+
+	if (!space)
+		die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
+		    "got 'gpgsig %s'", args);
+	*space = '\0';
+
+	sig->hash_algo = args;
+	sig->sig_format = space + 1;
+
+	/* Validate hash algorithm */
+	if (strcmp(sig->hash_algo, "sha1") &&
+	    strcmp(sig->hash_algo, "sha256"))
+		die("Unknown git hash algorithm in gpgsig: '%s'", sig->hash_algo);
+
+	/* Validate signature format */
+	if (!valid_signature_format(sig->sig_format))
+		die("Invalid signature format in gpgsig: '%s'", sig->sig_format);
+	if (!strcmp(sig->sig_format, "unknown"))
+		warning("'unknown' signature format in gpgsig");
+
+	/* Read signature data */
+	read_next_command();
+	parse_data(&sig->data, 0, NULL);
+}
+
+static void add_gpgsig_to_commit(struct strbuf *commit_data,
+				 const char *header,
+				 struct signature_data *sig)
+{
+	struct string_list siglines = STRING_LIST_INIT_NODUP;
+
+	if (!sig->hash_algo)
+		return;
+
+	strbuf_addstr(commit_data, header);
+	string_list_split_in_place(&siglines, sig->data.buf, "\n", -1);
+	strbuf_add_separated_string_list(commit_data, "\n ", &siglines);
+	strbuf_addch(commit_data, '\n');
+	string_list_clear(&siglines, 1);
+	strbuf_release(&sig->data);
+	free(sig->hash_algo);
+}
+
+static void store_signature(struct signature_data *stored_sig,
+			    struct signature_data *new_sig,
+			    const char *hash_type)
+{
+	if (stored_sig->hash_algo) {
+		warning("multiple %s signatures found, "
+			"ignoring additional signature",
+			hash_type);
+		strbuf_release(&new_sig->data);
+		free(new_sig->hash_algo);
+	} else {
+		*stored_sig = *new_sig;
+	}
+}
+
 static void parse_new_commit(const char *arg)
 {
-	static struct strbuf sig = STRBUF_INIT;
 	static struct strbuf msg = STRBUF_INIT;
-	struct string_list siglines = STRING_LIST_INIT_NODUP;
+	struct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT };
+	struct signature_data sig_sha256 = { NULL, NULL, STRBUF_INIT };
 	struct branch *b;
 	char *author = NULL;
 	char *committer = NULL;
-	char *sig_alg = NULL;
 	char *encoding = NULL;
 	struct hash_list *merge_list = NULL;
 	unsigned int merge_count;
@@ -2750,13 +2818,23 @@ static void parse_new_commit(const char *arg)
 	}
 	if (!committer)
 		die("Expected committer but didn't get one");
-	if (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
-		sig_alg = xstrdup(v);
-		read_next_command();
-		parse_data(&sig, 0, NULL);
+
+	/* Process signatures (up to 2: one "sha1" and one "sha256") */
+	while (skip_prefix(command_buf.buf, "gpgsig ", &v)) {
+		struct signature_data sig = { NULL, NULL, STRBUF_INIT };
+
+		parse_one_signature(&sig, v);
+
+		if (!strcmp(sig.hash_algo, "sha1"))
+			store_signature(&sig_sha1, &sig, "SHA-1");
+		else if (!strcmp(sig.hash_algo, "sha256"))
+			store_signature(&sig_sha256, &sig, "SHA-256");
+		else
+			BUG("parse_one_signature() returned unknown hash algo");
+
 		read_next_command();
-	} else
-		strbuf_setlen(&sig, 0);
+	}
+
 	if (skip_prefix(command_buf.buf, "encoding ", &v)) {
 		encoding = xstrdup(v);
 		read_next_command();
@@ -2830,23 +2908,14 @@ static void parse_new_commit(const char *arg)
 		strbuf_addf(&new_data,
 			"encoding %s\n",
 			encoding);
-	if (sig_alg) {
-		if (!strcmp(sig_alg, "sha1"))
-			strbuf_addstr(&new_data, "gpgsig ");
-		else if (!strcmp(sig_alg, "sha256"))
-			strbuf_addstr(&new_data, "gpgsig-sha256 ");
-		else
-			die("Expected gpgsig algorithm sha1 or sha256, got %s", sig_alg);
-		string_list_split_in_place(&siglines, sig.buf, "\n", -1);
-		strbuf_add_separated_string_list(&new_data, "\n ", &siglines);
-		strbuf_addch(&new_data, '\n');
-	}
+
+	add_gpgsig_to_commit(&new_data, "gpgsig ", &sig_sha1);
+	add_gpgsig_to_commit(&new_data, "gpgsig-sha256 ", &sig_sha256);
+
 	strbuf_addch(&new_data, '\n');
 	strbuf_addbuf(&new_data, &msg);
-	string_list_clear(&siglines, 1);
 	free(author);
 	free(committer);
-	free(sig_alg);
 	free(encoding);
 
 	if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark))
diff --git a/gpg-interface.c b/gpg-interface.c
index 0896458de5..6f2d87475f 100644
--- a/gpg-interface.c
+++ b/gpg-interface.c
@@ -144,6 +144,18 @@ static struct gpg_format *get_format_by_sig(const char *sig)
 	return NULL;
 }
 
+const char *get_signature_format(const char *buf)
+{
+	struct gpg_format *format = get_format_by_sig(buf);
+	return format ? format->name : "unknown";
+}
+
+int valid_signature_format(const char *format)
+{
+       return (!!get_format_by_name(format) ||
+	       !strcmp(format, "unknown"));
+}
+
 void signature_check_clear(struct signature_check *sigc)
 {
 	FREE_AND_NULL(sigc->payload);
diff --git a/gpg-interface.h b/gpg-interface.h
index e09f12e8d0..60ddf8bbfa 100644
--- a/gpg-interface.h
+++ b/gpg-interface.h
@@ -47,6 +47,18 @@ struct signature_check {
 
 void signature_check_clear(struct signature_check *sigc);
 
+/*
+ * Return the format of the signature (like "openpgp", "x509", "ssh"
+ * or "unknown").
+ */
+const char *get_signature_format(const char *buf);
+
+/*
+ * Is the signature format valid (like "openpgp", "x509", "ssh" or
+ * "unknown")
+ */
+int valid_signature_format(const char *format);
+
 /*
  * Look at a GPG signed tag object.  If such a signature exists, store it in
  * signature and the signed content in payload.  Return 1 if a signature was
diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh
index 76619765fc..46700dbc40 100755
--- a/t/t9350-fast-export.sh
+++ b/t/t9350-fast-export.sh
@@ -314,7 +314,7 @@ test_expect_success GPG 'signed-commits=abort' '
 test_expect_success GPG 'signed-commits=verbatim' '
 
 	git fast-export --signed-commits=verbatim --reencode=no commit-signing >output &&
-	grep "^gpgsig sha" output &&
+	test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	(
 		cd new &&
@@ -328,7 +328,7 @@ test_expect_success GPG 'signed-commits=verbatim' '
 test_expect_success GPG 'signed-commits=warn-verbatim' '
 
 	git fast-export --signed-commits=warn-verbatim --reencode=no commit-signing >output 2>err &&
-	grep "^gpgsig sha" output &&
+	test_grep -E "^gpgsig $GIT_DEFAULT_HASH openpgp" output &&
 	grep "encoding ISO-8859-1" output &&
 	test -s err &&
 	(
@@ -369,6 +369,62 @@ test_expect_success GPG 'signed-commits=warn-strip' '
 
 '
 
+test_expect_success GPGSM 'setup X.509 signed commit' '
+
+	git checkout -b x509-signing main &&
+	test_config gpg.format x509 &&
+	test_config user.signingkey $GIT_COMMITTER_EMAIL &&
+	echo "X.509 content" >file &&
+	git add file &&
+	git commit -S -m "X.509 signed commit" &&
+	X509_COMMIT=$(git rev-parse HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSM 'round-trip X.509 signed commit' '
+
+	git fast-export --signed-commits=verbatim x509-signing >output &&
+	test_grep -E "^gpgsig $GIT_DEFAULT_HASH x509" output &&
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/x509-signing >actual &&
+		grep "^gpgsig" actual &&
+		IMPORTED=$(git rev-parse refs/heads/x509-signing) &&
+		test $X509_COMMIT = $IMPORTED
+	) <output
+
+'
+
+test_expect_success GPGSSH 'setup SSH signed commit' '
+
+	git checkout -b ssh-signing main &&
+	test_config gpg.format ssh &&
+	test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
+	echo "SSH content" >file &&
+	git add file &&
+	git commit -S -m "SSH signed commit" &&
+	SSH_COMMIT=$(git rev-parse HEAD) &&
+	git checkout main
+
+'
+
+test_expect_success GPGSSH 'round-trip SSH signed commit' '
+
+	git fast-export --signed-commits=verbatim ssh-signing >output &&
+	test_grep -E "^gpgsig $GIT_DEFAULT_HASH ssh" output &&
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/ssh-signing >actual &&
+		grep "^gpgsig" actual &&
+		IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&
+		test $SSH_COMMIT = $IMPORTED
+	) <output
+
+'
+
 test_expect_success 'setup submodule' '
 
 	test_config_global protocol.file.allow always &&
@@ -905,4 +961,46 @@ test_expect_success 'fast-export handles --end-of-options' '
 	test_cmp expect actual
 '
 
+test_expect_success GPG 'setup a commit with dual signatures on its SHA-1 and SHA-256 formats' '
+	# Create a signed SHA-256 commit
+	git init --object-format=sha256 explicit-sha256 &&
+	git -C explicit-sha256 config extensions.compatObjectFormat sha1 &&
+	git -C explicit-sha256 checkout -b dual-signed &&
+	test_commit -C explicit-sha256 A &&
+	echo B >explicit-sha256/B &&
+	git -C explicit-sha256 add B &&
+	test_tick &&
+	git -C explicit-sha256 commit -S -m "signed" B &&
+	SHA256_B=$(git -C explicit-sha256 rev-parse dual-signed) &&
+
+	# Create the corresponding SHA-1 commit
+	SHA1_B=$(git -C explicit-sha256 rev-parse --output-object-format=sha1 dual-signed) &&
+
+	# Check that the resulting SHA-1 commit has both signatures
+	echo $SHA1_B | git -C explicit-sha256 cat-file --batch >out &&
+	test_grep -E "^gpgsig " out &&
+	test_grep -E "^gpgsig-sha256 " out
+'
+
+test_expect_success GPG 'export and import of doubly signed commit' '
+	git -C explicit-sha256 fast-export --signed-commits=verbatim dual-signed >output &&
+	test_grep -E "^gpgsig sha1 openpgp" output &&
+	test_grep -E "^gpgsig sha256 openpgp" output &&
+
+	(
+		cd new &&
+		git fast-import &&
+		git cat-file commit refs/heads/dual-signed >actual &&
+		test_grep -E "^gpgsig " actual &&
+		test_grep -E "^gpgsig-sha256 " actual &&
+		IMPORTED=$(git rev-parse refs/heads/dual-signed) &&
+		if test "$GIT_DEFAULT_HASH" = "sha1"
+		then
+			test $SHA1_B = $IMPORTED
+		else
+			test $SHA256_B = $IMPORTED
+		fi
+	) <output
+'
+
 test_done
-- 
2.50.0.174.g6e07895ea1


^ permalink raw reply related	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-09  0:19                   ` Christian Couder
@ 2025-07-09 15:35                     ` Junio C Hamano
  2025-07-10  8:25                     ` Patrick Steinhardt
  1 sibling, 0 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-07-09 15:35 UTC (permalink / raw)
  To: Christian Couder
  Cc: Patrick Steinhardt, git, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

>> And I totally agree with you that reviews often deserve very well
>> reasoned responses, which take time to prepare; a response that
>> comes as spinal reflex without much thought is often not very
>> useful.
>>
>> It really depends on the definition of "fast" in "fast response".
>>
>> If we need a week to come up with a newer iteration,
>
> The issue is that whatever the time we could set as a norm, like "a
> week" here or 2 or 3 days, or one month, or whatever,

Yeah, topic sizes varies.

Historically, summer is a slower season and these messages I sent
are primarily for me to keep track of topics on flight.  "I am on
vacation for a few more weeks so expect response time longer than
usual" would have been a perfectly fine response.

Not even acknowledging receipt of review comments was what I
primarily saw as a communication gap.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v6] fast-(import|export): improve on commit signature output format
  2025-07-09 14:12         ` [PATCH v6] " Christian Couder
@ 2025-07-09 23:14           ` Junio C Hamano
  2025-07-14 21:07           ` Elijah Newren
  1 sibling, 0 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-07-09 23:14 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Patrick Steinhardt, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Christian Couder <christian.couder@gmail.com> writes:

> +static void parse_one_signature(struct signature_data *sig, const char *v)
> +{
> +	char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */
> +	char *space = strchr(args, ' ');
> +
> +	if (!space)
> +		die("Expected gpgsig format: 'gpgsig <hash-algo> <signature-format>', "
> +		    "got 'gpgsig %s'", args);
> +	*space = '\0';
> +
> +	sig->hash_algo = args;
> +	sig->sig_format = space + 1;

This is minor, but as I already said in the discussion of the
previous round, let me remind readers.

I think "*space++ = '\0'" followed by "->sig_format = space", as you
wrote originally, was easier to follow.  If I were doing this 6th
iteration, I would have kept that part of the code around here, but
would have renamed "space" to a more generic "cp" (very often used
in this codebase to stand for a character pointer).

Will replace and requeue (unless you have v7 before my tomorrow's
integration cycle, in which case this iteration may be skipped).

Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-09  0:19                   ` Christian Couder
  2025-07-09 15:35                     ` Junio C Hamano
@ 2025-07-10  8:25                     ` Patrick Steinhardt
  2025-07-10 15:29                       ` Christian Couder
  2025-07-10 15:33                       ` Junio C Hamano
  1 sibling, 2 replies; 65+ messages in thread
From: Patrick Steinhardt @ 2025-07-10  8:25 UTC (permalink / raw)
  To: Christian Couder
  Cc: Junio C Hamano, git, Elijah Newren, Jeff King, brian m . carlson,
	Johannes Schindelin, Christian Couder

On Wed, Jul 09, 2025 at 02:19:06AM +0200, Christian Couder wrote:
> On Tue, Jul 8, 2025 at 6:38 PM Junio C Hamano <gitster@pobox.com> wrote:
> > Christian Couder <christian.couder@gmail.com> writes:
> >
> > > Also if a contributor comes back with improved patches that try to
> > > follow closely what a reviewer suggested, then I think it can (and
> > > should) make a reviewer feel like they have really been heard better
> > > than just a hollow reply right away followed later by less well
> > > thought out patches.
> >
> > That is kind of "better late than never".  I would expect better
> > than that from more senior prominent contributors ;-)
> >
> > And I totally agree with you that reviews often deserve very well
> > reasoned responses, which take time to prepare; a response that
> > comes as spinal reflex without much thought is often not very
> > useful.
> >
> > It really depends on the definition of "fast" in "fast response".
> >
> > If we need a week to come up with a newer iteration,
> 
> The issue is that whatever the time we could set as a norm, like "a
> week" here or 2 or 3 days, or one month, or whatever, we actually
> don't know what happens in the life of contributors. Maybe some have
> health issues, maybe some work only a few days per week, maybe some
> oare working on their free time and don't have much free time, maybe
> they are asked to work a lot by their company on other internal urgent
> things, maybe they have to take care of dependent people in their
> family, etc... So setting any norm here that everyone should try to
> respect might just not work for some people.
> 
> For example there was a former Outreachy intern who continued working
> for about 2 years on their project after the internship was over. She
> was a young mother so didn't have much time to work on Git and would
> come back to the mailing list only every few months with new patches
> and replies to reviewers. What would we have gained exactly by
> imposing a norm on her?

There are always going to be exceptions, that is of course true. But I
also think that long-time contributors that are employed to work on Git
are somewhat special and don't (typically) fall into the mentioned
groups. From my perspective, it's especially this group of people that
should lead by example and encourage others to behave in a way that is
good for the overall Git community. And leading by example in this
context also means that they should encourage healthy discussions.

That of course doesn't mean that such people should always respond
within an hour, or even within a day. We all have to context switch, and
context switches are costly, so it's entirely reasonable to try and
minimize them. But outside of any special circumstances (vacations,
health or similar) I think it should be possible to engage in such
discussions within a small handful of days.

In any case, there of course is a distinction between people employed to
work on Git and those that do it in their own free time. The expectation
that is extended towards people who work on Git is way higher than the
expectation extended towards people who don't.

> > it would be
> > fair to expect that we can say something like "I agree, I'll fix",
> > "I am not convinced because ...", "I am skeptical but let me first
> > see how it pans out", etc. by day #2 or #3, wouldn't it?  Upon
> > recieving a response at the same time or soon after an updated
> > iteration was sent, especially when the response is "no, I do not
> > think so", what is the reviewer expected to do?  Saying "you may not
> > think so but here is another point that may make you reconsider"
> > would be too late, so it would actively discourage continued
> > discussion.
> >
> > > It doesn't mean that I think oldtimers should have some kind of
> > > privilege, and yeah they should also try to give a good example. But
> > > we should allow people to not always behave in a very formatted way.
> >
> > Old timers learn from experience how other old timers operate ;-)
> > and I have learned to ignore the usual signal when anticipating what
> > your next iteration may look like (in other words, interim responses
> > or lack thereof is usually a good signal for most developers, but
> > not for you---you tend to come back with your next iteration without
> > much interim interactions).  But other contributors shouldn't be
> > forced to. That is what we need some community norm for.
> >
> > >> On our team's handbook page [1] we have the following couple of bullet
> > >> points regarding how to respond to reviews:
> > >
> > > Yeah, I think they are likely to be good for newcomers.
> >
> > The handbook here is gitlab's team handbook, and it may not apply to
> > open source Git development community, but "this rule applies only
> > to newcomers, I am above that rule" is the same thing as saying
> > "oldtimers like me should have some kind of privilege".  I do not
> > know what to think about this and what you said above.
> 
> I think it's fair to say that oldtimers are less likely to disappear
> tomorrow or to not follow up on reviewer feedback. So arguments like
> "replying fast makes reviewers confident that they have been heard"
> are just less true for oldtimers than for newcomers.

I think these are two different things. It's probably true that you get
some privileges by being an old timer. But I think it's more in the
sense of "You get to tackle bigger things that may not be done in a
single patch series, and we trust you to not just disappear".

But with that privilege also comes responsibility. It's those old timers
that newcomers look to, so they need to lead by example.

> Another argument is that oldtimers are more likely to burn out or have
> mental health issues related to their Git work than newcomers. Adding
> a norm that would put pressure on them to work more, or at times they
> would prefer to do other things, significantly increases the burnout
> and mental health risks for them.
> 
> So putting pressure on oldtimers to follow a norm that is less
> relevant for them but puts them at greater risk is just a bad idea in
> my opinion.

This is of course something we must avoid. Nobody is being helped in
case people burn out. There needs to be a middle ground that works for
everyone.

Patrick

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-10  8:25                     ` Patrick Steinhardt
@ 2025-07-10 15:29                       ` Christian Couder
  2025-07-10 15:33                       ` Junio C Hamano
  1 sibling, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-10 15:29 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: Junio C Hamano, git, Elijah Newren, Jeff King, brian m . carlson,
	Johannes Schindelin, Christian Couder

On Thu, Jul 10, 2025 at 2:01 PM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Wed, Jul 09, 2025 at 02:19:06AM +0200, Christian Couder wrote:
> > On Tue, Jul 8, 2025 at 6:38 PM Junio C Hamano <gitster@pobox.com> wrote:

> > > If we need a week to come up with a newer iteration,
> >
> > The issue is that whatever the time we could set as a norm, like "a
> > week" here or 2 or 3 days, or one month, or whatever, we actually
> > don't know what happens in the life of contributors. Maybe some have
> > health issues, maybe some work only a few days per week, maybe some
> > oare working on their free time and don't have much free time, maybe
> > they are asked to work a lot by their company on other internal urgent
> > things, maybe they have to take care of dependent people in their
> > family, etc... So setting any norm here that everyone should try to
> > respect might just not work for some people.
> >
> > For example there was a former Outreachy intern who continued working
> > for about 2 years on their project after the internship was over. She
> > was a young mother so didn't have much time to work on Git and would
> > come back to the mailing list only every few months with new patches
> > and replies to reviewers. What would we have gained exactly by
> > imposing a norm on her?
>
> There are always going to be exceptions, that is of course true.

Ok, so how are we going to manage those exceptions? Are we going to
ask people publicly why they take a long time to reply, and expect
that they are going to tell everyone things that might be quite
personal?

How will they feel if they are asked repeatedly by different
reviewers, in the case some reviewers didn't notice that other
reviewers have already asked?

> But I
> also think that long-time contributors that are employed to work on Git
> are somewhat special and don't (typically) fall into the mentioned
> groups.

They could fall into similar groups. They could have a child they take
care of, they could have health issues (including burnout) or they
could just have a few bad days sometimes. They could also stop being
employed to work on Git and still want to contribute.

Are we going to have a public list of people who are allowed to reply
late, so that every reviewer can check if it's Ok for the contributor
to reply late?

> From my perspective, it's especially this group of people that
> should lead by example and encourage others to behave in a way that is
> good for the overall Git community.

So you think it would be good for the community if people start to
keep count of how fast others reply and annoy them when they don't
reply fast enough? In my opinion, insisting on this is just a bad idea
that could backfire in many ways.

I am not saying that it wouldn't be good if people in general tried to
reply soon to reviewers when they receive feedback. I am saying that
it's not worth the possible costs to insist on this small aspect of
what makes reviewers happy.

> And leading by example in this
> context also means that they should encourage healthy discussions.

Why is it not healthy when contributors reply when they think it's
best for them to reply? It seems to me that, on the contrary, it's not
healthy to put pressure on them to reply when they might not think
it's best for them to reply.

Responding soon is in my opinion a very small part of what makes
discussions healthy. There is also the tone, the thankfulness, the
constructiveness, the technical accuracy of the information shared,
the work that might have been done to try solutions, the willingness
to share knowledge, the effort to try to understand and act on what
others say, and so on.

Reducing the quality or healthiness of discussions to just how fast a
person replies, is just not a good metric. It's like measuring how
productive a person is by counting the lines of code produced.

> That of course doesn't mean that such people should always respond
> within an hour, or even within a day. We all have to context switch, and
> context switches are costly, so it's entirely reasonable to try and
> minimize them. But outside of any special circumstances (vacations,
> health or similar) I think it should be possible to engage in such
> discussions within a small handful of days.

You could also say "it should be possible for most people outside any
special circumstances to 'produce' 20 lines of tested code per working
day". Would that make it worth measuring and annoying people in case
the measure is not up to the norm?

> In any case, there of course is a distinction between people employed to
> work on Git and those that do it in their own free time. The expectation
> that is extended towards people who work on Git is way higher than the
> expectation extended towards people who don't.

What kind of expectation? Do you mean that the resulting code after
review, or even before review, should be way better too? Even when the
person employed might be a junior programmer while there are very
experienced contributors working on Git in their free time? And if for
example someone retires from work and then starts contributing to Git
10 hours per day every day of the year, then should people employed to
work on Git try to match that amount of working hours? If someone
working on Git in their free time has an especially good mastery of
the English language and starts to write awesome commit messages that
are on average 100 lines long, then are we going to expect people
employed to work on Git especially those who are not native speakers
to do as well in this regard?

People are just different in many different ways, and what you might
very well expect trivially from some, might be very difficult for
others.

> > I think it's fair to say that oldtimers are less likely to disappear
> > tomorrow or to not follow up on reviewer feedback. So arguments like
> > "replying fast makes reviewers confident that they have been heard"
> > are just less true for oldtimers than for newcomers.
>
> I think these are two different things. It's probably true that you get
> some privileges by being an old timer. But I think it's more in the
> sense of "You get to tackle bigger things that may not be done in a
> single patch series, and we trust you to not just disappear".
>
> But with that privilege also comes responsibility. It's those old timers
> that newcomers look to, so they need to lead by example.

People often are good in some ways and bad in others. You cannot
expect all old timers to be perfect examples in every way. And that
should be fine.

> > Another argument is that oldtimers are more likely to burn out or have
> > mental health issues related to their Git work than newcomers. Adding
> > a norm that would put pressure on them to work more, or at times they
> > would prefer to do other things, significantly increases the burnout
> > and mental health risks for them.
> >
> > So putting pressure on oldtimers to follow a norm that is less
> > relevant for them but puts them at greater risk is just a bad idea in
> > my opinion.
>
> This is of course something we must avoid. Nobody is being helped in
> case people burn out. There needs to be a middle ground that works for
> everyone.

My opinion is that in cases like this, there is no norm that works for everyone.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v4] fast-(import|export): improve on commit signature output format
  2025-07-10  8:25                     ` Patrick Steinhardt
  2025-07-10 15:29                       ` Christian Couder
@ 2025-07-10 15:33                       ` Junio C Hamano
  1 sibling, 0 replies; 65+ messages in thread
From: Junio C Hamano @ 2025-07-10 15:33 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: Christian Couder, git, Elijah Newren, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Patrick Steinhardt <ps@pks.im> writes:

> There are always going to be exceptions, that is of course true. But I
> also think that long-time contributors that are employed to work on Git
> are somewhat special and don't (typically) fall into the mentioned
> groups. From my perspective, it's especially this group of people that
> should lead by example and encourage others to behave in a way that is
> good for the overall Git community. And leading by example in this
> context also means that they should encourage healthy discussions.

Thanks.  

I do not have very much to add to what you already have said.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v6] fast-(import|export): improve on commit signature output format
  2025-07-09 14:12         ` [PATCH v6] " Christian Couder
  2025-07-09 23:14           ` Junio C Hamano
@ 2025-07-14 21:07           ` Elijah Newren
  2025-07-14 21:23             ` Junio C Hamano
  1 sibling, 1 reply; 65+ messages in thread
From: Elijah Newren @ 2025-07-14 21:07 UTC (permalink / raw)
  To: Christian Couder
  Cc: git, Junio C Hamano, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Sorry for the delay; noticed this in what's cooking and wanted to
double check whether I missed anything, and decided to add a comment
here...

On Wed, Jul 9, 2025 at 7:13 AM Christian Couder
<christian.couder@gmail.com> wrote:
>
[...]
> Let's improve on these limitations by improving fast-export and
> fast-import so that:
>
>   - all the signatures are exported,
>   - at most one signature on the SHA-1 object and one on the SHA-256
>     are imported,
>   - if there is more than one signature on the SHA-1 object or on
>     the SHA-256 object, fast-import emits a warning for each
>     additional signature,
>   - the output format is "gpgsig <git-hash-algo> <signature-format>",
>     where <git-hash-algo> is the Git object format as before, and
>     <signature-format> is the signature type ("openpgp", "x509",
>     "ssh" or "unknown"),
>   - the output is properly documented.
>
[...]
>
> +test_expect_success GPGSM 'setup X.509 signed commit' '
> +
> +       git checkout -b x509-signing main &&
> +       test_config gpg.format x509 &&
> +       test_config user.signingkey $GIT_COMMITTER_EMAIL &&
> +       echo "X.509 content" >file &&
> +       git add file &&
> +       git commit -S -m "X.509 signed commit" &&
> +       X509_COMMIT=$(git rev-parse HEAD) &&
> +       git checkout main
> +
> +'
> +
> +test_expect_success GPGSM 'round-trip X.509 signed commit' '
> +
> +       git fast-export --signed-commits=verbatim x509-signing >output &&
> +       test_grep -E "^gpgsig $GIT_DEFAULT_HASH x509" output &&
> +       (
> +               cd new &&
> +               git fast-import &&
> +               git cat-file commit refs/heads/x509-signing >actual &&
> +               grep "^gpgsig" actual &&
> +               IMPORTED=$(git rev-parse refs/heads/x509-signing) &&
> +               test $X509_COMMIT = $IMPORTED
> +       ) <output
> +
[...]
> +
> +test_expect_success GPGSSH 'round-trip SSH signed commit' '
> +
> +       git fast-export --signed-commits=verbatim ssh-signing >output &&
> +       test_grep -E "^gpgsig $GIT_DEFAULT_HASH ssh" output &&
> +       (
> +               cd new &&
> +               git fast-import &&
> +               git cat-file commit refs/heads/ssh-signing >actual &&
> +               grep "^gpgsig" actual &&
> +               IMPORTED=$(git rev-parse refs/heads/ssh-signing) &&
> +               test $SSH_COMMIT = $IMPORTED
> +       ) <output
> +
> +'
> +

Overall, the patch looks great now.  There's just one little nit-pick
left; I'm still not a fan of tests of the form

  (
    cd dir &&
    git fast-import
    ...lots of other commands...
  ) <output

because I think the "<output" should really be moved to the "git
fast-import" line since it's only meant to be used there.

This series adds 2 such tests.  You did point out in the discussion on
v5 that the testsuite already uses this idiom and you wanted to match
existing style.  (Though there were only 2 tests previously that used
it, and you already modified both as part of this patch.)

However...we've been through enough rounds and this is really just a
nit-pick; I can submit a follow-on patch later to clean up the four
tests and see if others agree with me that this is an eyesore, or if
I'm just weird.

I think it's good to merge down.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v6] fast-(import|export): improve on commit signature output format
  2025-07-14 21:07           ` Elijah Newren
@ 2025-07-14 21:23             ` Junio C Hamano
  2025-07-25 16:11               ` Christian Couder
  0 siblings, 1 reply; 65+ messages in thread
From: Junio C Hamano @ 2025-07-14 21:23 UTC (permalink / raw)
  To: Elijah Newren
  Cc: Christian Couder, git, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

Elijah Newren <newren@gmail.com> writes:

> Overall, the patch looks great now.  There's just one little nit-pick
> left; I'm still not a fan of tests of the form
>
>   (
>     cd dir &&
>     git fast-import
>     ...lots of other commands...
>   ) <output
>
> because I think the "<output" should really be moved to the "git
> fast-import" line since it's only meant to be used there.
>
> This series adds 2 such tests.  You did point out in the discussion on
> v5 that the testsuite already uses this idiom and you wanted to match
> existing style.  (Though there were only 2 tests previously that used
> it, and you already modified both as part of this patch.)
>
> However...we've been through enough rounds and this is really just a
> nit-pick; I can submit a follow-on patch later to clean up the four
> tests and see if others agree with me that this is an eyesore, or if
> I'm just weird.

FWIW, I think it makes sense to ensure that the "output" is consumed
only by the intended command.  And "there are already two cases"
would not work very well as an excuse to add two more to make the
codebase even worse.

> I think it's good to merge down.

OK.  As long as somebody promises that the result will be cleaned up
soon later, I am OK with that.

Thanks.

^ permalink raw reply	[flat|nested] 65+ messages in thread

* Re: [PATCH v6] fast-(import|export): improve on commit signature output format
  2025-07-14 21:23             ` Junio C Hamano
@ 2025-07-25 16:11               ` Christian Couder
  0 siblings, 0 replies; 65+ messages in thread
From: Christian Couder @ 2025-07-25 16:11 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Elijah Newren, git, Patrick Steinhardt, Jeff King,
	brian m . carlson, Johannes Schindelin, Christian Couder

On Mon, Jul 14, 2025 at 11:23 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Elijah Newren <newren@gmail.com> writes:
>
> > Overall, the patch looks great now.  There's just one little nit-pick
> > left; I'm still not a fan of tests of the form
> >
> >   (
> >     cd dir &&
> >     git fast-import
> >     ...lots of other commands...
> >   ) <output
> >
> > because I think the "<output" should really be moved to the "git
> > fast-import" line since it's only meant to be used there.
> >
> > This series adds 2 such tests.  You did point out in the discussion on
> > v5 that the testsuite already uses this idiom and you wanted to match
> > existing style.  (Though there were only 2 tests previously that used
> > it, and you already modified both as part of this patch.)
> >
> > However...we've been through enough rounds and this is really just a
> > nit-pick; I can submit a follow-on patch later to clean up the four
> > tests and see if others agree with me that this is an eyesore, or if
> > I'm just weird.
>
> FWIW, I think it makes sense to ensure that the "output" is consumed
> only by the intended command.  And "there are already two cases"
> would not work very well as an excuse to add two more to make the
> codebase even worse.

I have just sent the following patch to fix all the instances of this
in t9350-fast-export.sh:

https://lore.kernel.org/git/20250725160536.2909011-1-christian.couder@gmail.com/

> > I think it's good to merge down.
>
> OK.  As long as somebody promises that the result will be cleaned up
> soon later, I am OK with that.

Thanks both for reviewing the previous patch.

^ permalink raw reply	[flat|nested] 65+ messages in thread

end of thread, other threads:[~2025-07-25 16:11 UTC | newest]

Thread overview: 65+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-04-24 20:39 [PATCH] fast-(import|export): improve on the signature algorithm name Christian Couder
2025-04-24 21:19 ` Junio C Hamano
2025-04-24 21:59   ` Elijah Newren
2025-04-24 22:58     ` Junio C Hamano
2025-05-26 10:35       ` Christian Couder
2025-05-27 15:18         ` Junio C Hamano
2025-05-28 17:29           ` Junio C Hamano
2025-05-28 20:06             ` Elijah Newren
2025-05-28 21:59               ` Junio C Hamano
2025-05-28 23:15                 ` Elijah Newren
2025-05-29  3:14                   ` Junio C Hamano
2025-06-02 15:56                     ` Christian Couder
2025-06-02 15:56             ` Christian Couder
2025-06-02 16:20               ` Junio C Hamano
2025-05-26 10:34   ` Christian Couder
2025-04-24 21:41 ` Elijah Newren
2025-05-26 10:34   ` Christian Couder
2025-04-24 22:05 ` brian m. carlson
2025-05-26 10:35   ` Christian Couder
2025-04-24 23:25 ` Junio C Hamano
2025-05-26 10:33 ` [PATCH v2 0/6] extract algo information from signatures Christian Couder
2025-05-26 10:33   ` [PATCH v2 1/6] gpg-interface: simplify ssh fingerprint parsing Christian Couder
2025-05-26 10:33   ` [PATCH v2 2/6] gpg-interface: use left shift to define GPG_VERIFY_* Christian Couder
2025-05-26 10:33   ` [PATCH v2 3/6] doc/verify-commit: update and improve the whole doc Christian Couder
2025-05-26 10:33   ` [PATCH v2 4/6] gpg-interface: extract hash algorithm from signature status output Christian Couder
2025-05-26 10:33   ` [PATCH v2 5/6] gpg-interface: extract SSH key type " Christian Couder
2025-05-26 10:33   ` [PATCH v2 6/6] verify-commit: add a --summary flag Christian Couder
2025-05-26 16:03   ` [PATCH v2 0/6] extract algo information from signatures Elijah Newren
2025-06-19 13:38     ` Christian Couder
2025-06-02 22:17   ` brian m. carlson
2025-06-19 13:37     ` Christian Couder
2025-06-18 15:18   ` [PATCH v3] fast-(import|export): improve on commit signature output format Christian Couder
2025-06-19 13:36     ` [PATCH v4] " Christian Couder
2025-06-19 14:55       ` Junio C Hamano
2025-07-08  9:16         ` Christian Couder
2025-06-19 21:44       ` Elijah Newren
2025-06-20 16:12         ` Christian Couder
2025-06-20 19:20           ` Junio C Hamano
2025-07-08  9:16             ` Christian Couder
2025-06-26 19:11           ` Elijah Newren
2025-07-08  9:16             ` Christian Couder
2025-07-07 22:58       ` Junio C Hamano
2025-07-08  3:35         ` Christian Couder
2025-07-08  5:03           ` Junio C Hamano
2025-07-08  6:38             ` Patrick Steinhardt
2025-07-08 11:08               ` Christian Couder
2025-07-08 16:38                 ` Junio C Hamano
2025-07-09  0:19                   ` Christian Couder
2025-07-09 15:35                     ` Junio C Hamano
2025-07-10  8:25                     ` Patrick Steinhardt
2025-07-10 15:29                       ` Christian Couder
2025-07-10 15:33                       ` Junio C Hamano
2025-07-08 10:17             ` Christian Couder
2025-07-08  9:17       ` [PATCH v5] " Christian Couder
2025-07-08 21:58         ` Junio C Hamano
2025-07-08 23:08         ` Elijah Newren
2025-07-09  0:03           ` Junio C Hamano
2025-07-09  0:10             ` Elijah Newren
2025-07-09 10:18             ` Christian Couder
2025-07-09 10:15           ` Christian Couder
2025-07-09 14:12         ` [PATCH v6] " Christian Couder
2025-07-09 23:14           ` Junio C Hamano
2025-07-14 21:07           ` Elijah Newren
2025-07-14 21:23             ` Junio C Hamano
2025-07-25 16:11               ` Christian Couder

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).