{"thread":{"id":"65339","subject":"[PATCH 0/3] Add option --ref to git-replay(1)","startedAt":"2026-03-23T16:10:13Z","lastAt":"2026-04-01T20:55:33Z","messageCount":25,"participants":["Toon Claes","Tian Yuchen","Kristoffer Haugsbakk","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"539752","messageId":"20260323-toon-replay-arbitrary-ref-v1-0-5c7172f675ec@iotcl.com","threadId":"65339","inReplyTo":null,"subject":"[PATCH 0/3] Add option --ref to git-replay(1)","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-23T16:09:49Z","receivedAt":"2026-03-23T16:10:13Z","isPatch":true,"body":"In a previous RFC[1] I suggested to implement subcommands into\ngit-replay(1). While it would be arguable nice to have subcommands for\nthe different modes, because git-replay(1) is a plumbing commands, it's\nfine to keep a status quo.\n\nThis series takes one thing for that RFC though: adding option --ref.\nThis new option is useful if you want to have full control over which\nref is being updated, and not want to rely on the refs that are using\nin the <revision-range> or as the value for --advance and --revert.\n\nIn the leadup to the final change (the last commit), some\ndocumentational consistency is added in first two commits.\n\nThese changes answer the needs expressed in[2].\n\nThis series is based on Siddharth's series[3] to add '--revert' to\ngit-replay(1) (sa/replay-revert @ ba5c0d03d3).\n\n[1]: https://lore.kernel.org/git/20260309-toon-replay-subcommands-v1-1-864ec82ef68a@iotcl.com/\n[2]: https://lore.kernel.org/git/CAHTeOx-SMLh_idKhGczPKzZNOKy04uYXmUhL8Z79yRuNpmE4eA@mail.gmail.com/\n[3]: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/\n\n---\nToon Claes (3):\n      t3650: use option with value consistenly with equal sign\n      builtin/replay: improve documentation on options\n      replay: allow to specify a ref with option --ref\n\n Documentation/git-replay.adoc |  22 ++++++-\n builtin/replay.c              |  39 ++++++++-----\n replay.c                      |  29 +++++++---\n replay.h                      |   7 +++\n t/t3650-replay-basics.sh      | 131 +++++++++++++++++++++++++++++++-----------\n 5 files changed, 171 insertions(+), 57 deletions(-)\n\n\n\n---\nbase-commit: ba5c0d03d39a8b1798c8f988bdc95f9a08f10fac\nchange-id: 20260323-toon-replay-arbitrary-ref-5a81f5f976c7\n\n"},{"id":"539753","messageId":"20260323-toon-replay-arbitrary-ref-v1-1-5c7172f675ec@iotcl.com","threadId":"65339","inReplyTo":"20260323-toon-replay-arbitrary-ref-v1-0-5c7172f675ec@iotcl.com","subject":"[PATCH 1/3] t3650: use option with value consistenly with equal sign","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-23T16:09:50Z","receivedAt":"2026-03-23T16:10:17Z","isPatch":true,"body":"The tests in t3650-replay-basics have mixed use of option arguments\nwith value with and without equal sign. Bring in consistency and use\nequal sign for all options that expect a value. This makes it easier to\ndistinguish them from positional arguments.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n t/t3650-replay-basics.sh | 70 ++++++++++++++++++++++++------------------------\n 1 file changed, 35 insertions(+), 35 deletions(-)\n\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 0c1e03e0fb..47811b3197 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -95,8 +95,8 @@ test_expect_success '--contained requires --onto' '\n '\n \n test_expect_success 'cannot advance target ... ordering would be ill-defined' '\n-\tcat >expect <<-\\EOF &&\n-\tfatal: '\"'\"'--advance'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n+\tcat >expect <<-EOF &&\n+\tfatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\n \tEOF\n \ttest_must_fail git replay --advance=main main topic1 topic2 2>actual &&\n \ttest_cmp expect actual\n@@ -109,7 +109,7 @@ test_expect_success 'replaying merge commits is not supported yet' '\n '\n \n test_expect_success 'using replay to rebase two branches, one on top of other' '\n-\tgit replay --ref-action=print --onto main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --onto=main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -125,16 +125,16 @@ test_expect_success 'using replay to rebase two branches, one on top of other' '\n '\n \n test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '\n-\tgit -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --onto=main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n test_expect_success 'using replay to rebase with a conflict' '\n-\ttest_expect_code 1 git replay --onto topic1 B..conflict\n+\ttest_expect_code 1 git replay --onto=topic1 B..conflict\n '\n \n test_expect_success 'using replay on bare repo to rebase with a conflict' '\n-\ttest_expect_code 1 git -C bare replay --onto topic1 B..conflict\n+\ttest_expect_code 1 git -C bare replay --onto=topic1 B..conflict\n '\n \n test_expect_success 'using replay to perform basic cherry-pick' '\n@@ -143,7 +143,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n \t# 2nd field of result is refs/heads/main vs. refs/heads/topic2\n \t# 4th field of result is hash for main instead of hash for topic2\n \n-\tgit replay --ref-action=print --advance main topic1..topic2 >result &&\n+\tgit replay --ref-action=print --advance=main topic1..topic2 >result &&\n \n \ttest_line_count = 1 result &&\n \n@@ -159,7 +159,7 @@ test_expect_success 'using replay to perform basic cherry-pick' '\n '\n \n test_expect_success 'using replay on bare repo to perform basic cherry-pick' '\n-\tgit -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&\n+\tgit -C bare replay --ref-action=print --advance=main topic1..topic2 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n@@ -170,11 +170,11 @@ test_expect_success 'commits that become empty are dropped' '\n \ttest_when_finished \"git update-ref --stdin <original-branches &&\n \t\trm original-branches\" &&\n \t# Cherry-pick tip of topic1 (\"F\"), from the middle of A..empty, to main\n-\tgit replay --advance main topic1^! &&\n+\tgit replay --advance=main topic1^! &&\n \n \t# Replay all of A..empty onto main (which includes topic1 & thus F\n \t# in the middle)\n-\tgit replay --onto main --branches --ancestry-path=empty ^A \\\n+\tgit replay --onto=main --branches --ancestry-path=empty ^A \\\n \t\t>result &&\n \tgit log --format=\"%s%d\" L..empty >actual &&\n \ttest_write_lines >expect \\\n@@ -183,7 +183,7 @@ test_expect_success 'commits that become empty are dropped' '\n '\n \n test_expect_success 'replay on bare repo fails with both --advance and --onto' '\n-\ttest_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare\n+\ttest_must_fail git -C bare replay --advance=main --onto=main topic1..topic2 >result-bare\n '\n \n test_expect_success 'replay fails when both --advance and --onto are omitted' '\n@@ -191,7 +191,7 @@ test_expect_success 'replay fails when both --advance and --onto are omitted' '\n '\n \n test_expect_success 'using replay to also rebase a contained branch' '\n-\tgit replay --ref-action=print --contained --onto main main..topic3 >result &&\n+\tgit replay --ref-action=print --contained --onto=main main..topic3 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -215,12 +215,12 @@ test_expect_success 'using replay to also rebase a contained branch' '\n '\n \n test_expect_success 'using replay on bare repo to also rebase a contained branch' '\n-\tgit -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&\n+\tgit -C bare replay --ref-action=print --contained --onto=main main..topic3 >result-bare &&\n \ttest_cmp expect result-bare\n '\n \n test_expect_success 'using replay to rebase multiple divergent branches' '\n-\tgit replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&\n+\tgit replay --ref-action=print --onto=main ^topic1 topic2 topic4 >result &&\n \n \ttest_line_count = 2 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -244,7 +244,7 @@ test_expect_success 'using replay to rebase multiple divergent branches' '\n '\n \n test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '\n-\tgit -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&\n+\tgit -C bare replay --ref-action=print --contained --onto=main ^main topic2 topic3 topic4 >result &&\n \n \ttest_line_count = 4 result &&\n \tcut -f 3 -d \" \" result >new-branch-tips &&\n@@ -276,7 +276,7 @@ test_expect_success 'using replay to update detached HEAD' '\n \ttest_when_finished git switch \"$current_head\" &&\n \tgit switch --detach &&\n \ttest_commit something &&\n-\tgit replay --ref-action=print --onto HEAD~2 --ref-action=print HEAD~..HEAD >updates &&\n+\tgit replay --ref-action=print --onto=HEAD~2 --ref-action=print HEAD~..HEAD >updates &&\n \ttest_grep \"update HEAD \" updates\n '\n \n@@ -299,7 +299,7 @@ test_expect_success 'merge.directoryRenames=false' '\n \tgit commit -m modified to-rename/add-a-file.t &&\n \n \tgit -c merge.directoryRenames=false replay \\\n-\t\t--onto rename-onto rename-onto..rename-from\n+\t\t--onto=rename-onto rename-onto..rename-from\n '\n \n test_expect_success 'default atomic behavior updates refs directly' '\n@@ -308,7 +308,7 @@ test_expect_success 'default atomic behavior updates refs directly' '\n \ttest_when_finished \"git branch -D test-atomic\" &&\n \n \t# Test default atomic behavior (no output, refs updated)\n-\tgit replay --onto main topic1..test-atomic >output &&\n+\tgit replay --onto=main topic1..test-atomic >output &&\n \ttest_must_be_empty output &&\n \n \t# Verify ref was updated\n@@ -329,7 +329,7 @@ test_expect_success 'atomic behavior in bare repository' '\n \ttest_when_finished \"git -C bare update-ref refs/heads/topic2 $START\" &&\n \n \t# Test atomic updates work in bare repo\n-\tgit -C bare replay --onto main topic1..topic2 >output &&\n+\tgit -C bare replay --onto=main topic1..topic2 >output &&\n \ttest_must_be_empty output &&\n \n \t# Verify ref was updated in bare repo\n@@ -344,7 +344,7 @@ test_expect_success 'reflog message for --advance mode' '\n \ttest_when_finished \"git update-ref refs/heads/main $START\" &&\n \n \t# Test --advance mode reflog message\n-\tgit replay --advance main topic1..topic2 >output &&\n+\tgit replay --advance=main topic1..topic2 >output &&\n \ttest_must_be_empty output &&\n \n \t# Verify reflog message includes --advance and branch name\n@@ -360,7 +360,7 @@ test_expect_success 'replay.refAction=print config option' '\n \n \t# Test with config set to print\n \ttest_config replay.refAction print &&\n-\tgit replay --onto main topic1..topic2 >output &&\n+\tgit replay --onto=main topic1..topic2 >output &&\n \ttest_line_count = 1 output &&\n \ttest_grep \"^update refs/heads/topic2 \" output\n '\n@@ -372,7 +372,7 @@ test_expect_success 'replay.refAction=update config option' '\n \n \t# Test with config set to update\n \ttest_config replay.refAction update &&\n-\tgit replay --onto main topic1..topic2 >output &&\n+\tgit replay --onto=main topic1..topic2 >output &&\n \ttest_must_be_empty output &&\n \n \t# Verify ref was updated\n@@ -388,14 +388,14 @@ test_expect_success 'command-line --ref-action overrides config' '\n \n \t# Set config to update but use --ref-action=print\n \ttest_config replay.refAction update &&\n-\tgit replay --ref-action=print --onto main topic1..topic2 >output &&\n+\tgit replay --ref-action=print --onto=main topic1..topic2 >output &&\n \ttest_line_count = 1 output &&\n \ttest_grep \"^update refs/heads/topic2 \" output\n '\n \n test_expect_success 'invalid replay.refAction value' '\n \ttest_config replay.refAction invalid &&\n-\ttest_must_fail git replay --onto main topic1..topic2 2>error &&\n+\ttest_must_fail git replay --onto=main topic1..topic2 2>error &&\n \ttest_grep \"invalid.*replay.refAction.*value\" error\n '\n \n@@ -407,10 +407,10 @@ test_expect_success 'argument to --revert must be a reference' '\n '\n \n test_expect_success 'cannot revert with multiple sources' '\n-\tcat >expect <<-\\EOF &&\n-\tfatal: '\"'\"'--revert'\"'\"' cannot be used with multiple revision ranges because the ordering would be ill-defined\n+\tcat >expect <<-EOF &&\n+\tfatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined\n \tEOF\n-\ttest_must_fail git replay --revert main main topic1 topic2 2>actual &&\n+\ttest_must_fail git replay --revert=main main topic1 topic2 2>actual &&\n \ttest_cmp expect actual\n '\n \n@@ -420,7 +420,7 @@ test_expect_success 'using replay --revert to revert commits' '\n \ttest_when_finished \"git branch -f topic4 $START\" &&\n \n \t# Revert commits I and J\n-\tgit replay --revert topic4 topic4~2..topic4 &&\n+\tgit replay --revert=topic4 topic4~2..topic4 &&\n \n \t# Verify the revert commits were created (newest-first ordering\n \t# means J is reverted first, then I on top)\n@@ -452,7 +452,7 @@ test_expect_success 'using replay --revert in bare repo' '\n \ttest_when_finished \"git -C bare update-ref refs/heads/topic4 $START\" &&\n \n \t# Revert commit J in bare repo\n-\tgit -C bare replay --revert topic4 topic4~1..topic4 &&\n+\tgit -C bare replay --revert=topic4 topic4~1..topic4 &&\n \n \t# Verify revert was created\n \tgit -C bare log -1 --format=%s topic4 >actual &&\n@@ -466,11 +466,11 @@ test_expect_success 'revert of revert uses Reapply' '\n \ttest_when_finished \"git branch -f topic4 $START\" &&\n \n \t# First revert J\n-\tgit replay --revert topic4 topic4~1..topic4 &&\n+\tgit replay --revert=topic4 topic4~1..topic4 &&\n \tREVERT_J=$(git rev-parse topic4) &&\n \n \t# Now revert the revert - should become Reapply\n-\tgit replay --revert topic4 topic4~1..topic4 &&\n+\tgit replay --revert=topic4 topic4~1..topic4 &&\n \n \t# Verify Reapply prefix and message format\n \ttest_commit_message topic4 <<-EOF\n@@ -482,21 +482,21 @@ test_expect_success 'revert of revert uses Reapply' '\n \n test_expect_success 'git replay --revert with conflict' '\n \t# conflict branch has C.conflict which conflicts with topic1s C\n-\ttest_expect_code 1 git replay --revert conflict B..topic1\n+\ttest_expect_code 1 git replay --revert=conflict B..topic1\n '\n \n test_expect_success 'git replay --revert incompatible with --contained' '\n-\ttest_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&\n+\ttest_must_fail git replay --revert=topic4 --contained topic4~1..topic4 2>error &&\n \ttest_grep \"requires --onto\" error\n '\n \n test_expect_success 'git replay --revert incompatible with --onto' '\n-\ttest_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&\n+\ttest_must_fail git replay --revert=topic4 --onto=main topic4~1..topic4 2>error &&\n \ttest_grep \"cannot be used together\" error\n '\n \n test_expect_success 'git replay --revert incompatible with --advance' '\n-\ttest_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&\n+\ttest_must_fail git replay --revert=topic4 --advance=main topic4~1..topic4 2>error &&\n \ttest_grep \"cannot be used together\" error\n '\n \n\n-- \n2.53.0.310.g728cabbaf7\n\n"},{"id":"539754","messageId":"20260323-toon-replay-arbitrary-ref-v1-2-5c7172f675ec@iotcl.com","threadId":"65339","inReplyTo":"20260323-toon-replay-arbitrary-ref-v1-0-5c7172f675ec@iotcl.com","subject":"[PATCH 2/3] builtin/replay: improve documentation on options","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-23T16:09:51Z","receivedAt":"2026-03-23T16:10:31Z","isPatch":true,"body":"Most of the options for the command git-replay(1) expect a value\nargument. Make that more clear in the docs, by using an equal sign\nbetween the option and the value.\n\nAlso, none of the options are negatable, thus mark them as such using\nPARSE_OPT_NONEG.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-replay.adoc |  3 ++-\n builtin/replay.c              | 32 ++++++++++++++++++--------------\n 2 files changed, 20 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 6698cfc047..e4fef548e0 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,8 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n+\t\t\t     [--ref-action=<mode>] <revision-range>\n \n DESCRIPTION\n -----------\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex fe69f6f8ce..2e9fd6281d 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -83,25 +83,29 @@ int cmd_replay(int argc,\n \n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n-\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n+\t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n+\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n-\t\tOPT_STRING(0, \"advance\", &opts.advance,\n-\t\t\t   N_(\"branch\"),\n-\t\t\t   N_(\"make replay advance given branch\")),\n-\t\tOPT_STRING(0, \"onto\", &opts.onto,\n-\t\t\t   N_(\"revision\"),\n-\t\t\t   N_(\"replay onto given commit\")),\n+\t\tOPT_STRING_F(0, \"onto\", &opts.onto,\n+\t\t\t     N_(\"revision\"),\n+\t\t\t     N_(\"replay onto given commit\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"advance\", &opts.advance,\n+\t\t\t     N_(\"branch\"),\n+\t\t\t     N_(\"make replay advance given branch\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"revert\", &opts.revert,\n+\t\t\t     N_(\"branch\"),\n+\t\t\t     N_(\"revert commits onto given branch\"),\n+\t\t\t     PARSE_OPT_NONEG),\n \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n-\t\tOPT_STRING(0, \"revert\", &opts.revert,\n-\t\t\t   N_(\"branch\"),\n-\t\t\t   N_(\"revert commits onto given branch\")),\n-\t\tOPT_STRING(0, \"ref-action\", &ref_action,\n-\t\t\t   N_(\"mode\"),\n-\t\t\t   N_(\"control ref update behavior (update|print)\")),\n+\t\tOPT_STRING_F(0, \"ref-action\", &ref_action,\n+\t\t\t     N_(\"mode\"),\n+\t\t\t     N_(\"control ref update behavior (update|print)\"),\n+\t\t\t     PARSE_OPT_NONEG),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.53.0.310.g728cabbaf7\n\n"},{"id":"539755","messageId":"20260323-toon-replay-arbitrary-ref-v1-3-5c7172f675ec@iotcl.com","threadId":"65339","inReplyTo":"20260323-toon-replay-arbitrary-ref-v1-0-5c7172f675ec@iotcl.com","subject":"[PATCH 3/3] replay: allow to specify a ref with option --ref","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-23T16:09:52Z","receivedAt":"2026-03-23T16:10:35Z","isPatch":true,"body":"To enable users to replay commits without having refs in\n<revision-range> or as the value for --advance and --revert, start\naccepting option --ref. When the user specifies a --ref, the refs from\nthe other option are ignored, and only the ref is acted on.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-replay.adoc | 21 ++++++++++++++-\n builtin/replay.c              |  9 ++++++-\n replay.c                      | 29 +++++++++++++++-----\n replay.h                      |  7 +++++\n t/t3650-replay-basics.sh      | 61 +++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 118 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex e4fef548e0..f90d93ae31 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -10,7 +10,8 @@ SYNOPSIS\n --------\n [verse]\n (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n-\t\t\t     [--ref-action=<mode>] <revision-range>\n+\t\t\t     [--ref=<branch>] [--ref-action=<mode>]\n+\t\t\t     <revision-range>\n \n DESCRIPTION\n -----------\n@@ -62,6 +63,14 @@ the current user as the author, matching the behavior of `git revert`.\n This option is mutually exclusive with `--onto` and `--advance`. It is also\n incompatible with `--contained` (which is a modifier for `--onto` only).\n \n+--ref <branch>::\n+\tOverride which reference is updated with the result of the\n+\treplay. When used with `--onto`, only the specified reference\n+\tis updated instead of inferring refs from the revision range.\n+\tWhen used with `--advance` or `--revert`, the specified\n+\treference is updated instead of the branch given to those\n+\toptions. This option is incompatible with `--contained`.\n+\n --contained::\n \tUpdate all branches that point at commits in\n \t<revision-range>. Requires `--onto`.\n@@ -184,6 +193,16 @@ top of `main`, and updates `main` to point at the result. This is useful when\n commits from `topic` were previously merged or cherry-picked into `main` and\n need to be undone.\n \n+To replay onto a specific commit while updating a different reference:\n+\n+------------\n+$ git replay --onto 112233 --ref refs/heads/mybranch aabbcc..ddeeff\n+------------\n+\n+This replays the range `aabbcc..ddeeff` onto commit `112233` and updates\n+`refs/heads/mybranch` to point at the result. This is useful when you want\n+to use bare commit IDs instead of branch names.\n+\n NOTE: For reverting an entire merge request as a single commit (rather than\n commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n which can avoid unnecessary merge conflicts.\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 2e9fd6281d..f4fc18d16f 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -84,7 +84,8 @@ int cmd_replay(int argc,\n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n-\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n+\t\t   \"[--ref=<branch>] [--ref-action=<mode>]\\n\"\n+\t\t   \"<revision-range>\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -102,6 +103,10 @@ int cmd_replay(int argc,\n \t\t\t     PARSE_OPT_NONEG),\n \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n+\t\tOPT_STRING_F(0, \"ref\", &opts.ref,\n+\t\t\t     N_(\"branch\"),\n+\t\t\t     N_(\"reference to update with result\"),\n+\t\t\t     PARSE_OPT_NONEG),\n \t\tOPT_STRING_F(0, \"ref-action\", &ref_action,\n \t\t\t     N_(\"mode\"),\n \t\t\t     N_(\"control ref update behavior (update|print)\"),\n@@ -121,6 +126,8 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n \t\t\t\t  !!opts.advance, \"--advance\",\n \t\t\t\t  !!opts.revert, \"--revert\");\n+\tdie_for_incompatible_opt2(!!opts.ref, \"--ref\",\n+\t\t\t\t  !!opts.contained, \"--contained\");\n \tif (opts.contained && !opts.onto)\n \t\tdie(_(\"--contained requires --onto\"));\n \ndiff --git a/replay.c b/replay.c\nindex 199066f6b3..07b94cddde 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -348,6 +348,8 @@ int replay_revisions(struct rev_info *revs,\n \tbool detached_head;\n \tchar *advance;\n \tchar *revert;\n+\tconst char *ref;\n+\tstruct object_id old_oid;\n \tenum replay_mode mode = REPLAY_MODE_PICK;\n \tint ret;\n \n@@ -358,6 +360,23 @@ int replay_revisions(struct rev_info *revs,\n \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n \t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n \n+\tif (opts->ref) {\n+\t\tstruct object_id oid;\n+\n+\t\tif (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, \"refs/\")) {\n+\t\t\tret = error(_(\"'%s' is not a valid refname\"), opts->ref);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tref = opts->ref;\n+\t\tif (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))\n+\t\t\toidcpy(&old_oid, &oid);\n+\t\telse\n+\t\t\toidclr(&old_oid, revs->repo->hash_algo);\n+\t} else {\n+\t\tref = advance ? advance : revert;\n+\t\toidcpy(&old_oid, &onto->object.oid);\n+\t}\n+\n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n \tif (prepare_revision_walk(revs) < 0) {\n@@ -393,7 +412,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance || revert)\n+\t\tif (ref)\n \t\t\tcontinue;\n \n \t\tfor (decoration = get_name_decoration(&commit->object);\n@@ -427,13 +446,9 @@ int replay_revisions(struct rev_info *revs,\n \t\tgoto out;\n \t}\n \n-\t/* In --advance or --revert mode, update the target ref */\n-\tif (advance || revert) {\n-\t\tconst char *ref = advance ? advance : revert;\n-\t\treplay_result_queue_update(out, ref,\n-\t\t\t\t\t   &onto->object.oid,\n+\tif (ref)\n+\t\treplay_result_queue_update(out, ref, &old_oid,\n \t\t\t\t\t   &last_commit->object.oid);\n-\t}\n \n \tret = 0;\n \ndiff --git a/replay.h b/replay.h\nindex e916a5f975..99933f2268 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -24,6 +24,13 @@ struct replay_revisions_options {\n \t */\n \tconst char *onto;\n \n+\t/*\n+\t * Reference to update with the result of the replay. This stops replay\n+\t * from taking refs from `onto`, `advance`, and `revert`. Ignores\n+\t * `contained`.\n+\t */\n+\tconst char *ref;\n+\n \t/*\n \t * Starting point at which to create revert commits; must be a branch\n \t * name. The branch will be updated to point to the revert commits.\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 47811b3197..b7465b5f18 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -500,4 +500,65 @@ test_expect_success 'git replay --revert incompatible with --advance' '\n \ttest_grep \"cannot be used together\" error\n '\n \n+test_expect_success 'using --onto with --ref' '\n+\tgit branch test-ref-onto topic2 &&\n+\ttest_when_finished \"git branch -D test-ref-onto\" &&\n+\n+\tgit replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\ttest_grep \"^update refs/heads/test-ref-onto \" result &&\n+\n+\tgit log --format=%s $(cut -f 3 -d \" \" result) >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using --advance with --ref' '\n+\tgit branch test-ref-advance main &&\n+\tgit branch test-ref-target main &&\n+\ttest_when_finished \"git branch -D test-ref-advance test-ref-target\" &&\n+\n+\tgit replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\ttest_grep \"^update refs/heads/test-ref-target \" result\n+'\n+\n+test_expect_success 'using --revert with --ref' '\n+\tgit branch test-ref-revert topic4 &&\n+\tgit branch test-ref-revert-target topic4 &&\n+\ttest_when_finished \"git branch -D test-ref-revert test-ref-revert-target\" &&\n+\n+\tgit replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\ttest_grep \"^update refs/heads/test-ref-revert-target \" result\n+'\n+\n+test_expect_success '--ref is incompatible with --contained' '\n+\ttest_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err &&\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n+test_expect_success '--ref with nonexistent fully-qualified ref' '\n+\ttest_when_finished \"git update-ref -d refs/heads/new-branch\" &&\n+\n+\tgit replay --onto=main --ref=refs/heads/new-branch topic1..topic2 &&\n+\n+\tgit log --format=%s -2 new-branch >actual &&\n+\ttest_write_lines E D >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--ref must be a valid refname' '\n+\ttest_must_fail git replay --onto=main --ref=\"refs/heads/bad..ref\" topic1..topic2 2>err &&\n+\ttest_grep \"is not a valid refname\" err\n+'\n+\n+test_expect_success '--ref requires fully qualified ref' '\n+\ttest_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&\n+\ttest_grep \"is not a valid refname\" err\n+'\n+\n test_done\n\n-- \n2.53.0.310.g728cabbaf7\n\n"},{"id":"539772","messageId":"b0d49276-a9d3-435d-bb39-8cf7a08fbd11@malon.dev","threadId":"65339","inReplyTo":"20260323-toon-replay-arbitrary-ref-v1-3-5c7172f675ec@iotcl.com","subject":"Re: [PATCH 3/3] replay: allow to specify a ref with option --ref","fromName":"Tian Yuchen","fromEmail":"cat@malon.dev","sentAt":"2026-03-23T18:01:52Z","receivedAt":"2026-03-23T18:02:58Z","isPatch":true,"body":"Hi Toon,\n\nOn 3/24/26 00:09, Toon Claes wrote:\n\n\n> +\tdie_for_incompatible_opt2(!!opts.ref, \"--ref\",\n> +\t\t\t\t  !!opts.contained, \"--contained\");\n\n> +\t/*\n> +\t * Reference to update with the result of the replay. This stops replay\n> +\t * from taking refs from `onto`, `advance`, and `revert`. Ignores\n> +\t * `contained`.\n> +\t */\n> +\tconst char *ref;\n> +\n\nDo the comment and the code implementation match up here?\n\n> @@ -427,13 +446,9 @@ int replay_revisions(struct rev_info *revs,\n>  \t\tgoto out;\n>  \t}\n>  \n> -\t/* In --advance or --revert mode, update the target ref */\n> -\tif (advance || revert) {\n> -\t\tconst char *ref = advance ? advance : revert;\n> -\t\treplay_result_queue_update(out, ref,\n> -\t\t\t\t\t   &onto->object.oid,\n> +\tif (ref)\n> +\t\treplay_result_queue_update(out, ref, &old_oid,\n>  \t\t\t\t\t   &last_commit->object.oid);\n> -\t}\n>  \n>  \tret = 0;\n\nIt doesn't seem like there's anything here to prevent users from \ninputting multiple branches? I think this will result in at least one \nbranch becoming orphaned, without any error being reported.\n\nConsider this command:\n\n\tgit replay --onto main --ref refs/heads/target topic1 topic2\n\nIn replay.c we have:\n\n> if (onto_name) {\n> \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n> \t\tif (rinfo.positive_refexprs <\n> \t\t    strset_get_size(&rinfo.positive_refs))\n> \t\t\tdie(_(\"all positive revisions given must be references\"));\n> \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n> \t\t**update_refs = rinfo.positive_refs;\n> \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n\n\nAfter topic1 and topic2 are put into update_refs, only one ref \n(refs/heads/target) is updated, since what is written in the patch is:\n\n > +\tif (ref)\n > +\t\treplay_result_queue_update(out, ref, &old_oid,\n >  \t\t\t\t\t   &last_commit->object.oid);\n\nI believe this is not what we expect :P\n\nRegards, Yuchen\n\n"},{"id":"539774","messageId":"63a66d11-d138-4d4e-ab5c-f7669c218293@app.fastmail.com","threadId":"65339","inReplyTo":"20260323-toon-replay-arbitrary-ref-v1-3-5c7172f675ec@iotcl.com","subject":"Re: [PATCH 3/3] replay: allow to specify a ref with option --ref","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-23T19:07:17Z","receivedAt":"2026-03-23T19:07:39Z","isPatch":true,"body":"On Mon, Mar 23, 2026, at 17:09, Toon Claes wrote:\n> To enable users to replay commits without having refs in\n> <revision-range> or as the value for --advance and --revert, start\n> accepting option --ref. When the user specifies a --ref, the refs from\n> the other option are ignored, and only the ref is acted on.\n\ns/option are/options are/\n\n>\n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n>[snip]\n> +To replay onto a specific commit while updating a different reference:\n> +\n> +------------\n> +$ git replay --onto 112233 --ref refs/heads/mybranch aabbcc..ddeeff\n> +------------\n\nThis example could use the stuck form like in the two previous commits.\n\n    git replay --onto=112233 --ref=refs/heads/mybranch ...\n\n> +\n> +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates\n> +`refs/heads/mybranch` to point at the result. This is useful when you want\n> +to use bare commit IDs instead of branch names.\n> +\n>  NOTE: For reverting an entire merge request as a single commit (rather\n> than\n>[snip]\n"},{"id":"539775","messageId":"ccc995f1-4da2-468a-97d6-f20993ca4b4c@app.fastmail.com","threadId":"65339","inReplyTo":"20260323-toon-replay-arbitrary-ref-v1-1-5c7172f675ec@iotcl.com","subject":"Re: [PATCH 1/3] t3650: use option with value consistenly with equal sign","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-23T19:17:56Z","receivedAt":"2026-03-23T19:18:18Z","isPatch":true,"body":"On Mon, Mar 23, 2026, at 17:09, Toon Claes wrote:\n> The tests in t3650-replay-basics have mixed use of option arguments\n> with value with and without equal sign. Bring in consistency and use\n> equal sign for all options that expect a value.\n\nIf it is about consistency, could you pick one or the other either way\nor go with whatever happened to be most used right now?\n\nConsistency by itself is a weaker argument than arguing that stuck form\nis better for invoking git(1) commands, which is what gitcli(7) argues.\n\nWhich is to say: arguing for stuck form in the commit message based on\nit being better is a stronger argument than wanting consistency. :)\n\nThen once one form has been argued for or referenced it follows that you\nshould be consistent and use the best approach throughout.\n\n> This makes it easier to distinguish them from positional arguments.\n\nMaybe it’s just me, but sticking with the stuck form makes it harder to\nmess up writing unintended options and positional arguments. Once\nwritten it might be slightly more readable, but the main benefit is\nusing a style that makes messing up harder to pull off.\n\n>\n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n>  t/t3650-replay-basics.sh | 70 ++++++++++++++++++++++++------------------------\n>  1 file changed, 35 insertions(+), 35 deletions(-)\n>[snip]\n"},{"id":"539777","messageId":"xmqqv7em48fw.fsf@gitster.g","threadId":"65339","inReplyTo":"ccc995f1-4da2-468a-97d6-f20993ca4b4c@app.fastmail.com","subject":"Re: [PATCH 1/3] t3650: use option with value consistenly with equal sign","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-23T20:06:27Z","receivedAt":"2026-03-23T20:06:30Z","isPatch":true,"body":"\"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n\n> On Mon, Mar 23, 2026, at 17:09, Toon Claes wrote:\n>> The tests in t3650-replay-basics have mixed use of option arguments\n>> with value with and without equal sign. Bring in consistency and use\n>> equal sign for all options that expect a value.\n>\n> If it is about consistency, could you pick one or the other either way\n> or go with whatever happened to be most used right now?\n>\n> Consistency by itself is a weaker argument than arguing that stuck form\n> is better for invoking git(1) commands, which is what gitcli(7) argues.\n>\n> Which is to say: arguing for stuck form in the commit message based on\n> it being better is a stronger argument than wanting consistency. :)\n>\n> Then once one form has been argued for or referenced it follows that you\n> should be consistent and use the best approach throughout.\n>\n>> This makes it easier to distinguish them from positional arguments.\n>\n> Maybe it’s just me, but sticking with the stuck form makes it harder to\n> mess up writing unintended options and positional arguments. Once\n> written it might be slightly more readable, but the main benefit is\n> using a style that makes messing up harder to pull off.\n\nI am not sure if a patch whose purpose is only to make the CLI\ninvocation \"consistent\" is a welcome change, though.  If we support\ntwo forms, exercising both forms and making sure they mean the same\nthing may even be better, but short of that, a random mixture of\nstyles as if end-user human may have picked one form on this day and\nthe other form on another day may be better than a complete\nmonoculture.\n\n"},{"id":"539937","messageId":"87mrzw13mz.fsf@iotcl.com","threadId":"65339","inReplyTo":"xmqqv7em48fw.fsf@gitster.g","subject":"Re: [PATCH 1/3] t3650: use option with value consistenly with equal sign","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-25T12:43:00Z","receivedAt":"2026-03-25T12:43:14Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I am not sure if a patch whose purpose is only to make the CLI\n> invocation \"consistent\" is a welcome change, though.  If we support\n> two forms, exercising both forms and making sure they mean the same\n> thing may even be better, but short of that, a random mixture of\n> styles as if end-user human may have picked one form on this day and\n> the other form on another day may be better than a complete\n> monoculture.\n\nOkay, I'll drop this commit.\n\n-- \nCheers,\nToon\n"},{"id":"539938","messageId":"87ikak13c4.fsf@iotcl.com","threadId":"65339","inReplyTo":"63a66d11-d138-4d4e-ab5c-f7669c218293@app.fastmail.com","subject":"Re: [PATCH 3/3] replay: allow to specify a ref with option --ref","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-25T12:49:31Z","receivedAt":"2026-03-25T12:49:41Z","isPatch":true,"body":"\"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n\n> On Mon, Mar 23, 2026, at 17:09, Toon Claes wrote:\n>> To enable users to replay commits without having refs in\n>> <revision-range> or as the value for --advance and --revert, start\n>> accepting option --ref. When the user specifies a --ref, the refs from\n>> the other option are ignored, and only the ref is acted on.\n>\n> s/option are/options are/\n\nThanks!\n\n>> Signed-off-by: Toon Claes <toon@iotcl.com>\n>> ---\n>>[snip]\n>> +To replay onto a specific commit while updating a different reference:\n>> +\n>> +------------\n>> +$ git replay --onto 112233 --ref refs/heads/mybranch aabbcc..ddeeff\n>> +------------\n>\n> This example could use the stuck form like in the two previous commits.\n>\n>     git replay --onto=112233 --ref=refs/heads/mybranch ...\n\nGood point.\n\n>> +\n>> +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates\n>> +`refs/heads/mybranch` to point at the result. This is useful when you want\n>> +to use bare commit IDs instead of branch names.\n>> +\n>>  NOTE: For reverting an entire merge request as a single commit (rather\n>> than\n>>[snip]\n\n-- \nCheers,\nToon\n"},{"id":"539939","messageId":"87h5q413b5.fsf@iotcl.com","threadId":"65339","inReplyTo":"b0d49276-a9d3-435d-bb39-8cf7a08fbd11@malon.dev","subject":"Re: [PATCH 3/3] replay: allow to specify a ref with option --ref","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-25T12:50:06Z","receivedAt":"2026-03-25T12:50:12Z","isPatch":true,"body":"Tian Yuchen <cat@malon.dev> writes:\n\n> Hi Toon,\n>\n> On 3/24/26 00:09, Toon Claes wrote:\n>\n>\n>> +\tdie_for_incompatible_opt2(!!opts.ref, \"--ref\",\n>> +\t\t\t\t  !!opts.contained, \"--contained\");\n>\n>> +\t/*\n>> +\t * Reference to update with the result of the replay. This stops replay\n>> +\t * from taking refs from `onto`, `advance`, and `revert`. Ignores\n>> +\t * `contained`.\n>> +\t */\n>> +\tconst char *ref;\n>> +\n>\n> Do the comment and the code implementation match up here?\n\nThe comment is a bit misleading. A user cannot set the field `contained`\nin the struct when it passed `--ref`. But if a bug is made somewhere,\nbut both fields can be set, that's what the comment is saying.\n\nI can add a check to verify in replay.c that not both are set, and\nupdate the comment they are incompatible?\n\n>> @@ -427,13 +446,9 @@ int replay_revisions(struct rev_info *revs,\n>>  \t\tgoto out;\n>>  \t}\n>>  \n>> -\t/* In --advance or --revert mode, update the target ref */\n>> -\tif (advance || revert) {\n>> -\t\tconst char *ref = advance ? advance : revert;\n>> -\t\treplay_result_queue_update(out, ref,\n>> -\t\t\t\t\t   &onto->object.oid,\n>> +\tif (ref)\n>> +\t\treplay_result_queue_update(out, ref, &old_oid,\n>>  \t\t\t\t\t   &last_commit->object.oid);\n>> -\t}\n>>  \n>>  \tret = 0;\n>\n> It doesn't seem like there's anything here to prevent users from \n> inputting multiple branches? I think this will result in at least one \n> branch becoming orphaned, without any error being reported.\n>\n> Consider this command:\n>\n> \tgit replay --onto main --ref refs/heads/target topic1 topic2\n\nThat's interesting, I didn't think of that.\n\n>\n> In replay.c we have:\n>\n>> if (onto_name) {\n>> \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n>> \t\tif (rinfo.positive_refexprs <\n>> \t\t    strset_get_size(&rinfo.positive_refs))\n>> \t\t\tdie(_(\"all positive revisions given must be references\"));\n>> \t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n>> \t\t**update_refs = rinfo.positive_refs;\n>> \t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n>\n>\n> After topic1 and topic2 are put into update_refs, only one ref \n> (refs/heads/target) is updated, since what is written in the patch is:\n>\n>  > +\tif (ref)\n>  > +\t\treplay_result_queue_update(out, ref, &old_oid,\n>  >  \t\t\t\t\t   &last_commit->object.oid);\n>\n> I believe this is not what we expect :P\n\nLet me look into that how we can deal with that.\n\n-- \nCheers,\nToon\n"},{"id":"539950","messageId":"20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com","threadId":"65339","inReplyTo":"20260323-toon-replay-arbitrary-ref-v1-0-5c7172f675ec@iotcl.com","subject":"[PATCH v2 0/3] Add option --ref to git-replay(1)","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-25T15:59:28Z","receivedAt":"2026-03-25T16:00:01Z","isPatch":true,"body":"In a previous RFC[1] I suggested to implement subcommands into\ngit-replay(1). While it would be arguable nice to have subcommands for\nthe different modes, because git-replay(1) is a plumbing commands, it's\nfine to keep a status quo.\n\nThis series takes one thing for that RFC though: adding option --ref.\nThis new option is useful if you want to have full control over which\nref is being updated, and not want to rely on the refs that are using\nin the <revision-range> or as the value for --advance and --revert.\n\nThese changes answer the needs expressed in[2].\n\nThis series is based on Siddharth's series[3] to add '--revert' to\ngit-replay(1) (sa/replay-revert @ ba5c0d03d3).\n\n[1]: https://lore.kernel.org/git/20260309-toon-replay-subcommands-v1-1-864ec82ef68a@iotcl.com/\n[2]: https://lore.kernel.org/git/CAHTeOx-SMLh_idKhGczPKzZNOKy04uYXmUhL8Z79yRuNpmE4eA@mail.gmail.com/\n[3]: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/\n\n---\nChanges in v2:\n- Dropped the test-only consistency patch.\n- Separated commit to mark options as not negatable.\n- Modified git-replay(1) docs to everywhere use stuck form.\n- Added code and test ensure the revision range has a single tip when\n  both --onto and --ref are given.\n- Rephrased some comments and docs.\n- Link to v1: https://patch.msgid.link/20260323-toon-replay-arbitrary-ref-v1-0-5c7172f675ec@iotcl.com\n\n---\nToon Claes (3):\n      builtin/replay: mark options as not negatable\n      replay: use stuck form in documentation and help message\n      replay: allow to specify a ref with option --ref\n\n Documentation/git-replay.adoc | 42 +++++++++++++++++++--------\n builtin/replay.c              | 38 ++++++++++++++++---------\n replay.c                      | 33 +++++++++++++++++-----\n replay.h                      |  7 +++++\n t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 154 insertions(+), 32 deletions(-)\n\nRange-diff versus v1:\n\n1:  fe4daa4c89 < -:  ---------- t3650: use option with value consistenly with equal sign\n2:  3170bcc916 < -:  ---------- builtin/replay: improve documentation on options\n-:  ---------- > 1:  61ba35cc08 builtin/replay: mark options as not negatable\n-:  ---------- > 2:  19e54b8326 replay: use stuck form in documentation and help message\n3:  bdb166a9ff ! 3:  341b94f937 replay: allow to specify a ref with option --ref\n    @@ Metadata\n      ## Commit message ##\n         replay: allow to specify a ref with option --ref\n     \n    -    To enable users to replay commits without having refs in\n    -    <revision-range> or as the value for --advance and --revert, start\n    -    accepting option --ref. When the user specifies a --ref, the refs from\n    -    the other option are ignored, and only the ref is acted on.\n    +    When option '--onto' is passed to git-replay(1), the command will update\n    +    refs from the <revision-range> passed to the command. When using option\n    +    '--advance' or '--revert', the argument of that option is a ref that\n    +    will be updated.\n    +\n    +    To enable users to specify which ref to update, add option '--ref'. When\n    +    using option '--ref', the refs described above are left untouched and\n    +    instead the argument of this option is updated instead.\n     \n         Signed-off-by: Toon Claes <toon@iotcl.com>\n     \n    @@ Documentation/git-replay.adoc: SYNOPSIS\n      [verse]\n      (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n     -\t\t\t     [--ref-action=<mode>] <revision-range>\n    -+\t\t\t     [--ref=<branch>] [--ref-action=<mode>]\n    -+\t\t\t     <revision-range>\n    ++\t\t\t     [--ref=<branch>] [--ref-action=<mode>] <revision-range>\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-replay.adoc: the current user as the author, matching the behavior of `git revert`.\n    - This option is mutually exclusive with `--onto` and `--advance`. It is also\n    - incompatible with `--contained` (which is a modifier for `--onto` only).\n    - \n    -+--ref <branch>::\n    -+\tOverride which reference is updated with the result of the\n    -+\treplay. When used with `--onto`, only the specified reference\n    -+\tis updated instead of inferring refs from the revision range.\n    -+\tWhen used with `--advance` or `--revert`, the specified\n    -+\treference is updated instead of the branch given to those\n    -+\toptions. This option is incompatible with `--contained`.\n    -+\n    - --contained::\n    +@@ Documentation/git-replay.adoc: incompatible with `--contained` (which is a modifier for `--onto` only).\n      \tUpdate all branches that point at commits in\n      \t<revision-range>. Requires `--onto`.\n    -@@ Documentation/git-replay.adoc: top of `main`, and updates `main` to point at the result. This is useful when\n    - commits from `topic` were previously merged or cherry-picked into `main` and\n    - need to be undone.\n    + \n    ++--ref=<branch>::\n    ++\tOverride which reference is updated with the result of the replay.\n    ++\tWhen used with `--onto`, the `<revision-range>` should have a\n    ++\tsingle tip and only the specified reference is updated instead of\n    ++\tinferring refs from the revision range.\n    ++\tWhen used with `--advance` or `--revert`, the specified reference is\n    ++\tupdated instead of the branch given to those options. This option is\n    ++\tincompatible with `--contained`.\n    ++\n    + --ref-action[=<mode>]::\n    + \tControl how references are updated. The mode can be:\n    + +\n    +@@ Documentation/git-replay.adoc: NOTE: For reverting an entire merge request as a single commit (rather than\n    + commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n    + which can avoid unnecessary merge conflicts.\n      \n     +To replay onto a specific commit while updating a different reference:\n     +\n     +------------\n    -+$ git replay --onto 112233 --ref refs/heads/mybranch aabbcc..ddeeff\n    ++$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff\n     +------------\n     +\n     +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates\n    -+`refs/heads/mybranch` to point at the result. This is useful when you want\n    ++`refs/heads/mybranch` to point at the result. This can be useful when you want\n     +to use bare commit IDs instead of branch names.\n     +\n    - NOTE: For reverting an entire merge request as a single commit (rather than\n    - commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n    - which can avoid unnecessary merge conflicts.\n    + GIT\n    + ---\n    + Part of the linkgit:git[1] suite\n     \n      ## builtin/replay.c ##\n     @@ builtin/replay.c: int cmd_replay(int argc,\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\tN_(\"(EXPERIMENTAL!) git replay \"\n      \t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n     -\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n    -+\t\t   \"[--ref=<branch>] [--ref-action=<mode>]\\n\"\n    -+\t\t   \"<revision-range>\"),\n    ++\t\t   \"[--ref=<branch>] [--ref-action=<mode>] <revision-range>\"),\n      \t\tNULL\n      \t};\n      \tstruct option replay_options[] = {\n     @@ builtin/replay.c: int cmd_replay(int argc,\n    + \t\t\t     N_(\"branch\"),\n    + \t\t\t     N_(\"revert commits onto given branch\"),\n      \t\t\t     PARSE_OPT_NONEG),\n    - \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n    - \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n     +\t\tOPT_STRING_F(0, \"ref\", &opts.ref,\n     +\t\t\t     N_(\"branch\"),\n     +\t\t\t     N_(\"reference to update with result\"),\n    @@ replay.c: int replay_revisions(struct rev_info *revs,\n     +\tif (opts->ref) {\n     +\t\tstruct object_id oid;\n     +\n    ++\t\tif (update_refs && strset_get_size(update_refs) > 1) {\n    ++\t\t\tret = error(_(\"'--ref' cannot be used with multiple revision ranges\"));\n    ++\t\t\tgoto out;\n    ++\t\t}\n     +\t\tif (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, \"refs/\")) {\n     +\t\t\tret = error(_(\"'%s' is not a valid refname\"), opts->ref);\n     +\t\t\tgoto out;\n    @@ replay.h: struct replay_revisions_options {\n      \tconst char *onto;\n      \n     +\t/*\n    -+\t * Reference to update with the result of the replay. This stops replay\n    -+\t * from taking refs from `onto`, `advance`, and `revert`. Ignores\n    ++\t * Reference to update with the result of the replay. This will not\n    ++\t * update any refs from `onto`, `advance`, or `revert`. Ignores\n     +\t * `contained`.\n     +\t */\n     +\tconst char *ref;\n    @@ t/t3650-replay-basics.sh: test_expect_success 'git replay --revert incompatible\n     +\ttest_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&\n     +\ttest_grep \"is not a valid refname\" err\n     +'\n    ++\n    ++test_expect_success '--onto with --ref rejects multiple revision ranges' '\n    ++\ttest_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err &&\n    ++\ttest_grep \"cannot be used with multiple revision ranges\" err\n    ++'\n     +\n      test_done\n\n\n---\nbase-commit: ba5c0d03d39a8b1798c8f988bdc95f9a08f10fac\nchange-id: 20260323-toon-replay-arbitrary-ref-5a81f5f976c7\n\n"},{"id":"539951","messageId":"20260325-toon-replay-arbitrary-ref-v2-1-553038702c9c@iotcl.com","threadId":"65339","inReplyTo":"20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com","subject":"[PATCH v2 1/3] builtin/replay: mark options as not negatable","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-25T15:59:29Z","receivedAt":"2026-03-25T16:00:06Z","isPatch":true,"body":"The options '--onto', '--advance', '--revert', and '--ref-action' of\ngit-replay(1) are not negatable. Mark them as such using\nPARSE_OPT_NONEG.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/replay.c | 28 ++++++++++++++++------------\n 1 file changed, 16 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex fe69f6f8ce..1a04f33390 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -88,20 +88,24 @@ int cmd_replay(int argc,\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n-\t\tOPT_STRING(0, \"advance\", &opts.advance,\n-\t\t\t   N_(\"branch\"),\n-\t\t\t   N_(\"make replay advance given branch\")),\n-\t\tOPT_STRING(0, \"onto\", &opts.onto,\n-\t\t\t   N_(\"revision\"),\n-\t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n-\t\tOPT_STRING(0, \"revert\", &opts.revert,\n-\t\t\t   N_(\"branch\"),\n-\t\t\t   N_(\"revert commits onto given branch\")),\n-\t\tOPT_STRING(0, \"ref-action\", &ref_action,\n-\t\t\t   N_(\"mode\"),\n-\t\t\t   N_(\"control ref update behavior (update|print)\")),\n+\t\tOPT_STRING_F(0, \"onto\", &opts.onto,\n+\t\t\t     N_(\"revision\"),\n+\t\t\t     N_(\"replay onto given commit\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"advance\", &opts.advance,\n+\t\t\t     N_(\"branch\"),\n+\t\t\t     N_(\"make replay advance given branch\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"revert\", &opts.revert,\n+\t\t\t     N_(\"branch\"),\n+\t\t\t     N_(\"revert commits onto given branch\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"ref-action\", &ref_action,\n+\t\t\t     N_(\"mode\"),\n+\t\t\t     N_(\"control ref update behavior (update|print)\"),\n+\t\t\t     PARSE_OPT_NONEG),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.53.0.310.g728cabbaf7\n\n"},{"id":"539952","messageId":"20260325-toon-replay-arbitrary-ref-v2-2-553038702c9c@iotcl.com","threadId":"65339","inReplyTo":"20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com","subject":"[PATCH v2 2/3] replay: use stuck form in documentation and help message","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-25T15:59:30Z","receivedAt":"2026-03-25T16:00:09Z","isPatch":true,"body":"gitcli(7) suggests to use stuck form. Change the documentation strings\nto use this form.\n\nWhile at it, reorder them to match the order in the docs.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-replay.adoc | 23 ++++++++++++-----------\n builtin/replay.c              |  4 ++--\n 2 files changed, 14 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 6698cfc047..7e749a0477 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,8 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n+\t\t\t     [--ref-action=<mode>] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -26,7 +27,7 @@ THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n OPTIONS\n -------\n \n---onto <newbase>::\n+--onto=<newbase>::\n \tStarting point at which to create the new commits.  May be any\n \tvalid commit, and not just an existing branch name.\n +\n@@ -34,7 +35,7 @@ When `--onto` is specified, the branch(es) in the revision range will be\n updated to point at the new commits, similar to the way `git rebase --update-refs`\n updates multiple branches in the affected range.\n \n---advance <branch>::\n+--advance=<branch>::\n \tStarting point at which to create the new commits; must be a\n \tbranch name.\n +\n@@ -42,7 +43,7 @@ The history is replayed on top of the <branch> and <branch> is updated to\n point at the tip of the resulting history. This is different from `--onto`,\n which uses the target only as a starting point without updating it.\n \n---revert <branch>::\n+--revert=<branch>::\n \tStarting point at which to create the reverted commits; must be a\n \tbranch name.\n +\n@@ -79,7 +80,7 @@ The default mode can be configured via the `replay.refAction` configuration vari\n \n <revision-range>::\n \tRange of commits to replay; see \"Specifying Ranges\" in\n-\tlinkgit:git-rev-parse[1]. In `--advance <branch>` mode, the\n+\tlinkgit:git-rev-parse[1]. In `--advance=<branch>` mode, the\n \trange should have a single tip, so that it's clear to which tip the\n \tadvanced <branch> should point. Any commits in the range whose\n \tchanges are already present in the branch the commits are being\n@@ -126,7 +127,7 @@ EXAMPLES\n To simply rebase `mybranch` onto `target`:\n \n ------------\n-$ git replay --onto target origin/main..mybranch\n+$ git replay --onto=target origin/main..mybranch\n ------------\n \n The refs are updated atomically and no output is produced on success.\n@@ -134,14 +135,14 @@ The refs are updated atomically and no output is produced on success.\n To see what would be updated without actually updating:\n \n ------------\n-$ git replay --ref-action=print --onto target origin/main..mybranch\n+$ git replay --ref-action=print --onto=target origin/main..mybranch\n update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n ------------\n \n To cherry-pick the commits from mybranch onto target:\n \n ------------\n-$ git replay --advance target origin/main..mybranch\n+$ git replay --advance=target origin/main..mybranch\n ------------\n \n Note that the first two examples replay the exact same commits and on\n@@ -153,7 +154,7 @@ What if you have a stack of branches, one depending upon another, and\n you'd really like to rebase the whole set?\n \n ------------\n-$ git replay --contained --onto origin/main origin/main..tipbranch\n+$ git replay --contained --onto=origin/main origin/main..tipbranch\n ------------\n \n All three branches (`branch1`, `branch2`, and `tipbranch`) are updated\n@@ -164,7 +165,7 @@ commits to replay using the syntax `A..B`; any range expression will\n do:\n \n ------------\n-$ git replay --onto origin/main ^base branch1 branch2 branch3\n+$ git replay --onto=origin/main ^base branch1 branch2 branch3\n ------------\n \n This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\n@@ -175,7 +176,7 @@ that they have in common, but that does not need to be the case.\n To revert commits on a branch:\n \n ------------\n-$ git replay --revert main topic~2..topic\n+$ git replay --revert=main topic~2..topic\n ------------\n \n This reverts the last two commits from `topic`, creating revert commits on\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 1a04f33390..a5f81b67d4 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -83,8 +83,8 @@ int cmd_replay(int argc,\n \n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n-\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n+\t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n+\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n\n-- \n2.53.0.310.g728cabbaf7\n\n"},{"id":"539953","messageId":"20260325-toon-replay-arbitrary-ref-v2-3-553038702c9c@iotcl.com","threadId":"65339","inReplyTo":"20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com","subject":"[PATCH v2 3/3] replay: allow to specify a ref with option --ref","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-25T15:59:31Z","receivedAt":"2026-03-25T16:00:12Z","isPatch":true,"body":"When option '--onto' is passed to git-replay(1), the command will update\nrefs from the <revision-range> passed to the command. When using option\n'--advance' or '--revert', the argument of that option is a ref that\nwill be updated.\n\nTo enable users to specify which ref to update, add option '--ref'. When\nusing option '--ref', the refs described above are left untouched and\ninstead the argument of this option is updated instead.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-replay.adoc | 21 +++++++++++++-\n builtin/replay.c              |  8 +++++-\n replay.c                      | 33 +++++++++++++++++-----\n replay.h                      |  7 +++++\n t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 126 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 7e749a0477..5952ecb50d 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -10,7 +10,7 @@ SYNOPSIS\n --------\n [verse]\n (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n-\t\t\t     [--ref-action=<mode>] <revision-range>\n+\t\t\t     [--ref=<branch>] [--ref-action=<mode>] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -66,6 +66,15 @@ incompatible with `--contained` (which is a modifier for `--onto` only).\n \tUpdate all branches that point at commits in\n \t<revision-range>. Requires `--onto`.\n \n+--ref=<branch>::\n+\tOverride which reference is updated with the result of the replay.\n+\tWhen used with `--onto`, the `<revision-range>` should have a\n+\tsingle tip and only the specified reference is updated instead of\n+\tinferring refs from the revision range.\n+\tWhen used with `--advance` or `--revert`, the specified reference is\n+\tupdated instead of the branch given to those options. This option is\n+\tincompatible with `--contained`.\n+\n --ref-action[=<mode>]::\n \tControl how references are updated. The mode can be:\n +\n@@ -188,6 +197,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than\n commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n which can avoid unnecessary merge conflicts.\n \n+To replay onto a specific commit while updating a different reference:\n+\n+------------\n+$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff\n+------------\n+\n+This replays the range `aabbcc..ddeeff` onto commit `112233` and updates\n+`refs/heads/mybranch` to point at the result. This can be useful when you want\n+to use bare commit IDs instead of branch names.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex a5f81b67d4..876026549e 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -84,7 +84,7 @@ int cmd_replay(int argc,\n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n-\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n+\t\t   \"[--ref=<branch>] [--ref-action=<mode>] <revision-range>\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -102,6 +102,10 @@ int cmd_replay(int argc,\n \t\t\t     N_(\"branch\"),\n \t\t\t     N_(\"revert commits onto given branch\"),\n \t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"ref\", &opts.ref,\n+\t\t\t     N_(\"branch\"),\n+\t\t\t     N_(\"reference to update with result\"),\n+\t\t\t     PARSE_OPT_NONEG),\n \t\tOPT_STRING_F(0, \"ref-action\", &ref_action,\n \t\t\t     N_(\"mode\"),\n \t\t\t     N_(\"control ref update behavior (update|print)\"),\n@@ -121,6 +125,8 @@ int cmd_replay(int argc,\n \tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n \t\t\t\t  !!opts.advance, \"--advance\",\n \t\t\t\t  !!opts.revert, \"--revert\");\n+\tdie_for_incompatible_opt2(!!opts.ref, \"--ref\",\n+\t\t\t\t  !!opts.contained, \"--contained\");\n \tif (opts.contained && !opts.onto)\n \t\tdie(_(\"--contained requires --onto\"));\n \ndiff --git a/replay.c b/replay.c\nindex 199066f6b3..63cec56d48 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -348,6 +348,8 @@ int replay_revisions(struct rev_info *revs,\n \tbool detached_head;\n \tchar *advance;\n \tchar *revert;\n+\tconst char *ref;\n+\tstruct object_id old_oid;\n \tenum replay_mode mode = REPLAY_MODE_PICK;\n \tint ret;\n \n@@ -358,6 +360,27 @@ int replay_revisions(struct rev_info *revs,\n \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n \t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n \n+\tif (opts->ref) {\n+\t\tstruct object_id oid;\n+\n+\t\tif (update_refs && strset_get_size(update_refs) > 1) {\n+\t\t\tret = error(_(\"'--ref' cannot be used with multiple revision ranges\"));\n+\t\t\tgoto out;\n+\t\t}\n+\t\tif (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, \"refs/\")) {\n+\t\t\tret = error(_(\"'%s' is not a valid refname\"), opts->ref);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tref = opts->ref;\n+\t\tif (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))\n+\t\t\toidcpy(&old_oid, &oid);\n+\t\telse\n+\t\t\toidclr(&old_oid, revs->repo->hash_algo);\n+\t} else {\n+\t\tref = advance ? advance : revert;\n+\t\toidcpy(&old_oid, &onto->object.oid);\n+\t}\n+\n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n \tif (prepare_revision_walk(revs) < 0) {\n@@ -393,7 +416,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance || revert)\n+\t\tif (ref)\n \t\t\tcontinue;\n \n \t\tfor (decoration = get_name_decoration(&commit->object);\n@@ -427,13 +450,9 @@ int replay_revisions(struct rev_info *revs,\n \t\tgoto out;\n \t}\n \n-\t/* In --advance or --revert mode, update the target ref */\n-\tif (advance || revert) {\n-\t\tconst char *ref = advance ? advance : revert;\n-\t\treplay_result_queue_update(out, ref,\n-\t\t\t\t\t   &onto->object.oid,\n+\tif (ref)\n+\t\treplay_result_queue_update(out, ref, &old_oid,\n \t\t\t\t\t   &last_commit->object.oid);\n-\t}\n \n \tret = 0;\n \ndiff --git a/replay.h b/replay.h\nindex e916a5f975..0ab74b9805 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -24,6 +24,13 @@ struct replay_revisions_options {\n \t */\n \tconst char *onto;\n \n+\t/*\n+\t * Reference to update with the result of the replay. This will not\n+\t * update any refs from `onto`, `advance`, or `revert`. Ignores\n+\t * `contained`.\n+\t */\n+\tconst char *ref;\n+\n \t/*\n \t * Starting point at which to create revert commits; must be a branch\n \t * name. The branch will be updated to point to the revert commits.\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 0c1e03e0fb..938be64770 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -500,4 +500,70 @@ test_expect_success 'git replay --revert incompatible with --advance' '\n \ttest_grep \"cannot be used together\" error\n '\n \n+test_expect_success 'using --onto with --ref' '\n+\tgit branch test-ref-onto topic2 &&\n+\ttest_when_finished \"git branch -D test-ref-onto\" &&\n+\n+\tgit replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\ttest_grep \"^update refs/heads/test-ref-onto \" result &&\n+\n+\tgit log --format=%s $(cut -f 3 -d \" \" result) >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using --advance with --ref' '\n+\tgit branch test-ref-advance main &&\n+\tgit branch test-ref-target main &&\n+\ttest_when_finished \"git branch -D test-ref-advance test-ref-target\" &&\n+\n+\tgit replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\ttest_grep \"^update refs/heads/test-ref-target \" result\n+'\n+\n+test_expect_success 'using --revert with --ref' '\n+\tgit branch test-ref-revert topic4 &&\n+\tgit branch test-ref-revert-target topic4 &&\n+\ttest_when_finished \"git branch -D test-ref-revert test-ref-revert-target\" &&\n+\n+\tgit replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\ttest_grep \"^update refs/heads/test-ref-revert-target \" result\n+'\n+\n+test_expect_success '--ref is incompatible with --contained' '\n+\ttest_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err &&\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n+test_expect_success '--ref with nonexistent fully-qualified ref' '\n+\ttest_when_finished \"git update-ref -d refs/heads/new-branch\" &&\n+\n+\tgit replay --onto=main --ref=refs/heads/new-branch topic1..topic2 &&\n+\n+\tgit log --format=%s -2 new-branch >actual &&\n+\ttest_write_lines E D >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--ref must be a valid refname' '\n+\ttest_must_fail git replay --onto=main --ref=\"refs/heads/bad..ref\" topic1..topic2 2>err &&\n+\ttest_grep \"is not a valid refname\" err\n+'\n+\n+test_expect_success '--ref requires fully qualified ref' '\n+\ttest_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&\n+\ttest_grep \"is not a valid refname\" err\n+'\n+\n+test_expect_success '--onto with --ref rejects multiple revision ranges' '\n+\ttest_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err &&\n+\ttest_grep \"cannot be used with multiple revision ranges\" err\n+'\n+\n test_done\n\n-- \n2.53.0.310.g728cabbaf7\n\n"},{"id":"539977","messageId":"xmqq8qbfvjv7.fsf@gitster.g","threadId":"65339","inReplyTo":"20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com","subject":"Re: [PATCH v2 0/3] Add option --ref to git-replay(1)","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-25T18:34:20Z","receivedAt":"2026-03-25T18:34:23Z","isPatch":true,"body":"Toon Claes <toon@iotcl.com> writes:\n\n> In a previous RFC[1] I suggested to implement subcommands into\n> git-replay(1). While it would be arguable nice to have subcommands for\n> the different modes, because git-replay(1) is a plumbing commands, it's\n> fine to keep a status quo.\n>\n> This series takes one thing for that RFC though: adding option --ref.\n> This new option is useful if you want to have full control over which\n> ref is being updated, and not want to rely on the refs that are using\n> in the <revision-range> or as the value for --advance and --revert.\n>\n> These changes answer the needs expressed in[2].\n>\n> This series is based on Siddharth's series[3] to add '--revert' to\n> git-replay(1) (sa/replay-revert @ ba5c0d03d3).\n\nWhich unfortunately got updated just 20 hours ago X-<.\n\nWill try to wriggle it in, but please double check when I push the\nintegration result of today.\n\n"},{"id":"539980","messageId":"xmqqwlyzu4ua.fsf@gitster.g","threadId":"65339","inReplyTo":"20260325-toon-replay-arbitrary-ref-v2-3-553038702c9c@iotcl.com","subject":"Re: [PATCH v2 3/3] replay: allow to specify a ref with option --ref","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-25T18:44:13Z","receivedAt":"2026-03-25T18:44:15Z","isPatch":true,"body":"Toon Claes <toon@iotcl.com> writes:\n\n> When option '--onto' is passed to git-replay(1), the command will update\n> refs from the <revision-range> passed to the command. When using option\n> '--advance' or '--revert', the argument of that option is a ref that\n> will be updated.\n>\n> To enable users to specify which ref to update, add option '--ref'. When\n> using option '--ref', the refs described above are left untouched and\n> instead the argument of this option is updated instead.\n>\n> Signed-off-by: Toon Claes <toon@iotcl.com>\n> ---\n>  Documentation/git-replay.adoc | 21 +++++++++++++-\n>  builtin/replay.c              |  8 +++++-\n>  replay.c                      | 33 +++++++++++++++++-----\n>  replay.h                      |  7 +++++\n>  t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++\n>  5 files changed, 126 insertions(+), 9 deletions(-)\n>\n> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\n> index 7e749a0477..5952ecb50d 100644\n> --- a/Documentation/git-replay.adoc\n> +++ b/Documentation/git-replay.adoc\n> @@ -10,7 +10,7 @@ SYNOPSIS\n>  --------\n>  [verse]\n>  (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n> -\t\t\t     [--ref-action=<mode>] <revision-range>\n> +\t\t\t     [--ref=<branch>] [--ref-action=<mode>] <revision-range>\n>  \n>  DESCRIPTION\n>  -----------\n> @@ -66,6 +66,15 @@ incompatible with `--contained` (which is a modifier for `--onto` only).\n>  \tUpdate all branches that point at commits in\n>  \t<revision-range>. Requires `--onto`.\n>  \n> +--ref=<branch>::\n\nAs this thing takes a full refname (e.g., \"--ref=refsheads/mybranch\"\nin the example in hunk ll.197,+16), we probably want\n\n    --ref=<ref>::\n\ninstead.\n\nIn the modern documentation style, this should be `--ref=<ref>`::\nbut let's consistently use traditional style and leave the clean-up\nuntil the dust settles and when the command becomes more quiescent.\n\n> @@ -188,6 +197,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than\n>  commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n>  which can avoid unnecessary merge conflicts.\n>  \n> +To replay onto a specific commit while updating a different reference:\n> +\n> +------------\n> +$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff\n> +------------\n> +\n> +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates\n> +`refs/heads/mybranch` to point at the result. This can be useful when you want\n> +to use bare commit IDs instead of branch names.\n> +\n>  GIT\n>  ---\n>  Part of the linkgit:git[1] suite\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index a5f81b67d4..876026549e 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -84,7 +84,7 @@ int cmd_replay(int argc,\n>  \tconst char *const replay_usage[] = {\n>  \t\tN_(\"(EXPERIMENTAL!) git replay \"\n>  \t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n> -\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n> +\t\t   \"[--ref=<branch>] [--ref-action=<mode>] <revision-range>\"),\n\nDitto.\n\n>  \t\tNULL\n>  \t};\n>  \tstruct option replay_options[] = {\n> @@ -102,6 +102,10 @@ int cmd_replay(int argc,\n>  \t\t\t     N_(\"branch\"),\n>  \t\t\t     N_(\"revert commits onto given branch\"),\n>  \t\t\t     PARSE_OPT_NONEG),\n> +\t\tOPT_STRING_F(0, \"ref\", &opts.ref,\n> +\t\t\t     N_(\"branch\"),\n> +\t\t\t     N_(\"reference to update with result\"),\n> +\t\t\t     PARSE_OPT_NONEG),\n\nDitto.\n\n> diff --git a/replay.c b/replay.c\n> index 199066f6b3..63cec56d48 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -348,6 +348,8 @@ int replay_revisions(struct rev_info *revs,\n>  \tbool detached_head;\n>  \tchar *advance;\n>  \tchar *revert;\n> +\tconst char *ref;\n> +\tstruct object_id old_oid;\n>  \tenum replay_mode mode = REPLAY_MODE_PICK;\n>  \tint ret;\n>  \n> @@ -358,6 +360,27 @@ int replay_revisions(struct rev_info *revs,\n>  \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n>  \t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n>  \n> +\tif (opts->ref) {\n> +\t\tstruct object_id oid;\n> +\n> +\t\tif (update_refs && strset_get_size(update_refs) > 1) {\n> +\t\t\tret = error(_(\"'--ref' cannot be used with multiple revision ranges\"));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tif (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, \"refs/\")) {\n\nCan we do something about this overly long line?\n\n> +\t\t\tret = error(_(\"'%s' is not a valid refname\"), opts->ref);\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tref = opts->ref;\n> +\t\tif (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))\n> +\t\t\toidcpy(&old_oid, &oid);\n> +\t\telse\n> +\t\t\toidclr(&old_oid, revs->repo->hash_algo);\n> +\t} else {\n> +\t\tref = advance ? advance : revert;\n> +\t\toidcpy(&old_oid, &onto->object.oid);\n> +\t}\n> +\n>  \t/* FIXME: Should allow replaying commits with the first as a root commit */\n>  \n>  \tif (prepare_revision_walk(revs) < 0) {\n> @@ -393,7 +416,7 @@ int replay_revisions(struct rev_info *revs,\n>  \t\tkh_value(replayed_commits, pos) = last_commit;\n>  \n>  \t\t/* Update any necessary branches */\n> -\t\tif (advance || revert)\n> +\t\tif (ref)\n>  \t\t\tcontinue;\n\nNice.\n\n> @@ -427,13 +450,9 @@ int replay_revisions(struct rev_info *revs,\n>  \t\tgoto out;\n>  \t}\n>  \n> -\t/* In --advance or --revert mode, update the target ref */\n> -\tif (advance || revert) {\n> -\t\tconst char *ref = advance ? advance : revert;\n> -\t\treplay_result_queue_update(out, ref,\n> -\t\t\t\t\t   &onto->object.oid,\n> +\tif (ref)\n> +\t\treplay_result_queue_update(out, ref, &old_oid,\n>  \t\t\t\t\t   &last_commit->object.oid);\n\nNice, too.\n\n"},{"id":"540144","messageId":"xmqqjyuynv99.fsf@gitster.g","threadId":"65339","inReplyTo":"20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com","subject":"Re: [PATCH v2 0/3] Add option --ref to git-replay(1)","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-26T21:20:02Z","receivedAt":"2026-03-26T21:20:07Z","isPatch":true,"body":"Toon Claes <toon@iotcl.com> writes:\n\n> This series is based on Siddharth's series[3] to add '--revert' to\n> git-replay(1) (sa/replay-revert @ ba5c0d03d3).\n\nAs sa/replay-revert has been updated, I rebased these three patches\non top of the updated version of that other topic and merged it to\n'seen', which broke CI with t3650.\n\nI naturally suspected that I made some stupid mistakes while\nrebasing, so I applied these three patches directly on top of that\nold sa/replay-revert.  Unfortunately, the same test t3650 fails\nexactly the same way with merge-ort aborting.\n\nThe failing test run fails like so:\n\nok 45 - --ref with nonexistent fully-qualified ref\n\nexpecting success of 3650.46 '--ref must be a valid refname':\n        test_must_fail git replay --onto=main --ref=\"refs/heads/bad..ref\" topic1..topic2 2>err &&\n        test_grep \"is not a valid refname\" err\n\n/home/gitster/w/git.git/t/test-lib-functions.sh: line 1180: 2840466 Aborted                    (core dumped) \"$@\" 2>&7\ntest_must_fail: died by signal 6: git replay --onto=main --ref=refs/heads/bad..ref topic1..topic2\nnot ok 46 - --ref must be a valid refname\n#\n#               test_must_fail git replay --onto=main --ref=\"refs/heads/bad..ref\" topic1..topic2 2>err &&\n#               test_grep \"is not a valid refname\" err\n#\n1\n"},{"id":"540490","messageId":"87tstwl9g7.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","threadId":"65339","inReplyTo":"xmqqjyuynv99.fsf@gitster.g","subject":"Re: [PATCH v2 0/3] Add option --ref to git-replay(1)","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-31T07:55:20Z","receivedAt":"2026-03-31T07:55:27Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> As sa/replay-revert has been updated, I rebased these three patches\n> on top of the updated version of that other topic and merged it to\n> 'seen', which broke CI with t3650.\n>\n> I naturally suspected that I made some stupid mistakes while\n> rebasing, so I applied these three patches directly on top of that\n> old sa/replay-revert.  Unfortunately, the same test t3650 fails\n> exactly the same way with merge-ort aborting.\n>\n> The failing test run fails like so:\n>\n> ok 45 - --ref with nonexistent fully-qualified ref\n>\n> expecting success of 3650.46 '--ref must be a valid refname':\n>         test_must_fail git replay --onto=main --ref=\"refs/heads/bad..ref\" topic1..topic2 2>err &&\n>         test_grep \"is not a valid refname\" err\n>\n> /home/gitster/w/git.git/t/test-lib-functions.sh: line 1180: 2840466 Aborted                    (core dumped) \"$@\" 2>&7\n> test_must_fail: died by signal 6: git replay --onto=main --ref=refs/heads/bad..ref topic1..topic2\n> not ok 46 - --ref must be a valid refname\n> #\n> #               test_must_fail git replay --onto=main --ref=\"refs/heads/bad..ref\" topic1..topic2 2>err &&\n> #               test_grep \"is not a valid refname\" err\n> #\n\nNo need for you to worry about those conflicts. I'm happy to do the\nrebase myself.\n\n-- \nCheers,\nToon\n"},{"id":"540491","messageId":"87qzp0l9dy.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","threadId":"65339","inReplyTo":"xmqqwlyzu4ua.fsf@gitster.g","subject":"Re: [PATCH v2 3/3] replay: allow to specify a ref with option --ref","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-03-31T07:56:41Z","receivedAt":"2026-03-31T07:56:51Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> In the modern documentation style, this should be `--ref=<ref>`::\n> but let's consistently use traditional style and leave the clean-up\n> until the dust settles and when the command becomes more quiescent.\n\nYes, I noticed various things aren't up to stadard. I'm happy to include\na patch that cleans it up. But agreed, the dust needs to settle first.\n\n\n-- \nCheers,\nToon\n"},{"id":"540563","messageId":"xmqqfr5fy8ui.fsf@gitster.g","threadId":"65339","inReplyTo":"87tstwl9g7.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set","subject":"Re: [PATCH v2 0/3] Add option --ref to git-replay(1)","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-31T21:42:13Z","receivedAt":"2026-03-31T21:42:17Z","isPatch":true,"body":"Toon Claes <toon@iotcl.com> writes:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> As sa/replay-revert has been updated, I rebased these three patches\n>> on top of the updated version of that other topic and merged it to\n>> 'seen', which broke CI with t3650.\n>>\n>> I naturally suspected that I made some stupid mistakes while\n>> rebasing, so I applied these three patches directly on top of that\n>> old sa/replay-revert.  Unfortunately, the same test t3650 fails\n>> exactly the same way with merge-ort aborting.\n>>\n>> The failing test run fails like so:\n>>\n>> ok 45 - --ref with nonexistent fully-qualified ref\n>>\n>> expecting success of 3650.46 '--ref must be a valid refname':\n>>         test_must_fail git replay --onto=main --ref=\"refs/heads/bad..ref\" topic1..topic2 2>err &&\n>>         test_grep \"is not a valid refname\" err\n>>\n>> /home/gitster/w/git.git/t/test-lib-functions.sh: line 1180: 2840466 Aborted                    (core dumped) \"$@\" 2>&7\n>> test_must_fail: died by signal 6: git replay --onto=main --ref=refs/heads/bad..ref topic1..topic2\n>> not ok 46 - --ref must be a valid refname\n>> #\n>> #               test_must_fail git replay --onto=main --ref=\"refs/heads/bad..ref\" topic1..topic2 2>err &&\n>> #               test_grep \"is not a valid refname\" err\n>> #\n>\n> No need for you to worry about those conflicts. I'm happy to do the\n> rebase myself.\n\nWill wait for updates.  Thanks.\n\nI said I suspected that my rebasing introduced a bug, but what I was\nreporting in the message you are responding to is that the patches\nwithout my rebases are breaking tests.  So perhaps you do not have\nto rebase and there won't be any problematic conflicts, but the\npatches do need to be updated to address the test failure.\n"},{"id":"540669","messageId":"20260401-toon-replay-arbitrary-ref-v3-0-a0b4fabb34b3@iotcl.com","threadId":"65339","inReplyTo":"20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com","subject":"[PATCH v3 0/3] Add option --ref to git-replay(1)","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-04-01T20:55:09Z","receivedAt":"2026-04-01T20:55:18Z","isPatch":true,"body":"In a previous RFC[1] I suggested to implement subcommands into\ngit-replay(1). While it would be arguable nice to have subcommands for\nthe different modes, because git-replay(1) is a plumbing commands, it's\nfine to keep a status quo.\n\nThis series takes one thing for that RFC though: adding option --ref.\nThis new option is useful if you want to have full control over which\nref is being updated, and not want to rely on the refs that are using\nin the <revision-range> or as the value for --advance and --revert.\n\nThese changes answer the needs expressed in[2].\n\nThis series is based on Siddharth's series[3] to add '--revert' to\ngit-replay(1) (sa/replay-revert @ ba5c0d03d3).\n\n[1]: https://lore.kernel.org/git/20260309-toon-replay-subcommands-v1-1-864ec82ef68a@iotcl.com/\n[2]: https://lore.kernel.org/git/CAHTeOx-SMLh_idKhGczPKzZNOKy04uYXmUhL8Z79yRuNpmE4eA@mail.gmail.com/\n[3]: https://lore.kernel.org/git/20260313054035.26605-1-siddharthasthana31@gmail.com/\n\n---\nChanges in v3:\n- In the docs, clarify --ref requires a fully qualified ref, not just a\n  branch name.\n- Fix crash by initializing merge_opt to `{ 0 }`.\n- Link to v2: https://patch.msgid.link/20260325-toon-replay-arbitrary-ref-v2-0-553038702c9c@iotcl.com\n\nChanges in v2:\n- Dropped the test-only consistency patch.\n- Separated commit to mark options as not negatable.\n- Modified git-replay(1) docs to everywhere use stuck form.\n- Added code and test ensure the revision range has a single tip when\n  both --onto and --ref are given.\n- Rephrased some comments and docs.\n- Link to v1: https://patch.msgid.link/20260323-toon-replay-arbitrary-ref-v1-0-5c7172f675ec@iotcl.com\n\n---\nToon Claes (3):\n      builtin/replay: mark options as not negatable\n      replay: use stuck form in documentation and help message\n      replay: allow to specify a ref with option --ref\n\n Documentation/git-replay.adoc | 45 +++++++++++++++++++++--------\n builtin/replay.c              | 38 ++++++++++++++++---------\n replay.c                      | 35 +++++++++++++++++------\n replay.h                      |  7 +++++\n t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 157 insertions(+), 34 deletions(-)\n\nRange-diff versus v2:\n\n1:  260dff6c82 = 1:  dd46a0efd5 builtin/replay: mark options as not negatable\n2:  eca4dffa18 ! 2:  b290493bd0 replay: use stuck form in documentation and help message\n    @@ Documentation/git-replay.adoc: git-replay - EXPERIMENTAL: Replay commits on a ne\n      SYNOPSIS\n      --------\n      [verse]\n    --(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>...\n    +-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>\n     +(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n     +\t\t\t     [--ref-action=<mode>] <revision-range>\n      \n    @@ Documentation/git-replay.adoc: The default mode can be configured via the `repla\n      \n      <revision-range>::\n      \tRange of commits to replay; see \"Specifying Ranges\" in\n    --\tlinkgit:git-rev-parse[1]. In `--advance <branch>` mode, the\n    -+\tlinkgit:git-rev-parse[1]. In `--advance=<branch>` mode, the\n    - \trange should have a single tip, so that it's clear to which tip the\n    - \tadvanced <branch> should point. Any commits in the range whose\n    - \tchanges are already present in the branch the commits are being\n    +-\tlinkgit:git-rev-parse[1]. In `--advance <branch>` or\n    +-\t`--revert <branch>` mode, the range should have a single tip,\n    ++\tlinkgit:git-rev-parse[1]. In `--advance=<branch>` or\n    ++\t`--revert=<branch>` mode, the range should have a single tip,\n    + \tso that it's clear to which tip the advanced or reverted\n    + \t<branch> should point. Any commits in the range whose changes\n    + \tare already present in the branch the commits are being\n     @@ Documentation/git-replay.adoc: EXAMPLES\n      To simply rebase `mybranch` onto `target`:\n      \n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \tconst char *const replay_usage[] = {\n      \t\tN_(\"(EXPERIMENTAL!) git replay \"\n     -\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n    --\t\t   \"[--ref-action[=<mode>]] <revision-range>...\"),\n    +-\t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n     +\t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n     +\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n      \t\tNULL\n3:  2676c2ae78 ! 3:  44e825d9e6 replay: allow to specify a ref with option --ref\n    @@ Commit message\n         using option '--ref', the refs described above are left untouched and\n         instead the argument of this option is updated instead.\n     \n    +    Because this introduces code paths in replay.c that jump to `out` before\n    +    init_basic_merge_options() is called on `merge_opt`, zero-initialize the\n    +    struct.\n    +\n         Signed-off-by: Toon Claes <toon@iotcl.com>\n     \n      ## Documentation/git-replay.adoc ##\n    @@ Documentation/git-replay.adoc: SYNOPSIS\n      [verse]\n      (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n     -\t\t\t     [--ref-action=<mode>] <revision-range>\n    -+\t\t\t     [--ref=<branch>] [--ref-action=<mode>] <revision-range>\n    ++\t\t\t     [--ref=<ref>] [--ref-action=<mode>] <revision-range>\n      \n      DESCRIPTION\n      -----------\n    @@ Documentation/git-replay.adoc: incompatible with `--contained` (which is a modif\n      \tUpdate all branches that point at commits in\n      \t<revision-range>. Requires `--onto`.\n      \n    -+--ref=<branch>::\n    ++--ref=<ref>::\n     +\tOverride which reference is updated with the result of the replay.\n    ++\tThe ref must be fully qualified.\n     +\tWhen used with `--onto`, the `<revision-range>` should have a\n     +\tsingle tip and only the specified reference is updated instead of\n     +\tinferring refs from the revision range.\n     +\tWhen used with `--advance` or `--revert`, the specified reference is\n    -+\tupdated instead of the branch given to those options. This option is\n    -+\tincompatible with `--contained`.\n    ++\tupdated instead of the branch given to those options.\n    ++\tThis option is incompatible with `--contained`.\n     +\n      --ref-action[=<mode>]::\n      \tControl how references are updated. The mode can be:\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\tN_(\"(EXPERIMENTAL!) git replay \"\n      \t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n     -\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n    -+\t\t   \"[--ref=<branch>] [--ref-action=<mode>] <revision-range>\"),\n    ++\t\t   \"[--ref=<ref>] [--ref-action=<mode>] <revision-range>\"),\n      \t\tNULL\n      \t};\n      \tstruct option replay_options[] = {\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\t\t     N_(\"mode\"),\n      \t\t\t     N_(\"control ref update behavior (update|print)\"),\n     @@ builtin/replay.c: int cmd_replay(int argc,\n    - \tdie_for_incompatible_opt3(!!opts.onto, \"--onto\",\n    - \t\t\t\t  !!opts.advance, \"--advance\",\n    - \t\t\t\t  !!opts.revert, \"--revert\");\n    + \t\t\t\t  opts.contained, \"--contained\");\n    + \tdie_for_incompatible_opt2(!!opts.revert, \"--revert\",\n    + \t\t\t\t  opts.contained, \"--contained\");\n     +\tdie_for_incompatible_opt2(!!opts.ref, \"--ref\",\n     +\t\t\t\t  !!opts.contained, \"--contained\");\n    - \tif (opts.contained && !opts.onto)\n    - \t\tdie(_(\"--contained requires --onto\"));\n      \n    + \t/* Parse ref action mode from command line or config */\n    + \tref_mode = get_ref_action_mode(repo, ref_action);\n     \n      ## replay.c ##\n     @@ replay.c: int replay_revisions(struct rev_info *revs,\n    + \tstruct commit *last_commit = NULL;\n    + \tstruct commit *commit;\n    + \tstruct commit *onto = NULL;\n    +-\tstruct merge_options merge_opt;\n    ++\tstruct merge_options merge_opt = { 0 };\n    + \tstruct merge_result result = {\n    + \t\t.clean = 1,\n    + \t};\n      \tbool detached_head;\n      \tchar *advance;\n      \tchar *revert;\n\n\n---\nbase-commit: 2760ee49834953c0860fa5d7983a6af4d27cb6a9\nchange-id: 20260323-toon-replay-arbitrary-ref-5a81f5f976c7\n\n"},{"id":"540670","messageId":"20260401-toon-replay-arbitrary-ref-v3-1-a0b4fabb34b3@iotcl.com","threadId":"65339","inReplyTo":"20260401-toon-replay-arbitrary-ref-v3-0-a0b4fabb34b3@iotcl.com","subject":"[PATCH v3 1/3] builtin/replay: mark options as not negatable","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-04-01T20:55:10Z","receivedAt":"2026-04-01T20:55:25Z","isPatch":true,"body":"The options '--onto', '--advance', '--revert', and '--ref-action' of\ngit-replay(1) are not negatable. Mark them as such using\nPARSE_OPT_NONEG.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n builtin/replay.c | 28 ++++++++++++++++------------\n 1 file changed, 16 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex a0879b020f..85aa9fa0a4 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -89,20 +89,24 @@ int cmd_replay(int argc,\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n-\t\tOPT_STRING(0, \"advance\", &opts.advance,\n-\t\t\t   N_(\"branch\"),\n-\t\t\t   N_(\"make replay advance given branch\")),\n-\t\tOPT_STRING(0, \"onto\", &opts.onto,\n-\t\t\t   N_(\"revision\"),\n-\t\t\t   N_(\"replay onto given commit\")),\n \t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n-\t\tOPT_STRING(0, \"revert\", &opts.revert,\n-\t\t\t   N_(\"branch\"),\n-\t\t\t   N_(\"revert commits onto given branch\")),\n-\t\tOPT_STRING(0, \"ref-action\", &ref_action,\n-\t\t\t   N_(\"mode\"),\n-\t\t\t   N_(\"control ref update behavior (update|print)\")),\n+\t\tOPT_STRING_F(0, \"onto\", &opts.onto,\n+\t\t\t     N_(\"revision\"),\n+\t\t\t     N_(\"replay onto given commit\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"advance\", &opts.advance,\n+\t\t\t     N_(\"branch\"),\n+\t\t\t     N_(\"make replay advance given branch\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"revert\", &opts.revert,\n+\t\t\t     N_(\"branch\"),\n+\t\t\t     N_(\"revert commits onto given branch\"),\n+\t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"ref-action\", &ref_action,\n+\t\t\t     N_(\"mode\"),\n+\t\t\t     N_(\"control ref update behavior (update|print)\"),\n+\t\t\t     PARSE_OPT_NONEG),\n \t\tOPT_END()\n \t};\n \n\n-- \n2.53.0.310.g728cabbaf7\n\n"},{"id":"540671","messageId":"20260401-toon-replay-arbitrary-ref-v3-2-a0b4fabb34b3@iotcl.com","threadId":"65339","inReplyTo":"20260401-toon-replay-arbitrary-ref-v3-0-a0b4fabb34b3@iotcl.com","subject":"[PATCH v3 2/3] replay: use stuck form in documentation and help message","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-04-01T20:55:11Z","receivedAt":"2026-04-01T20:55:30Z","isPatch":true,"body":"gitcli(7) suggests to use stuck form. Change the documentation strings\nto use this form.\n\nWhile at it, reorder them to match the order in the docs.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-replay.adoc | 25 +++++++++++++------------\n builtin/replay.c              |  4 ++--\n 2 files changed, 15 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 997097e420..5bb478c281 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -9,7 +9,8 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t\n SYNOPSIS\n --------\n [verse]\n-(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>\n+(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n+\t\t\t     [--ref-action=<mode>] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -26,7 +27,7 @@ THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n OPTIONS\n -------\n \n---onto <newbase>::\n+--onto=<newbase>::\n \tStarting point at which to create the new commits.  May be any\n \tvalid commit, and not just an existing branch name.\n +\n@@ -34,7 +35,7 @@ When `--onto` is specified, the branch(es) in the revision range will be\n updated to point at the new commits, similar to the way `git rebase --update-refs`\n updates multiple branches in the affected range.\n \n---advance <branch>::\n+--advance=<branch>::\n \tStarting point at which to create the new commits; must be a\n \tbranch name.\n +\n@@ -42,7 +43,7 @@ The history is replayed on top of the <branch> and <branch> is updated to\n point at the tip of the resulting history. This is different from `--onto`,\n which uses the target only as a starting point without updating it.\n \n---revert <branch>::\n+--revert=<branch>::\n \tStarting point at which to create the reverted commits; must be a\n \tbranch name.\n +\n@@ -79,8 +80,8 @@ The default mode can be configured via the `replay.refAction` configuration vari\n \n <revision-range>::\n \tRange of commits to replay; see \"Specifying Ranges\" in\n-\tlinkgit:git-rev-parse[1]. In `--advance <branch>` or\n-\t`--revert <branch>` mode, the range should have a single tip,\n+\tlinkgit:git-rev-parse[1]. In `--advance=<branch>` or\n+\t`--revert=<branch>` mode, the range should have a single tip,\n \tso that it's clear to which tip the advanced or reverted\n \t<branch> should point. Any commits in the range whose changes\n \tare already present in the branch the commits are being\n@@ -127,7 +128,7 @@ EXAMPLES\n To simply rebase `mybranch` onto `target`:\n \n ------------\n-$ git replay --onto target origin/main..mybranch\n+$ git replay --onto=target origin/main..mybranch\n ------------\n \n The refs are updated atomically and no output is produced on success.\n@@ -135,14 +136,14 @@ The refs are updated atomically and no output is produced on success.\n To see what would be updated without actually updating:\n \n ------------\n-$ git replay --ref-action=print --onto target origin/main..mybranch\n+$ git replay --ref-action=print --onto=target origin/main..mybranch\n update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}\n ------------\n \n To cherry-pick the commits from mybranch onto target:\n \n ------------\n-$ git replay --advance target origin/main..mybranch\n+$ git replay --advance=target origin/main..mybranch\n ------------\n \n Note that the first two examples replay the exact same commits and on\n@@ -154,7 +155,7 @@ What if you have a stack of branches, one depending upon another, and\n you'd really like to rebase the whole set?\n \n ------------\n-$ git replay --contained --onto origin/main origin/main..tipbranch\n+$ git replay --contained --onto=origin/main origin/main..tipbranch\n ------------\n \n All three branches (`branch1`, `branch2`, and `tipbranch`) are updated\n@@ -165,7 +166,7 @@ commits to replay using the syntax `A..B`; any range expression will\n do:\n \n ------------\n-$ git replay --onto origin/main ^base branch1 branch2 branch3\n+$ git replay --onto=origin/main ^base branch1 branch2 branch3\n ------------\n \n This will simultaneously rebase `branch1`, `branch2`, and `branch3`,\n@@ -176,7 +177,7 @@ that they have in common, but that does not need to be the case.\n To revert commits on a branch:\n \n ------------\n-$ git replay --revert main topic~2..topic\n+$ git replay --revert=main topic~2..topic\n ------------\n \n This reverts the last two commits from `topic`, creating revert commits on\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 85aa9fa0a4..fbfeb780b6 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -84,8 +84,8 @@ int cmd_replay(int argc,\n \n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n-\t\t   \"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) \"\n-\t\t   \"[--ref-action[=<mode>]] <revision-range>\"),\n+\t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n+\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n\n-- \n2.53.0.310.g728cabbaf7\n\n"},{"id":"540672","messageId":"20260401-toon-replay-arbitrary-ref-v3-3-a0b4fabb34b3@iotcl.com","threadId":"65339","inReplyTo":"20260401-toon-replay-arbitrary-ref-v3-0-a0b4fabb34b3@iotcl.com","subject":"[PATCH v3 3/3] replay: allow to specify a ref with option --ref","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-04-01T20:55:12Z","receivedAt":"2026-04-01T20:55:33Z","isPatch":true,"body":"When option '--onto' is passed to git-replay(1), the command will update\nrefs from the <revision-range> passed to the command. When using option\n'--advance' or '--revert', the argument of that option is a ref that\nwill be updated.\n\nTo enable users to specify which ref to update, add option '--ref'. When\nusing option '--ref', the refs described above are left untouched and\ninstead the argument of this option is updated instead.\n\nBecause this introduces code paths in replay.c that jump to `out` before\ninit_basic_merge_options() is called on `merge_opt`, zero-initialize the\nstruct.\n\nSigned-off-by: Toon Claes <toon@iotcl.com>\n---\n Documentation/git-replay.adoc | 22 ++++++++++++++-\n builtin/replay.c              |  8 +++++-\n replay.c                      | 35 +++++++++++++++++------\n replay.h                      |  7 +++++\n t/t3650-replay-basics.sh      | 66 +++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 128 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc\nindex 5bb478c281..a32f72aead 100644\n--- a/Documentation/git-replay.adoc\n+++ b/Documentation/git-replay.adoc\n@@ -10,7 +10,7 @@ SYNOPSIS\n --------\n [verse]\n (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n-\t\t\t     [--ref-action=<mode>] <revision-range>\n+\t\t\t     [--ref=<ref>] [--ref-action=<mode>] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -66,6 +66,16 @@ incompatible with `--contained` (which is a modifier for `--onto` only).\n \tUpdate all branches that point at commits in\n \t<revision-range>. Requires `--onto`.\n \n+--ref=<ref>::\n+\tOverride which reference is updated with the result of the replay.\n+\tThe ref must be fully qualified.\n+\tWhen used with `--onto`, the `<revision-range>` should have a\n+\tsingle tip and only the specified reference is updated instead of\n+\tinferring refs from the revision range.\n+\tWhen used with `--advance` or `--revert`, the specified reference is\n+\tupdated instead of the branch given to those options.\n+\tThis option is incompatible with `--contained`.\n+\n --ref-action[=<mode>]::\n \tControl how references are updated. The mode can be:\n +\n@@ -189,6 +199,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than\n commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`\n which can avoid unnecessary merge conflicts.\n \n+To replay onto a specific commit while updating a different reference:\n+\n+------------\n+$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff\n+------------\n+\n+This replays the range `aabbcc..ddeeff` onto commit `112233` and updates\n+`refs/heads/mybranch` to point at the result. This can be useful when you want\n+to use bare commit IDs instead of branch names.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex fbfeb780b6..39e3a86f6c 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -85,7 +85,7 @@ int cmd_replay(int argc,\n \tconst char *const replay_usage[] = {\n \t\tN_(\"(EXPERIMENTAL!) git replay \"\n \t\t   \"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\\n\"\n-\t\t   \"[--ref-action=<mode>] <revision-range>\"),\n+\t\t   \"[--ref=<ref>] [--ref-action=<mode>] <revision-range>\"),\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n@@ -103,6 +103,10 @@ int cmd_replay(int argc,\n \t\t\t     N_(\"branch\"),\n \t\t\t     N_(\"revert commits onto given branch\"),\n \t\t\t     PARSE_OPT_NONEG),\n+\t\tOPT_STRING_F(0, \"ref\", &opts.ref,\n+\t\t\t     N_(\"branch\"),\n+\t\t\t     N_(\"reference to update with result\"),\n+\t\t\t     PARSE_OPT_NONEG),\n \t\tOPT_STRING_F(0, \"ref-action\", &ref_action,\n \t\t\t     N_(\"mode\"),\n \t\t\t     N_(\"control ref update behavior (update|print)\"),\n@@ -126,6 +130,8 @@ int cmd_replay(int argc,\n \t\t\t\t  opts.contained, \"--contained\");\n \tdie_for_incompatible_opt2(!!opts.revert, \"--revert\",\n \t\t\t\t  opts.contained, \"--contained\");\n+\tdie_for_incompatible_opt2(!!opts.ref, \"--ref\",\n+\t\t\t\t  !!opts.contained, \"--contained\");\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\ndiff --git a/replay.c b/replay.c\nindex 199066f6b3..444f7ceefc 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -341,13 +341,15 @@ int replay_revisions(struct rev_info *revs,\n \tstruct commit *last_commit = NULL;\n \tstruct commit *commit;\n \tstruct commit *onto = NULL;\n-\tstruct merge_options merge_opt;\n+\tstruct merge_options merge_opt = { 0 };\n \tstruct merge_result result = {\n \t\t.clean = 1,\n \t};\n \tbool detached_head;\n \tchar *advance;\n \tchar *revert;\n+\tconst char *ref;\n+\tstruct object_id old_oid;\n \tenum replay_mode mode = REPLAY_MODE_PICK;\n \tint ret;\n \n@@ -358,6 +360,27 @@ int replay_revisions(struct rev_info *revs,\n \tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n \t\t\t   &detached_head, &advance, &revert, &onto, &update_refs);\n \n+\tif (opts->ref) {\n+\t\tstruct object_id oid;\n+\n+\t\tif (update_refs && strset_get_size(update_refs) > 1) {\n+\t\t\tret = error(_(\"'--ref' cannot be used with multiple revision ranges\"));\n+\t\t\tgoto out;\n+\t\t}\n+\t\tif (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, \"refs/\")) {\n+\t\t\tret = error(_(\"'%s' is not a valid refname\"), opts->ref);\n+\t\t\tgoto out;\n+\t\t}\n+\t\tref = opts->ref;\n+\t\tif (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))\n+\t\t\toidcpy(&old_oid, &oid);\n+\t\telse\n+\t\t\toidclr(&old_oid, revs->repo->hash_algo);\n+\t} else {\n+\t\tref = advance ? advance : revert;\n+\t\toidcpy(&old_oid, &onto->object.oid);\n+\t}\n+\n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n \tif (prepare_revision_walk(revs) < 0) {\n@@ -393,7 +416,7 @@ int replay_revisions(struct rev_info *revs,\n \t\tkh_value(replayed_commits, pos) = last_commit;\n \n \t\t/* Update any necessary branches */\n-\t\tif (advance || revert)\n+\t\tif (ref)\n \t\t\tcontinue;\n \n \t\tfor (decoration = get_name_decoration(&commit->object);\n@@ -427,13 +450,9 @@ int replay_revisions(struct rev_info *revs,\n \t\tgoto out;\n \t}\n \n-\t/* In --advance or --revert mode, update the target ref */\n-\tif (advance || revert) {\n-\t\tconst char *ref = advance ? advance : revert;\n-\t\treplay_result_queue_update(out, ref,\n-\t\t\t\t\t   &onto->object.oid,\n+\tif (ref)\n+\t\treplay_result_queue_update(out, ref, &old_oid,\n \t\t\t\t\t   &last_commit->object.oid);\n-\t}\n \n \tret = 0;\n \ndiff --git a/replay.h b/replay.h\nindex e916a5f975..0ab74b9805 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -24,6 +24,13 @@ struct replay_revisions_options {\n \t */\n \tconst char *onto;\n \n+\t/*\n+\t * Reference to update with the result of the replay. This will not\n+\t * update any refs from `onto`, `advance`, or `revert`. Ignores\n+\t * `contained`.\n+\t */\n+\tconst char *ref;\n+\n \t/*\n \t * Starting point at which to create revert commits; must be a branch\n \t * name. The branch will be updated to point to the revert commits.\ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 217f6fb292..d5c7dd1bf4 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -495,4 +495,70 @@ test_expect_success 'git replay --revert incompatible with --advance' '\n \ttest_grep \"cannot be used together\" error\n '\n \n+test_expect_success 'using --onto with --ref' '\n+\tgit branch test-ref-onto topic2 &&\n+\ttest_when_finished \"git branch -D test-ref-onto\" &&\n+\n+\tgit replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\ttest_grep \"^update refs/heads/test-ref-onto \" result &&\n+\n+\tgit log --format=%s $(cut -f 3 -d \" \" result) >actual &&\n+\ttest_write_lines E D M L B A >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'using --advance with --ref' '\n+\tgit branch test-ref-advance main &&\n+\tgit branch test-ref-target main &&\n+\ttest_when_finished \"git branch -D test-ref-advance test-ref-target\" &&\n+\n+\tgit replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\ttest_grep \"^update refs/heads/test-ref-target \" result\n+'\n+\n+test_expect_success 'using --revert with --ref' '\n+\tgit branch test-ref-revert topic4 &&\n+\tgit branch test-ref-revert-target topic4 &&\n+\ttest_when_finished \"git branch -D test-ref-revert test-ref-revert-target\" &&\n+\n+\tgit replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result &&\n+\n+\ttest_line_count = 1 result &&\n+\ttest_grep \"^update refs/heads/test-ref-revert-target \" result\n+'\n+\n+test_expect_success '--ref is incompatible with --contained' '\n+\ttest_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err &&\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n+test_expect_success '--ref with nonexistent fully-qualified ref' '\n+\ttest_when_finished \"git update-ref -d refs/heads/new-branch\" &&\n+\n+\tgit replay --onto=main --ref=refs/heads/new-branch topic1..topic2 &&\n+\n+\tgit log --format=%s -2 new-branch >actual &&\n+\ttest_write_lines E D >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--ref must be a valid refname' '\n+\ttest_must_fail git replay --onto=main --ref=\"refs/heads/bad..ref\" topic1..topic2 2>err &&\n+\ttest_grep \"is not a valid refname\" err\n+'\n+\n+test_expect_success '--ref requires fully qualified ref' '\n+\ttest_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&\n+\ttest_grep \"is not a valid refname\" err\n+'\n+\n+test_expect_success '--onto with --ref rejects multiple revision ranges' '\n+\ttest_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err &&\n+\ttest_grep \"cannot be used with multiple revision ranges\" err\n+'\n+\n test_done\n\n-- \n2.53.0.310.g728cabbaf7\n\n"}]}