]> git.ipfire.org Git - thirdparty/git.git/commitdiff
replay: allow to specify a ref with option --ref
authorToon Claes <toon@iotcl.com>
Wed, 1 Apr 2026 20:55:12 +0000 (22:55 +0200)
committerJunio C Hamano <gitster@pobox.com>
Thu, 2 Apr 2026 04:34:25 +0000 (21:34 -0700)
When option '--onto' is passed to git-replay(1), the command will update
refs from the <revision-range> passed to the command. When using option
'--advance' or '--revert', the argument of that option is a ref that
will be updated.

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

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

Signed-off-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-replay.adoc
builtin/replay.c
replay.c
replay.h
t/t3650-replay-basics.sh

index 5bb478c281df85e6e3012e452b8e2e0e9b497123..a32f72aead3750c4c34fb397e1a4d644621d9fb2 100644 (file)
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [verse]
 (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
-                            [--ref-action=<mode>] <revision-range>
+                            [--ref=<ref>] [--ref-action=<mode>] <revision-range>
 
 DESCRIPTION
 -----------
@@ -66,6 +66,16 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
        Update all branches that point at commits in
        <revision-range>. Requires `--onto`.
 
+--ref=<ref>::
+       Override which reference is updated with the result of the replay.
+       The ref must be fully qualified.
+       When used with `--onto`, the `<revision-range>` should have a
+       single tip and only the specified reference is updated instead of
+       inferring refs from the revision range.
+       When used with `--advance` or `--revert`, the specified reference is
+       updated instead of the branch given to those options.
+       This option is incompatible with `--contained`.
+
 --ref-action[=<mode>]::
        Control how references are updated. The mode can be:
 +
@@ -189,6 +199,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than
 commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
 which can avoid unnecessary merge conflicts.
 
+To replay onto a specific commit while updating a different reference:
+
+------------
+$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff
+------------
+
+This replays the range `aabbcc..ddeeff` onto commit `112233` and updates
+`refs/heads/mybranch` to point at the result. This can be useful when you want
+to use bare commit IDs instead of branch names.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
index fbfeb780b6a6ad686f5f985be39fdd558528d618..39e3a86f6c10ab70cf7fb56b0335dbf569fde2a3 100644 (file)
@@ -85,7 +85,7 @@ int cmd_replay(int argc,
        const char *const replay_usage[] = {
                N_("(EXPERIMENTAL!) git replay "
                   "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
-                  "[--ref-action=<mode>] <revision-range>"),
+                  "[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
                NULL
        };
        struct option replay_options[] = {
@@ -103,6 +103,10 @@ int cmd_replay(int argc,
                             N_("branch"),
                             N_("revert commits onto given branch"),
                             PARSE_OPT_NONEG),
+               OPT_STRING_F(0, "ref", &opts.ref,
+                            N_("branch"),
+                            N_("reference to update with result"),
+                            PARSE_OPT_NONEG),
                OPT_STRING_F(0, "ref-action", &ref_action,
                             N_("mode"),
                             N_("control ref update behavior (update|print)"),
@@ -126,6 +130,8 @@ int cmd_replay(int argc,
                                  opts.contained, "--contained");
        die_for_incompatible_opt2(!!opts.revert, "--revert",
                                  opts.contained, "--contained");
+       die_for_incompatible_opt2(!!opts.ref, "--ref",
+                                 !!opts.contained, "--contained");
 
        /* Parse ref action mode from command line or config */
        ref_mode = get_ref_action_mode(repo, ref_action);
index d7239d4c8396d4dd6d9a0e22f5b17ae95a2bac0c..b958ddabfa1363ec5258cace994dc8cc019f1098 100644 (file)
--- 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;
 
index e916a5f975be2628133483e1ac18bb9e354b5bad..0ab74b9805af1664a9464fbd12d727b200db2f48 100644 (file)
--- 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.
index 217f6fb292a068723446f52e53c589617fd5a8ee..d5c7dd1bf4aad8ff84ac5887f9f66e6ae525cdee 100755 (executable)
@@ -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