The reason you cannot combine --no-ff
with --squash
is that a squash merge is not a merge.
A squash merge is not a merge!
I know this sounds weird and wrong. And, worse, when you are in the process of running git merge --squash
, you are merging.
It's the result that's not "a merge".
How can this be? How can the act of merging result in something that is not a merge?
In part, this is a terminology issue. Git uses merge as both a verb, meaning the action of merging some code changes, and as an adjective, meaning a particular kind of commit: a merge commit. People—including me—shorten this adjectival form to just "a merge", turning it into a noun.
The commit you get from doing git merge --squash
is not a merge commit
A merge commit, in Git, is a commit with at least two parent commits.
That's the definition of a merge commit. It's clear and simple. It means that in the future, when people—perhaps yourself again, perhaps others—are looking at the history of what happened over time in this repository, and they find the commit, they will be able to see: "Aha, this commit is a merge, and it was achieved by merging these two parent commits."
The command git merge --squash
does not make one of these commits. Instead, it runs the regular merge code, and then stops and forces you to make your own commit, even if the merge succeeded. When you do make that commit, it's an ordinary, non-merge, single-parent commit.
Therefore, git merge --squash
may merge (code), but it does not produce a merge.
Meanwhile, the --no-ff
flag means "make a merge"
If you run a regular, non-squash-type, git merge
, Git has two options, one of which is only sometimes available. Git can either make a real merge, or—if the real merge would be trivial—it can skip the merge entirely and do a "fast forward" on the current branch name.
A fast-forward is not a merge either
This is another one of those places where Git's weird terminology gets in the way. Calling this a "fast forward merge" is counterproductive: not only do we not get a merge commit, we don't even get any merge action. The verb action, to merge, never happens, and the nouned-adjective "a merge" does not result. Instead, we just get a label (branch name) advanced along the commit graph.1
In any case, there are times when Git could do a trivial merge and just fast-forward a label, but you really do want Git to make a real merge. In particular, if you are merging development or feature lines into a release or master branch, you often want to keep the lineage (history of commits) separate. A fast-forward operation joins the histories, instead of keeping them separate. This is why git merge
has --no-ff
: to force it to make a real merge, even if Git could do a fake one and just fast-forward the branch.
It makes no sense to combine --no-ff
(or, for that matter, --ff-only
) with --squash
, since --squash
means "invoke the merge machinery, but make no commit, nor record the merge action for a future commit." A "squash merge"—the commit you make after running git merge --squash
—is not a merge commit.
In short, a squash merge is not a merge.
1This kind of advancement is what you want when you do a git push
, and sometimes what you want after a successful git fetch
. If you have some upstream branch, like origin/master
, and other people have added commits to the upstream, you can git fetch
those commits. Then you usually want to git rebase
your own work atop that, but if you have no new work of your own yet, you can simply fast forward your master
.
The command that does this is git merge
, which is annoying, because if you do have your own work, you probably should be using git rebase
. Fortunately you can just run git rebase
anyway; this has the same effect on your branch name as a fast-forward merge, when you have no work.
Alternatively, you can run git merge --ff-only
, which attempts the fast forward, then fails if that's not possible. This is a bit long to spell out, so I myself use an alias, git mff
, where the m
stands for either "merge" or "move" and the ff
stands for "fast forward". I have a second alias, lin
, for log/look-at what is incoming
. Making these two aliases is quite trivial:
git config --global alias.lin "log ..@{u}"
git config --global alias.mff "merge --ff-only"
Now I can run git fetch
, then git lin
to look at what has come in, then git mff
if I just need to fast-forward to that, or git show
particularly interesting parts, make a new branch and/or git rebase
if necessary, and so on.