Volume XXII, number 280Wednesday, October 7, 2026Latest message 1 hour ago

The Git List

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

patch, 3 partsAdd option --ref to git-replay(1)

25 messages between Mar 23, 2026 and Apr 1, 2026, from Toon Claes, Tian Yuchen, Kristoffer Haugsbakk, Junio C Hamano.

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

Toon ClaesMar 23, 2026, 16:09 UTC on lore

In a previous RFC[1] I suggested to implement subcommands into git-replay(1). While it would be arguable nice to have subcommands for the different modes, because git-replay(1) is a plumbing commands, it's fine to keep a status quo.

This series takes one thing for that RFC though: adding option --ref. This new option is useful if you want to have full control over which ref is being updated, and not want to rely on the refs that are using in the <revision-range> or as the value for --advance and --revert.

In the leadup to the final change (the last commit), some documentational consistency is added in first two commits.

These changes answer the needs expressed in[2].

This series is based on Siddharth's series[3] to add '--revert' to git-replay(1) (sa/replay-revert @ ba5c0d03d3).

[1]: https://lore.kernel.org/git/20260309-toon-replay-subcommands-v1-1-864ec82ef68a@iotcl.com/ [2]: https://lore.kernel.org/git/CAHTeOx-SMLh_idKhGczPKzZNOKy04uYXmUhL8Z79yRuNpmE4eA@mail.gmail.com/ [3]: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/

---
Toon Claes (3):
      t3650: use option with value consistenly with equal sign
      builtin/replay: improve documentation on options
      replay: allow to specify a ref with option --ref
 Documentation/git-replay.adoc |  22 ++++++-
 builtin/replay.c              |  39 ++++++++-----
 replay.c                      |  29 +++++++---
 replay.h                      |   7 +++
 t/t3650-replay-basics.sh      | 131 +++++++++++++++++++++++++++++++-----------
 5 files changed, 171 insertions(+), 57 deletions(-)

--- base-commit: ba5c0d03d39a8b1798c8f988bdc95f9a08f10fac change-id: 20260323-toon-replay-arbitrary-ref-5a81f5f976c7

Toon ClaesMar 23, 2026, 16:09 UTC in reply to Toon Claes on lore

[PATCH 1/3] t3650: use option with value consistenly with equal sign

The tests in t3650-replay-basics have mixed use of option arguments with value with and without equal sign. Bring in consistency and use equal sign for all options that expect a value. This makes it easier to distinguish them from positional arguments.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 t/t3650-replay-basics.sh | 70 ++++++++++++++++++++++++------------------------
 1 file changed, 35 insertions(+), 35 deletions(-)
Show changes to t/t3650-replay-basics.sh +35 −35
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 0c1e03e0fb..47811b3197 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -95,8 +95,8 @@ test_expect_success '--contained requires --onto' '
 '
 
 test_expect_success 'cannot advance target ... ordering would be ill-defined' '
-	cat >expect <<-\EOF &&
-	fatal: '"'"'--advance'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
+	cat >expect <<-EOF &&
+	fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined
 	EOF
 	test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
 	test_cmp expect actual
@@ -109,7 +109,7 @@ test_expect_success 'replaying merge commits is not supported yet' '
 '
 
 test_expect_success 'using replay to rebase two branches, one on top of other' '
-	git replay --ref-action=print --onto main topic1..topic2 >result &&
+	git replay --ref-action=print --onto=main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -125,16 +125,16 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '
 '
 
 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
-	git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --onto=main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
 test_expect_success 'using replay to rebase with a conflict' '
-	test_expect_code 1 git replay --onto topic1 B..conflict
+	test_expect_code 1 git replay --onto=topic1 B..conflict
 '
 
 test_expect_success 'using replay on bare repo to rebase with a conflict' '
-	test_expect_code 1 git -C bare replay --onto topic1 B..conflict
+	test_expect_code 1 git -C bare replay --onto=topic1 B..conflict
 '
 
 test_expect_success 'using replay to perform basic cherry-pick' '
@@ -143,7 +143,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 	# 2nd field of result is refs/heads/main vs. refs/heads/topic2
 	# 4th field of result is hash for main instead of hash for topic2
 
-	git replay --ref-action=print --advance main topic1..topic2 >result &&
+	git replay --ref-action=print --advance=main topic1..topic2 >result &&
 
 	test_line_count = 1 result &&
 
@@ -159,7 +159,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '
 '
 
 test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
-	git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&
+	git -C bare replay --ref-action=print --advance=main topic1..topic2 >result-bare &&
 	test_cmp expect result-bare
 '
 
@@ -170,11 +170,11 @@ test_expect_success 'commits that become empty are dropped' '
 	test_when_finished "git update-ref --stdin <original-branches &&
 		rm original-branches" &&
 	# Cherry-pick tip of topic1 ("F"), from the middle of A..empty, to main
-	git replay --advance main topic1^! &&
+	git replay --advance=main topic1^! &&
 
 	# Replay all of A..empty onto main (which includes topic1 & thus F
 	# in the middle)
-	git replay --onto main --branches --ancestry-path=empty ^A \
+	git replay --onto=main --branches --ancestry-path=empty ^A \
 		>result &&
 	git log --format="%s%d" L..empty >actual &&
 	test_write_lines >expect \
@@ -183,7 +183,7 @@ test_expect_success 'commits that become empty are dropped' '
 '
 
 test_expect_success 'replay on bare repo fails with both --advance and --onto' '
-	test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
+	test_must_fail git -C bare replay --advance=main --onto=main topic1..topic2 >result-bare
 '
 
 test_expect_success 'replay fails when both --advance and --onto are omitted' '
@@ -191,7 +191,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '
 '
 
 test_expect_success 'using replay to also rebase a contained branch' '
-	git replay --ref-action=print --contained --onto main main..topic3 >result &&
+	git replay --ref-action=print --contained --onto=main main..topic3 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -215,12 +215,12 @@ test_expect_success 'using replay to also rebase a contained branch' '
 '
 
 test_expect_success 'using replay on bare repo to also rebase a contained branch' '
-	git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&
+	git -C bare replay --ref-action=print --contained --onto=main main..topic3 >result-bare &&
 	test_cmp expect result-bare
 '
 
 test_expect_success 'using replay to rebase multiple divergent branches' '
-	git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&
+	git replay --ref-action=print --onto=main ^topic1 topic2 topic4 >result &&
 
 	test_line_count = 2 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -244,7 +244,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '
 '
 
 test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
-	git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
+	git -C bare replay --ref-action=print --contained --onto=main ^main topic2 topic3 topic4 >result &&
 
 	test_line_count = 4 result &&
 	cut -f 3 -d " " result >new-branch-tips &&
@@ -276,7 +276,7 @@ test_expect_success 'using replay to update detached HEAD' '
 	test_when_finished git switch "$current_head" &&
 	git switch --detach &&
 	test_commit something &&
-	git replay --ref-action=print --onto HEAD~2 --ref-action=print HEAD~..HEAD >updates &&
+	git replay --ref-action=print --onto=HEAD~2 --ref-action=print HEAD~..HEAD >updates &&
 	test_grep "update HEAD " updates
 '
 
@@ -299,7 +299,7 @@ test_expect_success 'merge.directoryRenames=false' '
 	git commit -m modified to-rename/add-a-file.t &&
 
 	git -c merge.directoryRenames=false replay \
-		--onto rename-onto rename-onto..rename-from
+		--onto=rename-onto rename-onto..rename-from
 '
 
 test_expect_success 'default atomic behavior updates refs directly' '
@@ -308,7 +308,7 @@ test_expect_success 'default atomic behavior updates refs directly' '
 	test_when_finished "git branch -D test-atomic" &&
 
 	# Test default atomic behavior (no output, refs updated)
-	git replay --onto main topic1..test-atomic >output &&
+	git replay --onto=main topic1..test-atomic >output &&
 	test_must_be_empty output &&
 
 	# Verify ref was updated
@@ -329,7 +329,7 @@ test_expect_success 'atomic behavior in bare repository' '
 	test_when_finished "git -C bare update-ref refs/heads/topic2 $START" &&
 
 	# Test atomic updates work in bare repo
-	git -C bare replay --onto main topic1..topic2 >output &&
+	git -C bare replay --onto=main topic1..topic2 >output &&
 	test_must_be_empty output &&
 
 	# Verify ref was updated in bare repo
@@ -344,7 +344,7 @@ test_expect_success 'reflog message for --advance mode' '
 	test_when_finished "git update-ref refs/heads/main $START" &&
 
 	# Test --advance mode reflog message
-	git replay --advance main topic1..topic2 >output &&
+	git replay --advance=main topic1..topic2 >output &&
 	test_must_be_empty output &&
 
 	# Verify reflog message includes --advance and branch name
@@ -360,7 +360,7 @@ test_expect_success 'replay.refAction=print config option' '
 
 	# Test with config set to print
 	test_config replay.refAction print &&
-	git replay --onto main topic1..topic2 >output &&
+	git replay --onto=main topic1..topic2 >output &&
 	test_line_count = 1 output &&
 	test_grep "^update refs/heads/topic2 " output
 '
@@ -372,7 +372,7 @@ test_expect_success 'replay.refAction=update config option' '
 
 	# Test with config set to update
 	test_config replay.refAction update &&
-	git replay --onto main topic1..topic2 >output &&
+	git replay --onto=main topic1..topic2 >output &&
 	test_must_be_empty output &&
 
 	# Verify ref was updated
@@ -388,14 +388,14 @@ test_expect_success 'command-line --ref-action overrides config' '
 
 	# Set config to update but use --ref-action=print
 	test_config replay.refAction update &&
-	git replay --ref-action=print --onto main topic1..topic2 >output &&
+	git replay --ref-action=print --onto=main topic1..topic2 >output &&
 	test_line_count = 1 output &&
 	test_grep "^update refs/heads/topic2 " output
 '
 
 test_expect_success 'invalid replay.refAction value' '
 	test_config replay.refAction invalid &&
-	test_must_fail git replay --onto main topic1..topic2 2>error &&
+	test_must_fail git replay --onto=main topic1..topic2 2>error &&
 	test_grep "invalid.*replay.refAction.*value" error
 '
 
@@ -407,10 +407,10 @@ test_expect_success 'argument to --revert must be a reference' '
 '
 
 test_expect_success 'cannot revert with multiple sources' '
-	cat >expect <<-\EOF &&
-	fatal: '"'"'--revert'"'"' cannot be used with multiple revision ranges because the ordering would be ill-defined
+	cat >expect <<-EOF &&
+	fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined
 	EOF
-	test_must_fail git replay --revert main main topic1 topic2 2>actual &&
+	test_must_fail git replay --revert=main main topic1 topic2 2>actual &&
 	test_cmp expect actual
 '
 
@@ -420,7 +420,7 @@ test_expect_success 'using replay --revert to revert commits' '
 	test_when_finished "git branch -f topic4 $START" &&
 
 	# Revert commits I and J
-	git replay --revert topic4 topic4~2..topic4 &&
+	git replay --revert=topic4 topic4~2..topic4 &&
 
 	# Verify the revert commits were created (newest-first ordering
 	# means J is reverted first, then I on top)
@@ -452,7 +452,7 @@ test_expect_success 'using replay --revert in bare repo' '
 	test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
 
 	# Revert commit J in bare repo
-	git -C bare replay --revert topic4 topic4~1..topic4 &&
+	git -C bare replay --revert=topic4 topic4~1..topic4 &&
 
 	# Verify revert was created
 	git -C bare log -1 --format=%s topic4 >actual &&
@@ -466,11 +466,11 @@ test_expect_success 'revert of revert uses Reapply' '
 	test_when_finished "git branch -f topic4 $START" &&
 
 	# First revert J
-	git replay --revert topic4 topic4~1..topic4 &&
+	git replay --revert=topic4 topic4~1..topic4 &&
 	REVERT_J=$(git rev-parse topic4) &&
 
 	# Now revert the revert - should become Reapply
-	git replay --revert topic4 topic4~1..topic4 &&
+	git replay --revert=topic4 topic4~1..topic4 &&
 
 	# Verify Reapply prefix and message format
 	test_commit_message topic4 <<-EOF
@@ -482,21 +482,21 @@ test_expect_success 'revert of revert uses Reapply' '
 
 test_expect_success 'git replay --revert with conflict' '
 	# conflict branch has C.conflict which conflicts with topic1s C
-	test_expect_code 1 git replay --revert conflict B..topic1
+	test_expect_code 1 git replay --revert=conflict B..topic1
 '
 
 test_expect_success 'git replay --revert incompatible with --contained' '
-	test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
+	test_must_fail git replay --revert=topic4 --contained topic4~1..topic4 2>error &&
 	test_grep "requires --onto" error
 '
 
 test_expect_success 'git replay --revert incompatible with --onto' '
-	test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
+	test_must_fail git replay --revert=topic4 --onto=main topic4~1..topic4 2>error &&
 	test_grep "cannot be used together" error
 '
 
 test_expect_success 'git replay --revert incompatible with --advance' '
-	test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
+	test_must_fail git replay --revert=topic4 --advance=main topic4~1..topic4 2>error &&
 	test_grep "cannot be used together" error
 '
 
-- 
2.53.0.310.g728cabbaf7
Toon ClaesMar 23, 2026, 16:09 UTC in reply to Toon Claes on lore

[PATCH 2/3] builtin/replay: improve documentation on options

Most of the options for the command git-replay(1) expect a value argument. Make that more clear in the docs, by using an equal sign between the option and the value.

Also, none of the options are negatable, thus mark them as such using PARSE_OPT_NONEG.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-replay.adoc |  3 ++-
 builtin/replay.c              | 32 ++++++++++++++++++--------------
 2 files changed, 20 insertions(+), 15 deletions(-)
Show changes to 2 files +20 −15

Documentation/git-replay.adoc, builtin/replay.c

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 6698cfc047..e4fef548e0 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,7 +9,8 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
+			     [--ref-action=<mode>] <revision-range>
 
 DESCRIPTION
 -----------
