CathrineVaage
CathrineVaage

Reputation: 33

Amending commit in history with merges without flattening it

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

Upvotes: 0

Views: 41

Answers (1)

torek
torek

Reputation: 487785

Technically, you cannot change anything about any existing commit.

What this means is that to "change" b to B, you do in fact have to make a new-and-improved B. The old b will continue to exist (for how long, is another question entirely). Regular git 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 running git 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 use git rebase -i. If you lack the --rebase-merges, update your Git version, or use git 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 commit B. You can assign a new branch name here, if you like, or just do the whole operation with a detached HEAD the way git rebase would:

git checkout -b temp-rebuild <hash-of-b>

to use a new temporary branch name, for instance. Then use git commit --amend (perhaps with additional stuff first) to make your new B:

  B   <-- temp-rebuild
 /
a---b---c---d---i---j---k---o   <-- master
 \             /           /
  e---f---g---h---l---m---n   <-- develop

Now run git cherry-pick on the hash of commit c to copy it to new commit C, and repeat for d:

  B---C---D   <-- temp-rebuild
 /
a---b---c---d---i---j---k---o   <-- master
 \             /           /
  e---f---g---h---l---m---n   <-- develop

Now run git merge on the hash of commit h, i's second parent, to make new merge I, resolving any merge conflicts if needed:

  B---C---D------I   <-- temp-rebuild
 /              /
a---b---c---d--/i---j---k---o   <-- master
 \            |/           /
  e---f---g---h---l---m---n   <-- develop

Use more cherry-pick and merge commands to complete the process:

  B---C---D------I---J---K--O   <-- temp-rebuild
 /              /          /
a---b---c---d--/i---j---k-|-o   <-- master
 \            |/          |/
  e---f---g---h---l---m---n   <-- develop

Now that the new commits are built, force the name master to point to the final commit, and stop drawing the old b-c-d-i-j-k-o chain, and you have what you wanted. This is what git rebase --rebase-merges and git 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.

Upvotes: 4

Related Questions