The prior comments and answers (including my comment) are factually correct as far as why what you're doing may not work, but probably aren't great as far as being useful answers.
First thing is, while the answer "it's a documented bug" may not be very satisfying, I would point out that the re-ordered TODO list in your example doesn't actually tell git what topology you're trying to reproduce. And that's the first clue that what you want to do won't be so easy, because how would you tell git what topology you want? How would you explain to git that you mean for C
and D
to both change parent from B
to F
, in a way that couldn't also be read as "put F
before C
, so that F'
and D'
each have B
as parent"; or "put F
before D
, so that F'
and C'
each have B
as parent"? Of course it turns out git did none of those things, which I suppose is the buggy bit. (That said, I'll bet it did rewrite F
, but then made the parent of F'
, C'
, and D'
all be B
, which makes F'
ultimately unreachable.)
Anyway, I think you mean to ask just: Can you get from
A -- B - C - E -- F <--(master)
\ /
- D -
to
A -- B -- F' - C' - E' <--(master)
\ /
- D' -
without manually re-resolving the merge conflicts at E
.
And the answer is, you can; but it won't be as simple as finding the right rebase
options and issuing a single command.
The truth is rebase
is designed to produce linear histories; its proponents tend to insist that a linear history is "easier to read" or "cleaner", and that this is more important than being "accurate" or "made up of commits that have been tested". Those statements are not objectively universal, but that is what the tool of rebase
is designed to do. The preserve-merges
option tries to let you have your cake and eat it to, but it has a lot of problems (not just the bug when using it with -i
). It might happen to work for some simple cases, but in general trying to rebase through a merge you want to keep is likely to be a headache at best.
So what to do instead? Here's one way... Note that when referring to a commit I've given an expression that resolves to the correct commit in this example, rather than just using the placeholder letter.
git checkout master
git branch temp
git rebase -i master~4
// in the editor, move F after B and remove D
At this point you should have
F' -- C' <--(master)
/
A -- B - C - E -- F <--(temp)
\ /
- D -
Then you need to rebase D
. (In this case, since it's a single commit, you could possibly do a cherry-pick
instead.)
git checkout temp^^2
git checkout -b branch
git rebase --onto master^ master branch
At this point you have
D' <--(branch)
/
F' -- C' <--(master)
/
A -- B - C - E -- F <--(temp)
\ /
- D -
and you need to reproduce the merge. There are again several ways to look at this problem, but the simplifying observation is that your end content should match what you had at F
in the original history. So a valid solution is
git checkout master
git reset --hard $(git commit-tree -p master -p branch -m "Merging branch into master" temp^{tree})
(For the -m
you could give whatever commit message was originally at E
.) And then you can delete the temp
branch and, if you're done with it, the branch
branch.
In more complex scenarios, this little cheat of grabbing the merge result TREE
from an existing commit may not work, because maybe no existing commit matches the intended state. In that case I think your best bet would be to try git rerere
(documented at https://git-scm.com/docs/git-rerere). This command's purpose is to record the resolution to a merge conflict so it can be reapplied if the same conflict is seen in a different merge, and I would think it could be applied here. (That said, I never use it since I disagree with the premises that lead to a workflow that routinely requires it, so I can't speak to exactly how well it will server here.)
I should add that if the merge were non-conflicting (and non-"evil"; i.e. the default merge strategy and options produce the desired result), and remain non-conflicting under the desired re-ordering, then the following procedure would work in place of the above:
// first rewrite F
git checkout master
git checkout -b temp
git rebase --onto master~3 master^
// now move master to exclude the original F
git checkout master
git reset --hard HEAD^
// now move the rest of the history
git rebase --preserve-merges temp
So it may be that, with rerere
in place, this (conceptually simpler) approach might work even in a conflicting case.