Lets say I have a history similar to this.
Develop is periodically merged into master.
a---b---c---d---i---j---k---o master
\ / /
e---f---g---h---l---m---n develop
How to I amend b without completely flattening the history?
If I interactively rebase, and amend b, I'll end up with a history looking like this:
(upper case signifies that the commit changed)
a---B---C---E---F---G---H---J---K---L---M---N master
\
e---f---g---h---l---m---n develop
How can I retain history structure, and have it look like this instead:
a---B---C---D---I---J---K---O master
\ / /
e---f---g---h---l---m---n develop
Technically, you cannot change anything about any existing commit.
What this means is that to "change"
b
toB
, you do in fact have to make a new-and-improvedB
. The oldb
will continue to exist (for how long, is another question entirely). Regulargit rebase
, interactive or not, works by copying the commits to new-and-improved ones. Regular rebase also removes merges and flattens, as you noted.Since Git 2.18,
git rebase
has had a mode called--rebase-merges
. This retains merges—or more accurately, re-creates them as new merge commits, by literally runninggit merge
again. Pre-2.18,git rebase
has-p
, which uses the interactive rebase machinery and otherwise preserves merges (by repeating them) as well. It's somewhat defective compared to the fancier new version, but—I think (have not tested!)—should work for this case.Hence, use
git rebase -i --rebase-merges
the way you would usegit rebase -i
. If you lack the--rebase-merges
, update your Git version, or usegit rebase -i -p
and be very careful not to disturb the order of the various operations, or build your new chain by hand.To build it by hand, run
git checkout
on commitB
. You can assign a new branch name here, if you like, or just do the whole operation with a detachedHEAD
the waygit rebase
would:to use a new temporary branch name, for instance. Then use
git commit --amend
(perhaps with additional stuff first) to make your newB
:Now run
git cherry-pick
on the hash of commitc
to copy it to new commitC
, and repeat ford
:Now run
git merge
on the hash of commith
,i
's second parent, to make new mergeI
, resolving any merge conflicts if needed:Use more cherry-pick and merge commands to complete the process:
Now that the new commits are built, force the name
master
to point to the final commit, and stop drawing the oldb-c-d-i-j-k-o
chain, and you have what you wanted. This is whatgit rebase --rebase-merges
andgit rebase -p
do:-p
just uses a fragile algorithm that , while--rebase-merges
uses a new interactive instruction sheet format, that lets you specify the new graph in a way that doesn't break when you move commits around.