I performed multiple merge
commits but they should have been merge --squash
instead. The conflict resolution took more than a day so I can't afford to redo the merging by hand.
Is there a way to convert the merge
to merge --squash
?
I performed multiple merge
commits but they should have been merge --squash
instead. The conflict resolution took more than a day so I can't afford to redo the merging by hand.
Is there a way to convert the merge
to merge --squash
?
It's worth noting here that
git merge
andgit merge --squash
are closely related, butgit merge --squash
does not create a merge.The phrasing here is very important, particularly the article "a" in front of "merge": "a merge" is a noun, while "to merge" is a verb. Both commands perform the act of merging. The difference lies in how the result is saved.
It's also worth a quick reminder, in the form of a diagram of commits, of how merges look. Each round
o
node here represents a commit, with earlier commits towards the left. Branch names are labels that point to one specific commit (the tip commit of a branch). You might start with this, for instance:You then decide to merge the one specific
feature
branch back into themain
branch, so you run:This does a merge (verb) and makes a merge (noun), and the result looks like this:
Git has added one new commit to
main
, and this new commit is a merge commit: it points back to both the previous tip ofmain
, and also back (and in this case, downward as well) to the still-current tip offeature
. (The namefeature
continues to point to the same commit as before.)What
git merge --squash
does is to modify the last step. Instead of making a merge commit, it suppresses committing entirely—for no obvious reason—and forces you to rungit commit
. When you do rungit commit
, this makes an ordinary commit, instead of a merge commit, so that the result looks like this:There are two key items here:
The contents of the new commits are the same. Both new commits are made from the index (the index is Git's term for "what goes into the next commit you make"). This index is set up by the merge-as-a-verb process. The first parent is the "main-line" commit, from the branch we are on when we do the merge. The second parent is the other commit, the one we just merged.
The parent linkages of the new commits differ. A real merge has both previous commits as its parents, but a "squash merge" has only one previous commit as its parent—the "main-line" commit. This means it does not—it can not—remember which commit was merged-in.
In the case of a conflicted (but real) merge,
git merge
cannot make the new commit on its own, so it stops and forces you to resolve the conflicts. Once you have finished resolving those conflicts, you must manually rungit commit
, just as you must always (for no obvious reason) do withgit merge --squash
and its fake merges. You can also request that any real merge stop and let you inspect the result, usinggit merge --no-commit
.This leads to the easy method to turn a real merge into a fake (squash) merge, as long as the real merge is not yet committed:
For
git commit
to know to make a merge, it relies on a file left behind by the conflicted (or--no-commit
) merge. This file is named.git/MERGE_HEAD
. (It also leaves behind a file named.git/MERGE_MSG
although this extra file is harmless.)Therefore, you can simply remove
.git/MERGE_HEAD
and rungit commit
. (You may want to remove the MERGE_MSG file as well, once you've written your commit with its message. Until then you can use it as the message, or as its starting point.) Thegit commit
step will no longer know to make a merge commit, and will instead make an ordinary commit—et voila, you have made a squash merge.If you have already made a real merge commit, the process may be harder. In particular, if you have published the merge, you must now make everyone who has obtained this merge, take it back. Otherwise the real merge will return (they are likely to merge it again), or create problems for them, or even both. If you can do that, though, or if it has not been published, you merely need to "shove the merge aside", as it were, and put in a fake merge commit.
Let's redraw what we have, to make some room. This is the same graph as before, just spread out onto more lines:
What if we could move
main
back up to the top line, then make a new commit? Well, we would get the following drawing:Note that all the arrows—including the internal commit arrows that the commits use to record history (not shown here as they're too hard to produce in text drawings)—point leftward, so there's no knowledge, in any of the top-line commits, of any of the commits below them: those are all "to their right". Only the bottom-line commits, plus the one we're going to abandon, know anything about the top-line commits.
Moreover, if we're going to abandon the middle-line commit entirely, let's just stop drawing it, and its two merge arrows:
And look at that: we've just drawn the kind of commit graph we want for one of those fake squash "merge"s. This is just what we want, as long as we get the right contents in our new commit.
But—this is a sort of an exercise; see if you know the answer before plunging on—where do the contents of a new commit come from? The answer is above, in boldface, in the first bullet point. (If you've forgotten it, go back and check.)
Now that you know that
git commit
uses the index to make the new commit, let's consider the real merge commit we have now. It has contents. (All commits have contents.) Where did they come from? They came from the index! If we can just somehow get those contents back into the index, we're golden. And in fact, we can: all we have to do is check out that commit, withgit checkout
, or already have it as the current commit.Since we just made the new merge commit, we already have the merge commit as the current commit. The index is clean—if we run
git status
, it says there's nothing to commit. So now we usegit reset --soft
to reset the branch pointermain
back one step, to get our intermediate drawing. The--soft
argument tells Git: "move the branch pointer, but don't change the index and work-tree." So we'll still have the index (and work-tree) from the original merge. Now we just rungit commit
to make an ordinary commit, supply some appropriate commit message, and we're done: we have a squash merge. The original merge is now abandoned, and eventually—some time after 30 days, by default—Git will notice that it's no longer used and will remove it.(To move back one step, you can use
HEAD~1
orHEAD^
; both mean the same thing. Hence the command sequence is justgit reset --soft HEAD^ && git commit
, assuming the current commit is the real merge you wish to replace with a fake merge.)The longer method above can be used even if you made multiple merge commits. You will have to decide, though, whether you want multiple fake merges, or one big fake merge. For instance, suppose you have this:
Do you want your final picture to look like:
where the last two commits on
develop
are the two fake (squash) merges, or do you just want one big fake-squash:where that last commit is the final result? If you want two fake-squashes, you will need to retain both original merge commits long enough to get them into the index and make two ordinary commits from those. If you just want one big fake-squash of the final merge, that takes only two Git commands:
since we'll retain the second merge's index, move back two steps, then make the new commit that shoves aside both merges.
Again, it's important that none of the real merges have been published—or, if they are published, that you convince everyone else who has picked them up, to stop using them.