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

8 messages from 2026-09-25 to 2026-10-02. Participants: Patrick Monette, Patrick Steinhardt, Junio C Hamano.
Thread: https://gitlist.dev/t/66393

## Patrick Monette, 2026-09-25 20:53

Subject: [PATCH 0/2] replay: add signing support
Message-ID: <20260925205348.1210154-1-pmonette@google.com>

```
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 Monette, 2026-09-25 20:53

Subject: [PATCH 1/2] replay: handle failure to create commits
Message-ID: <20260925205348.1210154-2-pmonette@google.com>
In-Reply-To: <20260925205348.1210154-1-pmonette@google.com>

```
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(-)

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 Monette, 2026-09-25 20:53

Subject: [PATCH 2/2] replay: add the -S option
Message-ID: <20260925205348.1210154-3-pmonette@google.com>
In-Reply-To: <20260925205348.1210154-1-pmonette@google.com>

```
`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

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 Steinhardt, 2026-09-28 07:41

Subject: Re: [PATCH 1/2] replay: handle failure to create commits
Message-ID: <aroaLu02NQ65Y2Ju@pks.im>
In-Reply-To: <20260925205348.1210154-2-pmonette@google.com>

```
On Fri, Sep 25, 2026 at 04:53:47PM -0400, Patrick Monette wrote:
> 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.

> 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 Steinhardt, 2026-09-28 07:41

Subject: Re: [PATCH 2/2] replay: add the -S option
Message-ID: <aroaNcYqNIXcnQ-L@pks.im>
In-Reply-To: <20260925205348.1210154-3-pmonette@google.com>

```
On Fri, Sep 25, 2026 at 04:53:48PM -0400, Patrick Monette wrote:
> 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 Steinhardt, 2026-09-28 07:41

Subject: Re: [PATCH 0/2] replay: add signing support
Message-ID: <aroaOsUFWt2lYOVS@pks.im>
In-Reply-To: <20260925205348.1210154-1-pmonette@google.com>

```
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.

Patrick

[1]: <20260912160045.36064-1-git@5ouma.me>

```

## Junio C Hamano, 2026-09-28 15:23

Subject: Re: [PATCH 0/2] replay: add signing support
Message-ID: <xmqqcxtxo0vx.fsf@gitster.g>
In-Reply-To: <aroaOsUFWt2lYOVS@pks.im>

```
Patrick Steinhardt <ps@pks.im> writes:

> 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 Hamano, 2026-10-02 21:23

Subject: Re: [PATCH 2/2] replay: add the -S option
Message-ID: <xmqqse2nvltp.fsf@gitster.g>
In-Reply-To: <20260925205348.1210154-3-pmonette@google.com>

```
Patrick Monette <pmonette@google.com> writes:

> `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 ;-)


> 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()`. */

```
