When working with git, I like linear history because otherwise merging concurrent feature branches makes the history graph look like spaghetti.
Two common ways to get a linear history are squashing and rebasing.
Squashing applies all the changes on the feature branch as a single linear commit on the target branch.
# Before squashing:
A---B---C main
\
D---E---F feature
# After squashing:
A---B---C---G(=D+E+F) main
\
D---E---F feature
Rebasing moves D-E-F
to the tip of main. A normal merge will
fast-forward, keeping the history linear.
# After rebase:
A---B---C main
\
D---E---F feature
# After fast-forward merge:
A---B---C---D---E---F main,feature
The downside of squashing is that it can easily lead to large commits that do too many things. It’s much better to have single-purpose commits. And if any small part needs to be reverted, you have to do that by hand.
Rebasing keeps your commits properly sized. Later on, though, it’s hard to pick out all the changes associated with a feature from the commit graph. And if you have to revert a feature, you have to figure out what the starting commit is.
A no-fast-forward merge after rebasing makes an empty merge commit, even
though a feature branch could have fast-forwarded. You can do that with
git merge --no-ff
:
# After rebase:
A---B---C main
\
D---E---F feature
# After merge --no-ff:
A---B---C-----------G main
\ /
D---E---F feature
Now the history is effectively linear – there’s no spaghetti of other merges
between C
and G
, but the commits associated with the feature are clearly
delineated.
I wouldn’t recommend this for small features with only one or two commits, but for large changes spanning multiple commits, I think it’s very useful. Large changes are also the riskier ones that benefit most from the context of seeing commits together and from easy reverts, which no-fast-forward merges give you.
GitHub’s default “merge button”
behavior
uses --no-ff
merges, so if you manually rebase first, the button will do
this for you.