Volume XXII, number 279Tuesday, October 6, 2026Latest message 22 minutes ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patch, 2 partsreplay: add signing support

8 messages between Sep 25, 2026 and Oct 2, 2026, from Patrick Monette, Patrick Steinhardt, Junio C Hamano.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Patrick MonetteSep 25, 2026, 20:53 UTC on lore

This pair of commits fixes a FIXME in replay.c. With this, it's possible to sign commits using `git replay`.

To follow the convention of git plumbing commands, where they must behave the same regardless of user config, `commit.gpgSign` is intentionally ignored.

The first patch fixes pick_regular_commit() to ensure failures to create commits are correctly handled, which can now happen more easily because of signing.

Patrick Monette (2):
  replay: handle failure to create commits
  replay: add the -S option
 Documentation/git-replay.adoc |  10 +++-
 builtin/replay.c              |  12 +++-
 replay.c                      |  17 ++++--
 replay.h                      |   6 ++
 t/meson.build                 |   1 +
 t/t3651-replay-gpg-sign.sh    | 107 ++++++++++++++++++++++++++++++++++
 6 files changed, 146 insertions(+), 7 deletions(-)
 create mode 100755 t/t3651-replay-gpg-sign.sh
-- 
2.56.0.rc1.315.gc6ed9934b7-goog
Patrick MonetteSep 25, 2026, 20:53 UTC in reply to Patrick Monette on lore

[PATCH 1/2] replay: handle failure to create commits

When pick_regular_commit() returns NULL, the caller relies on `result->clean` to figure out what happened. 1 means success, 0 means a conflict, and a negative value means an error.

Right now, if the commit creation fails, `result->clean` stays at 1. The caller doesn't expect the combination of NULL + clean == 1, so it breaks out of the loop, but the rest of the function treats this as a success.

The next commit will add a failure mode (signing) to the commit creation, so this needs to be handled correctly. Set `result->clean` to -1 when the commit creation fails.

Signed-off-by: Patrick Monette <pmonette@google.com>
---
 replay.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)
Show changes to replay.c +6 −1
diff --git a/replay.c b/replay.c
index f415103023..ad87863565 100644
--- a/replay.c
+++ b/replay.c
@@ -291,6 +291,7 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  enum replay_empty_commit_action empty)
 {
 	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
+	struct commit *new_commit;
 
 	if (pickme->parents)
 		base_tree = repo_get_commit_tree(repo, pickme->parents->item);
@@ -361,7 +362,11 @@ static struct commit *pick_regular_commit(struct repository *repo,
 		}
 	}
 
-	return create_commit(repo, result->tree, pickme, replayed_base, mode);
+	new_commit = create_commit(repo, result->tree, pickme, replayed_base,
+				   mode);
+	if (!new_commit)
+		result->clean = -1;
+	return new_commit;
 }
 
 void replay_result_release(struct replay_result *result)
-- 
2.56.0.rc1.315.gc6ed9934b7-goog
Patrick MonetteSep 25, 2026, 20:53 UTC in reply to Patrick Monette on lore

[PATCH 2/2] replay: add the -S option

`git replay` currently doesn't support signing. In fact, there is a FIXME to address this.

Add the -S option and its related options --gpg-sign and --no-gpg-sign.
Signed-off-by: Patrick Monette <pmonette@google.com>
---
 Documentation/git-replay.adoc |  10 +++-
 builtin/replay.c              |  12 +++-
 replay.c                      |  12 ++--
 replay.h                      |   6 ++
 t/meson.build                 |   1 +
 t/t3651-replay-gpg-sign.sh    | 107 ++++++++++++++++++++++++++++++++++
 6 files changed, 141 insertions(+), 7 deletions(-)
 create mode 100755 t/t3651-replay-gpg-sign.sh
Show changes to 6 files +141 −7

Documentation/git-replay.adoc, builtin/replay.c, replay.c, replay.h, t/meson.build, t/t3651-replay-gpg-sign.sh

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 58b4c0c470..2e5caba0be 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [verse]
 (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
-			     [--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>
+			     [--ref=<ref>] [--ref-action=<mode>] [--linearize] [-S[<keyid>]] <revision-range>
 
 DESCRIPTION
 -----------
@@ -106,6 +106,14 @@ behavior of git-rebase(1)'s `--no-rebase-merges` option.)
 `--contained`. To linearize several branches, replay them in separate
 `git replay` invocations.
 
+-S[<keyid>]::
+--gpg-sign[=<keyid>]::
+--no-gpg-sign::
+	GPG-sign commits. The `keyid` argument is optional and
+	defaults to the committer identity; if specified, it must be
+	stuck to the option without a space. `--no-gpg-sign` is useful to
+	countermand a `--gpg-sign` option given earlier on the command line.
+
 <revision-range>::
 	Range of commits to replay; see "Specifying Ranges" in
 	linkgit:git-rev-parse[1]. In `--advance=<branch>` or
diff --git a/builtin/replay.c b/builtin/replay.c
index d39626a37d..87c628e2ed 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -85,7 +85,7 @@ int cmd_replay(int argc,
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
-		   "[--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>"),
+		   "[--ref=<ref>] [--ref-action=<mode>] [--linearize] [-S[<keyid>]] <revision-range>"),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -113,6 +113,16 @@ int cmd_replay(int argc,
 			     PARSE_OPT_NONEG),
 		OPT_BOOL(0, "linearize", &opts.linearize,
 			 N_("drop merge commits, replaying only non-merge commits")),
+		{
+			.type = OPTION_STRING,
+			.short_name = 'S',
+			.long_name = "gpg-sign",
+			.value = &opts.sign_commit,
+			.argh = N_("key-id"),
+			.help = N_("GPG-sign commits"),
+			.flags = PARSE_OPT_OPTARG,
+			.defval = (intptr_t) "",
+		},
 		OPT_END()
 	};
 
diff --git a/replay.c b/replay.c
index ad87863565..9a84e297b1 100644
--- a/replay.c
+++ b/replay.c
@@ -85,13 +85,13 @@ static struct commit *create_commit(struct repository *repo,
 				    struct tree *tree,
 				    struct commit *based_on,
 				    struct commit *parent,
-				    enum replay_mode mode)
+				    enum replay_mode mode,
+				    const char *sign_commit)
 {
 	struct object_id ret;
 	struct object *obj = NULL;
 	struct commit_list *parents = NULL;
 	char *author = NULL;
-	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
 	struct commit_extra_header *extra = NULL;
 	struct strbuf msg = STRBUF_INIT;
 	const char *out_enc = get_commit_output_encoding();
@@ -288,7 +288,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result,
 					  enum replay_mode mode,
-					  enum replay_empty_commit_action empty)
+					  enum replay_empty_commit_action empty,
+					  const char *sign_commit)
 {
 	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
 	struct commit *new_commit;
@@ -363,7 +364,7 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	}
 
 	new_commit = create_commit(repo, result->tree, pickme, replayed_base,
-				   mode);
+				   mode, sign_commit);
 	if (!new_commit)
 		result->clean = -1;
 	return new_commit;
@@ -486,7 +487,8 @@ int replay_revisions(struct rev_info *revs,
 
 			last_commit = pick_regular_commit(revs->repo, commit, base,
 							  &merge_opt, &result,
-							  mode, opts->empty);
+							  mode, opts->empty,
+							  opts->sign_commit);
 		}
 
 		if (!last_commit)
diff --git a/replay.h b/replay.h
index 2c71afbfde..7e93ab9565 100644
--- a/replay.h
+++ b/replay.h
@@ -67,6 +67,12 @@ struct replay_revisions_options {
 	 * Whether to linearize the commits (i.e. drop merge commits).
 	 */
 	int linearize;
+
+	/*
+	 * If non-NULL, GPG-sign the new commits. An empty string signs with
+	 * the default key (the committer identity); otherwise, the key ID.
+	 */
+	const char *sign_commit;
 };
 
 /* This struct is used as an out-parameter by `replay_revisions()`. */
diff --git a/t/meson.build b/t/meson.build
index 3ca7b27104..93c01e26a2 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -426,6 +426,7 @@ integration_tests = [
   't3601-rm-pathspec-file.sh',
   't3602-rm-sparse-checkout.sh',
   't3650-replay-basics.sh',
+  't3651-replay-gpg-sign.sh',
   't3700-add.sh',
   't3701-add-interactive.sh',
   't3702-add-edit.sh',
diff --git a/t/t3651-replay-gpg-sign.sh b/t/t3651-replay-gpg-sign.sh
new file mode 100755
index 0000000000..61a3375b52
--- /dev/null
+++ b/t/t3651-replay-gpg-sign.sh
@@ -0,0 +1,107 @@
+#!/bin/sh
+
+test_description='git replay --[no-]gpg-sign'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-gpg.sh"
+
+if ! test_have_prereq GPG
+then
+	skip_all='skip all git replay --[no-]gpg-sign tests, gpg not available'
+	test_done
+fi
+
+# Checks that "topic" was replayed onto "main", and that the replayed
+# commits are all signed ("signed") or all unsigned ("unsigned").
+check_replayed () {
+	git merge-base --is-ancestor main topic &&
+	git rev-list main..topic >replayed &&
+	test_line_count = 2 replayed &&
+	for commit in $(cat replayed)
+	do
+		case "$1" in
+		signed)
+			git verify-commit $commit || return 1
+			;;
+		unsigned)
+			test_must_fail git verify-commit $commit || return 1
+			;;
+		esac
+	done
+}
+
+test_expect_success 'setup' '
+	test_commit A &&
+	test_commit B &&
+	git switch -c topic A &&
+	echo C >C &&
+	git add C &&
+	git commit -S -m C &&
+	git tag C &&
+	echo D >D &&
+	git add D &&
+	git commit -S -m D &&
+	git tag D &&
+	git switch main
+'
+
+test_expect_success 'replay without --gpg-sign does not sign' '
+	git branch -f topic D &&
+	git verify-commit C &&
+	git verify-commit D &&
+	git replay --onto main A..topic &&
+	check_replayed unsigned
+'
+
+test_expect_success 'replay --gpg-sign signs with the default key' '
+	git branch -f topic D &&
+	git replay --gpg-sign --onto main A..topic &&
+	check_replayed signed &&
+	echo "C O Mitter <committer@example.com>" >expect &&
+	git log -1 --format="%GS" topic >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'replay -S<keyid> signs with the given key' '
+	git branch -f topic D &&
+	git replay -SB7227189 --onto main A..topic &&
+	git rev-list main..topic >replayed &&
+	test_line_count = 2 replayed &&
+	echo D4BE22311AD3131E5EDA29A461092E85B7227189 >expect &&
+	for commit in $(cat replayed)
+	do
+		git log -1 --format="%GP" $commit >actual &&
+		test_cmp expect actual || return 1
+	done
+'
+
+test_expect_success 'replay --no-gpg-sign countermands --gpg-sign' '
+	git branch -f topic D &&
+	git replay --gpg-sign --no-gpg-sign --onto main A..topic &&
+	check_replayed unsigned
+'
+
+test_expect_success 'replay ignores commit.gpgSign' '
+	git branch -f topic D &&
+	git -c commit.gpgSign=true replay --onto main A..topic &&
+	check_replayed unsigned
+'
+
+test_expect_success 'replay fails and updates no ref when signing fails' '
+	git branch -f topic D &&
+	test_must_fail git replay -Snonexistent-key --onto main A..topic &&
+	test_cmp_rev D topic
+'
+
+test_expect_success 'replay --ref fails and updates no ref when signing fails' '
+	git branch -f topic D &&
+	test_must_fail git replay -Snonexistent-key --onto main \
+		--ref refs/heads/other A..topic &&
+	test_must_fail git rev-parse --verify refs/heads/other &&
+	test_cmp_rev D topic
+'
+
+test_done
-- 
2.56.0.rc1.315.gc6ed9934b7-goog
Patrick SteinhardtSep 28, 2026, 07:41 UTC in reply to Patrick Monette on lore

Re: [PATCH 1/2] replay: handle failure to create commits

On Fri, Sep 25, 2026 at 04:53:47PM -0400, Patrick Monette wrote:
Show 11 quoted lines
> When pick_regular_commit() returns NULL, the caller relies on
> `result->clean` to figure out what happened. 1 means success, 0 means a
> conflict, and a negative value means an error.
> 
> Right now, if the commit creation fails, `result->clean` stays at 1. The
> caller doesn't expect the combination of NULL + clean == 1, so it breaks
> out of the loop, but the rest of the function treats this as a success.
> 
> The next commit will add a failure mode (signing) to the commit
> creation, so this needs to be handled correctly. Set `result->clean`
> to -1 when the commit creation fails.
True, this is something we should fix indeed.
Show 15 quoted lines
> diff --git a/replay.c b/replay.c
> index f415103023..ad87863565 100644
> --- a/replay.c
> +++ b/replay.c
> @@ -361,7 +362,11 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  		}
>  	}
>  
> -	return create_commit(repo, result->tree, pickme, replayed_base, mode);
> +	new_commit = create_commit(repo, result->tree, pickme, replayed_base,
> +				   mode);
> +	if (!new_commit)
> +		result->clean = -1;
> +	return new_commit;
>  }