diff --git a/builtin/replay.c b/builtin/replay.c
index fe69f6f8ce..2e9fd6281d 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -83,25 +83,29 @@ int cmd_replay(int argc,
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
-		   "[--ref-action[=<mode>]] <revision-range>..."),
+		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
+		   "[--ref-action=<mode>] <revision-range>"),
 		NULL
 	};
 	struct option replay_options[] = {
-		OPT_STRING(0, "advance", &opts.advance,
-			   N_("branch"),
-			   N_("make replay advance given branch")),
-		OPT_STRING(0, "onto", &opts.onto,
-			   N_("revision"),
-			   N_("replay onto given commit")),
+		OPT_STRING_F(0, "onto", &opts.onto,
+			     N_("revision"),
+			     N_("replay onto given commit"),
+			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "advance", &opts.advance,
+			     N_("branch"),
+			     N_("make replay advance given branch"),
+			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "revert", &opts.revert,
+			     N_("branch"),
+			     N_("revert commits onto given branch"),
+			     PARSE_OPT_NONEG),
 		OPT_BOOL(0, "contained", &opts.contained,
 			 N_("update all branches that point at commits in <revision-range>")),
-		OPT_STRING(0, "revert", &opts.revert,
-			   N_("branch"),
-			   N_("revert commits onto given branch")),
-		OPT_STRING(0, "ref-action", &ref_action,
-			   N_("mode"),
-			   N_("control ref update behavior (update|print)")),
+		OPT_STRING_F(0, "ref-action", &ref_action,
+			     N_("mode"),
+			     N_("control ref update behavior (update|print)"),
+			     PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
-- 
2.53.0.310.g728cabbaf7
Toon ClaesMar 23, 2026, 16:09 UTC in reply to Toon Claes on lore

[PATCH 3/3] replay: allow to specify a ref with option --ref

To enable users to replay commits without having refs in <revision-range> or as the value for --advance and --revert, start accepting option --ref. When the user specifies a --ref, the refs from the other option are ignored, and only the ref is acted on.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-replay.adoc | 21 ++++++++++++++-
 builtin/replay.c              |  9 ++++++-
 replay.c                      | 29 +++++++++++++++-----
 replay.h                      |  7 +++++
 t/t3650-replay-basics.sh      | 61 +++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 118 insertions(+), 9 deletions(-)
Show changes to 5 files +118 −9

Documentation/git-replay.adoc, builtin/replay.c, replay.c, replay.h, t/t3650-replay-basics.sh

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index e4fef548e0..f90d93ae31 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -10,7 +10,8 @@ SYNOPSIS
 --------
 [verse]
 (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
-			     [--ref-action=<mode>] <revision-range>
+			     [--ref=<branch>] [--ref-action=<mode>]
+			     <revision-range>
 
 DESCRIPTION
 -----------
@@ -62,6 +63,14 @@ the current user as the author, matching the behavior of `git revert`.
 This option is mutually exclusive with `--onto` and `--advance`. It is also
 incompatible with `--contained` (which is a modifier for `--onto` only).
 
+--ref <branch>::
+	Override which reference is updated with the result of the
+	replay. When used with `--onto`, only the specified reference
+	is updated instead of inferring refs from the revision range.
+	When used with `--advance` or `--revert`, the specified
+	reference is updated instead of the branch given to those
+	options. This option is incompatible with `--contained`.
+
 --contained::
 	Update all branches that point at commits in
 	<revision-range>. Requires `--onto`.
@@ -184,6 +193,16 @@ top of `main`, and updates `main` to point at the result. This is useful when
 commits from `topic` were previously merged or cherry-picked into `main` and
 need to be undone.
 
+To replay onto a specific commit while updating a different reference:
+
+------------
+$ git replay --onto 112233 --ref refs/heads/mybranch aabbcc..ddeeff
+------------
+
+This replays the range `aabbcc..ddeeff` onto commit `112233` and updates
+`refs/heads/mybranch` to point at the result. This is useful when you want
+to use bare commit IDs instead of branch names.
+
 NOTE: For reverting an entire merge request as a single commit (rather than
 commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
 which can avoid unnecessary merge conflicts.
diff --git a/builtin/replay.c b/builtin/replay.c
index 2e9fd6281d..f4fc18d16f 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -84,7 +84,8 @@ int cmd_replay(int argc,
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
-		   "[--ref-action=<mode>] <revision-range>"),
+		   "[--ref=<branch>] [--ref-action=<mode>]\n"
+		   "<revision-range>"),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -102,6 +103,10 @@ int cmd_replay(int argc,
 			     PARSE_OPT_NONEG),
 		OPT_BOOL(0, "contained", &opts.contained,
 			 N_("update all branches that point at commits in <revision-range>")),
+		OPT_STRING_F(0, "ref", &opts.ref,
+			     N_("branch"),
+			     N_("reference to update with result"),
+			     PARSE_OPT_NONEG),
 		OPT_STRING_F(0, "ref-action", &ref_action,
 			     N_("mode"),
 			     N_("control ref update behavior (update|print)"),
@@ -121,6 +126,8 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt3(!!opts.onto, "--onto",
 				  !!opts.advance, "--advance",
 				  !!opts.revert, "--revert");
+	die_for_incompatible_opt2(!!opts.ref, "--ref",
+				  !!opts.contained, "--contained");
 	if (opts.contained && !opts.onto)
 		die(_("--contained requires --onto"));
 
diff --git a/replay.c b/replay.c
index 199066f6b3..07b94cddde 100644
--- a/replay.c
+++ b/replay.c
@@ -348,6 +348,8 @@ int replay_revisions(struct rev_info *revs,
 	bool detached_head;
 	char *advance;
 	char *revert;
+	const char *ref;
+	struct object_id old_oid;
 	enum replay_mode mode = REPLAY_MODE_PICK;
 	int ret;
 
@@ -358,6 +360,23 @@ int replay_revisions(struct rev_info *revs,
 	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
 			   &detached_head, &advance, &revert, &onto, &update_refs);
 
+	if (opts->ref) {
+		struct object_id oid;
+
+		if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) {
+			ret = error(_("'%s' is not a valid refname"), opts->ref);
+			goto out;
+		}
+		ref = opts->ref;
+		if (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))
+			oidcpy(&old_oid, &oid);
+		else
+			oidclr(&old_oid, revs->repo->hash_algo);
+	} else {
+		ref = advance ? advance : revert;
+		oidcpy(&old_oid, &onto->object.oid);
+	}
+
 	/* FIXME: Should allow replaying commits with the first as a root commit */
 
 	if (prepare_revision_walk(revs) < 0) {
@@ -393,7 +412,7 @@ int replay_revisions(struct rev_info *revs,
 		kh_value(replayed_commits, pos) = last_commit;
 
 		/* Update any necessary branches */
-		if (advance || revert)
+		if (ref)
 			continue;
 
 		for (decoration = get_name_decoration(&commit->object);
@@ -427,13 +446,9 @@ int replay_revisions(struct rev_info *revs,
 		goto out;
 	}
 
-	/* In --advance or --revert mode, update the target ref */
-	if (advance || revert) {
-		const char *ref = advance ? advance : revert;
-		replay_result_queue_update(out, ref,
-					   &onto->object.oid,
+	if (ref)
+		replay_result_queue_update(out, ref, &old_oid,
 					   &last_commit->object.oid);
-	}
 
 	ret = 0;
 
diff --git a/replay.h b/replay.h
index e916a5f975..99933f2268 100644
--- a/replay.h
+++ b/replay.h
@@ -24,6 +24,13 @@ struct replay_revisions_options {
 	 */
 	const char *onto;
 
+	/*
+	 * Reference to update with the result of the replay. This stops replay
+	 * from taking refs from `onto`, `advance`, and `revert`. Ignores
+	 * `contained`.
+	 */
+	const char *ref;
+
 	/*
 	 * Starting point at which to create revert commits; must be a branch
 	 * name. The branch will be updated to point to the revert commits.
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 47811b3197..b7465b5f18 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -500,4 +500,65 @@ test_expect_success 'git replay --revert incompatible with --advance' '
 	test_grep "cannot be used together" error
 '
 
+test_expect_success 'using --onto with --ref' '
+	git branch test-ref-onto topic2 &&
+	test_when_finished "git branch -D test-ref-onto" &&
+
+	git replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+	test_grep "^update refs/heads/test-ref-onto " result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using --advance with --ref' '
+	git branch test-ref-advance main &&
+	git branch test-ref-target main &&
+	test_when_finished "git branch -D test-ref-advance test-ref-target" &&
+
+	git replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+	test_grep "^update refs/heads/test-ref-target " result
+'
+
+test_expect_success 'using --revert with --ref' '
+	git branch test-ref-revert topic4 &&
+	git branch test-ref-revert-target topic4 &&
+	test_when_finished "git branch -D test-ref-revert test-ref-revert-target" &&
+
+	git replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result &&
+
+	test_line_count = 1 result &&
+	test_grep "^update refs/heads/test-ref-revert-target " result
+'
+
+test_expect_success '--ref is incompatible with --contained' '
+	test_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err &&
+	test_grep "cannot be used together" err
+'
+
+test_expect_success '--ref with nonexistent fully-qualified ref' '
+	test_when_finished "git update-ref -d refs/heads/new-branch" &&
+
+	git replay --onto=main --ref=refs/heads/new-branch topic1..topic2 &&
+
+	git log --format=%s -2 new-branch >actual &&
+	test_write_lines E D >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--ref must be a valid refname' '
+	test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
+	test_grep "is not a valid refname" err
+'
+
+test_expect_success '--ref requires fully qualified ref' '
+	test_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&
+	test_grep "is not a valid refname" err
+'
+
 test_done
-- 
2.53.0.310.g728cabbaf7
Tian YuchenMar 23, 2026, 18:01 UTC in reply to Toon Claes on lore

Re: [PATCH 3/3] replay: allow to specify a ref with option --ref

Hi Toon,
On 3/24/26 00:09, Toon Claes wrote:
> +	die_for_incompatible_opt2(!!opts.ref, "--ref",
> +				  !!opts.contained, "--contained");
Show 7 quoted lines
> +	/*
> +	 * Reference to update with the result of the replay. This stops replay
> +	 * from taking refs from `onto`, `advance`, and `revert`. Ignores
> +	 * `contained`.
> +	 */
> +	const char *ref;
> +
Do the comment and the code implementation match up here?
Show 15 quoted lines
> @@ -427,13 +446,9 @@ int replay_revisions(struct rev_info *revs,
>  		goto out;
>  	}
>  
> -	/* In --advance or --revert mode, update the target ref */
> -	if (advance || revert) {
> -		const char *ref = advance ? advance : revert;
> -		replay_result_queue_update(out, ref,
> -					   &onto->object.oid,
> +	if (ref)
> +		replay_result_queue_update(out, ref, &old_oid,
>  					   &last_commit->object.oid);
> -	}
>  
>  	ret = 0;

It doesn't seem like there's anything here to prevent users from inputting multiple branches? I think this will result in at least one branch becoming orphaned, without any error being reported.

Consider this command:
	git replay --onto main --ref refs/heads/target topic1 topic2
In replay.c we have:
Show 8 quoted lines
> if (onto_name) {
> 		*onto = peel_committish(repo, onto_name, "--onto");
> 		if (rinfo.positive_refexprs <
> 		    strset_get_size(&rinfo.positive_refs))
> 			die(_("all positive revisions given must be references"));
> 		*update_refs = xcalloc(1, sizeof(**update_refs));
> 		**update_refs = rinfo.positive_refs;
> 		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));

After topic1 and topic2 are put into update_refs, only one ref (refs/heads/target) is updated, since what is written in the patch is:

 > +	if (ref)
 > +		replay_result_queue_update(out, ref, &old_oid,
 >  					   &last_commit->object.oid);
I believe this is not what we expect :P
Regards, Yuchen
Kristoffer HaugsbakkMar 23, 2026, 19:07 UTC in reply to Toon Claes on lore

Re: [PATCH 3/3] replay: allow to specify a ref with option --ref

On Mon, Mar 23, 2026, at 17:09, Toon Claes wrote:
> To enable users to replay commits without having refs in
> <revision-range> or as the value for --advance and --revert, start
> accepting option --ref. When the user specifies a --ref, the refs from
> the other option are ignored, and only the ref is acted on.
s/option are/options are/
Show 9 quoted lines
>
> Signed-off-by: Toon Claes <toon@iotcl.com>
> ---
>[snip]
> +To replay onto a specific commit while updating a different reference:
> +
> +------------
> +$ git replay --onto 112233 --ref refs/heads/mybranch aabbcc..ddeeff
> +------------
This example could use the stuck form like in the two previous commits.
    git replay --onto=112233 --ref=refs/heads/mybranch ...
Show 8 quoted lines
> +
> +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates
> +`refs/heads/mybranch` to point at the result. This is useful when you want
> +to use bare commit IDs instead of branch names.
> +
>  NOTE: For reverting an entire merge request as a single commit (rather
> than
>[snip]
Kristoffer HaugsbakkMar 23, 2026, 19:17 UTC in reply to Toon Claes on lore

Re: [PATCH 1/3] t3650: use option with value consistenly with equal sign

On Mon, Mar 23, 2026, at 17:09, Toon Claes wrote:
> The tests in t3650-replay-basics have mixed use of option arguments
> with value with and without equal sign. Bring in consistency and use
> equal sign for all options that expect a value.

If it is about consistency, could you pick one or the other either way or go with whatever happened to be most used right now?

Consistency by itself is a weaker argument than arguing that stuck form is better for invoking git(1) commands, which is what gitcli(7) argues.

Which is to say: arguing for stuck form in the commit message based on it being better is a stronger argument than wanting consistency. :)

Then once one form has been argued for or referenced it follows that you should be consistent and use the best approach throughout.

> This makes it easier to distinguish them from positional arguments.

Maybe it’s just me, but sticking with the stuck form makes it harder to mess up writing unintended options and positional arguments. Once written it might be slightly more readable, but the main benefit is using a style that makes messing up harder to pull off.

Show 6 quoted lines
>
> Signed-off-by: Toon Claes <toon@iotcl.com>
> ---
>  t/t3650-replay-basics.sh | 70 ++++++++++++++++++++++++------------------------
>  1 file changed, 35 insertions(+), 35 deletions(-)
>[snip]
Junio C HamanoMar 23, 2026, 20:06 UTC in reply to Kristoffer Haugsbakk on lore

Re: [PATCH 1/3] t3650: use option with value consistenly with equal sign

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
Show 23 quoted lines
> On Mon, Mar 23, 2026, at 17:09, Toon Claes wrote:
>> The tests in t3650-replay-basics have mixed use of option arguments
>> with value with and without equal sign. Bring in consistency and use
>> equal sign for all options that expect a value.
>
> If it is about consistency, could you pick one or the other either way
> or go with whatever happened to be most used right now?
>
> Consistency by itself is a weaker argument than arguing that stuck form
> is better for invoking git(1) commands, which is what gitcli(7) argues.
>
> Which is to say: arguing for stuck form in the commit message based on
> it being better is a stronger argument than wanting consistency. :)
>
> Then once one form has been argued for or referenced it follows that you
> should be consistent and use the best approach throughout.
>
>> This makes it easier to distinguish them from positional arguments.
>
> Maybe it’s just me, but sticking with the stuck form makes it harder to
> mess up writing unintended options and positional arguments. Once
> written it might be slightly more readable, but the main benefit is
> using a style that makes messing up harder to pull off.

I am not sure if a patch whose purpose is only to make the CLI invocation "consistent" is a welcome change, though. If we support two forms, exercising both forms and making sure they mean the same thing may even be better, but short of that, a random mixture of styles as if end-user human may have picked one form on this day and the other form on another day may be better than a complete monoculture.

Toon ClaesMar 25, 2026, 12:43 UTC in reply to Junio C Hamano on lore

Re: [PATCH 1/3] t3650: use option with value consistenly with equal sign

Junio C Hamano <gitster@pobox.com> writes:
Show 7 quoted lines
> I am not sure if a patch whose purpose is only to make the CLI
> invocation "consistent" is a welcome change, though.  If we support
> two forms, exercising both forms and making sure they mean the same
> thing may even be better, but short of that, a random mixture of
> styles as if end-user human may have picked one form on this day and
> the other form on another day may be better than a complete
> monoculture.
Okay, I'll drop this commit.
-- 
Cheers,
Toon
Toon ClaesMar 25, 2026, 12:49 UTC in reply to Kristoffer Haugsbakk on lore

Re: [PATCH 3/3] replay: allow to specify a ref with option --ref

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
Show 7 quoted lines
> On Mon, Mar 23, 2026, at 17:09, Toon Claes wrote:
>> To enable users to replay commits without having refs in
>> <revision-range> or as the value for --advance and --revert, start
>> accepting option --ref. When the user specifies a --ref, the refs from
>> the other option are ignored, and only the ref is acted on.
>
> s/option are/options are/
Thanks!
Show 12 quoted lines
>> Signed-off-by: Toon Claes <toon@iotcl.com>
>> ---
>>[snip]
>> +To replay onto a specific commit while updating a different reference:
>> +
>> +------------
>> +$ git replay --onto 112233 --ref refs/heads/mybranch aabbcc..ddeeff
>> +------------
>
> This example could use the stuck form like in the two previous commits.
>
>     git replay --onto=112233 --ref=refs/heads/mybranch ...
Good point.
Show 8 quoted lines
>> +
>> +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates
>> +`refs/heads/mybranch` to point at the result. This is useful when you want
>> +to use bare commit IDs instead of branch names.
>> +
>>  NOTE: For reverting an entire merge request as a single commit (rather
>> than
>>[snip]
-- 
Cheers,
Toon
Toon ClaesMar 25, 2026, 12:50 UTC in reply to Tian Yuchen on lore

Re: [PATCH 3/3] replay: allow to specify a ref with option --ref

Tian Yuchen <cat@malon.dev> writes:
Show 17 quoted lines
> Hi Toon,
>
> On 3/24/26 00:09, Toon Claes wrote:
>
>
>> +	die_for_incompatible_opt2(!!opts.ref, "--ref",
>> +				  !!opts.contained, "--contained");
>
>> +	/*
>> +	 * Reference to update with the result of the replay. This stops replay
>> +	 * from taking refs from `onto`, `advance`, and `revert`. Ignores
>> +	 * `contained`.
>> +	 */
>> +	const char *ref;
>> +
>
> Do the comment and the code implementation match up here?

The comment is a bit misleading. A user cannot set the field `contained` in the struct when it passed `--ref`. But if a bug is made somewhere, but both fields can be set, that's what the comment is saying.

I can add a check to verify in replay.c that not both are set, and update the comment they are incompatible?

Show 23 quoted lines
>> @@ -427,13 +446,9 @@ int replay_revisions(struct rev_info *revs,
>>  		goto out;
>>  	}
>>  
>> -	/* In --advance or --revert mode, update the target ref */
>> -	if (advance || revert) {
>> -		const char *ref = advance ? advance : revert;
>> -		replay_result_queue_update(out, ref,
>> -					   &onto->object.oid,
>> +	if (ref)
>> +		replay_result_queue_update(out, ref, &old_oid,
>>  					   &last_commit->object.oid);
>> -	}
>>  
>>  	ret = 0;
>
> It doesn't seem like there's anything here to prevent users from 
> inputting multiple branches? I think this will result in at least one 
> branch becoming orphaned, without any error being reported.
>
> Consider this command:
>
> 	git replay --onto main --ref refs/heads/target topic1 topic2
That's interesting, I didn't think of that.
Show 21 quoted lines
>
> In replay.c we have:
>
>> if (onto_name) {
>> 		*onto = peel_committish(repo, onto_name, "--onto");
>> 		if (rinfo.positive_refexprs <
>> 		    strset_get_size(&rinfo.positive_refs))
>> 			die(_("all positive revisions given must be references"));
>> 		*update_refs = xcalloc(1, sizeof(**update_refs));
>> 		**update_refs = rinfo.positive_refs;
>> 		memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
>
>
> After topic1 and topic2 are put into update_refs, only one ref 
> (refs/heads/target) is updated, since what is written in the patch is:
>
>  > +	if (ref)
>  > +		replay_result_queue_update(out, ref, &old_oid,
>  >  					   &last_commit->object.oid);
>
> I believe this is not what we expect :P
Let me look into that how we can deal with that.
-- 
Cheers,
Toon
Toon ClaesMar 25, 2026, 15:59 UTC in reply to Toon Claes on lore

[PATCH v2 0/3] Add option --ref to git-replay(1)

In a previous RFC[1] I suggested to implement subcommands into git-replay(1). While it would be arguable nice to have subcommands for the different modes, because git-replay(1) is a plumbing commands, it's fine to keep a status quo.

This series takes one thing for that RFC though: adding option --ref. This new option is useful if you want to have full control over which ref is being updated, and not want to rely on the refs that are using in the <revision-range> or as the value for --advance and --revert.

These changes answer the needs expressed in[2].

This series is based on Siddharth's series[3] to add '--revert' to git-replay(1) (sa/replay-revert @ ba5c0d03d3).

[1]: https://lore.kernel.org/git/20260309-toon-replay-subcommands-v1-1-864ec82ef68a@iotcl.com/ [2]: https://lore.kernel.org/git/CAHTeOx-SMLh_idKhGczPKzZNOKy04uYXmUhL8Z79yRuNpmE4eA@mail.gmail.com/ [3]: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/

---
Changes in v2:
- Dropped the test-only consistency patch.
- Separated commit to mark options as not negatable.
- Modified git-replay(1) docs to everywhere use stuck form.
- Added code and test ensure the revision range has a single tip when
  both --onto and --ref are given.
- Rephrased some comments and docs.
- Link to v1: https://patch.msgid.link/20260323-toon-replay-arbitrary-ref-v1-0-5c7172f675ec@iotcl.com
---
Toon Claes (3):
      builtin/replay: mark options as not negatable
      replay: use stuck form in documentation and help message
      replay: allow to specify a ref with option --ref
 Documentation/git-replay.adoc | 42 +++++++++++++++++++--------
 builtin/replay.c              | 38 ++++++++++++++++---------
 replay.c                      | 33 +++++++++++++++++-----
 replay.h                      |  7 +++++
 t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 154 insertions(+), 32 deletions(-)
Range-diff versus v1:
1:  fe4daa4c89 < -:  ---------- t3650: use option with value consistenly with equal sign
2:  3170bcc916 < -:  ---------- builtin/replay: improve documentation on options
-:  ---------- > 1:  61ba35cc08 builtin/replay: mark options as not negatable
-:  ---------- > 2:  19e54b8326 replay: use stuck form in documentation and help message
3:  bdb166a9ff ! 3:  341b94f937 replay: allow to specify a ref with option --ref
    @@ Metadata
      ## Commit message ##
         replay: allow to specify a ref with option --ref
     
    -    To enable users to replay commits without having refs in
    -    <revision-range> or as the value for --advance and --revert, start
    -    accepting option --ref. When the user specifies a --ref, the refs from
    -    the other option are ignored, and only the ref is acted on.
    +    When option '--onto' is passed to git-replay(1), the command will update
    +    refs from the <revision-range> passed to the command. When using option
    +    '--advance' or '--revert', the argument of that option is a ref that
    +    will be updated.
    +
    +    To enable users to specify which ref to update, add option '--ref'. When
    +    using option '--ref', the refs described above are left untouched and
    +    instead the argument of this option is updated instead.
     
         Signed-off-by: Toon Claes <toon@iotcl.com>
     
    @@ Documentation/git-replay.adoc: SYNOPSIS
      [verse]
      (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
     -			     [--ref-action=<mode>] <revision-range>
    -+			     [--ref=<branch>] [--ref-action=<mode>]
    -+			     <revision-range>
    ++			     [--ref=<branch>] [--ref-action=<mode>] <revision-range>
      
      DESCRIPTION
      -----------
    -@@ Documentation/git-replay.adoc: the current user as the author, matching the behavior of `git revert`.
    - This option is mutually exclusive with `--onto` and `--advance`. It is also
    - incompatible with `--contained` (which is a modifier for `--onto` only).
    - 
    -+--ref <branch>::
    -+	Override which reference is updated with the result of the
    -+	replay. When used with `--onto`, only the specified reference
    -+	is updated instead of inferring refs from the revision range.
    -+	When used with `--advance` or `--revert`, the specified
    -+	reference is updated instead of the branch given to those
    -+	options. This option is incompatible with `--contained`.
    -+
    - --contained::
    +@@ Documentation/git-replay.adoc: incompatible with `--contained` (which is a modifier for `--onto` only).
      	Update all branches that point at commits in
      	<revision-range>. Requires `--onto`.
    -@@ Documentation/git-replay.adoc: top of `main`, and updates `main` to point at the result. This is useful when
    - commits from `topic` were previously merged or cherry-picked into `main` and
    - need to be undone.
    + 
    ++--ref=<branch>::
    ++	Override which reference is updated with the result of the replay.
    ++	When used with `--onto`, the `<revision-range>` should have a
    ++	single tip and only the specified reference is updated instead of
    ++	inferring refs from the revision range.
    ++	When used with `--advance` or `--revert`, the specified reference is
    ++	updated instead of the branch given to those options. This option is
    ++	incompatible with `--contained`.
    ++
    + --ref-action[=<mode>]::
    + 	Control how references are updated. The mode can be:
    + +
    +@@ Documentation/git-replay.adoc: NOTE: For reverting an entire merge request as a single commit (rather than
    + commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
    + which can avoid unnecessary merge conflicts.
      
     +To replay onto a specific commit while updating a different reference:
     +
     +------------
    -+$ git replay --onto 112233 --ref refs/heads/mybranch aabbcc..ddeeff
    ++$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff
     +------------
     +
     +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates
    -+`refs/heads/mybranch` to point at the result. This is useful when you want
    ++`refs/heads/mybranch` to point at the result. This can be useful when you want
     +to use bare commit IDs instead of branch names.
     +
    - NOTE: For reverting an entire merge request as a single commit (rather than
    - commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
    - which can avoid unnecessary merge conflicts.
    + GIT
    + ---
    + Part of the linkgit:git[1] suite
     
      ## builtin/replay.c ##
     @@ builtin/replay.c: int cmd_replay(int argc,
    @@ builtin/replay.c: int cmd_replay(int argc,
      		N_("(EXPERIMENTAL!) git replay "
      		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
     -		   "[--ref-action=<mode>] <revision-range>"),
    -+		   "[--ref=<branch>] [--ref-action=<mode>]\n"
    -+		   "<revision-range>"),
    ++		   "[--ref=<branch>] [--ref-action=<mode>] <revision-range>"),
      		NULL
      	};
      	struct option replay_options[] = {
     @@ builtin/replay.c: int cmd_replay(int argc,
    + 			     N_("branch"),
    + 			     N_("revert commits onto given branch"),
      			     PARSE_OPT_NONEG),
    - 		OPT_BOOL(0, "contained", &opts.contained,
    - 			 N_("update all branches that point at commits in <revision-range>")),
     +		OPT_STRING_F(0, "ref", &opts.ref,
     +			     N_("branch"),
     +			     N_("reference to update with result"),
    @@ replay.c: int replay_revisions(struct rev_info *revs,
     +	if (opts->ref) {
     +		struct object_id oid;
     +
    ++		if (update_refs && strset_get_size(update_refs) > 1) {
    ++			ret = error(_("'--ref' cannot be used with multiple revision ranges"));
    ++			goto out;
    ++		}
     +		if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) {
     +			ret = error(_("'%s' is not a valid refname"), opts->ref);
     +			goto out;
    @@ replay.h: struct replay_revisions_options {
      	const char *onto;
      
     +	/*
    -+	 * Reference to update with the result of the replay. This stops replay
    -+	 * from taking refs from `onto`, `advance`, and `revert`. Ignores
    ++	 * Reference to update with the result of the replay. This will not
    ++	 * update any refs from `onto`, `advance`, or `revert`. Ignores
     +	 * `contained`.
     +	 */
     +	const char *ref;
    @@ t/t3650-replay-basics.sh: test_expect_success 'git replay --revert incompatible
     +	test_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&
     +	test_grep "is not a valid refname" err
     +'
    ++
    ++test_expect_success '--onto with --ref rejects multiple revision ranges' '
    ++	test_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err &&
    ++	test_grep "cannot be used with multiple revision ranges" err
    ++'
     +
      test_done

--- base-commit: ba5c0d03d39a8b1798c8f988bdc95f9a08f10fac change-id: 20260323-toon-replay-arbitrary-ref-5a81f5f976c7

Toon ClaesMar 25, 2026, 15:59 UTC in reply to Toon Claes on lore

[PATCH v2 1/3] builtin/replay: mark options as not negatable

The options '--onto', '--advance', '--revert', and '--ref-action' of git-replay(1) are not negatable. Mark them as such using PARSE_OPT_NONEG.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 builtin/replay.c | 28 ++++++++++++++++------------
 1 file changed, 16 insertions(+), 12 deletions(-)
Show changes to builtin/replay.c +16 −12
diff --git a/builtin/replay.c b/builtin/replay.c
index fe69f6f8ce..1a04f33390 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -88,20 +88,24 @@ int cmd_replay(int argc,
 		NULL
 	};
 	struct option replay_options[] = {
-		OPT_STRING(0, "advance", &opts.advance,
-			   N_("branch"),
-			   N_("make replay advance given branch")),
-		OPT_STRING(0, "onto", &opts.onto,
-			   N_("revision"),
-			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &opts.contained,
 			 N_("update all branches that point at commits in <revision-range>")),
-		OPT_STRING(0, "revert", &opts.revert,
-			   N_("branch"),
-			   N_("revert commits onto given branch")),
-		OPT_STRING(0, "ref-action", &ref_action,
-			   N_("mode"),
-			   N_("control ref update behavior (update|print)")),
+		OPT_STRING_F(0, "onto", &opts.onto,
+			     N_("revision"),
+			     N_("replay onto given commit"),
+			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "advance", &opts.advance,
+			     N_("branch"),
+			     N_("make replay advance given branch"),
+			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "revert", &opts.revert,
+			     N_("branch"),
+			     N_("revert commits onto given branch"),
+			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "ref-action", &ref_action,
+			     N_("mode"),
+			     N_("control ref update behavior (update|print)"),
+			     PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
-- 
2.53.0.310.g728cabbaf7
Toon ClaesMar 25, 2026, 15:59 UTC in reply to Toon Claes on lore

[PATCH v2 2/3] replay: use stuck form in documentation and help message

gitcli(7) suggests to use stuck form. Change the documentation strings to use this form.

While at it, reorder them to match the order in the docs.
Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-replay.adoc | 23 ++++++++++++-----------
 builtin/replay.c              |  4 ++--
 2 files changed, 14 insertions(+), 13 deletions(-)
Show changes to 2 files +14 −10

Documentation/git-replay.adoc, builtin/replay.c

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 6698cfc047..7e749a0477 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,7 +9,8 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
+			     [--ref-action=<mode>] <revision-range>
 
 DESCRIPTION
 -----------
@@ -26,7 +27,7 @@ THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 OPTIONS
 -------
 
---onto <newbase>::
+--onto=<newbase>::
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
@@ -34,7 +35,7 @@ When `--onto` is specified, the branch(es) in the revision range will be
 updated to point at the new commits, similar to the way `git rebase --update-refs`
 updates multiple branches in the affected range.
 
---advance <branch>::
+--advance=<branch>::
 	Starting point at which to create the new commits; must be a
 	branch name.
 +
@@ -42,7 +43,7 @@ The history is replayed on top of the <branch> and <branch> is updated to
 point at the tip of the resulting history. This is different from `--onto`,
 which uses the target only as a starting point without updating it.
 
---revert <branch>::
+--revert=<branch>::
 	Starting point at which to create the reverted commits; must be a
 	branch name.
 +
@@ -79,7 +80,7 @@ The default mode can be configured via the `replay.refAction` configuration vari
 
 <revision-range>::
 	Range of commits to replay; see "Specifying Ranges" in
-	linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
+	linkgit:git-rev-parse[1]. In `--advance=<branch>` mode, the
 	range should have a single tip, so that it's clear to which tip the
 	advanced <branch> should point. Any commits in the range whose
 	changes are already present in the branch the commits are being
@@ -126,7 +127,7 @@ EXAMPLES
 To simply rebase `mybranch` onto `target`:
 
 ------------
-$ git replay --onto target origin/main..mybranch
+$ git replay --onto=target origin/main..mybranch
 ------------
 
 The refs are updated atomically and no output is produced on success.
@@ -134,14 +135,14 @@ The refs are updated atomically and no output is produced on success.
 To see what would be updated without actually updating:
 
 ------------
-$ git replay --ref-action=print --onto target origin/main..mybranch
+$ git replay --ref-action=print --onto=target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
 To cherry-pick the commits from mybranch onto target:
 
 ------------
-$ git replay --advance target origin/main..mybranch
+$ git replay --advance=target origin/main..mybranch
 ------------
 
 Note that the first two examples replay the exact same commits and on
@@ -153,7 +154,7 @@ What if you have a stack of branches, one depending upon another, and
 you'd really like to rebase the whole set?
 
 ------------
-$ git replay --contained --onto origin/main origin/main..tipbranch
+$ git replay --contained --onto=origin/main origin/main..tipbranch
 ------------
 
 All three branches (`branch1`, `branch2`, and `tipbranch`) are updated
@@ -164,7 +165,7 @@ commits to replay using the syntax `A..B`; any range expression will
 do:
 
 ------------
-$ git replay --onto origin/main ^base branch1 branch2 branch3
+$ git replay --onto=origin/main ^base branch1 branch2 branch3
 ------------
 
 This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
@@ -175,7 +176,7 @@ that they have in common, but that does not need to be the case.
 To revert commits on a branch:
 
 ------------
-$ git replay --revert main topic~2..topic
+$ git replay --revert=main topic~2..topic
 ------------
 
 This reverts the last two commits from `topic`, creating revert commits on
diff --git a/builtin/replay.c b/builtin/replay.c
index 1a04f33390..a5f81b67d4 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -83,8 +83,8 @@ int cmd_replay(int argc,
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
-		   "[--ref-action[=<mode>]] <revision-range>..."),
+		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
+		   "[--ref-action=<mode>] <revision-range>"),
 		NULL
 	};
 	struct option replay_options[] = {
-- 
2.53.0.310.g728cabbaf7
Toon ClaesMar 25, 2026, 15:59 UTC in reply to Toon Claes on lore

[PATCH v2 3/3] replay: allow to specify a ref with option --ref

When option '--onto' is passed to git-replay(1), the command will update refs from the <revision-range> passed to the command. When using option '--advance' or '--revert', the argument of that option is a ref that will be updated.

To enable users to specify which ref to update, add option '--ref'. When using option '--ref', the refs described above are left untouched and instead the argument of this option is updated instead.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-replay.adoc | 21 +++++++++++++-
 builtin/replay.c              |  8 +++++-
 replay.c                      | 33 +++++++++++++++++-----
 replay.h                      |  7 +++++
 t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 126 insertions(+), 9 deletions(-)
Show changes to 5 files +126 −9

Documentation/git-replay.adoc, builtin/replay.c, replay.c, replay.h, t/t3650-replay-basics.sh

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 7e749a0477..5952ecb50d 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-action=<mode>] <revision-range>
+			     [--ref=<branch>] [--ref-action=<mode>] <revision-range>
 
 DESCRIPTION
 -----------
@@ -66,6 +66,15 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
 	Update all branches that point at commits in
 	<revision-range>. Requires `--onto`.
 
+--ref=<branch>::
+	Override which reference is updated with the result of the replay.
+	When used with `--onto`, the `<revision-range>` should have a
+	single tip and only the specified reference is updated instead of
+	inferring refs from the revision range.
+	When used with `--advance` or `--revert`, the specified reference is
+	updated instead of the branch given to those options. This option is
+	incompatible with `--contained`.
+
 --ref-action[=<mode>]::
 	Control how references are updated. The mode can be:
 +
@@ -188,6 +197,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than
 commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
 which can avoid unnecessary merge conflicts.
 
+To replay onto a specific commit while updating a different reference:
+
+------------
+$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff
+------------
+
+This replays the range `aabbcc..ddeeff` onto commit `112233` and updates
+`refs/heads/mybranch` to point at the result. This can be useful when you want
+to use bare commit IDs instead of branch names.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index a5f81b67d4..876026549e 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -84,7 +84,7 @@ int cmd_replay(int argc,
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
 		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
-		   "[--ref-action=<mode>] <revision-range>"),
+		   "[--ref=<branch>] [--ref-action=<mode>] <revision-range>"),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -102,6 +102,10 @@ int cmd_replay(int argc,
 			     N_("branch"),
 			     N_("revert commits onto given branch"),
 			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "ref", &opts.ref,
+			     N_("branch"),
+			     N_("reference to update with result"),
+			     PARSE_OPT_NONEG),
 		OPT_STRING_F(0, "ref-action", &ref_action,
 			     N_("mode"),
 			     N_("control ref update behavior (update|print)"),
@@ -121,6 +125,8 @@ int cmd_replay(int argc,
 	die_for_incompatible_opt3(!!opts.onto, "--onto",
 				  !!opts.advance, "--advance",
 				  !!opts.revert, "--revert");
+	die_for_incompatible_opt2(!!opts.ref, "--ref",
+				  !!opts.contained, "--contained");
 	if (opts.contained && !opts.onto)
 		die(_("--contained requires --onto"));
 
diff --git a/replay.c b/replay.c
index 199066f6b3..63cec56d48 100644
--- a/replay.c
+++ b/replay.c
@@ -348,6 +348,8 @@ int replay_revisions(struct rev_info *revs,
 	bool detached_head;
 	char *advance;
 	char *revert;
+	const char *ref;
+	struct object_id old_oid;
 	enum replay_mode mode = REPLAY_MODE_PICK;
 	int ret;
 
@@ -358,6 +360,27 @@ int replay_revisions(struct rev_info *revs,
 	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
 			   &detached_head, &advance, &revert, &onto, &update_refs);
 
+	if (opts->ref) {
+		struct object_id oid;
+
+		if (update_refs && strset_get_size(update_refs) > 1) {
+			ret = error(_("'--ref' cannot be used with multiple revision ranges"));
+			goto out;
+		}
+		if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) {
+			ret = error(_("'%s' is not a valid refname"), opts->ref);
+			goto out;
+		}
+		ref = opts->ref;
+		if (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))
+			oidcpy(&old_oid, &oid);
+		else
+			oidclr(&old_oid, revs->repo->hash_algo);
+	} else {
+		ref = advance ? advance : revert;
+		oidcpy(&old_oid, &onto->object.oid);
+	}
+
 	/* FIXME: Should allow replaying commits with the first as a root commit */
 
 	if (prepare_revision_walk(revs) < 0) {
@@ -393,7 +416,7 @@ int replay_revisions(struct rev_info *revs,
 		kh_value(replayed_commits, pos) = last_commit;
 
 		/* Update any necessary branches */
-		if (advance || revert)
+		if (ref)
 			continue;
 
 		for (decoration = get_name_decoration(&commit->object);
@@ -427,13 +450,9 @@ int replay_revisions(struct rev_info *revs,
 		goto out;
 	}
 
-	/* In --advance or --revert mode, update the target ref */
-	if (advance || revert) {
-		const char *ref = advance ? advance : revert;
-		replay_result_queue_update(out, ref,
-					   &onto->object.oid,
+	if (ref)
+		replay_result_queue_update(out, ref, &old_oid,
 					   &last_commit->object.oid);
-	}
 
 	ret = 0;
 
diff --git a/replay.h b/replay.h
index e916a5f975..0ab74b9805 100644
--- a/replay.h
+++ b/replay.h
@@ -24,6 +24,13 @@ struct replay_revisions_options {
 	 */
 	const char *onto;
 
+	/*
+	 * Reference to update with the result of the replay. This will not
+	 * update any refs from `onto`, `advance`, or `revert`. Ignores
+	 * `contained`.
+	 */
+	const char *ref;
+
 	/*
 	 * Starting point at which to create revert commits; must be a branch
 	 * name. The branch will be updated to point to the revert commits.
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 0c1e03e0fb..938be64770 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -500,4 +500,70 @@ test_expect_success 'git replay --revert incompatible with --advance' '
 	test_grep "cannot be used together" error
 '
 
+test_expect_success 'using --onto with --ref' '
+	git branch test-ref-onto topic2 &&
+	test_when_finished "git branch -D test-ref-onto" &&
+
+	git replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+	test_grep "^update refs/heads/test-ref-onto " result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using --advance with --ref' '
+	git branch test-ref-advance main &&
+	git branch test-ref-target main &&
+	test_when_finished "git branch -D test-ref-advance test-ref-target" &&
+
+	git replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+	test_grep "^update refs/heads/test-ref-target " result
+'
+
+test_expect_success 'using --revert with --ref' '
+	git branch test-ref-revert topic4 &&
+	git branch test-ref-revert-target topic4 &&
+	test_when_finished "git branch -D test-ref-revert test-ref-revert-target" &&
+
+	git replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result &&
+
+	test_line_count = 1 result &&
+	test_grep "^update refs/heads/test-ref-revert-target " result
+'
+
+test_expect_success '--ref is incompatible with --contained' '
+	test_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err &&
+	test_grep "cannot be used together" err
+'
+
+test_expect_success '--ref with nonexistent fully-qualified ref' '
+	test_when_finished "git update-ref -d refs/heads/new-branch" &&
+
+	git replay --onto=main --ref=refs/heads/new-branch topic1..topic2 &&
+
+	git log --format=%s -2 new-branch >actual &&
+	test_write_lines E D >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--ref must be a valid refname' '
+	test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
+	test_grep "is not a valid refname" err
+'
+
+test_expect_success '--ref requires fully qualified ref' '
+	test_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&
+	test_grep "is not a valid refname" err
+'
+
+test_expect_success '--onto with --ref rejects multiple revision ranges' '
+	test_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err &&
+	test_grep "cannot be used with multiple revision ranges" err
+'
+
 test_done
-- 
2.53.0.310.g728cabbaf7
Junio C HamanoMar 25, 2026, 18:34 UTC in reply to Toon Claes on lore

Re: [PATCH v2 0/3] Add option --ref to git-replay(1)

Toon Claes <toon@iotcl.com> writes:
Show 14 quoted lines
> In a previous RFC[1] I suggested to implement subcommands into
> git-replay(1). While it would be arguable nice to have subcommands for
> the different modes, because git-replay(1) is a plumbing commands, it's
> fine to keep a status quo.
>
> This series takes one thing for that RFC though: adding option --ref.
> This new option is useful if you want to have full control over which
> ref is being updated, and not want to rely on the refs that are using
> in the <revision-range> or as the value for --advance and --revert.
>
> These changes answer the needs expressed in[2].
>
> This series is based on Siddharth's series[3] to add '--revert' to
> git-replay(1) (sa/replay-revert @ ba5c0d03d3).
Which unfortunately got updated just 20 hours ago X-<.

Will try to wriggle it in, but please double check when I push the integration result of today.

Junio C HamanoMar 25, 2026, 18:44 UTC in reply to Toon Claes on lore

Re: [PATCH v2 3/3] replay: allow to specify a ref with option --ref

Toon Claes <toon@iotcl.com> writes:
Show 36 quoted lines
> When option '--onto' is passed to git-replay(1), the command will update
> refs from the <revision-range> passed to the command. When using option
> '--advance' or '--revert', the argument of that option is a ref that
> will be updated.
>
> To enable users to specify which ref to update, add option '--ref'. When
> using option '--ref', the refs described above are left untouched and
> instead the argument of this option is updated instead.
>
> Signed-off-by: Toon Claes <toon@iotcl.com>
> ---
>  Documentation/git-replay.adoc | 21 +++++++++++++-
>  builtin/replay.c              |  8 +++++-
>  replay.c                      | 33 +++++++++++++++++-----
>  replay.h                      |  7 +++++
>  t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++
>  5 files changed, 126 insertions(+), 9 deletions(-)
>
> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
> index 7e749a0477..5952ecb50d 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-action=<mode>] <revision-range>
> +			     [--ref=<branch>] [--ref-action=<mode>] <revision-range>
>  
>  DESCRIPTION
>  -----------
> @@ -66,6 +66,15 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
>  	Update all branches that point at commits in
>  	<revision-range>. Requires `--onto`.
>  
> +--ref=<branch>::

As this thing takes a full refname (e.g., "--ref=refsheads/mybranch" in the example in hunk ll.197,+16), we probably want

    --ref=<ref>::
instead.

In the modern documentation style, this should be `--ref=<ref>`:: but let's consistently use traditional style and leave the clean-up until the dust settles and when the command becomes more quiescent.

Show 27 quoted lines
> @@ -188,6 +197,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than
>  commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
>  which can avoid unnecessary merge conflicts.
>  
> +To replay onto a specific commit while updating a different reference:
> +
> +------------
> +$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff
> +------------
> +
> +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates
> +`refs/heads/mybranch` to point at the result. This can be useful when you want
> +to use bare commit IDs instead of branch names.
> +
>  GIT
>  ---
>  Part of the linkgit:git[1] suite
> diff --git a/builtin/replay.c b/builtin/replay.c
> index a5f81b67d4..876026549e 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -84,7 +84,7 @@ int cmd_replay(int argc,
>  	const char *const replay_usage[] = {
>  		N_("(EXPERIMENTAL!) git replay "
>  		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
> -		   "[--ref-action=<mode>] <revision-range>"),
> +		   "[--ref=<branch>] [--ref-action=<mode>] <revision-range>"),
Ditto.
Show 11 quoted lines
>  		NULL
>  	};
>  	struct option replay_options[] = {
> @@ -102,6 +102,10 @@ int cmd_replay(int argc,
>  			     N_("branch"),
>  			     N_("revert commits onto given branch"),
>  			     PARSE_OPT_NONEG),
> +		OPT_STRING_F(0, "ref", &opts.ref,
> +			     N_("branch"),
> +			     N_("reference to update with result"),
> +			     PARSE_OPT_NONEG),
Ditto.
Show 25 quoted lines
> diff --git a/replay.c b/replay.c
> index 199066f6b3..63cec56d48 100644
> --- a/replay.c
> +++ b/replay.c
> @@ -348,6 +348,8 @@ int replay_revisions(struct rev_info *revs,
>  	bool detached_head;
>  	char *advance;
>  	char *revert;
> +	const char *ref;
> +	struct object_id old_oid;
>  	enum replay_mode mode = REPLAY_MODE_PICK;
>  	int ret;
>  
> @@ -358,6 +360,27 @@ int replay_revisions(struct rev_info *revs,
>  	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
>  			   &detached_head, &advance, &revert, &onto, &update_refs);
>  
> +	if (opts->ref) {
> +		struct object_id oid;
> +
> +		if (update_refs && strset_get_size(update_refs) > 1) {
> +			ret = error(_("'--ref' cannot be used with multiple revision ranges"));
> +			goto out;
> +		}
> +		if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) {
Can we do something about this overly long line?
Show 23 quoted lines
> +			ret = error(_("'%s' is not a valid refname"), opts->ref);
> +			goto out;
> +		}
> +		ref = opts->ref;
> +		if (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))
> +			oidcpy(&old_oid, &oid);
> +		else
> +			oidclr(&old_oid, revs->repo->hash_algo);
> +	} else {
> +		ref = advance ? advance : revert;
> +		oidcpy(&old_oid, &onto->object.oid);
> +	}
> +
>  	/* FIXME: Should allow replaying commits with the first as a root commit */
>  
>  	if (prepare_revision_walk(revs) < 0) {
> @@ -393,7 +416,7 @@ int replay_revisions(struct rev_info *revs,
>  		kh_value(replayed_commits, pos) = last_commit;
>  
>  		/* Update any necessary branches */
> -		if (advance || revert)
> +		if (ref)
>  			continue;
Nice.
Show 12 quoted lines
> @@ -427,13 +450,9 @@ int replay_revisions(struct rev_info *revs,
>  		goto out;
>  	}
>  
> -	/* In --advance or --revert mode, update the target ref */
> -	if (advance || revert) {
> -		const char *ref = advance ? advance : revert;
> -		replay_result_queue_update(out, ref,
> -					   &onto->object.oid,
> +	if (ref)
> +		replay_result_queue_update(out, ref, &old_oid,
>  					   &last_commit->object.oid);
Nice, too.
Junio C HamanoMar 26, 2026, 21:20 UTC in reply to Toon Claes on lore

Re: [PATCH v2 0/3] Add option --ref to git-replay(1)

Toon Claes <toon@iotcl.com> writes:
> This series is based on Siddharth's series[3] to add '--revert' to
> git-replay(1) (sa/replay-revert @ ba5c0d03d3).

As sa/replay-revert has been updated, I rebased these three patches on top of the updated version of that other topic and merged it to 'seen', which broke CI with t3650.

I naturally suspected that I made some stupid mistakes while rebasing, so I applied these three patches directly on top of that old sa/replay-revert. Unfortunately, the same test t3650 fails exactly the same way with merge-ort aborting.

The failing test run fails like so:
ok 45 - --ref with nonexistent fully-qualified ref
expecting success of 3650.46 '--ref must be a valid refname':
        test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
        test_grep "is not a valid refname" err

/home/gitster/w/git.git/t/test-lib-functions.sh: line 1180: 2840466 Aborted (core dumped) "$@" 2>&7 test_must_fail: died by signal 6: git replay --onto=main --ref=refs/heads/bad..ref topic1..topic2 not ok 46 - --ref must be a valid refname # # test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err && # test_grep "is not a valid refname" err # 1

Toon ClaesMar 31, 2026, 07:55 UTC in reply to Junio C Hamano on lore

Re: [PATCH v2 0/3] Add option --ref to git-replay(1)

Junio C Hamano <gitster@pobox.com> writes:
Show 24 quoted lines
> As sa/replay-revert has been updated, I rebased these three patches
> on top of the updated version of that other topic and merged it to
> 'seen', which broke CI with t3650.
>
> I naturally suspected that I made some stupid mistakes while
> rebasing, so I applied these three patches directly on top of that
> old sa/replay-revert.  Unfortunately, the same test t3650 fails
> exactly the same way with merge-ort aborting.
>
> The failing test run fails like so:
>
> ok 45 - --ref with nonexistent fully-qualified ref
>
> expecting success of 3650.46 '--ref must be a valid refname':
>         test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
>         test_grep "is not a valid refname" err
>
> /home/gitster/w/git.git/t/test-lib-functions.sh: line 1180: 2840466 Aborted                    (core dumped) "$@" 2>&7
> test_must_fail: died by signal 6: git replay --onto=main --ref=refs/heads/bad..ref topic1..topic2
> not ok 46 - --ref must be a valid refname
> #
> #               test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
> #               test_grep "is not a valid refname" err
> #

No need for you to worry about those conflicts. I'm happy to do the rebase myself.

-- 
Cheers,
Toon
Toon ClaesMar 31, 2026, 07:56 UTC in reply to Junio C Hamano on lore

Re: [PATCH v2 3/3] replay: allow to specify a ref with option --ref

Junio C Hamano <gitster@pobox.com> writes:
> In the modern documentation style, this should be `--ref=<ref>`::
> but let's consistently use traditional style and leave the clean-up
> until the dust settles and when the command becomes more quiescent.

Yes, I noticed various things aren't up to stadard. I'm happy to include a patch that cleans it up. But agreed, the dust needs to settle first.

-- 
Cheers,
Toon
Junio C HamanoMar 31, 2026, 21:42 UTC in reply to Toon Claes on lore

Re: [PATCH v2 0/3] Add option --ref to git-replay(1)

Toon Claes <toon@iotcl.com> writes:
Show 29 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
>
>> As sa/replay-revert has been updated, I rebased these three patches
>> on top of the updated version of that other topic and merged it to
>> 'seen', which broke CI with t3650.
>>
>> I naturally suspected that I made some stupid mistakes while
>> rebasing, so I applied these three patches directly on top of that
>> old sa/replay-revert.  Unfortunately, the same test t3650 fails
>> exactly the same way with merge-ort aborting.
>>
>> The failing test run fails like so:
>>
>> ok 45 - --ref with nonexistent fully-qualified ref
>>
>> expecting success of 3650.46 '--ref must be a valid refname':
>>         test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
>>         test_grep "is not a valid refname" err
>>
>> /home/gitster/w/git.git/t/test-lib-functions.sh: line 1180: 2840466 Aborted                    (core dumped) "$@" 2>&7
>> test_must_fail: died by signal 6: git replay --onto=main --ref=refs/heads/bad..ref topic1..topic2
>> not ok 46 - --ref must be a valid refname
>> #
>> #               test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
>> #               test_grep "is not a valid refname" err
>> #
>
> No need for you to worry about those conflicts. I'm happy to do the
> rebase myself.
Will wait for updates.  Thanks.

I said I suspected that my rebasing introduced a bug, but what I was reporting in the message you are responding to is that the patches without my rebases are breaking tests. So perhaps you do not have to rebase and there won't be any problematic conflicts, but the patches do need to be updated to address the test failure.

Toon ClaesApr 1, 2026, 20:55 UTC in reply to Toon Claes on lore

[PATCH v3 0/3] Add option --ref to git-replay(1)

In a previous RFC[1] I suggested to implement subcommands into git-replay(1). While it would be arguable nice to have subcommands for the different modes, because git-replay(1) is a plumbing commands, it's fine to keep a status quo.

This series takes one thing for that RFC though: adding option --ref. This new option is useful if you want to have full control over which ref is being updated, and not want to rely on the refs that are using in the <revision-range> or as the value for --advance and --revert.

These changes answer the needs expressed in[2].

This series is based on Siddharth's series[3] to add '--revert' to git-replay(1) (sa/replay-revert @ ba5c0d03d3).

[1]: https://lore.kernel.org/git/20260309-toon-replay-subcommands-v1-1-864ec82ef68a@iotcl.com/ [2]: https://lore.kernel.org/git/CAHTeOx-SMLh_idKhGczPKzZNOKy04uYXmUhL8Z79yRuNpmE4eA@mail.gmail.com/ [3]: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/

---
Changes in v3:
- In the docs, clarify --ref requires a fully qualified ref, not just a
  branch name.
- Fix crash by initializing merge_opt to `{ 0 }`.
- Link to v2: https://patch.msgid.link/20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com
Changes in v2:
- Dropped the test-only consistency patch.
- Separated commit to mark options as not negatable.
- Modified git-replay(1) docs to everywhere use stuck form.
- Added code and test ensure the revision range has a single tip when
  both --onto and --ref are given.
- Rephrased some comments and docs.
- Link to v1: https://patch.msgid.link/20260323-toon-replay-arbitrary-ref-v1-0-5c7172f675ec@iotcl.com
---
Toon Claes (3):
      builtin/replay: mark options as not negatable
      replay: use stuck form in documentation and help message
      replay: allow to specify a ref with option --ref
 Documentation/git-replay.adoc | 45 +++++++++++++++++++++--------
 builtin/replay.c              | 38 ++++++++++++++++---------
 replay.c                      | 35 +++++++++++++++++------
 replay.h                      |  7 +++++
 t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 157 insertions(+), 34 deletions(-)
Range-diff versus v2:
1:  260dff6c82 = 1:  dd46a0efd5 builtin/replay: mark options as not negatable
2:  eca4dffa18 ! 2:  b290493bd0 replay: use stuck form in documentation and help message
    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne
      SYNOPSIS
      --------
      [verse]
    --(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...
    +-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>
     +(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
     +			     [--ref-action=<mode>] <revision-range>
      
    @@ Documentation/git-replay.adoc: The default mode can be configured via the `repla
      
      <revision-range>::
      	Range of commits to replay; see "Specifying Ranges" in
    --	linkgit:git-rev-parse[1]. In `--advance <branch>` mode, the
    -+	linkgit:git-rev-parse[1]. In `--advance=<branch>` mode, the
    - 	range should have a single tip, so that it's clear to which tip the
    - 	advanced <branch> should point. Any commits in the range whose
    - 	changes are already present in the branch the commits are being
    +-	linkgit:git-rev-parse[1]. In `--advance <branch>` or
    +-	`--revert <branch>` mode, the range should have a single tip,
    ++	linkgit:git-rev-parse[1]. In `--advance=<branch>` or
    ++	`--revert=<branch>` mode, the range should have a single tip,
    + 	so that it's clear to which tip the advanced or reverted
    + 	<branch> should point. Any commits in the range whose changes
    + 	are already present in the branch the commits are being
     @@ Documentation/git-replay.adoc: EXAMPLES
      To simply rebase `mybranch` onto `target`:
      
    @@ builtin/replay.c: int cmd_replay(int argc,
      	const char *const replay_usage[] = {
      		N_("(EXPERIMENTAL!) git replay "
     -		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
    --		   "[--ref-action[=<mode>]] <revision-range>..."),
    +-		   "[--ref-action[=<mode>]] <revision-range>"),
     +		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
     +		   "[--ref-action=<mode>] <revision-range>"),
      		NULL
3:  2676c2ae78 ! 3:  44e825d9e6 replay: allow to specify a ref with option --ref
    @@ Commit message
         using option '--ref', the refs described above are left untouched and
         instead the argument of this option is updated instead.
     
    +    Because this introduces code paths in replay.c that jump to `out` before
    +    init_basic_merge_options() is called on `merge_opt`, zero-initialize the
    +    struct.
    +
         Signed-off-by: Toon Claes <toon@iotcl.com>
     
      ## Documentation/git-replay.adoc ##
    @@ Documentation/git-replay.adoc: SYNOPSIS
      [verse]
      (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
     -			     [--ref-action=<mode>] <revision-range>
    -+			     [--ref=<branch>] [--ref-action=<mode>] <revision-range>
    ++			     [--ref=<ref>] [--ref-action=<mode>] <revision-range>
      
      DESCRIPTION
      -----------
    @@ Documentation/git-replay.adoc: incompatible with `--contained` (which is a modif
      	Update all branches that point at commits in
      	<revision-range>. Requires `--onto`.
      
    -+--ref=<branch>::
    ++--ref=<ref>::
     +	Override which reference is updated with the result of the replay.
    ++	The ref must be fully qualified.
     +	When used with `--onto`, the `<revision-range>` should have a
     +	single tip and only the specified reference is updated instead of
     +	inferring refs from the revision range.
     +	When used with `--advance` or `--revert`, the specified reference is
    -+	updated instead of the branch given to those options. This option is
    -+	incompatible with `--contained`.
    ++	updated instead of the branch given to those options.
    ++	This option is incompatible with `--contained`.
     +
      --ref-action[=<mode>]::
      	Control how references are updated. The mode can be:
    @@ builtin/replay.c: int cmd_replay(int argc,
      		N_("(EXPERIMENTAL!) git replay "
      		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
     -		   "[--ref-action=<mode>] <revision-range>"),
    -+		   "[--ref=<branch>] [--ref-action=<mode>] <revision-range>"),
    ++		   "[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
      		NULL
      	};
      	struct option replay_options[] = {
    @@ builtin/replay.c: int cmd_replay(int argc,
      			     N_("mode"),
      			     N_("control ref update behavior (update|print)"),
     @@ builtin/replay.c: int cmd_replay(int argc,
    - 	die_for_incompatible_opt3(!!opts.onto, "--onto",
    - 				  !!opts.advance, "--advance",
    - 				  !!opts.revert, "--revert");
    + 				  opts.contained, "--contained");
    + 	die_for_incompatible_opt2(!!opts.revert, "--revert",
    + 				  opts.contained, "--contained");
     +	die_for_incompatible_opt2(!!opts.ref, "--ref",
     +				  !!opts.contained, "--contained");
    - 	if (opts.contained && !opts.onto)
    - 		die(_("--contained requires --onto"));
      
    + 	/* Parse ref action mode from command line or config */
    + 	ref_mode = get_ref_action_mode(repo, ref_action);
     
      ## replay.c ##
     @@ replay.c: int replay_revisions(struct rev_info *revs,
    + 	struct commit *last_commit = NULL;
    + 	struct commit *commit;
    + 	struct commit *onto = NULL;
    +-	struct merge_options merge_opt;
    ++	struct merge_options merge_opt = { 0 };
    + 	struct merge_result result = {
    + 		.clean = 1,
    + 	};
      	bool detached_head;
      	char *advance;
      	char *revert;

--- base-commit: 2760ee49834953c0860fa5d7983a6af4d27cb6a9 change-id: 20260323-toon-replay-arbitrary-ref-5a81f5f976c7

Toon ClaesApr 1, 2026, 20:55 UTC in reply to Toon Claes on lore

[PATCH v3 1/3] builtin/replay: mark options as not negatable

The options '--onto', '--advance', '--revert', and '--ref-action' of git-replay(1) are not negatable. Mark them as such using PARSE_OPT_NONEG.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 builtin/replay.c | 28 ++++++++++++++++------------
 1 file changed, 16 insertions(+), 12 deletions(-)
Show changes to builtin/replay.c +16 −12
diff --git a/builtin/replay.c b/builtin/replay.c
index a0879b020f..85aa9fa0a4 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -89,20 +89,24 @@ int cmd_replay(int argc,
 		NULL
 	};
 	struct option replay_options[] = {
-		OPT_STRING(0, "advance", &opts.advance,
-			   N_("branch"),
-			   N_("make replay advance given branch")),
-		OPT_STRING(0, "onto", &opts.onto,
-			   N_("revision"),
-			   N_("replay onto given commit")),
 		OPT_BOOL(0, "contained", &opts.contained,
 			 N_("update all branches that point at commits in <revision-range>")),
-		OPT_STRING(0, "revert", &opts.revert,
-			   N_("branch"),
-			   N_("revert commits onto given branch")),
-		OPT_STRING(0, "ref-action", &ref_action,
-			   N_("mode"),
-			   N_("control ref update behavior (update|print)")),
+		OPT_STRING_F(0, "onto", &opts.onto,
+			     N_("revision"),
+			     N_("replay onto given commit"),
+			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "advance", &opts.advance,
+			     N_("branch"),
+			     N_("make replay advance given branch"),
+			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "revert", &opts.revert,
+			     N_("branch"),
+			     N_("revert commits onto given branch"),
+			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "ref-action", &ref_action,
+			     N_("mode"),
+			     N_("control ref update behavior (update|print)"),
+			     PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
-- 
2.53.0.310.g728cabbaf7
Toon ClaesApr 1, 2026, 20:55 UTC in reply to Toon Claes on lore

[PATCH v3 2/3] replay: use stuck form in documentation and help message

gitcli(7) suggests to use stuck form. Change the documentation strings to use this form.

While at it, reorder them to match the order in the docs.
Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-replay.adoc | 25 +++++++++++++------------
 builtin/replay.c              |  4 ++--
 2 files changed, 15 insertions(+), 14 deletions(-)
Show changes to 2 files +15 −11

Documentation/git-replay.adoc, builtin/replay.c

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 997097e420..5bb478c281 100644
--- a/Documentation/git-replay.adoc
+++ b/Documentation/git-replay.adoc
@@ -9,7 +9,8 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
 SYNOPSIS
 --------
 [verse]
-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>
+(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
+			     [--ref-action=<mode>] <revision-range>
 
 DESCRIPTION
 -----------
@@ -26,7 +27,7 @@ THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 OPTIONS
 -------
 
---onto <newbase>::
+--onto=<newbase>::
 	Starting point at which to create the new commits.  May be any
 	valid commit, and not just an existing branch name.
 +
@@ -34,7 +35,7 @@ When `--onto` is specified, the branch(es) in the revision range will be
 updated to point at the new commits, similar to the way `git rebase --update-refs`
 updates multiple branches in the affected range.
 
---advance <branch>::
+--advance=<branch>::
 	Starting point at which to create the new commits; must be a
 	branch name.
 +
@@ -42,7 +43,7 @@ The history is replayed on top of the <branch> and <branch> is updated to
 point at the tip of the resulting history. This is different from `--onto`,
 which uses the target only as a starting point without updating it.
 
---revert <branch>::
+--revert=<branch>::
 	Starting point at which to create the reverted commits; must be a
 	branch name.
 +
@@ -79,8 +80,8 @@ The default mode can be configured via the `replay.refAction` configuration vari
 
 <revision-range>::
 	Range of commits to replay; see "Specifying Ranges" in
-	linkgit:git-rev-parse[1]. In `--advance <branch>` or
-	`--revert <branch>` mode, the range should have a single tip,
+	linkgit:git-rev-parse[1]. In `--advance=<branch>` or
+	`--revert=<branch>` mode, the range should have a single tip,
 	so that it's clear to which tip the advanced or reverted
 	<branch> should point. Any commits in the range whose changes
 	are already present in the branch the commits are being
@@ -127,7 +128,7 @@ EXAMPLES
 To simply rebase `mybranch` onto `target`:
 
 ------------
-$ git replay --onto target origin/main..mybranch
+$ git replay --onto=target origin/main..mybranch
 ------------
 
 The refs are updated atomically and no output is produced on success.
@@ -135,14 +136,14 @@ The refs are updated atomically and no output is produced on success.
 To see what would be updated without actually updating:
 
 ------------
-$ git replay --ref-action=print --onto target origin/main..mybranch
+$ git replay --ref-action=print --onto=target origin/main..mybranch
 update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
 ------------
 
 To cherry-pick the commits from mybranch onto target:
 
 ------------
-$ git replay --advance target origin/main..mybranch
+$ git replay --advance=target origin/main..mybranch
 ------------
 
 Note that the first two examples replay the exact same commits and on
@@ -154,7 +155,7 @@ What if you have a stack of branches, one depending upon another, and
 you'd really like to rebase the whole set?
 
 ------------
-$ git replay --contained --onto origin/main origin/main..tipbranch
+$ git replay --contained --onto=origin/main origin/main..tipbranch
 ------------
 
 All three branches (`branch1`, `branch2`, and `tipbranch`) are updated
@@ -165,7 +166,7 @@ commits to replay using the syntax `A..B`; any range expression will
 do:
 
 ------------
-$ git replay --onto origin/main ^base branch1 branch2 branch3
+$ git replay --onto=origin/main ^base branch1 branch2 branch3
 ------------
 
 This will simultaneously rebase `branch1`, `branch2`, and `branch3`,
@@ -176,7 +177,7 @@ that they have in common, but that does not need to be the case.
 To revert commits on a branch:
 
 ------------
-$ git replay --revert main topic~2..topic
+$ git replay --revert=main topic~2..topic
 ------------
 
 This reverts the last two commits from `topic`, creating revert commits on
diff --git a/builtin/replay.c b/builtin/replay.c
index 85aa9fa0a4..fbfeb780b6 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -84,8 +84,8 @@ int cmd_replay(int argc,
 
 	const char *const replay_usage[] = {
 		N_("(EXPERIMENTAL!) git replay "
-		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
-		   "[--ref-action[=<mode>]] <revision-range>"),
+		   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
+		   "[--ref-action=<mode>] <revision-range>"),
 		NULL
 	};
 	struct option replay_options[] = {
-- 
2.53.0.310.g728cabbaf7
Toon ClaesApr 1, 2026, 20:55 UTC in reply to Toon Claes on lore

[PATCH v3 3/3] replay: allow to specify a ref with option --ref

When option '--onto' is passed to git-replay(1), the command will update refs from the <revision-range> passed to the command. When using option '--advance' or '--revert', the argument of that option is a ref that will be updated.

To enable users to specify which ref to update, add option '--ref'. When using option '--ref', the refs described above are left untouched and instead the argument of this option is updated instead.

Because this introduces code paths in replay.c that jump to `out` before init_basic_merge_options() is called on `merge_opt`, zero-initialize the struct.

Signed-off-by: Toon Claes <toon@iotcl.com>
---
 Documentation/git-replay.adoc | 22 ++++++++++++++-
 builtin/replay.c              |  8 +++++-
 replay.c                      | 35 +++++++++++++++++------
 replay.h                      |  7 +++++
 t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 128 insertions(+), 10 deletions(-)
Show changes to 5 files +128 −10

Documentation/git-replay.adoc, builtin/replay.c, replay.c, replay.h, t/t3650-replay-basics.sh

diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
index 5bb478c281..a32f72aead 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-action=<mode>] <revision-range>
+			     [--ref=<ref>] [--ref-action=<mode>] <revision-range>
 
 DESCRIPTION
 -----------
@@ -66,6 +66,16 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
 	Update all branches that point at commits in
 	<revision-range>. Requires `--onto`.
 
+--ref=<ref>::
+	Override which reference is updated with the result of the replay.
+	The ref must be fully qualified.
+	When used with `--onto`, the `<revision-range>` should have a
+	single tip and only the specified reference is updated instead of
+	inferring refs from the revision range.
+	When used with `--advance` or `--revert`, the specified reference is
+	updated instead of the branch given to those options.
+	This option is incompatible with `--contained`.
+
 --ref-action[=<mode>]::
 	Control how references are updated. The mode can be:
 +
@@ -189,6 +199,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than
 commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
 which can avoid unnecessary merge conflicts.
 
+To replay onto a specific commit while updating a different reference:
+
+------------
+$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff
+------------
+
+This replays the range `aabbcc..ddeeff` onto commit `112233` and updates
+`refs/heads/mybranch` to point at the result. This can be useful when you want
+to use bare commit IDs instead of branch names.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/builtin/replay.c b/builtin/replay.c
index fbfeb780b6..39e3a86f6c 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-action=<mode>] <revision-range>"),
+		   "[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
 		NULL
 	};
 	struct option replay_options[] = {
@@ -103,6 +103,10 @@ int cmd_replay(int argc,
 			     N_("branch"),
 			     N_("revert commits onto given branch"),
 			     PARSE_OPT_NONEG),
+		OPT_STRING_F(0, "ref", &opts.ref,
+			     N_("branch"),
+			     N_("reference to update with result"),
+			     PARSE_OPT_NONEG),
 		OPT_STRING_F(0, "ref-action", &ref_action,
 			     N_("mode"),
 			     N_("control ref update behavior (update|print)"),
@@ -126,6 +130,8 @@ int cmd_replay(int argc,
 				  opts.contained, "--contained");
 	die_for_incompatible_opt2(!!opts.revert, "--revert",
 				  opts.contained, "--contained");
+	die_for_incompatible_opt2(!!opts.ref, "--ref",
+				  !!opts.contained, "--contained");
 
 	/* Parse ref action mode from command line or config */
 	ref_mode = get_ref_action_mode(repo, ref_action);
diff --git a/replay.c b/replay.c
index 199066f6b3..444f7ceefc 100644
--- a/replay.c
+++ b/replay.c
@@ -341,13 +341,15 @@ int replay_revisions(struct rev_info *revs,
 	struct commit *last_commit = NULL;
 	struct commit *commit;
 	struct commit *onto = NULL;
-	struct merge_options merge_opt;
+	struct merge_options merge_opt = { 0 };
 	struct merge_result result = {
 		.clean = 1,
 	};
 	bool detached_head;
 	char *advance;
 	char *revert;
+	const char *ref;
+	struct object_id old_oid;
 	enum replay_mode mode = REPLAY_MODE_PICK;
 	int ret;
 
@@ -358,6 +360,27 @@ int replay_revisions(struct rev_info *revs,
 	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
 			   &detached_head, &advance, &revert, &onto, &update_refs);
 
+	if (opts->ref) {
+		struct object_id oid;
+
+		if (update_refs && strset_get_size(update_refs) > 1) {
+			ret = error(_("'--ref' cannot be used with multiple revision ranges"));
+			goto out;
+		}
+		if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) {
+			ret = error(_("'%s' is not a valid refname"), opts->ref);
+			goto out;
+		}
+		ref = opts->ref;
+		if (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))
+			oidcpy(&old_oid, &oid);
+		else
+			oidclr(&old_oid, revs->repo->hash_algo);
+	} else {
+		ref = advance ? advance : revert;
+		oidcpy(&old_oid, &onto->object.oid);
+	}
+
 	/* FIXME: Should allow replaying commits with the first as a root commit */
 
 	if (prepare_revision_walk(revs) < 0) {
@@ -393,7 +416,7 @@ int replay_revisions(struct rev_info *revs,
 		kh_value(replayed_commits, pos) = last_commit;
 
 		/* Update any necessary branches */
-		if (advance || revert)
+		if (ref)
 			continue;
 
 		for (decoration = get_name_decoration(&commit->object);
@@ -427,13 +450,9 @@ int replay_revisions(struct rev_info *revs,
 		goto out;
 	}
 
-	/* In --advance or --revert mode, update the target ref */
-	if (advance || revert) {
-		const char *ref = advance ? advance : revert;
-		replay_result_queue_update(out, ref,
-					   &onto->object.oid,
+	if (ref)
+		replay_result_queue_update(out, ref, &old_oid,
 					   &last_commit->object.oid);
-	}
 
 	ret = 0;
 
diff --git a/replay.h b/replay.h
index e916a5f975..0ab74b9805 100644
--- a/replay.h
+++ b/replay.h
@@ -24,6 +24,13 @@ struct replay_revisions_options {
 	 */
 	const char *onto;
 
+	/*
+	 * Reference to update with the result of the replay. This will not
+	 * update any refs from `onto`, `advance`, or `revert`. Ignores
+	 * `contained`.
+	 */
+	const char *ref;
+
 	/*
 	 * Starting point at which to create revert commits; must be a branch
 	 * name. The branch will be updated to point to the revert commits.
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 217f6fb292..d5c7dd1bf4 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -495,4 +495,70 @@ test_expect_success 'git replay --revert incompatible with --advance' '
 	test_grep "cannot be used together" error
 '
 
+test_expect_success 'using --onto with --ref' '
+	git branch test-ref-onto topic2 &&
+	test_when_finished "git branch -D test-ref-onto" &&
+
+	git replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+	test_grep "^update refs/heads/test-ref-onto " result &&
+
+	git log --format=%s $(cut -f 3 -d " " result) >actual &&
+	test_write_lines E D M L B A >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'using --advance with --ref' '
+	git branch test-ref-advance main &&
+	git branch test-ref-target main &&
+	test_when_finished "git branch -D test-ref-advance test-ref-target" &&
+
+	git replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result &&
+
+	test_line_count = 1 result &&
+	test_grep "^update refs/heads/test-ref-target " result
+'
+
+test_expect_success 'using --revert with --ref' '
+	git branch test-ref-revert topic4 &&
+	git branch test-ref-revert-target topic4 &&
+	test_when_finished "git branch -D test-ref-revert test-ref-revert-target" &&
+
+	git replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result &&
+
+	test_line_count = 1 result &&
+	test_grep "^update refs/heads/test-ref-revert-target " result
+'
+
+test_expect_success '--ref is incompatible with --contained' '
+	test_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err &&
+	test_grep "cannot be used together" err
+'
+
+test_expect_success '--ref with nonexistent fully-qualified ref' '
+	test_when_finished "git update-ref -d refs/heads/new-branch" &&
+
+	git replay --onto=main --ref=refs/heads/new-branch topic1..topic2 &&
+
+	git log --format=%s -2 new-branch >actual &&
+	test_write_lines E D >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--ref must be a valid refname' '
+	test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
+	test_grep "is not a valid refname" err
+'
+
+test_expect_success '--ref requires fully qualified ref' '
+	test_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&
+	test_grep "is not a valid refname" err
+'
+
+test_expect_success '--onto with --ref rejects multiple revision ranges' '
+	test_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err &&
+	test_grep "cannot be used with multiple revision ranges" err
+'
+
 test_done
-- 
2.53.0.310.g728cabbaf7

Back to recent threads