From e5ae639f1a25642650cefedf6478ff5903ffb2f0 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 1 Apr 2026 22:55:10 +0200 Subject: [PATCH 1/3] builtin/replay: mark options as not negatable The options '--onto', '--advance', '--revert', and '--ref-action' of git-replay(1) are not negatable. Mark them as such using PARSE_OPT_NONEG. Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- builtin/replay.c | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/builtin/replay.c b/builtin/replay.c index a0879b020f..85aa9fa0a4 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -89,20 +89,24 @@ int cmd_replay(int argc, NULL }; struct option replay_options[] = { - OPT_STRING(0, "advance", &opts.advance, - N_("branch"), - N_("make replay advance given branch")), - OPT_STRING(0, "onto", &opts.onto, - N_("revision"), - N_("replay onto given commit")), OPT_BOOL(0, "contained", &opts.contained, N_("update all branches that point at commits in ")), - OPT_STRING(0, "revert", &opts.revert, - N_("branch"), - N_("revert commits onto given branch")), - OPT_STRING(0, "ref-action", &ref_action, - N_("mode"), - N_("control ref update behavior (update|print)")), + OPT_STRING_F(0, "onto", &opts.onto, + N_("revision"), + N_("replay onto given commit"), + PARSE_OPT_NONEG), + OPT_STRING_F(0, "advance", &opts.advance, + N_("branch"), + N_("make replay advance given branch"), + PARSE_OPT_NONEG), + OPT_STRING_F(0, "revert", &opts.revert, + N_("branch"), + N_("revert commits onto given branch"), + PARSE_OPT_NONEG), + OPT_STRING_F(0, "ref-action", &ref_action, + N_("mode"), + N_("control ref update behavior (update|print)"), + PARSE_OPT_NONEG), OPT_END() }; From 6542cacbb33490ab83ef87a5fbee694cd2863bdd Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 1 Apr 2026 22:55:11 +0200 Subject: [PATCH 2/3] replay: use stuck form in documentation and help message gitcli(7) suggests to use stuck form. Change the documentation strings to use this form. While at it, reorder them to match the order in the docs. Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- Documentation/git-replay.adoc | 25 +++++++++++++------------ builtin/replay.c | 4 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index 997097e420..5bb478c281 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -9,7 +9,8 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t SYNOPSIS -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto | --advance | --revert ) [--ref-action[=]] +(EXPERIMENTAL!) 'git replay' ([--contained] --onto= | --advance= | --revert=) + [--ref-action=] DESCRIPTION ----------- @@ -26,7 +27,7 @@ THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE. OPTIONS ------- ---onto :: +--onto=:: Starting point at which to create the new commits. May be any valid commit, and not just an existing branch name. + @@ -34,7 +35,7 @@ When `--onto` is specified, the branch(es) in the revision range will be updated to point at the new commits, similar to the way `git rebase --update-refs` updates multiple branches in the affected range. ---advance :: +--advance=:: Starting point at which to create the new commits; must be a branch name. + @@ -42,7 +43,7 @@ The history is replayed on top of the and is updated to point at the tip of the resulting history. This is different from `--onto`, which uses the target only as a starting point without updating it. ---revert :: +--revert=:: Starting point at which to create the reverted commits; must be a branch name. + @@ -79,8 +80,8 @@ The default mode can be configured via the `replay.refAction` configuration vari :: Range of commits to replay; see "Specifying Ranges" in - linkgit:git-rev-parse[1]. In `--advance ` or - `--revert ` mode, the range should have a single tip, + linkgit:git-rev-parse[1]. In `--advance=` or + `--revert=` mode, the range should have a single tip, so that it's clear to which tip the advanced or reverted should point. Any commits in the range whose changes are already present in the branch the commits are being @@ -127,7 +128,7 @@ EXAMPLES To simply rebase `mybranch` onto `target`: ------------ -$ git replay --onto target origin/main..mybranch +$ git replay --onto=target origin/main..mybranch ------------ The refs are updated atomically and no output is produced on success. @@ -135,14 +136,14 @@ The refs are updated atomically and no output is produced on success. To see what would be updated without actually updating: ------------ -$ git replay --ref-action=print --onto target origin/main..mybranch +$ git replay --ref-action=print --onto=target origin/main..mybranch update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH} ------------ To cherry-pick the commits from mybranch onto target: ------------ -$ git replay --advance target origin/main..mybranch +$ git replay --advance=target origin/main..mybranch ------------ Note that the first two examples replay the exact same commits and on @@ -154,7 +155,7 @@ What if you have a stack of branches, one depending upon another, and you'd really like to rebase the whole set? ------------ -$ git replay --contained --onto origin/main origin/main..tipbranch +$ git replay --contained --onto=origin/main origin/main..tipbranch ------------ All three branches (`branch1`, `branch2`, and `tipbranch`) are updated @@ -165,7 +166,7 @@ commits to replay using the syntax `A..B`; any range expression will do: ------------ -$ git replay --onto origin/main ^base branch1 branch2 branch3 +$ git replay --onto=origin/main ^base branch1 branch2 branch3 ------------ This will simultaneously rebase `branch1`, `branch2`, and `branch3`, @@ -176,7 +177,7 @@ that they have in common, but that does not need to be the case. To revert commits on a branch: ------------ -$ git replay --revert main topic~2..topic +$ git replay --revert=main topic~2..topic ------------ This reverts the last two commits from `topic`, creating revert commits on diff --git a/builtin/replay.c b/builtin/replay.c index 85aa9fa0a4..fbfeb780b6 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -84,8 +84,8 @@ int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto | --advance | --revert ) " - "[--ref-action[=]] "), + "([--contained] --onto= | --advance= | --revert=)\n" + "[--ref-action=] "), NULL }; struct option replay_options[] = { From 23d83f8ddbef9adcb87671358b473e55cf90c90b Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Wed, 1 Apr 2026 22:55:12 +0200 Subject: [PATCH 3/3] replay: allow to specify a ref with option --ref When option '--onto' is passed to git-replay(1), the command will update refs from the passed to the command. When using option '--advance' or '--revert', the argument of that option is a ref that will be updated. To enable users to specify which ref to update, add option '--ref'. When using option '--ref', the refs described above are left untouched and instead the argument of this option is updated instead. Because this introduces code paths in replay.c that jump to `out` before init_basic_merge_options() is called on `merge_opt`, zero-initialize the struct. Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- Documentation/git-replay.adoc | 22 +++++++++++- builtin/replay.c | 8 ++++- replay.c | 35 ++++++++++++++----- replay.h | 7 ++++ t/t3650-replay-basics.sh | 66 +++++++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 10 deletions(-) diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index 5bb478c281..a32f72aead 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -10,7 +10,7 @@ SYNOPSIS -------- [verse] (EXPERIMENTAL!) 'git replay' ([--contained] --onto= | --advance= | --revert=) - [--ref-action=] + [--ref=] [--ref-action=] DESCRIPTION ----------- @@ -66,6 +66,16 @@ incompatible with `--contained` (which is a modifier for `--onto` only). Update all branches that point at commits in . Requires `--onto`. +--ref=:: + Override which reference is updated with the result of the replay. + The ref must be fully qualified. + When used with `--onto`, the `` should have a + single tip and only the specified reference is updated instead of + inferring refs from the revision range. + When used with `--advance` or `--revert`, the specified reference is + updated instead of the branch given to those options. + This option is incompatible with `--contained`. + --ref-action[=]:: Control how references are updated. The mode can be: + @@ -189,6 +199,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` which can avoid unnecessary merge conflicts. +To replay onto a specific commit while updating a different reference: + +------------ +$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff +------------ + +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates +`refs/heads/mybranch` to point at the result. This can be useful when you want +to use bare commit IDs instead of branch names. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/replay.c b/builtin/replay.c index fbfeb780b6..39e3a86f6c 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -85,7 +85,7 @@ int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " "([--contained] --onto= | --advance= | --revert=)\n" - "[--ref-action=] "), + "[--ref=] [--ref-action=] "), NULL }; struct option replay_options[] = { @@ -103,6 +103,10 @@ int cmd_replay(int argc, N_("branch"), N_("revert commits onto given branch"), PARSE_OPT_NONEG), + OPT_STRING_F(0, "ref", &opts.ref, + N_("branch"), + N_("reference to update with result"), + PARSE_OPT_NONEG), OPT_STRING_F(0, "ref-action", &ref_action, N_("mode"), N_("control ref update behavior (update|print)"), @@ -126,6 +130,8 @@ int cmd_replay(int argc, opts.contained, "--contained"); die_for_incompatible_opt2(!!opts.revert, "--revert", opts.contained, "--contained"); + die_for_incompatible_opt2(!!opts.ref, "--ref", + !!opts.contained, "--contained"); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); diff --git a/replay.c b/replay.c index d7239d4c83..b958ddabfa 100644 --- a/replay.c +++ b/replay.c @@ -347,13 +347,15 @@ int replay_revisions(struct rev_info *revs, struct commit *last_commit = NULL; struct commit *commit; struct commit *onto = NULL; - struct merge_options merge_opt; + struct merge_options merge_opt = { 0 }; struct merge_result result = { .clean = 1, }; bool detached_head; char *advance; char *revert; + const char *ref; + struct object_id old_oid; enum replay_mode mode = REPLAY_MODE_PICK; int ret; @@ -364,6 +366,27 @@ int replay_revisions(struct rev_info *revs, set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &detached_head, &advance, &revert, &onto, &update_refs); + if (opts->ref) { + struct object_id oid; + + if (update_refs && strset_get_size(update_refs) > 1) { + ret = error(_("'--ref' cannot be used with multiple revision ranges")); + goto out; + } + if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) { + ret = error(_("'%s' is not a valid refname"), opts->ref); + goto out; + } + ref = opts->ref; + if (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid)) + oidcpy(&old_oid, &oid); + else + oidclr(&old_oid, revs->repo->hash_algo); + } else { + ref = advance ? advance : revert; + oidcpy(&old_oid, &onto->object.oid); + } + /* FIXME: Should allow replaying commits with the first as a root commit */ if (prepare_revision_walk(revs) < 0) { @@ -399,7 +422,7 @@ int replay_revisions(struct rev_info *revs, kh_value(replayed_commits, pos) = last_commit; /* Update any necessary branches */ - if (advance || revert) + if (ref) continue; for (decoration = get_name_decoration(&commit->object); @@ -433,13 +456,9 @@ int replay_revisions(struct rev_info *revs, goto out; } - /* In --advance or --revert mode, update the target ref */ - if (advance || revert) { - const char *ref = advance ? advance : revert; - replay_result_queue_update(out, ref, - &onto->object.oid, + if (ref) + replay_result_queue_update(out, ref, &old_oid, &last_commit->object.oid); - } ret = 0; diff --git a/replay.h b/replay.h index e916a5f975..0ab74b9805 100644 --- a/replay.h +++ b/replay.h @@ -24,6 +24,13 @@ struct replay_revisions_options { */ const char *onto; + /* + * Reference to update with the result of the replay. This will not + * update any refs from `onto`, `advance`, or `revert`. Ignores + * `contained`. + */ + const char *ref; + /* * Starting point at which to create revert commits; must be a branch * name. The branch will be updated to point to the revert commits. diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index 217f6fb292..d5c7dd1bf4 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -495,4 +495,70 @@ test_expect_success 'git replay --revert incompatible with --advance' ' test_grep "cannot be used together" error ' +test_expect_success 'using --onto with --ref' ' + git branch test-ref-onto topic2 && + test_when_finished "git branch -D test-ref-onto" && + + git replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result && + + test_line_count = 1 result && + test_grep "^update refs/heads/test-ref-onto " result && + + git log --format=%s $(cut -f 3 -d " " result) >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual +' + +test_expect_success 'using --advance with --ref' ' + git branch test-ref-advance main && + git branch test-ref-target main && + test_when_finished "git branch -D test-ref-advance test-ref-target" && + + git replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result && + + test_line_count = 1 result && + test_grep "^update refs/heads/test-ref-target " result +' + +test_expect_success 'using --revert with --ref' ' + git branch test-ref-revert topic4 && + git branch test-ref-revert-target topic4 && + test_when_finished "git branch -D test-ref-revert test-ref-revert-target" && + + git replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result && + + test_line_count = 1 result && + test_grep "^update refs/heads/test-ref-revert-target " result +' + +test_expect_success '--ref is incompatible with --contained' ' + test_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err && + test_grep "cannot be used together" err +' + +test_expect_success '--ref with nonexistent fully-qualified ref' ' + test_when_finished "git update-ref -d refs/heads/new-branch" && + + git replay --onto=main --ref=refs/heads/new-branch topic1..topic2 && + + git log --format=%s -2 new-branch >actual && + test_write_lines E D >expect && + test_cmp expect actual +' + +test_expect_success '--ref must be a valid refname' ' + test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err && + test_grep "is not a valid refname" err +' + +test_expect_success '--ref requires fully qualified ref' ' + test_must_fail git replay --onto=main --ref=main topic1..topic2 2>err && + test_grep "is not a valid refname" err +' + +test_expect_success '--onto with --ref rejects multiple revision ranges' ' + test_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err && + test_grep "cannot be used with multiple revision ranges" err +' + test_done