Other error paths end up printing an error message. But we don't have to because `create_commit()` already knows to do that for us.

Patrick
Patrick SteinhardtSep 28, 2026, 07:41 UTC in reply to Patrick Monette on lore

Re: [PATCH 2/2] replay: add the -S option

On Fri, Sep 25, 2026 at 04:53:48PM -0400, Patrick Monette wrote:
Show 20 quoted lines
> diff --git a/builtin/replay.c b/builtin/replay.c
> index d39626a37d..87c628e2ed 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -113,6 +113,16 @@ int cmd_replay(int argc,
>  			     PARSE_OPT_NONEG),
>  		OPT_BOOL(0, "linearize", &opts.linearize,
>  			 N_("drop merge commits, replaying only non-merge commits")),
> +		{
> +			.type = OPTION_STRING,
> +			.short_name = 'S',
> +			.long_name = "gpg-sign",
> +			.value = &opts.sign_commit,
> +			.argh = N_("key-id"),
> +			.help = N_("GPG-sign commits"),
> +			.flags = PARSE_OPT_OPTARG,
> +			.defval = (intptr_t) "",
> +		},
>  		OPT_END()
>  	};

Okay. We can't use `OPT_STRING()` or `OPT_STRING_F()` here because we want to set the default value.

I notice that we don't start to honor "commit.gpgSign". Is there a reason for this?

Patrick
Patrick SteinhardtSep 28, 2026, 07:41 UTC in reply to Patrick Monette on lore

Re: [PATCH 0/2] replay: add signing support

Hi,
On Fri, Sep 25, 2026 at 04:53:46PM -0400, Patrick Monette wrote:
Show 10 quoted lines
> This pair of commits fixes a FIXME in replay.c. With this, it's possible
> to sign commits using `git replay`.
> 
> To follow the convention of git plumbing commands, where they must
> behave the same regardless of user config, `commit.gpgSign` is
> intentionally ignored.
> 
> The first patch fixes pick_regular_commit() to ensure failures to create
> commits are correctly handled, which can now happen more easily because
> of signing.

Note that there's already a patch series in flight that's adding the infra to sign commits at [1]. Your patches will conflict with that series, even though you're ultimately adapting git-replay(1) and not git-history(1). So I'd suggest that once the series at [1] land, you can maybe rebase your changes and then send a new version.

Patrick
[1]: <20260912160045.36064-1-git@5ouma.me>
Junio C HamanoSep 28, 2026, 15:23 UTC in reply to Patrick Steinhardt on lore

Re: [PATCH 0/2] replay: add signing support

Patrick Steinhardt <ps@pks.im> writes:
Show 19 quoted lines
> Hi,
>
> On Fri, Sep 25, 2026 at 04:53:46PM -0400, Patrick Monette wrote:
>> This pair of commits fixes a FIXME in replay.c. With this, it's possible
>> to sign commits using `git replay`.
>> 
>> To follow the convention of git plumbing commands, where they must
>> behave the same regardless of user config, `commit.gpgSign` is
>> intentionally ignored.
>> 
>> The first patch fixes pick_regular_commit() to ensure failures to create
>> commits are correctly handled, which can now happen more easily because
>> of signing.
>
> Note that there's already a patch series in flight that's adding the
> infra to sign commits at [1]. Your patches will conflict with that
> series, even though you're ultimately adapting git-replay(1) and not
> git-history(1). So I'd suggest that once the series at [1] land, you can
> maybe rebase your changes and then send a new version.

Ah, the other one says "history" but touches the same replay machinery to update with the signature feature, hence this will need to take advantage of that. The sequencing makes sense.

> Patrick
>
> [1]: <20260912160045.36064-1-git@5ouma.me>
Junio C HamanoOct 2, 2026, 21:23 UTC in reply to Patrick Monette on lore

Re: [PATCH 2/2] replay: add the -S option

Patrick Monette <pmonette@google.com> writes:
Show 15 quoted lines
> `git replay` currently doesn't support signing. In fact, there is a
> FIXME to address this.
>
> Add the -S option and its related options --gpg-sign and --no-gpg-sign.
>
> Signed-off-by: Patrick Monette <pmonette@google.com>
> ---
>  Documentation/git-replay.adoc |  10 +++-
>  builtin/replay.c              |  12 +++-
>  replay.c                      |  12 ++--
>  replay.h                      |   6 ++
>  t/meson.build                 |   1 +
>  t/t3651-replay-gpg-sign.sh    | 107 ++++++++++++++++++++++++++++++++++
>  6 files changed, 141 insertions(+), 7 deletions(-)
>  create mode 100755 t/t3651-replay-gpg-sign.sh

The main part of the patch, which is the change to replay.[ch], has striking similarity to another topic from mid July [*].

  https://lore.kernel.org/git/20260717145142.39478-2-git@5ouma.me/

That topic has its latest reroll posted recently and it still looks very similar.

  https://lore.kernel.org/git/20261002132718.3830-2-git@5ouma.me/

Instead of making duplicated effort, given that this community is limited by reviewer bandwidth more than it is in need of new patches, it would be very much appreciated if you can give a review to the other topic to help another developer and move it forward.

There would be things your topic wanted to do that is different from what they wanted to achieve. Theirs is about "git history", and this topic is about "git replay". So after their topic stabilized, you can salvage the remainder of your topic and rebase them on top of their patch.

Thanks.
[Footnote]
 * It shows us that there are only certain ways to implement a
   thing, and it is hard to be "original" these days ;-)
Show 66 quoted lines
> diff --git a/replay.c b/replay.c
> index ad87863565..9a84e297b1 100644
> --- a/replay.c
> +++ b/replay.c
> @@ -85,13 +85,13 @@ static struct commit *create_commit(struct repository *repo,
>  				    struct tree *tree,
>  				    struct commit *based_on,
>  				    struct commit *parent,
> -				    enum replay_mode mode)
> +				    enum replay_mode mode,
> +				    const char *sign_commit)
>  {
>  	struct object_id ret;
>  	struct object *obj = NULL;
>  	struct commit_list *parents = NULL;
>  	char *author = NULL;
> -	char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
>  	struct commit_extra_header *extra = NULL;
>  	struct strbuf msg = STRBUF_INIT;
>  	const char *out_enc = get_commit_output_encoding();
> @@ -288,7 +288,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  					  struct merge_options *merge_opt,
>  					  struct merge_result *result,
>  					  enum replay_mode mode,
> -					  enum replay_empty_commit_action empty)
> +					  enum replay_empty_commit_action empty,
> +					  const char *sign_commit)
>  {
>  	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
>  	struct commit *new_commit;
> @@ -363,7 +364,7 @@ static struct commit *pick_regular_commit(struct repository *repo,
>  	}
>  
>  	new_commit = create_commit(repo, result->tree, pickme, replayed_base,
> -				   mode);
> +				   mode, sign_commit);
>  	if (!new_commit)
>  		result->clean = -1;
>  	return new_commit;
> @@ -486,7 +487,8 @@ int replay_revisions(struct rev_info *revs,
>  
>  			last_commit = pick_regular_commit(revs->repo, commit, base,
>  							  &merge_opt, &result,
> -							  mode, opts->empty);
> +							  mode, opts->empty,
> +							  opts->sign_commit);
>  		}
>  
>  		if (!last_commit)
> diff --git a/replay.h b/replay.h
> index 2c71afbfde..7e93ab9565 100644
> --- a/replay.h
> +++ b/replay.h
> @@ -67,6 +67,12 @@ struct replay_revisions_options {
>  	 * Whether to linearize the commits (i.e. drop merge commits).
>  	 */
>  	int linearize;
> +
> +	/*
> +	 * If non-NULL, GPG-sign the new commits. An empty string signs with
> +	 * the default key (the committer identity); otherwise, the key ID.
> +	 */
> +	const char *sign_commit;
>  };
>  
>  /* This struct is used as an out-parameter by `replay_revisions()`. */

Back to recent threads