From: Toon Claes <toon@iotcl.com>
To: Elijah Newren <newren@gmail.com>
Cc: git@vger.kernel.org, Johannes Schindelin <Johannes.Schindelin@gmx.de>
Subject: Re: [PATCH v2 3/3] replay: offer an option to linearize the commit topology
Date: Tue, 16 Jun 2026 09:09:16 +0200 [thread overview]
Message-ID: <874ij3ynab.fsf@emacs.iotcl.com> (raw)
In-Reply-To: <CABPp-BGRi2obnqRGEY9pSMyvRbNGs8AdVUpZmr0C6vZSgHb=cg@mail.gmail.com>
Elijah Newren <newren@gmail.com> writes:
> Hi,
>
> On Wed, Jun 10, 2026 at 7:51 AM Toon Claes <toon@iotcl.com> wrote:
>>
>> From: Johannes Schindelin <Johannes.Schindelin@gmx.de>
>>
>> One of the stated goals of git-replay(1) is to allow implementing the
>> git-rebase(1) functionality on the server side.
>>
>> The default mode of git-rebase(1) is to act as if `--no-rebase-merges`
>> was given. This mode drops merge commits instead of replaying them, and
>> linearizes the commit history into a sequence of the
>> regular (single-parent) commits.
>>
>> Add option `--linearize` to git-replay(1) to do the same.
>
> I think this version is nicer overall than the one from my
> replay-upstream branch; sorry for repeatedly getting distracted from
> that, but this does look nice.
Dscho gets most of the credit here. And don't worry about being
distracted, we know how things go around here. I appreciate the review!
>
> A few small comments:
>
>> Co-authored-by: Toon Claes <toon@iotcl.com>
>> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
>> Signed-off-by: Toon Claes <toon@iotcl.com>
>> ---
>> Documentation/git-replay.adoc | 5 +++++
>> builtin/replay.c | 4 ++++
>> replay.c | 30 +++++++++++++++++++++++-------
>> replay.h | 5 +++++
>> t/t3650-replay-basics.sh | 26 ++++++++++++++++++++++++++
>> 5 files changed, 63 insertions(+), 7 deletions(-)
>>
>> diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc
>> index a32f72aead..41c96c7061 100644
>> --- a/Documentation/git-replay.adoc
>> +++ b/Documentation/git-replay.adoc
>> @@ -88,6 +88,11 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
>> +
>> The default mode can be configured via the `replay.refAction` configuration variable.
>>
>> +--linearize::
>> + In this mode, `git replay` imitates `git rebase --no-rebase-merges`,
>> + i.e. it cherry-picks only non-merge commits, each one on top of the
>> + previous one.
>
> The SYNOPSIS block at the top of the file is missing this new flag.
>
> The replay_usage[] variable in cmd_replay is also missing this new flag.
>
>> <revision-range>::
>> Range of commits to replay; see "Specifying Ranges" in
>> linkgit:git-rev-parse[1]. In `--advance=<branch>` or
>> diff --git a/builtin/replay.c b/builtin/replay.c
>> index 39e3a86f6c..fedfe46dc6 100644
>> --- a/builtin/replay.c
>> +++ b/builtin/replay.c
>> @@ -111,6 +111,8 @@ int cmd_replay(int argc,
>> N_("mode"),
>> N_("control ref update behavior (update|print)"),
>> PARSE_OPT_NONEG),
>> + OPT_BOOL(0, "linearize", &opts.linearize,
>> + N_("ignore merge commits instead of replaying them")),
>
> "ignore" feels a bit ambiguous to me. Can we use "drop" instead,
> matching your commit message?
Agreed, I don't like it too. "drop" sounds better.
>> OPT_END()
>> };
>>
>> @@ -132,6 +134,8 @@ int cmd_replay(int argc,
>> opts.contained, "--contained");
>> die_for_incompatible_opt2(!!opts.ref, "--ref",
>> !!opts.contained, "--contained");
>> + die_for_incompatible_opt2(!!opts.revert, "--revert",
>> + opts.linearize, "--linearize");
>
> Sensible; should the docs mention this incompatibility? (I'm not sure
> myself; just throwing it out as food for thought.)
Let's add it.
>>
>> /* 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 7921d7dba3..81033fb889 100644
>> --- a/replay.c
>> +++ b/replay.c
>> @@ -277,12 +277,16 @@ static struct commit *pick_regular_commit(struct repository *repo,
>> struct commit *onto,
>> struct merge_options *merge_opt,
>> struct merge_result *result,
>> + struct commit *replayed_base,
>> bool reverse,
>> enum replay_empty_commit_action empty)
>> {
>> - struct commit *base, *replayed_base;
>> + struct commit *base;
>> struct tree *pickme_tree, *base_tree, *replayed_base_tree;
>>
>> + if (replayed_base && reverse)
>> + BUG("Linearizing commits is not supported when replaying in reverse");
>> +
>
> This is dead code given the die_for_incompatible_opt2 check above,
> right? Just extra defense in depth?
We also have another defense-in-depth for --onto/--advance/--revert:
BUG("expected one of onto_name, *advance_name, or *revert_name");
I don't mind having it for --linearize too.
>> if (pickme->parents) {
>> base = pickme->parents->item;
>> base_tree = repo_get_commit_tree(repo, base);
>> @@ -291,7 +295,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
>> base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
>> }
>>
>> - replayed_base = get_mapped_commit(replayed_commits, base, onto);
>> + if (!replayed_base)
>> + replayed_base = get_mapped_commit(replayed_commits, base, onto);
>> replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
>> pickme_tree = repo_get_commit_tree(repo, pickme);
>>
>> @@ -430,12 +435,23 @@ int replay_revisions(struct rev_info *revs,
>> while ((commit = get_revision(revs))) {
>> const struct name_decoration *decoration;
>>
>> - if (commit->parents && commit->parents->next)
>> - die(_("replaying merge commits is not supported yet!"));
>> + if (commit->parents && commit->parents->next) {
>> + if (!opts->linearize)
>> + die(_("replaying merge commits is not supported yet!"));
>> + /*
>> + * When linearizing, a merge commit itself is not picked,
>> + * but refs that point to it might need updating.
>> + */
>
> Is it worth pointing out that last_commit is intentionally not updated
> by this code path? That is implied by your comment, but it takes a
> bit of reasoning to get there, and I think it might help future
> readers to just explicitly state it.
Ah yes, I didn't realize that, but you make a good point. I'll rephrase
the comment a bit.
>> + } else {
>> + struct commit *to_pick = reverse ? last_commit : onto;
>> + last_commit =
>> + pick_regular_commit(revs->repo, commit,
>> + replayed_commits, to_pick,
>> + &merge_opt, &result,
>> + opts->linearize ? last_commit : NULL,
>> + reverse, opts->empty);
>> + }
>>
>> - last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
>> - reverse ? last_commit : onto,
>> - &merge_opt, &result, reverse, opts->empty);
>> if (!last_commit)
>> break;
>>
>> diff --git a/replay.h b/replay.h
>> index 1851a07705..07e6fdcca3 100644
>> --- a/replay.h
>> +++ b/replay.h
>> @@ -62,6 +62,11 @@ struct replay_revisions_options {
>> * Defaults to REPLAY_EMPTY_COMMIT_DROP.
>> */
>> enum replay_empty_commit_action empty;
>> +
>> + /*
>> + * Whether to linearize the commits (i.e. drop merge commits).
>> + */
>> + int linearize;
>> };
>>
>> /* This struct is used as an out-parameter by `replay_revisions()`. */
>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index 3353bc4a4d..64e0731188 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -565,4 +565,30 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' '
>> test_grep "cannot be used with multiple revision ranges" err
>> '
>>
>> +test_expect_success 'replay merge commit fails' '
>> + echo "fatal: replaying merge commits is not supported yet!" >expect &&
>> + test_must_fail git replay --ref-action=print --onto main I..P 2>actual &&
>> + test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'replay to rebase merge commit with --linearize' '
>> + git replay --ref-action=print --linearize --onto main I..topic-with-merge >result &&
>> +
>> + test_line_count = 1 result &&
>> +
>> + git log --format=%s $(cut -f 3 -d " " result) >actual &&
>> + test_write_lines O N J M L B A >expect &&
>> + test_cmp expect actual
>> +'
>> +
>> +test_expect_success 'replay to rebase merge commit with --linearize down to root commit' '
>> + git replay --ref-action=print --linearize --onto main A..topic-with-merge >result &&
>
> You'd need to drop "A.." to have it go down to the root commit, as
> Junio mentioned elsewhere.
Yes, thanks for double confirmation.
>> +
>> + test_line_count = 1 result &&
>> +
>> + git log --format=%s $(cut -f 3 -d " " result) >actual &&
>> + test_write_lines O N J I M L B A >expect &&
>> + test_cmp expect actual
>> +'
>> +
>> test_done
>
> Should there also be a testcase combining --linearize and --advance?
Sure.
> Should there be a test with the incompatibility of --revert &
> --linearize? I think we have a few other tests for incompatible
> options.
I was already about to add that.
> One additional testing idea, borrowed from an older variant of
> this patch I had sitting in a local branch (dscho's original
> linearize patch, adapted): in addition to checking specific commit
> subjects, it's worth verifying that the linearized chain produces
> the *same patches* as the original. Something along the lines of:
>
> test_expect_success '--linearize preserves patches' '
> test_when_finished "git update-ref -d refs/heads/merge_I_L" &&
> test_tick &&
> git checkout -b merge_I_L I &&
> git merge --no-edit L &&
>
> git replay --linearize --onto A B..merge_I_L &&
>
> # range-diff ignores merges, so the original
> # {I, L, merge} reduces to {I, L} on the LHS,
> # and the replayed chain on the RHS should match.
> git range-diff B..merge_I_L@{1} B..merge_I_L >out &&
> ! test_grep -v "=" out &&
>
> git log --oneline A..merge_I_L >out &&
> test_line_count = 2 out
> '
>
> The range-diff check is nice because it asserts patch equivalence
> rather than tying the test to a particular replay ordering, which
> makes the test less brittle if the rev-walk order ever changes.
> Feel free to take, adapt, or ignore.
Interesting idea and I like it. Lemme add it.
> Anyway, thanks for working on this; looking good.
Thanks!
--
Cheers,
Toon
next prev parent reply other threads:[~2026-06-16 7:09 UTC|newest]
Thread overview: 20+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-08 18:37 [PATCH 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-08 18:37 ` [PATCH 1/3] replay: refactor enum replay_mode into a bool Toon Claes
2026-06-08 18:37 ` [PATCH 2/3] replay: add helper to put entry into mapped_commits Toon Claes
2026-06-08 18:37 ` [PATCH 3/3] replay: offer an option to linearize the commit topology Toon Claes
2026-06-08 19:29 ` Junio C Hamano
2026-06-10 14:26 ` Toon Claes
2026-06-10 14:49 ` [PATCH v2 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-10 14:49 ` [PATCH v2 1/3] replay: refactor enum replay_mode into a bool Toon Claes
2026-06-11 15:09 ` Justin Tobler
2026-06-12 8:19 ` Toon Claes
2026-06-10 14:49 ` [PATCH v2 2/3] replay: add helper to put entry into mapped_commits Toon Claes
2026-06-10 14:49 ` [PATCH v2 3/3] replay: offer an option to linearize the commit topology Toon Claes
2026-06-10 17:02 ` Junio C Hamano
2026-06-16 8:38 ` Toon Claes
2026-06-14 6:56 ` Elijah Newren
2026-06-16 7:09 ` Toon Claes [this message]
2026-06-16 9:26 ` [PATCH v3 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-16 9:26 ` [PATCH v3 1/3] replay: refactor enum replay_mode into a bool Toon Claes
2026-06-16 9:26 ` [PATCH v3 2/3] replay: add helper to put entry into mapped_commits Toon Claes
2026-06-16 9:26 ` [PATCH v3 3/3] replay: offer an option to linearize the commit topology Toon Claes
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=874ij3ynab.fsf@emacs.iotcl.com \
--to=toon@iotcl.com \
--cc=Johannes.Schindelin@gmx.de \
--cc=git@vger.kernel.org \
--cc=newren@gmail.com \
/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