Joachim Breitner's Homepage
git post-squash
I wrote a little git
tool that helps in an environment where PRs are merged using squash merges, but you still want to deal with feature branches properly.
What is a squash merge?
One popular workflow involving Git and Github is a squash-merge based workflow: You develop your feature on a feature branch (say featureA
), adding commits as you go, possibly merging from master a few times:
M1 ─ M2 ─────────── M3 ────── M4 (master)
╲ ╲
A1 ─ A2 ─ A3 ─ A4 ─ A5 (featureA)
When the feature is ready, you merge master
into featureA
a last time (e.g. to check on your CI infrastructure that this merge does not break the build):
M1 ─ M2 ─────────── M3 ────── M4 (master)
╲ ╲ ╲
A1 ─ A2 ─ A3 ─ A4 ─ A5 ─ A6 (featureA)
and now you do a squash merge (or use Github’s green “Squash merge” button, or mergify.io’s squash merge action). The result is a new commit M5
on master
that contains all the changes from featureA
:
M1 ─ M2 ─────────── M3 ────── M4 ─ M5 (master)
╲ ╲ ╲
A1 ─ A2 ─ A3 ─ A4 ─ A5 ─ A6 (featureA)
Note that there is no line from A6
to M5
. This means that the git history of master
is clean, and does not contain the usually boring and unhelpful history of how featureA
came to be; no “fix typo” commits, no “merge master into featureA” commit.
But the downside is that, as far as git is concerned, this commit is totally unrelated to the featureA
branch. This is not a problem as long as featureA
lives on its own. But it becomes a problem if there are feature branches building off featureA:
What is the problem with squash merge?
Consider the situation above, but add featureB
to the mix, a feature branch that was created off featureA
:
M1 ─ M2 ─────────── M3 ────── M4 ── M5 (master)
╲ ╲ ╲
A1 ─ A2 ─ A3 ─ A4 ─ A5 ─ A6 (featureA)
╲ ╲
B1 ────── B2 ─ B3 (featureB)
We now want to bring the latest changes from featureA
and master
into featureB
. Merging featureA
into featureB
is straight-forward:
M1 ─ M2 ─────────── M3 ────── M4 ── M5 (master)
╲ ╲ ╲
A1 ─ A2 ─ A3 ─ A4 ─ A5 ─ A6 (featureA)
╲ ╲ ╲
B1 ────── B2 ─ B3 ─ B4 (featureB)
But if we run git merge master
now, we are likely running into very unfortunate git conflicts. Because to git, M5
is unrelated to featureA
, it does not know that all the changes already have been merged into featureB
when we created the merge commit B4
!
But we know that M5
contains nothing that isn’t already in featureB
, because M5
was a squash commit of A6
.
The manual way of resolving this is to run
git merge -s ours master
which tells git: Pretend that we merged master
into this, but don’t actually touch any of the files, everything on the current branch is already in the form we want. This way, we get
M1 ─ M2 ─────────── M3 ────── M4 ── M5 (master)
╲ ╲ ╲ ╲
A1 ─ A2 ─ A3 ─ A4 ─ A5 ─ A6 ╲ (featureA)
╲ ╲ ╲ ╲
B1 ────── B2 ─ B3 ─ B4 ─ B5 (featureB)
Note: git merge -s ours
is not the same as the git merge -X ours
! See the manpage for git merge
for details.
How does git-post-squash help?
While the manual way works, one needs to be careful: If master
has progressed further, or if featureA
was not fully up-to-date before the squash merge, using git merge -s ours
will easily and silently undo changes that were already committed to master.
So instead run
git post-squash master
which will do git merge -s ours
, but it will
- find the right commit on
master
to use (it may not be the latest) and - double-check that nothing is lost.
It does so by picking the latest commit on master
that has the same tree as some commit on the current branch that is not yet on master
.
In the example above, it would pick M5
because it has the same tree as A6
, which is a commit that exists on featureB
, but not on master
.
Convinced? Go and get it on https://github.com/nomeata/git-post-squash.
Have something to say? You can post a comment by sending an e-Mail to me at <mail@joachim-breitner.de>, and I will include it here.