Revert "Merge branch 'tc/replay-linearize' into next"

This reverts commit 371c2e9c3b9ad9a668ee0f5f47f81d479c1afac1, reversing changes made to fd2b979b73af2a6f37990768e06ca970519d4355, to re-queue the v8 iteration of the topic.

Junio C Hamano committed Jul 28, 2026 at 11:26 UTC b3b5e47172ef2180679b8b4a15aff66f98e8697d
5 files changed +28 -221
Documentation/git-replay.adoc
+1 -18
@@ -10,7 +10,7 @@ SYNOPSIS
10 --------
11 [verse]
12 (EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
13 - [--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>
13 + [--ref=<ref>] [--ref-action=<mode>] <revision-range>
14
15 DESCRIPTION
16 -----------
@@ -88,23 +88,6 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
88 +
89 The default mode can be configured via the `replay.refAction` configuration variable.
90
91 ---linearize::
92 - In this mode, each replayed commit is stacked on top of the
93 - previously replayed one, so all replayed commits are flattened into
94 - a single linear history.
95 -+
96 -When a merge commit is encountered, the behavior of git-rebase(1)'s
97 -option `--no-rebase-merges` is imitated. All commits in the range
98 -reachable from the merge commit are replayed into a linear history, and
99 -the merge commit itself is dropped. A ref that pointed to a merge commit
100 -is updated to the merge's last replayed ancestor.
101 -+
102 -This flattens the `<revision-range>` as a whole. When multiple revision
103 -ranges are given they are stacked on top of each other into one linear
104 -history. Each of their refs is updated to point to its position in that
105 -history. To linearize ranges separately, replay them in separate `git
106 -replay` invocations.
107 -
91 <revision-range>::
92 Range of commits to replay; see "Specifying Ranges" in
93 linkgit:git-rev-parse[1]. In `--advance=<branch>` or
builtin/replay.c
+1 -3
@@ -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=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>"),
88 + "[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
89 NULL
90 };
91 struct option replay_options[] = {
@@ -111,8 +111,6 @@ int cmd_replay(int argc,
111 N_("mode"),
112 N_("control ref update behavior (update|print)"),
113 PARSE_OPT_NONEG),
114 - OPT_BOOL(0, "linearize", &opts.linearize,
115 - N_("drop merge commits, replaying only non-merge commits")),
114 OPT_END()
115 };
116
replay.c
+25 -56
@@ -254,9 +254,9 @@ static void set_up_replay_mode(struct repository *repo,
254 strset_clear(&rinfo.positive_refs);
255 }
256
257 -static struct commit *get_mapped_commit(kh_oid_map_t *replayed_commits,
258 - struct commit *commit,
259 - struct commit *fallback)
257 +static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
258 + struct commit *commit,
259 + struct commit *fallback)
260 {
261 khint_t pos;
262 if (!commit)
@@ -267,36 +267,27 @@ static struct commit *get_mapped_commit(kh_oid_map_t *replayed_commits,
267 return kh_value(replayed_commits, pos);
268 }
269
270 -static void put_mapped_commit(kh_oid_map_t *replayed_commits,
271 - struct commit *commit,
272 - struct commit *new_commit)
273 -{
274 - khint_t pos;
275 - int ret;
276 -
277 - pos = kh_put_oid_map(replayed_commits, commit->object.oid, &ret);
278 - if (ret == 0)
279 - BUG("Duplicate rewritten commit: %s",
280 - oid_to_hex(&commit->object.oid));
281 -
282 - kh_value(replayed_commits, pos) = new_commit;
283 -}
284 -
270 static struct commit *pick_regular_commit(struct repository *repo,
271 struct commit *pickme,
287 - struct commit *replayed_base,
272 + kh_oid_map_t *replayed_commits,
273 + struct commit *onto,
274 struct merge_options *merge_opt,
275 struct merge_result *result,
276 enum replay_mode mode,
277 enum replay_empty_commit_action empty)
278 {
279 + struct commit *base, *replayed_base;
280 struct tree *pickme_tree, *base_tree, *replayed_base_tree;
281
295 - if (pickme->parents)
296 - base_tree = repo_get_commit_tree(repo, pickme->parents->item);
297 - else
282 + if (pickme->parents) {
283 + base = pickme->parents->item;
284 + base_tree = repo_get_commit_tree(repo, base);
285 + } else {
286 + base = NULL;
287 base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
288 + }
289
290 + replayed_base = mapped_commit(replayed_commits, base, onto);
291 replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
292 pickme_tree = repo_get_commit_tree(repo, pickme);
293
@@ -436,46 +427,24 @@ int replay_revisions(struct rev_info *revs,
427 replayed_commits = kh_init_oid_map();
428 while ((commit = get_revision(revs))) {
429 const struct name_decoration *decoration;
430 + khint_t pos;
431 + int hr;
432
440 - if (commit->parents && commit->parents->next) {
441 - if (!opts->linearize)
442 - die(_("replaying merge commits is not supported yet!"));
443 - /*
444 - * Drop the merge commit: do not pick it, leave
445 - * `last_commit` unchanged, and fall through to the
446 - * rest of the loop. As a result:
447 - * - refs pointing to the merge commit will be updated
448 - * to `last_commit`.
449 - * - the next replayed commit uses `last_commit` as its
450 - * `base`.
451 - */
452 - } else {
453 - /*
454 - * Decide where to replay this commit onto.
455 - * If the parent commit was replayed already, the replayed result
456 - * can be found in `replayed_commits`. Otherwise fall back to `onto`.
457 - * When reverting, commits are replayed in reverse order and thus
458 - * its parent isn't replayed yet. Therefore revert commits are
459 - * always replayed onto `last_commit`.
460 - * Also when opts->linearize is true, set the base to
461 - * `last_commit` to create a single linear history.
462 - */
463 - struct commit *parent = commit->parents ? commit->parents->item : NULL;
464 - struct commit *base = get_mapped_commit(replayed_commits, parent, onto);
465 -
466 - if (opts->linearize || mode == REPLAY_MODE_REVERT)
467 - base = last_commit;
468 -
469 - last_commit = pick_regular_commit(revs->repo, commit, base,
470 - &merge_opt, &result,
471 - mode, opts->empty);
472 - }
433 + if (commit->parents && commit->parents->next)
434 + die(_("replaying merge commits is not supported yet!"));
435
436 + last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
437 + mode == REPLAY_MODE_REVERT ? last_commit : onto,
438 + &merge_opt, &result, mode, opts->empty);
439 if (!last_commit)
440 break;
441
442 /* Record commit -> last_commit mapping */
478 - put_mapped_commit(replayed_commits, commit, last_commit);
443 + pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
444 + if (hr == 0)
445 + BUG("Duplicate rewritten commit: %s\n",
446 + oid_to_hex(&commit->object.oid));
447 + kh_value(replayed_commits, pos) = last_commit;
448
449 /* Update any necessary branches */
450 if (ref)
replay.h
-5
@@ -62,11 +62,6 @@ struct replay_revisions_options {
62 * Defaults to REPLAY_EMPTY_COMMIT_DROP.
63 */
64 enum replay_empty_commit_action empty;
65 -
66 - /*
67 - * Whether to linearize the commits (i.e. drop merge commits).
68 - */
69 - int linearize;
65 };
66
67 /* This struct is used as an out-parameter by `replay_revisions()`. */
t/t3650-replay-basics.sh
+1 -139
@@ -52,19 +52,8 @@ test_expect_success 'setup' '
52 test_merge P O --no-ff &&
53 git switch main &&
54
55 - git switch --orphan unrelated &&
56 - test_commit unrelated-root &&
57 -
55 git switch -c conflict B &&
59 - test_commit C.conflict C.t conflict &&
60 - git branch -D unrelated &&
61 -
62 - git switch -c divergent-x main &&
63 - test_commit X &&
64 - git switch -c divergent-y main &&
65 - test_commit Y &&
66 - git switch divergent-x &&
67 - test_merge Z divergent-y --no-ff
56 + test_commit C.conflict C.t conflict
57 '
58
59 test_expect_success 'setup bare' '
@@ -576,131 +565,4 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' '
565 test_grep "cannot be used with multiple revision ranges" err
566 '
567
579 -test_expect_success 'replay to rebase merge commit with --linearize' '
580 - git replay --ref-action=print --linearize \
581 - --onto main I..topic-with-merge >result &&
582 -
583 - test_line_count = 1 result &&
584 -
585 - git log --format=%s $(cut -f 3 -d " " result) >actual &&
586 - test_write_lines O N J M L B A >expect &&
587 - test_cmp expect actual
588 -'
589 -
590 -test_expect_success 'replay to rebase merge commit with --linearize down to the root commit' '
591 - git replay --ref-action=print --linearize \
592 - --onto unrelated-root topic-with-merge >result &&
593 -
594 - test_line_count = 1 result &&
595 -
596 - git log --format=%s $(cut -f 3 -d " " result) >actual &&
597 - test_write_lines O N J I B A unrelated-root >expect &&
598 - test_cmp expect actual
599 -'
600 -
601 -test_expect_success 'replay to cherry-pick merge commit with --linearize' '
602 - git replay --ref-action=print --linearize \
603 - --advance main I..topic-with-merge >result &&
604 -
605 - test_line_count = 1 result &&
606 -
607 - git log --format=%s $(cut -f 3 -d " " result) >actual &&
608 - test_write_lines O N J M L B A >expect &&
609 - test_cmp expect actual &&
610 -
611 - printf "update refs/heads/main " >expect &&
612 - printf "%s " $(cut -f 3 -d " " result) >>expect &&
613 - git rev-parse main >>expect &&
614 - test_cmp expect result
615 -'
616 -
617 -test_expect_success 'replay --linearize produces the same patches' '
618 - git replay --ref-action=print --linearize \
619 - --onto main I..topic-with-merge >result &&
620 -
621 - test_line_count = 1 result &&
622 - tip=$(cut -f 3 -d " " result) &&
623 -
624 - # range-diff does not care about the dropped merge,
625 - # so the original commits (I..topic-with-merge)
626 - # and the replayed chain (main..tip) must produce identical patches.
627 - git range-diff I..topic-with-merge main..$tip >out &&
628 - test_file_not_empty out &&
629 - test_grep ! -v "=" out &&
630 -
631 - git log --oneline main..$tip >out &&
632 - test_line_count = 3 out
633 -'
634 -
635 -test_expect_success 'replay with --linearize rebase multiple divergent branches into a single line' '
636 - git replay --ref-action=print --linearize \
637 - --onto main ^B topic2 topic3 topic4 >result &&
638 -
639 - test_line_count = 3 result &&
640 - cut -f 3 -d " " result >new-branch-tips &&
641 -
642 - >expect &&
643 - for i in 2 3 4
644 - do
645 - printf "update refs/heads/topic$i " >>expect &&
646 - printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
647 - git rev-parse topic$i >>expect || return 1
648 - done &&
649 -
650 - test_cmp expect result &&
651 -
652 - test_write_lines E D C M L B A >expect2 &&
653 - test_write_lines H G F E D C M L B A >expect3 &&
654 - test_write_lines J I H G F E D C M L B A >expect4 &&
655 -
656 - for i in 2 3 4
657 - do
658 - git log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
659 - test_cmp expect$i actual || return 1
660 - done
661 -'
662 -
663 -test_expect_success 'replay with --linearize of a divergent merge keeps both sides' '
664 - git replay --ref-action=print --linearize \
665 - --onto main main..divergent-x >result &&
666 - test_line_count = 1 result &&
667 - tip=$(cut -f 3 -d " " result) &&
668 -
669 - # The merge Z is dropped, but both X and Y are linearized onto main;
670 - # neither side is lost.
671 - git log --format=%s main..$tip >actual &&
672 - test_write_lines Y X >expect &&
673 - test_cmp expect actual
674 -'
675 -
676 -test_expect_success '--linearize with --contained updates contained refs' '
677 - git replay --ref-action=print --linearize --contained \
678 - --onto main ^B topic-with-merge >result &&
679 -
680 - test_line_count = 2 result &&
681 -
682 - git log --format=%s $(head -n 1 result | cut -f 3 -d " ") >actual &&
683 - test_write_lines J I M L B A >expect &&
684 - test_cmp expect actual &&
685 -
686 - git log --format=%s $(tail -n 1 result | cut -f 3 -d " ") >actual &&
687 - test_write_lines O N J I M L B A >expect &&
688 - test_cmp expect actual
689 -'
690 -
691 -test_expect_success 'replay --revert with --linearize reverts a range containing a merge' '
692 - git replay --ref-action=print --revert=divergent-x --linearize \
693 - main..divergent-x >result &&
694 - test_line_count = 1 result &&
695 - tip=$(cut -f 3 -d " " result) &&
696 -
697 - git log --format=%s $tip >actual &&
698 - test_write_lines \
699 - "Revert \"X\"" "Revert \"Y\"" Z Y X M L B A >expect &&
700 - test_cmp expect actual &&
701 -
702 - test_must_fail git cat-file -e $tip:X.t &&
703 - test_must_fail git cat-file -e $tip:Y.t
704 -'
705 -
568 test_done