Git development
 help / color / mirror / Atom feed
From: Patrick Monette <pmonette@google.com>
To: git@vger.kernel.org
Cc: newren@gmail.com, ps@pks.im, toon@iotcl.com,
	 Patrick Monette <pmonette@google.com>
Subject: [PATCH 2/2] replay: add the -S option
Date: Fri, 25 Sep 2026 16:53:48 -0400	[thread overview]
Message-ID: <20260925205348.1210154-3-pmonette@google.com> (raw)
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


  parent reply	other threads:[~2026-09-25 20:53 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-25 20:53 [PATCH 0/2] replay: add signing support Patrick Monette
2026-09-25 20:53 ` [PATCH 1/2] replay: handle failure to create commits Patrick Monette
2026-09-28  7:41   ` Patrick Steinhardt
2026-09-25 20:53 ` Patrick Monette [this message]
2026-09-28  7:41   ` [PATCH 2/2] replay: add the -S option Patrick Steinhardt
2026-10-02 21:23   ` Junio C Hamano
2026-09-28  7:41 ` [PATCH 0/2] replay: add signing support Patrick Steinhardt
2026-09-28 15:23   ` Junio C Hamano

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260925205348.1210154-3-pmonette@google.com \
    --to=pmonette@google.com \
    --cc=git@vger.kernel.org \
    --cc=newren@gmail.com \
    --cc=ps@pks.im \
    --cc=toon@iotcl.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox