All of lore.kernel.org
 help / color / mirror / Atom feed
From: Toon Claes <toon@iotcl.com>
To: Junio C Hamano <gitster@pobox.com>
Cc: git@vger.kernel.org, Elijah Newren <newren@gmail.com>,
	Johannes Schindelin <Johannes.Schindelin@gmx.de>
Subject: Re: [PATCH v5 3/3] replay: offer an option to linearize the commit topology
Date: Wed, 01 Jul 2026 10:50:41 +0200	[thread overview]
Message-ID: <874iij3xge.fsf@emacs.iotcl.com> (raw)
In-Reply-To: <xmqq5x358byf.fsf@gitster.g>

Junio C Hamano <gitster@pobox.com> writes:

> Toon Claes <toon@iotcl.com> writes:
>
>>  Documentation/git-replay.adoc |  8 ++++-
>>  builtin/replay.c              |  6 +++-
>>  replay.c                      | 50 ++++++++++++++++----------
>>  replay.h                      |  5 +++
>>  t/t3650-replay-basics.sh      | 84 ++++++++++++++++++++++++++++++++++++++++++-
>>  5 files changed, 132 insertions(+), 21 deletions(-)
>
> "replay --linearize" behaves differently from the flattening rebase
> in a case where X and Y that forked from A are merged at Z, and we
> ask to flatten the history leading to Z, doesn't it?
>
>      A----X
>       \    \
>        Y----Z (tip)
>
> A typical flattening rebase would rewrite X to X', Y to Y', while
> dropping Z, and would leave us a flattened history, like
>
>      A---X'---Y' (updated tip, the order of X' and Y' may be swapped)
>
> I may be misreading the logic, but doesn't "replay --linearize"
> instead produce
>
>      A----X' (dangling)
>       \ 
>        Y' (tip -- Z is dropped and gets mapped)
>
> and leave X' dangling (or Y'; the point is that only one of them
> will survive), never incorporating it in the resulting history?

You bring up a good point here, and it is very similar to what Dscho
brought up[1].

When I started working on v5, I realized multiple tips can be passed to
git-replay(1) and the code in v1-v4 would replay all commits into a
single linear history. I assumed that's not what we want.

In my mind, replaying unrelated histories with --onto and --linearize
should remain unrelated. Also assuming the --linearize option would only
linearize merge commits.

Looking at less obvious situations, like your example above, things
aren't really that simple. And I agree the new Y' tip is not correct and
X' shouldn't be dangling.

On the other hand though, if there was a branch pointing to X, we still
need a piece of history that has X', but doesn't have Y'. In your
flattened history X' isn't a descendant of Y', but the order may be
swapped and we would need to create something like:

      A----X' (other tip)
       \
        Y'----X' (tip)

That's quite complex trying to achieve something like that in code. In
short, --linearize will change the topology of the (merge) commits, and
we have to do this in a predictable way. Thus I'm currently leaning
toward bringing v1-v4 behavior back and linearize all commits in to a
single line when using --linearize. Meaning:

        B----C (other tip)
       /
      A----X
       \
        Y----Z (tip)

  $ git replay --onto X --linearize Z C

Would result into:

      A----X----Y----Z----B----C
                               ^ (new other tip)
                     ^ (new tip)

This might not always be the expected behavior, especially when
replaying multiple branches at once. But to those I would suggest: don't
replay multiple branches at once.

But then again: Given the above example, you want to replay Z and C on
top of eachother, but don't want to rewrite the "(other tip)"? They have
I think two options:

  $ git replay --onto X --linearize --ref=tip Z C

By passing --ref we could tell git-replay(1) to only update that ref.
(sidenote: --ref currently cannot be combined with multiple revision
ranges, because that normally produces multiple tips and it would be
ambiguous which one --ref should point at. But --linearize collapses
everything into a single tip, so that ambiguity goes away; maybe we
should loosen the constraint in that case.)

Or:

  $ git replay --onto X --linearize Z^{commit} C

By peeling Z to a commit, git-replay(1) doesn't see it as a ref to
update.

Anyhow, a lot to unpack and I'll try to do my best in the next version
to cover that in the commit message, docs and test cases.

[1]: <f8b520d1-edeb-9e45-c503-025c8b5833c3@gmx.de>

>> +		if (commit->parents && commit->parents->next) {
>> +			if (!opts->linearize)
>> +				die(_("replaying merge commits is not supported yet!"));
>> +			/*
>> +			 * Drop the merge commit: do not pick it, leave
>> +			 * `last_commit` unchanged, and fall through to the
>> +			 * rest of the loop. As a result:
>> +			 * - the merge commit is mapped to `last_commit` in
>> +			 *   `replayed_commits`, this will become the parent for
>> +			 *   the child commits.
>> +			 * - refs previously pointing to the merge commit are
>> +			 *   rewritten to point to the previous non-merge commit.
>> +			 */
>> +		} else {
>> +			/*
>> +			 * pick_regular_commit() looks up the parent of `commit` in
>> +			 * `replayed_commits` to determine the ancestor to replay onto.
>> +			 * The `default_base` parameter is used when no ancestor is found,
>> +			 * which happens for the first commit in the revision range.
>> +			 * When reverting, commits are replayed in reverse order, so the
>> +			 * lookup never succeeds, and we need to pass `last_commit`.
>> +			 */
>> +			struct commit *base = onto;
>> +			if (mode == REPLAY_MODE_REVERT)
>> +				base = last_commit;
>> +
>> +			last_commit = pick_regular_commit(revs->repo, commit, base,
>> +							  replayed_commits,
>> +							  &merge_opt, &result,
>> +							  mode, opts->empty);
>> +		}
>> +
>>  		if (!last_commit)
>>  			break;
>
> Immediately after this hunk beyond the post-context are these lines.
>
> 		/* Record commit -> last_commit mapping */
> 		put_mapped_commit(replayed_commits, commit, last_commit);
>
> Let's imagine X gets processed first. X (and other commits on its
> branch) gets replayed, last_commit is set to X' (which is the
> rewritten X).  replayed_commits mapping holds X->X' mapping.
>
> Then let's imagine the history leading to Y is replayed next.
> last_commit becomes Y', and Y->Y' mapping is stored in
> replayed_commits.
>
> Finally, we see Z.  We are going to _drop_ it.  last_commit is left
> unchanged, pointing at Y'.  Then last_commit (i.e., Y') is used as
> the merge commit Z maps to (i.e., correctly dropping Z).
>
> Any descendants of Z, if any, will be grafted as descendants of Y'.
> If X did not have any descendants other than Z in the rewritten part
> of the history, then X' (and commits leading to it) would be lost,
> no?

I appreciate you're breaking down the code here.

> This "loss of the other branch" may be an inherent characteristic of
> this feature (i.e., I do not think it is necessarily a bug, and it
> may even be that the "bug" is in the way I am reading the patch),
> but then I wonder if the user may want to have control over which
> side branch should survive, perhaps?  It would probably need to be
> documented, and a test or two to cast this behaviour in stone.
>
>> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
>> index 3353bc4a4d..34c038eab9 100755
>> --- a/t/t3650-replay-basics.sh
>> +++ b/t/t3650-replay-basics.sh
>> @@ -52,8 +52,12 @@ test_expect_success 'setup' '
>
> The pre-context here has
>
> 	git switch --detach topic4 &&
> 	test_commit N &&
> 	test_commit O &&
> 	git switch -c topic-with-merge topic4 &&
>
>>  	test_merge P O --no-ff &&
>>  	git switch main &&
>
> The above does prepare topic-with-merge branch, but ...
>
>> +test_expect_success 'replay to rebase merge commit with --linearize' '
>> +	git replay --ref-action=print --linearize \
>> +		--onto main I..topic-with-merge >result &&
>
> ... this does not really exersize linearizing replay in a typical
> mergy history.  P merges O with --no-ff because otherwise there
> won't be a merge, since O is a descendant of the commit "test_merge
> P O" runs on (i.e., topic4 == topic-with-merge).
>
>     topic4 --- N --- O
>           \           \
>            .-----------P
>
> So, as long as O is replayed later than the parent of N (which is
> true), O' will be the surviving tip (corresponds to Y' that the
> dropped Z was mapped to in the earlier example), and nothing gets
> orphaned, I think.
>
> Perhaps a test to try a real merge may look something like this.
>
> diff --git c/t/t3650-replay-basics.sh w/t/t3650-replay-basics.sh
> index 34c038eab9..bb737f729a 100755
> --- c/t/t3650-replay-basics.sh
> +++ w/t/t3650-replay-basics.sh
> @@ -647,4 +647,37 @@ test_expect_success 'replay with --linearize to rebase multiple divergent branch
>  	test_cmp expect actual
>  '
>  
> +test_expect_success 'replay with --linearize of a divergent merge drops one branch' '
> +	git switch -c topic-divergent-base main &&
> +	test_commit base &&
> +	# Fork 1: base -> X
> +	git switch -c topic-divergent-x &&
> +	test_commit X &&
> +	# Fork 2: base -> Y
> +	git switch topic-divergent-base &&
> +	git switch -c topic-divergent-y &&
> +	test_commit Y &&
> +	# Merge them at Z
> +	git switch topic-divergent-x &&
> +	test_merge Z topic-divergent-y --no-ff &&
> +
> +	# History is now:
> +	#
> +	#       X - Z (topic-divergent-x)
> +	#      /   /
> +	#  base - Y
> +	#
> +
> +	git replay --ref-action=print --linearize \
> +		--onto main topic-divergent-base..topic-divergent-x >result &&
> +	test_line_count = 1 result &&
> +	tip=$(cut -f 3 -d " " result) &&
> +	# Get the commits replayed onto main
> +	git log --format=%s main..$tip >actual &&
> +	# We expect exactly one commit to be replayed (either X or Y)
> +	# because the other one is left dangling due to the merge being dropped.
> +	test_line_count = 1 actual &&
> +	test_grep "^[XY]$" actual
> +'
> +
>  test_done

Thank you for providing this test case.

-- 
Cheers,
Toon

  parent reply	other threads:[~2026-07-01  8:50 UTC|newest]

Thread overview: 48+ 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
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
2026-06-22 12:41     ` [PATCH v4 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-22 12:41       ` [PATCH v4 1/3] replay: refactor enum replay_mode into a bool Toon Claes
2026-06-22 13:53         ` Patrick Steinhardt
2026-06-22 15:43           ` Junio C Hamano
2026-06-24 19:15             ` Toon Claes
2026-06-22 12:41       ` [PATCH v4 2/3] replay: add helper to put entry into mapped_commits Toon Claes
2026-06-22 13:53         ` Patrick Steinhardt
2026-06-22 12:41       ` [PATCH v4 3/3] replay: offer an option to linearize the commit topology Toon Claes
2026-06-22 13:53         ` Patrick Steinhardt
2026-06-26  5:36           ` Toon Claes
2026-06-29  8:04             ` Patrick Steinhardt
2026-06-30  9:44               ` Johannes Schindelin
2026-06-30 11:32                 ` Patrick Steinhardt
2026-06-26  5:48       ` [PATCH v5 0/3] Teach git-replay(1) to linearize merge commits Toon Claes
2026-06-26  5:48         ` [PATCH v5 1/3] replay: add helper to put entry into mapped_commits Toon Claes
2026-06-26 16:50           ` Junio C Hamano
2026-06-26  5:48         ` [PATCH v5 2/3] replay: better explain how pick_regular_commit() picks a base Toon Claes
2026-06-26  5:48         ` [PATCH v5 3/3] replay: offer an option to linearize the commit topology Toon Claes
2026-06-26 17:10           ` Junio C Hamano
2026-06-27 13:44             ` Phillip Wood
2026-07-01  8:50             ` Toon Claes [this message]
2026-06-28 12:20         ` [PATCH v5 0/3] Teach git-replay(1) to linearize merge commits Johannes Schindelin
2026-06-30 13:42           ` Toon Claes
2026-07-02 17:58         ` [PATCH v6 " Toon Claes
2026-07-02 17:58           ` [PATCH v6 1/3] replay: add helper to put entry into replayed_commits Toon Claes
2026-07-02 17:58           ` [PATCH v6 2/3] replay: resolve the replay base outside pick_regular_commit() Toon Claes
2026-07-02 17:58           ` [PATCH v6 3/3] replay: offer an option to linearize the commit topology Toon Claes
2026-07-03 20:57             ` Junio C Hamano

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=874iij3xge.fsf@emacs.iotcl.com \
    --to=toon@iotcl.com \
    --cc=Johannes.Schindelin@gmx.de \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.