Re: [PATCH v7 3/3] replay: offer an option to linearize the commit topology
From: Elijah Newren <hidden>
Date: 2026-07-15 07:34:15
On Mon, Jul 13, 2026 at 3:09 PM Junio C Hamano [off-list ref] wrote:
Elijah Newren [off-list ref] writes:quoted
For what it's worth, looking back at the v5 thread, it seems the `base = last_commit` rule came in to fix the real bug Junio and Phillip pointed out there -- that without it, only one side of a linearized merge survived. That fix is clearly correct for the single-branch case. My worry is only that applying it unconditionally reintroduces the multiple-positive-refs ordering problem we deliberately avoid elsewhere. Making `--linearize` reject multiple positive refs would keep the merge-flattening fix while sidestepping this entirely.quoted
A user who wants to linearize ranges independently is advised to use separate git-replay(1) invocations.Which, to me, is another argument for just disallowing multiple positive refs under `--linearize`: if the recommended way to do it is separate invocations anyway, we may as well require them.Hmph. To me, this is slightly different. It acts more like an escape hatch: "if you really do not want to mix unrelated things into a single linear history, you can do this other thing." Stepping back, the unpredictable order of multiple merged lines of history exists even without multiple positive refs. If you have independent lines of development that were merged and you linearize them, someone must choose which line comes first. If you let the machinery make that decision, the resulting commit order may not reflect your preferences. While I rarely perform octopus merges anymore, in situations where an octopus merge is appropriate (e.g., when you have N independent branches and their merge order does not matter), linearizing such a history into a random sequence of N segments, built on top of one another in an unspecified order, could actually be considered a feature. You do not have to make a decision about something that is inconsequential.
You're right that when flattening merges within a single branch, the
machinery must pick an order, and that's fine — unavoidable, even. My
objection isn't that; it's primarily the concatenation of distinct
branches named on the command line into one chain, and, as a secondary
point, the ignoring of the order of branches explicitly specified by
the user on the command line.
Concretely: I have three branches to rebase onto master; one of them
happens to contain a merge I'd like flattened. I add --linearize for
that one merge — and now all three branches are silently concatenated
into a single chain. That makes no sense to me, and I think won't to
most users.
Anyway, I think I must have explained my position rather poorly; your
response suggests I buried my main points, so let me try to restate
them:
TL;DR version; my problems with the current implementation of
`--linearize` are that it:
* Makes the rare usecase easy, and ignores the common usecase
* Makes it asymmetrically difficult to recover for those that wanted
the common usecase instead of the easy
* Makes `--linearize` mean something other than "remove non-linearity"
* Turns multiple branches into one, but updates several branches anyway
* Ignores order specified by the user on the command line
* Introduces an inconsistency within git-replay between `--advance`
and `--linearize --onto`
(The last three items being minor compared to the first three.)
Longer version:
Consider the following history
M1 M2 M3 M4 M5
*---*---*---*---* <- master
\ \
\ \ A1 A2 A3 A4
\ \-*---*---*---* <- branchA
\ \
\ -*---* <- branchC
\ C1 C2
\
\-*---*---* <- branchB
B1 B2 B3
git replay was designed to allow you to update all your branches at once.
For example, with this above history, running
git replay --onto master branchA branchB branchC
will rebase all three branches onto master (and handles the shared portion
of history between branchA and branchC in the obvious way):
M1 M2 M3 M4 M5
*---*---*---*---* <- master
|
| A1 A2 A3 A4
|--*---*---*---* <- branchA
| \
| -*---* <- branchC
| C1 C2
|
\-*---*---* <- branchB
B1 B2 B3
With the current implementation of --linearize, adding that flag, i.e.
git replay --linearize --onto master branchA branchB branchC
would instead give something like:
M1 M2 M3 M4 M5 B1 B2 B3 A1 A2 C1 C2 A3 A4
*---*---*---*---*---*---*---*---*---*---*---*---*---*
^ ^ ^ ^
| | | |
master branchB branchC branchA
This topology strikes me as something that users would very rarely ever
want. Further, it:
* Makes one question why branchB and branchC were kept instead of
deleted; if the whole point is to concatenate the branches, then
since whichever branch lands on top contains the other two, why not
just get rid of the others?
* Makes the command behave differently on *already linear* history
when --linearize is added, which makes no sense to me.
* (Minor point, but still confusing to me) Ignores the order of
branches the user employed on the command line
Of course, the above involves no merges, so let's introduce one; consider
the following alternate initial history:
M1 M2 M3 M4 M5
*---*---*---*---* <- master
| \
| \ A1 A2 A4 A6 A7 A8
| \-*---*---*---*---*---* <- branchA
\ \ / \
\ *---* -*---* <- branchC
\ A3 A5 C1 C2
\
\-*---* <- branchB
B1 B2
Replaying the three branches,
git replay --onto master branchA branchB branchC
we would expect the base of the branches to simply be updated to current
master:
M1 M2 M3 M4 M5
*---*---*---*---* <- master
|
| A1 A2 A4 A6 A7 A8
|---*---*---*---*---*---* <- branchA
| \ / \
| *---* -*---* <- branchC
| A3 A5 C1 C2
|
\-*---* <- branchB
B1 B2
If you were to add --linearize, i.e.
git replay --linearize --onto master branchA branchB branchC
I personally would expect:
M1 M2 M3 M4 M5
*---*---*---*---* <- master
|
| A1 A2 A4 A3 A5 A7 A8
|---*---*---*---*---*---*---* <- branchA
| \
| -*---* <- branchC
| C1 C2
|
\-*---* <- branchB
B1 B2
In other words, `--linearize` should remove the non-linearity in the graph.
Instead, the current implementation will return something like:
M1 M2 M3 M4 M5 A1 A2 A4 A3 A5 A7 C1 C2 B1 B2 A8
*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*
^ ^ ^ ^
| | | \
master branchC branchB branchA
I can only imagine this rarely being useful to the user.
But to make it worse, please consider the difficulty of someone who
wanted the bottom graph but got the top one, vs. the difficulty of
someone who wanted the top graph but got the bottom one:
* (wanted bottom, got top) Just rebase branchB and branchA again; easy
* (wanted top, got bottom) You need to meticulously figure out the common
points of history and which sets of commits belong to each branch in
order to sequentially rebase each branch into the expected result.
In particular, the need to meticulously track start and endpoints with
individual
rebases was one of the reasons that led to `git replay` rather than improvements
to `git rebase`; the latter was so focused on single branches, that it
wasn't really
possible to extend to multiple branches. It's thus rather
disappointing to see new
flags for `git replay` that make handling multiple branches more painful.
There's actually one more (admittedly minor) issue as well: it creates
an inconsistency within git-replay itself. The `--advance` flag has a
check to error out when multiple positive refs are specified solely
because I thought it was weird to override the order of branches the
user specified on the command line (and didn't want to implement
something that could force the ordering of the revision walk); the error
message even states "because the ordering would be ill-defined". For
consistency, either both should be fine with ignoring the order of
revisions specified by the user, or neither should be.
So, what to do?
Both paths I have in mind end at the same place; the only real question
is whether the desired behavior lands in this series or as follow-up.
The minimal move is to make --linearize reject multiple positive refs for
now (exactly as --advance and --revert already do), unblocking this series
so it can merge down nearly as-is, and leave per-branch linearization as
future work.
The complete move is to implement that desired behavior now, by tracking a
last_commit per command-line branch so each branch is linearized
independently.
The reason I am comfortable with erroring out as a stopgap: turning an
error into working behavior later never breaks anyone, whereas letting the
current concatenation semantics reach 'master' risks users coming to
depend on them, which would make switching to the better behavior a
compatibility break. Erroring now keeps our options open; merging as-is
quietly closes them. (git-replay is still EXPERIMENTAL, so this is not
fatal either way, but it seems better not to paint ourselves into a
corner.)
For this series I would be perfectly happy with just the error; the
per-branch last_commit tracking can come later.