replay: allow to specify a ref with option --ref

When option '--onto' is passed to git-replay(1), the command will update refs from the <revision-range> passed to the command. When using option '--advance' or '--revert', the argument of that option is a ref that will be updated. To enable users to specify which ref to update, add option '--ref'. When using option '--ref', the refs described above are left untouched and instead the argument of this option is updated instead. Because this introduces code paths in replay.c that jump to `out` before init_basic_merge_options() is called on `merge_opt`, zero-initialize the struct. Signed-off-by: Toon Claes <toon@iotcl.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Toon Claes committed Apr 1, 2026 at 22:55 UTC 23d83f8ddbef9adcb87671358b473e55cf90c90b
5 files changed +128 -10
Documentation/git-replay.adoc
+21 -1
@@ -10,7 +10,7 @@ SYNOPSIS
10 --------
11 [verse]
12 (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
13 - [--ref-action=<mode>] <revision-range>
13 + [--ref=<ref>] [--ref-action=<mode>] <revision-range>
14
15 DESCRIPTION
16 -----------
@@ -66,6 +66,16 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
66 Update all branches that point at commits in
67 <revision-range>. Requires `--onto`.
68
69 +--ref=<ref>::
70 + Override which reference is updated with the result of the replay.
71 + The ref must be fully qualified.
72 + When used with `--onto`, the `<revision-range>` should have a
73 + single tip and only the specified reference is updated instead of
74 + inferring refs from the revision range.
75 + When used with `--advance` or `--revert`, the specified reference is
76 + updated instead of the branch given to those options.
77 + This option is incompatible with `--contained`.
78 +
79 --ref-action[=<mode>]::
80 Control how references are updated. The mode can be:
81 +
@@ -189,6 +199,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than
199 commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
200 which can avoid unnecessary merge conflicts.
201
202 +To replay onto a specific commit while updating a different reference:
203 +
204 +------------
205 +$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff
206 +------------
207 +
208 +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates
209 +`refs/heads/mybranch` to point at the result. This can be useful when you want
210 +to use bare commit IDs instead of branch names.
211 +
212 GIT
213 ---
214 Part of the linkgit:git[1] suite
builtin/replay.c
+7 -1
@@ -85,7 +85,7 @@ int cmd_replay(int argc,
85 const char *const replay_usage[] = {
86 N_("(EXPERIMENTAL!) git replay "
87 "([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
88 - "[--ref-action=<mode>] <revision-range>"),
88 + "[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
89 NULL
90 };
91 struct option replay_options[] = {
@@ -103,6 +103,10 @@ int cmd_replay(int argc,
103 N_("branch"),
104 N_("revert commits onto given branch"),
105 PARSE_OPT_NONEG),
106 + OPT_STRING_F(0, "ref", &opts.ref,
107 + N_("branch"),
108 + N_("reference to update with result"),
109 + PARSE_OPT_NONEG),
110 OPT_STRING_F(0, "ref-action", &ref_action,
111 N_("mode"),
112 N_("control ref update behavior (update|print)"),
@@ -126,6 +130,8 @@ int cmd_replay(int argc,
130 opts.contained, "--contained");
131 die_for_incompatible_opt2(!!opts.revert, "--revert",
132 opts.contained, "--contained");
133 + die_for_incompatible_opt2(!!opts.ref, "--ref",
134 + !!opts.contained, "--contained");
135
136 /* Parse ref action mode from command line or config */
137 ref_mode = get_ref_action_mode(repo, ref_action);
replay.c
+27 -8
@@ -347,13 +347,15 @@ int replay_revisions(struct rev_info *revs,
347 struct commit *last_commit = NULL;
348 struct commit *commit;
349 struct commit *onto = NULL;
350 - struct merge_options merge_opt;
350 + struct merge_options merge_opt = { 0 };
351 struct merge_result result = {
352 .clean = 1,
353 };
354 bool detached_head;
355 char *advance;
356 char *revert;
357 + const char *ref;
358 + struct object_id old_oid;
359 enum replay_mode mode = REPLAY_MODE_PICK;
360 int ret;
361
@@ -364,6 +366,27 @@ int replay_revisions(struct rev_info *revs,
366 set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
367 &detached_head, &advance, &revert, &onto, &update_refs);
368
369 + if (opts->ref) {
370 + struct object_id oid;
371 +
372 + if (update_refs && strset_get_size(update_refs) > 1) {
373 + ret = error(_("'--ref' cannot be used with multiple revision ranges"));
374 + goto out;
375 + }
376 + if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) {
377 + ret = error(_("'%s' is not a valid refname"), opts->ref);
378 + goto out;
379 + }
380 + ref = opts->ref;
381 + if (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))
382 + oidcpy(&old_oid, &oid);
383 + else
384 + oidclr(&old_oid, revs->repo->hash_algo);
385 + } else {
386 + ref = advance ? advance : revert;
387 + oidcpy(&old_oid, &onto->object.oid);
388 + }
389 +
390 /* FIXME: Should allow replaying commits with the first as a root commit */
391
392 if (prepare_revision_walk(revs) < 0) {
@@ -399,7 +422,7 @@ int replay_revisions(struct rev_info *revs,
422 kh_value(replayed_commits, pos) = last_commit;
423
424 /* Update any necessary branches */
402 - if (advance || revert)
425 + if (ref)
426 continue;
427
428 for (decoration = get_name_decoration(&commit->object);
@@ -433,13 +456,9 @@ int replay_revisions(struct rev_info *revs,
456 goto out;
457 }
458
436 - /* In --advance or --revert mode, update the target ref */
437 - if (advance || revert) {
438 - const char *ref = advance ? advance : revert;
439 - replay_result_queue_update(out, ref,
440 - &onto->object.oid,
459 + if (ref)
460 + replay_result_queue_update(out, ref, &old_oid,
461 &last_commit->object.oid);
442 - }
462
463 ret = 0;
464
replay.h
+7
@@ -24,6 +24,13 @@ struct replay_revisions_options {
24 */
25 const char *onto;
26
27 + /*
28 + * Reference to update with the result of the replay. This will not
29 + * update any refs from `onto`, `advance`, or `revert`. Ignores
30 + * `contained`.
31 + */
32 + const char *ref;
33 +
34 /*
35 * Starting point at which to create revert commits; must be a branch
36 * name. The branch will be updated to point to the revert commits.
t/t3650-replay-basics.sh
+66
@@ -495,4 +495,70 @@ test_expect_success 'git replay --revert incompatible with --advance' '
495 test_grep "cannot be used together" error
496 '
497
498 +test_expect_success 'using --onto with --ref' '
499 + git branch test-ref-onto topic2 &&
500 + test_when_finished "git branch -D test-ref-onto" &&
501 +
502 + git replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result &&
503 +
504 + test_line_count = 1 result &&
505 + test_grep "^update refs/heads/test-ref-onto " result &&
506 +
507 + git log --format=%s $(cut -f 3 -d " " result) >actual &&
508 + test_write_lines E D M L B A >expect &&
509 + test_cmp expect actual
510 +'
511 +
512 +test_expect_success 'using --advance with --ref' '
513 + git branch test-ref-advance main &&
514 + git branch test-ref-target main &&
515 + test_when_finished "git branch -D test-ref-advance test-ref-target" &&
516 +
517 + git replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result &&
518 +
519 + test_line_count = 1 result &&
520 + test_grep "^update refs/heads/test-ref-target " result
521 +'
522 +
523 +test_expect_success 'using --revert with --ref' '
524 + git branch test-ref-revert topic4 &&
525 + git branch test-ref-revert-target topic4 &&
526 + test_when_finished "git branch -D test-ref-revert test-ref-revert-target" &&
527 +
528 + git replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result &&
529 +
530 + test_line_count = 1 result &&
531 + test_grep "^update refs/heads/test-ref-revert-target " result
532 +'
533 +
534 +test_expect_success '--ref is incompatible with --contained' '
535 + test_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err &&
536 + test_grep "cannot be used together" err
537 +'
538 +
539 +test_expect_success '--ref with nonexistent fully-qualified ref' '
540 + test_when_finished "git update-ref -d refs/heads/new-branch" &&
541 +
542 + git replay --onto=main --ref=refs/heads/new-branch topic1..topic2 &&
543 +
544 + git log --format=%s -2 new-branch >actual &&
545 + test_write_lines E D >expect &&
546 + test_cmp expect actual
547 +'
548 +
549 +test_expect_success '--ref must be a valid refname' '
550 + test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
551 + test_grep "is not a valid refname" err
552 +'
553 +
554 +test_expect_success '--ref requires fully qualified ref' '
555 + test_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&
556 + test_grep "is not a valid refname" err
557 +'
558 +
559 +test_expect_success '--onto with --ref rejects multiple revision ranges' '
560 + test_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err &&
561 + test_grep "cannot be used with multiple revision ranges" err
562 +'
563 +
564 test_done