When you run git reset --soft HEAD~20
, the last 20 commits are removed. If you had some merge commits in those 20 commits, they are removed too. That's what you told Git to do and Git did as instructed.
I'm not sure what you mean exactly by "keeping the merge commits" when you state that you want to squash the last 20 commits (i.e. remove many commits and replace them with a single new commit).
A merge commit is just like any other commit, with the only difference being that it has two (or more) parent:
lines pointing to its ancestor commits. A regular commit only has a single parent:
line. And just like any other commit references a tree object, a merge commit too references a tree object. The tree captures a full snapshot of your code at any given time.
With that information, there is one way to interpret your question: "How to pretend that a squashed commit actually merges history?" or in other words: "Can a squashed commit have multiple parents?".
The answer is yes, with a little trickery and usage of low-level plumbing commands.
A commit in Git always points to a single tree (tree:
line) and references 0 or more parent commits (parent:
line(s)). A merge commit references 2 or more parent commits. You now want to create a commit with the tree of your current HEAD
and many parents, each parent pointing to the tip of your merged branches (this is called an octopus merge).
To do that, git commit-tree
can be used:
git commit-tree HEAD^{tree} \
-p HEAD~20 \
-p tip-of-merged-branch-1 \
-p tip-of-merged-branch-2 \
-p … \
<<MSG
Your commit message comes here
With commit body, etc.
MSG
You need to manually find the commit hashes of the merged branch tips. Some clever scripting could help you. Get a list of commits, grep for commit message "Merge", get the second parent (or everything but the first parent in case of octopus merges). Be aware that this creates an evil merge.
Note that this is a low-level command and all warranty is void. Know what you are doing.