From: Phillip Wood <phillip.wood123@gmail.com>
To: Johannes Schindelin via GitGitGadget <gitgitgadget@gmail.com>,
git@vger.kernel.org
Cc: Elijah Newren <newren@gmail.com>, Patrick Steinhardt <ps@pks.im>,
Johannes Schindelin <johannes.schindelin@gmx.de>
Subject: Re: [PATCH/RFC 1/5] replay: support replaying 2-parent merges
Date: Fri, 8 May 2026 11:05:48 +0100 [thread overview]
Message-ID: <3dd21593-9945-4f9c-a9a0-f5c66504da49@gmail.com> (raw)
In-Reply-To: <72901ee2-1212-46cd-b752-f451cce6e1ff@gmail.com>
On 08/05/2026 10:36, Phillip Wood wrote:
> Hi Johannes
>
> On 06/05/2026 23:43, Johannes Schindelin via GitGitGadget wrote:
>>
>> Elijah Newren spelled out a way to lift this limitation in his
>> replay-design-notes [1] and prototyped it in a 2022
>> work-in-progress sketch [2]. The idea is that a merge commit M on
>> parents (P1, P2) records both an automatic merge of those parents
>> AND any manual layer the author put on top of that automatic merge
>> (textual conflict resolution and any semantic edit outside conflict
>> markers). Replaying M onto rewritten parents (P1', P2') must
>> preserve that manual layer, but the rewritten parents change the
>> automatic merge, so a simple cherry-pick is wrong: the manual layer
>> would be re-introduced on top of stale auto-merge text.
>>
>> What works instead is a three-way merge of three trees the existing
>> infrastructure already knows how to compute. Let R be the recursive
>> auto-merge of (P1, P2), O be M's actual tree and N be the recursive
>> auto-merge of (P1', P2'). Then `git diff R O` is morally
>> `git show --remerge-diff M`: it captures exactly what the author
>> added on top of the automatic merge. A non-recursive 3-way merge
>> with R as the merge base, O as side 1 and N as side 2 layers that
>> manual contribution onto the freshly auto-merged rewritten parents
>> (N) and produces the replayed tree.
>
> So we cherry-pick the difference between the user's conflict resolution
> O and the auto-merge M of the original parents onto the auto-merge N of
> the replayed parents. If we have a topology that looks like
>
> |
> A
> /|\
> / B \
> E | D
> C /
> |/
> O
>
> then running
>
> git replay --onto E --ancestry-path B..O
>
> will replay C and O onto E. If the changes in E and D conflict but those
> conflicts do not overlap with the conflicts in M that were resolved to
> create O then the replayed version of O will contain conflict markers
> from the conflicting changes in E and D. Because the previous conflict
> resolution applies to N without conflicts we do not recognize that there
> are still conflicts in N that need to be resolved.
>
> Having realized this I went to look at Elijah's notes and they recognize
> this possibility and suggest extending the xdiff merge code to detect
> when N has conflicts that do not correspond to the conflicts in M. That
> sounds like quite a lot of work. I've not put much effort into coming up
> with a counterexample but think that because "git replay" and "git
> history" do not yet allow the commits in the merged branches to be
> edited we may be able to safely use the implementation proposed in this
> series if both merge parents have been rebased (or we might want all the
> merge bases of the new merge to be a descendants of "--onto"). In the
> example above if both the parents were rebased onto E then any new
> conflicts would happen when picking D rather than when recreating the
> merge.
One further thought - if only one of the parents has been rebased (i.e.
we're replaying O with parents P1' and P2) then can we just cherry-pick
the merge - instead of merging P1' and P2, use P1 as the merge-base with
O and P1' as the merge heads?
Thanks
Phillip
> Thanks
>
> Phillip
>
>> Implement `pick_merge_commit()` along those lines and dispatch to it
>> from `replay_revisions()` when the commit being replayed has exactly
>> two parents. Two specific points (learned the hard way) keep
>> non-trivial cases working where the WIP sketch [2] bailed out.
>> First, R and N use identical `merge_options.branch1` and `branch2`
>> labels ("ours"/"theirs"). When the original parents conflicted on a
>> region of a file, both R and N produce textually identical conflict
>> markers; the outer non-recursive merge then sees N == R in that
>> region and the user's manual resolution from O wins cleanly. Without
>> this, the conflict-marker text would differ between R and N (because
>> the inner merges would label the conflicts differently), and the
>> outer merge would itself be unclean even when the user did supply a
>> clean resolution. Second, an unclean inner merge
>> (`result.clean == 0`) is _not_ fatal: the tree merge-ort produces in
>> that case still has well-defined contents (with conflict markers in
>> the conflicted files) and is a valid input to the outer
>> non-recursive merge. Only a real error (`< 0`) propagates as
>> failure.
>>
>> The replay propagates the textual diffs the user actually made in M;
>> it does _not_ extrapolate symbol-level intent. If rewriting the
>> parents pulls in genuinely new content (for example, a brand-new
>> caller of a function that the merge renamed), that new content stays
>> as the rewritten parents have it. Symbol-aware refactoring is out of
>> scope here, just as it is for plain rebase.
>>
>> Octopus merges (more than two parents) and revert-of-merge are not
>> supported and are surfaced as explicit errors at the dispatch point.
>> The "split" sub-command of `git history` continues to refuse when
>> the targeted commit is itself a merge: split semantics do not apply
>> to merges. The pre-walk gate in `builtin/history.c` that previously
>> rejected any merge in the rewrite path now only rejects octopus
>> merges; rename it accordingly.
>>
>> A small refactor in `create_commit()` makes the merge case possible:
>> the helper now takes a `struct commit_list *parents` rather than a
>> single parent pointer and takes ownership of the list. The single
>> existing caller in `pick_regular_commit()` builds and passes a
>> one-element list; the new `pick_merge_commit()` builds a two-element
>> list, with the order of the `from` and `merge` parents preserved.
>>
>> Update the negative expectations in t3451, t3452 and t3650 that were
>> asserting the now-retired "not supported yet" message, replacing
>> them with positive coverage where it fits. Octopus rejection and
>> revert-of-merge rejection are covered by new positive tests in
>> t3650. A dedicated test script with merge-replay scenarios driven by
>> a new test-tool fixture builder will follow in a subsequent commit.
>>
>> [1] https://github.com/newren/git/blob/replay/replay-design-notes.txt
>> [2] https://github.com/newren/git/
>> commit/4c45e8955ef9bf7d01fd15d9106b3bdb8ea91b45
>>
>> Helped-by: Elijah Newren <newren@gmail.com>
>> Assisted-by: Claude Opus 4.7
>> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
>> ---
>> builtin/history.c | 16 ++-
>> replay.c | 209 ++++++++++++++++++++++++++++++++++++--
>> t/t3451-history-reword.sh | 21 ++--
>> t/t3452-history-split.sh | 6 +-
>> t/t3650-replay-basics.sh | 46 ++++++++-
>> 5 files changed, 269 insertions(+), 29 deletions(-)
>>
>> diff --git a/builtin/history.c b/builtin/history.c
>> index 9526938085..00097b2226 100644
>> --- a/builtin/history.c
>> +++ b/builtin/history.c
>> @@ -195,15 +195,15 @@ static int parse_ref_action(const struct option
>> *opt, const char *value, int uns
>> return 0;
>> }
>> -static int revwalk_contains_merges(struct repository *repo,
>> - const struct strvec *revwalk_args)
>> +static int revwalk_contains_octopus_merges(struct repository *repo,
>> + const struct strvec *revwalk_args)
>> {
>> struct strvec args = STRVEC_INIT;
>> struct rev_info revs;
>> int ret;
>> strvec_pushv(&args, revwalk_args->v);
>> - strvec_push(&args, "--min-parents=2");
>> + strvec_push(&args, "--min-parents=3");
>> repo_init_revisions(repo, &revs, NULL);
>> @@ -217,7 +217,7 @@ static int revwalk_contains_merges(struct
>> repository *repo,
>> }
>> if (get_revision(&revs)) {
>> - ret = error(_("replaying merge commits is not supported yet!"));
>> + ret = error(_("replaying octopus merges is not supported"));
>> goto out;
>> }
>> @@ -289,7 +289,7 @@ static int setup_revwalk(struct repository *repo,
>> strvec_push(&args, "HEAD");
>> }
>> - ret = revwalk_contains_merges(repo, &args);
>> + ret = revwalk_contains_octopus_merges(repo, &args);
>> if (ret < 0)
>> goto out;
>> @@ -482,6 +482,9 @@ static int cmd_history_reword(int argc,
>> if (ret < 0) {
>> ret = error(_("failed replaying descendants"));
>> goto out;
>> + } else if (ret) {
>> + ret = error(_("conflict during replay; some descendants were
>> not rewritten"));
>> + goto out;
>> }
>> ret = 0;
>> @@ -721,6 +724,9 @@ static int cmd_history_split(int argc,
>> if (ret < 0) {
>> ret = error(_("failed replaying descendants"));
>> goto out;
>> + } else if (ret) {
>> + ret = error(_("conflict during replay; some descendants were
>> not rewritten"));
>> + goto out;
>> }
>> ret = 0;
>> diff --git a/replay.c b/replay.c
>> index f96f1f6551..3dbce095f9 100644
>> --- a/replay.c
>> +++ b/replay.c
>> @@ -1,6 +1,7 @@
>> #define USE_THE_REPOSITORY_VARIABLE
>> #include "git-compat-util.h"
>> +#include "commit-reach.h"
>> #include "environment.h"
>> #include "hex.h"
>> #include "merge-ort.h"
>> @@ -77,15 +78,21 @@ static void generate_revert_message(struct strbuf
>> *msg,
>> repo_unuse_commit_buffer(repo, commit, message);
>> }
>> +/*
>> + * Build a new commit with the given tree and parent list, copying
>> author,
>> + * extra headers and (for pick mode) the commit message from `based_on`.
>> + *
>> + * Takes ownership of `parents`: it will be freed before returning,
>> even on
>> + * error. Parent order is preserved as supplied by the caller.
>> + */
>> static struct commit *create_commit(struct repository *repo,
>> struct tree *tree,
>> struct commit *based_on,
>> - struct commit *parent,
>> + struct commit_list *parents,
>> enum replay_mode mode)
>> {
>> struct object_id ret;
>> struct object *obj = NULL;
>> - struct commit_list *parents = NULL;
>> char *author = NULL;
>> char *sign_commit = NULL; /* FIXME: cli users might want to sign
>> again */
>> struct commit_extra_header *extra = NULL;
>> @@ -96,7 +103,6 @@ static struct commit *create_commit(struct
>> repository *repo,
>> const char *orig_message = NULL;
>> const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
>> - commit_list_insert(parent, &parents);
>> extra = read_commit_extra_headers(based_on, exclude_gpgsig);
>> if (mode == REPLAY_MODE_REVERT) {
>> generate_revert_message(&msg, based_on, repo);
>> @@ -273,6 +279,7 @@ static struct commit *pick_regular_commit(struct
>> repository *repo,
>> {
>> struct commit *base, *replayed_base;
>> struct tree *pickme_tree, *base_tree, *replayed_base_tree;
>> + struct commit_list *parents = NULL;
>> if (pickme->parents) {
>> base = pickme->parents->item;
>> @@ -327,7 +334,143 @@ static struct commit *pick_regular_commit(struct
>> repository *repo,
>> if (oideq(&replayed_base_tree->object.oid, &result->tree-
>> >object.oid) &&
>> !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
>> return replayed_base;
>> - return create_commit(repo, result->tree, pickme, replayed_base,
>> mode);
>> + commit_list_insert(replayed_base, &parents);
>> + return create_commit(repo, result->tree, pickme, parents, mode);
>> +}
>> +
>> +/*
>> + * Replay a 2-parent merge commit by composing three calls into
>> merge-ort:
>> + *
>> + * R = recursive merge of pickme's two original parents (auto-
>> remerge of
>> + * the original merge, accepting any conflicts)
>> + * N = recursive merge of the (possibly rewritten) parents
>> + * O = pickme's tree (the user's actual merge, including any manual
>> + * resolutions)
>> + *
>> + * The picked tree comes from a non-recursive merge using R as the base,
>> + * O as side1 and N as side2. `git diff R O` is morally `git show
>> + * --remerge-diff $oldmerge`, so this layers the user's original manual
>> + * resolution on top of the freshly auto-merged rewritten parents (see
>> + * `replay-design-notes.txt` on the `replay` branch of newren/git).
>> + *
>> + * If the outer 3-way merge is unclean, propagate the conflict status to
>> + * the caller via `result->clean = 0` and return NULL. The two inner
>> + * merges (R and N) being unclean is _not_ fatal: the conflict-markered
>> + * trees they produce are valid inputs to the outer merge, and using
>> + * identical labels for both inner merges keeps the marker text
>> + * byte-equal between R and N so the user's resolution recorded in O
>> + * collapses the conflict cleanly there. Octopus merges (more than two
>> + * parents) and revert-of-merge are rejected by the caller before this
>> + * function is invoked.
>> + */
>> +static struct commit *pick_merge_commit(struct repository *repo,
>> + struct commit *pickme,
>> + kh_oid_map_t *replayed_commits,
>> + struct merge_options *merge_opt,
>> + struct merge_result *result)
>> +{
>> + struct commit *parent1, *parent2;
>> + struct commit *replayed_par1, *replayed_par2;
>> + struct tree *pickme_tree;
>> + struct merge_options remerge_opt = { 0 };
>> + struct merge_options new_merge_opt = { 0 };
>> + struct merge_result remerge_res = { 0 };
>> + struct merge_result new_merge_res = { 0 };
>> + struct commit_list *parent_bases = NULL;
>> + struct commit_list *replayed_bases = NULL;
>> + struct commit_list *parents;
>> + struct commit *picked = NULL;
>> + char *ancestor_name = NULL;
>> +
>> + parent1 = pickme->parents->item;
>> + parent2 = pickme->parents->next->item;
>> +
>> + /*
>> + * Map the merge's parents to their replayed counterparts. With the
>> + * boundary commits pre-seeded into `replayed_commits`, every parent
>> + * either has an explicit mapping (rewritten or boundary -> onto) or
>> + * sits outside the rewrite range entirely; the latter must stay at
>> + * the original parent commit, so use `parent` itself as the
>> fallback
>> + * for both sides.
>> + */
>> + replayed_par1 = mapped_commit(replayed_commits, parent1, parent1);
>> + replayed_par2 = mapped_commit(replayed_commits, parent2, parent2);
>> +
>> + /*
>> + * R: auto-remerge of the original parents.
>> + *
>> + * Use the same branch labels for the inner merges that compute R
>> + * and N so conflict markers (if any) are textually identical
>> + * between the two; the outer non-recursive merge can then collapse
>> + * the manual resolution from O against them.
>> + */
>> + init_basic_merge_options(&remerge_opt, repo);
>> + remerge_opt.show_rename_progress = 0;
>> + remerge_opt.branch1 = "ours";
>> + remerge_opt.branch2 = "theirs";
>> + if (repo_get_merge_bases(repo, parent1, parent2, &parent_bases) <
>> 0) {
>> + result->clean = -1;
>> + goto out;
>> + }
>> + merge_incore_recursive(&remerge_opt, parent_bases,
>> + parent1, parent2, &remerge_res);
>> + parent_bases = NULL; /* consumed by merge_incore_recursive */
>> + if (remerge_res.clean < 0) {
>> + result->clean = remerge_res.clean;
>> + goto out;
>> + }
>> +
>> + /* N: fresh merge of the (possibly rewritten) parents. */
>> + init_basic_merge_options(&new_merge_opt, repo);
>> + new_merge_opt.show_rename_progress = 0;
>> + new_merge_opt.branch1 = "ours";
>> + new_merge_opt.branch2 = "theirs";
>> + if (repo_get_merge_bases(repo, replayed_par1, replayed_par2,
>> + &replayed_bases) < 0) {
>> + result->clean = -1;
>> + goto out;
>> + }
>> + merge_incore_recursive(&new_merge_opt, replayed_bases,
>> + replayed_par1, replayed_par2, &new_merge_res);
>> + replayed_bases = NULL; /* consumed by merge_incore_recursive */
>> + if (new_merge_res.clean < 0) {
>> + result->clean = new_merge_res.clean;
>> + goto out;
>> + }
>> +
>> + /*
>> + * Outer non-recursive merge: base=R, side1=O (pickme), side2=N.
>> + */
>> + pickme_tree = repo_get_commit_tree(repo, pickme);
>> + ancestor_name = xstrfmt("auto-remerge of %s",
>> + oid_to_hex(&pickme->object.oid));
>> + merge_opt->ancestor = ancestor_name;
>> + merge_opt->branch1 = short_commit_name(repo, pickme);
>> + merge_opt->branch2 = "merge of replayed parents";
>> + merge_incore_nonrecursive(merge_opt,
>> + remerge_res.tree,
>> + pickme_tree,
>> + new_merge_res.tree,
>> + result);
>> + merge_opt->ancestor = NULL;
>> + merge_opt->branch1 = NULL;
>> + merge_opt->branch2 = NULL;
>> + if (!result->clean)
>> + goto out;
>> +
>> + parents = NULL;
>> + commit_list_insert(replayed_par2, &parents);
>> + commit_list_insert(replayed_par1, &parents);
>> + picked = create_commit(repo, result->tree, pickme, parents,
>> + REPLAY_MODE_PICK);
>> +
>> +out:
>> + free(ancestor_name);
>> + free_commit_list(parent_bases);
>> + free_commit_list(replayed_bases);
>> + merge_finalize(&remerge_opt, &remerge_res);
>> + merge_finalize(&new_merge_opt, &new_merge_res);
>> + return picked;
>> }
>> void replay_result_release(struct replay_result *result)
>> @@ -407,17 +550,63 @@ int replay_revisions(struct rev_info *revs,
>> merge_opt.show_rename_progress = 0;
>> last_commit = onto;
>> replayed_commits = kh_init_oid_map();
>> +
>> + /*
>> + * Seed the rewritten-commit map with each negative-side ("BOTTOM")
>> + * cmdline entry pointing at `onto`. This matters for merge replay:
>> + * a 2-parent merge whose first parent is the boundary (e.g. the
>> + * commit being reworded) must replay onto the rewritten boundary,
>> + * yet pick_merge_commit uses a self fallback so the second parent
>> + * (a side branch outside the rewrite range) is preserved as-is.
>> + * Pre-seeding the boundary disambiguates the two: in the map ->
>> + * rewritten, missing -> kept as-is.
>> + *
>> + * Only do this for the pick path; revert mode chains reverts
>> + * through last_commit and a pre-seeded boundary would short-circuit
>> + * that chain.
>> + */
>> + if (mode == REPLAY_MODE_PICK) {
>> + for (size_t i = 0; i < revs->cmdline.nr; i++) {
>> + struct rev_cmdline_entry *e = &revs->cmdline.rev[i];
>> + struct commit *boundary;
>> + khint_t pos;
>> + int hr;
>> +
>> + if (!(e->flags & BOTTOM))
>> + continue;
>> + boundary = lookup_commit_reference_gently(revs->repo,
>> + &e->item->oid, 1);
>> + if (!boundary)
>> + continue;
>> + pos = kh_put_oid_map(replayed_commits,
>> + boundary->object.oid, &hr);
>> + if (hr != 0)
>> + kh_value(replayed_commits, pos) = onto;
>> + }
>> + }
>> +
>> while ((commit = get_revision(revs))) {
>> const struct name_decoration *decoration;
>> khint_t pos;
>> int hr;
>> - if (commit->parents && commit->parents->next)
>> - die(_("replaying merge commits is not supported yet!"));
>> -
>> - last_commit = pick_regular_commit(revs->repo, commit,
>> replayed_commits,
>> - mode == REPLAY_MODE_REVERT ? last_commit :
>> onto,
>> - &merge_opt, &result, mode);
>> + if (commit->parents && commit->parents->next) {
>> + if (commit->parents->next->next) {
>> + ret = error(_("replaying octopus merges is not
>> supported"));
>> + goto out;
>> + }
>> + if (mode == REPLAY_MODE_REVERT) {
>> + ret = error(_("reverting merge commits is not
>> supported"));
>> + goto out;
>> + }
>> + last_commit = pick_merge_commit(revs->repo, commit,
>> + replayed_commits,
>> + &merge_opt, &result);
>> + } else {
>> + last_commit = pick_regular_commit(revs->repo, commit,
>> replayed_commits,
>> + mode == REPLAY_MODE_REVERT ?
>> last_commit : onto,
>> + &merge_opt, &result, mode);
>> + }
>> if (!last_commit)
>> break;
>> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
>> index de7b357685..d103f866a2 100755
>> --- a/t/t3451-history-reword.sh
>> +++ b/t/t3451-history-reword.sh
>> @@ -201,12 +201,21 @@ test_expect_success 'can reword a merge commit' '
>> git switch - &&
>> git merge theirs &&
>> - # It is not possible to replay merge commits embedded in the
>> - # history (yet).
>> - test_must_fail git -c core.editor=false history reword HEAD~
>> 2>err &&
>> - test_grep "replaying merge commits is not supported yet" err &&
>> + # Reword a non-merge commit whose descendants include the
>> + # merge: replay carries the merge through.
>> + reword_with_message HEAD~ <<-EOF &&
>> + ours reworded
>> + EOF
>> + expect_graph <<-EOF &&
>> + * Merge tag ${SQ}theirs${SQ}
>> + |\\
>> + | * theirs
>> + * | ours reworded
>> + |/
>> + * base
>> + EOF
>> - # But it is possible to reword a merge commit directly.
>> + # And reword a merge commit directly.
>> reword_with_message HEAD <<-EOF &&
>> Reworded merge commit
>> EOF
>> @@ -214,7 +223,7 @@ test_expect_success 'can reword a merge commit' '
>> * Reworded merge commit
>> |\
>> | * theirs
>> - * | ours
>> + * | ours reworded
>> |/
>> * base
>> EOF
>> diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
>> index 8ed0cebb50..ad6309f98b 100755
>> --- a/t/t3452-history-split.sh
>> +++ b/t/t3452-history-split.sh
>> @@ -36,7 +36,7 @@ expect_tree_entries () {
>> test_cmp expect actual
>> }
>> -test_expect_success 'refuses to work with merge commits' '
>> +test_expect_success 'refuses to split a merge commit' '
>> test_when_finished "rm -rf repo" &&
>> git init repo &&
>> (
>> @@ -49,9 +49,7 @@ test_expect_success 'refuses to work with merge
>> commits' '
>> git switch - &&
>> git merge theirs &&
>> test_must_fail git history split HEAD 2>err &&
>> - test_grep "cannot split up merge commit" err &&
>> - test_must_fail git history split HEAD~ 2>err &&
>> - test_grep "replaying merge commits is not supported yet" err
>> + test_grep "cannot split up merge commit" err
>> )
>> '
>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index 3353bc4a4d..368b1b0f9a 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -103,10 +103,48 @@ test_expect_success 'cannot advance target ...
>> ordering would be ill-defined' '
>> test_cmp expect actual
>> '
>> -test_expect_success 'replaying merge commits is not supported yet' '
>> - echo "fatal: replaying merge commits is not supported yet!"
>> >expect &&
>> - test_must_fail git replay --advance=main main..topic-with-merge
>> 2>actual &&
>> - test_cmp expect actual
>> +test_expect_success 'using replay to rebase a 2-parent merge' '
>> + # main..topic-with-merge contains a 2-parent merge (P) introduced
>> + # via test_merge. Use --ref-action=print so this test does not
>> + # mutate state for subsequent tests in this file.
>> + git replay --ref-action=print --onto main main..topic-with-merge
>> >result &&
>> + test_line_count = 1 result &&
>> +
>> + new_tip=$(cut -f 3 -d " " result) &&
>> +
>> + # Result is still a 2-parent merge.
>> + git cat-file -p $new_tip >cat &&
>> + grep -c "^parent " cat >count &&
>> + echo 2 >expect &&
>> + test_cmp expect count &&
>> +
>> + # Merge subject is preserved.
>> + echo P >expect &&
>> + git log -1 --format=%s $new_tip >actual &&
>> + test_cmp expect actual &&
>> +
>> + # The replayed merge sits on top of main: walking back via the
>> + # first-parent chain reaches main.
>> + git merge-base --is-ancestor main $new_tip
>> +'
>> +
>> +test_expect_success 'replaying an octopus merge is rejected' '
>> + # Build an octopus side-branch so the rest of the test state stays
>> + # untouched.
>> + test_when_finished "git update-ref -d refs/heads/octopus-tip" &&
>> + octopus_tip=$(git commit-tree -p topic4 -p topic1 -p topic3 \
>> + -m "octopus" $(git rev-parse topic4^{tree})) &&
>> + git update-ref refs/heads/octopus-tip "$octopus_tip" &&
>> +
>> + test_must_fail git replay --ref-action=print --onto main \
>> + topic4..octopus-tip 2>actual &&
>> + test_grep "octopus merges" actual
>> +'
>> +
>> +test_expect_success 'reverting a merge commit is rejected' '
>> + test_must_fail git replay --ref-action=print --revert=topic-with-
>> merge \
>> + topic4..topic-with-merge 2>actual &&
>> + test_grep "reverting merge commits" actual
>> '
>> test_expect_success 'using replay to rebase two branches, one on top
>> of other' '
>
next prev parent reply other threads:[~2026-05-08 10:05 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-06 22:43 [PATCH/RFC 0/5] replay: support replaying 2-parent merges Johannes Schindelin via GitGitGadget
2026-05-06 22:43 ` [PATCH/RFC 1/5] " Johannes Schindelin via GitGitGadget
2026-05-08 9:36 ` Phillip Wood
2026-05-08 10:05 ` Phillip Wood [this message]
2026-05-06 22:43 ` [PATCH/RFC 2/5] replay: short-circuit merge replay when parent and base trees are unchanged Johannes Schindelin via GitGitGadget
2026-05-06 22:43 ` [PATCH/RFC 3/5] history.adoc: describe merge-replay support and its limits Johannes Schindelin via GitGitGadget
2026-05-06 22:43 ` [PATCH/RFC 4/5] test-tool: add a "historian" subcommand for building merge fixtures Johannes Schindelin via GitGitGadget
2026-05-12 10:54 ` Toon Claes
2026-05-06 22:43 ` [PATCH/RFC 5/5] t3454: cover merge-replay scenarios with the historian helper Johannes Schindelin via GitGitGadget
2026-05-07 14:14 ` [PATCH/RFC 0/5] replay: support replaying 2-parent merges D. Ben Knoble
2026-05-07 15:06 ` Johannes Schindelin
2026-05-07 15:39 ` Ben Knoble
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=3dd21593-9945-4f9c-a9a0-f5c66504da49@gmail.com \
--to=phillip.wood123@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitgitgadget@gmail.com \
--cc=johannes.schindelin@gmx.de \
--cc=newren@gmail.com \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox