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"
btoB, you do in fact have to make a new-and-improvedB. The oldbwill 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 rebasehas had a mode called--rebase-merges. This retains merges—or more accurately, re-creates them as new merge commits, by literally runninggit mergeagain. Pre-2.18,git rebasehas-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-mergesthe way you would usegit rebase -i. If you lack the--rebase-merges, update your Git version, or usegit rebase -i -pand 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 checkouton commitB. You can assign a new branch name here, if you like, or just do the whole operation with a detachedHEADthe waygit rebasewould: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-pickon the hash of commitcto copy it to new commitC, and repeat ford:Now run
git mergeon 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
masterto point to the final commit, and stop drawing the oldb-c-d-i-j-k-ochain, and you have what you wanted. This is whatgit rebase --rebase-mergesandgit rebase -pdo:-pjust uses a fragile algorithm that , while--rebase-mergesuses 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.