TL;DR
What you want is "features as merge bubbles". To get these, use git merge --no-ff
from your mainline with each feature. You should generally put each feature on its own branch, but if you like, you can use the same name (e.g., dev
) each time. The branch names don't really matter and Git generally does not store them (you can get them into commit messages if you like, but messages of the form merge branch blergh
have no real value).
Long
The root of the answer is that git merge --squash
does not make a merge (commit).
The word merge in Git is used both as a verb, to merge, meaning to combine two different sets of changes, and as an adjective modifying the word commit: a merge commit is a commit with two or more parents.1 The adjective form, merge commit, is often shortened to a simple noun, a merge. So we need to keep in mind that some Git commands perform merge-type actions, i.e., do merge-as-a-verb, and some Git commands produce merge-type commits, i.e., make a merge, a noun.
The git merge
command often but not always does both. Sometimes it does just one of the two—the merge action, without the merge commit at the end—and sometimes it does neither.
The git cherry-pick
and git revert
commands always2 do the merge-as-a-verb part but never make a merge in the end.
The git commit
command can make an ordinary commit, or in some special cases, a merge commit or a root commit: a commit with no parents at all.
To understand how all these parts interact, we need to remember a few more things:
- Git actually builds new commits from what is in Git's index.
- The index gets expanded during a merge-as-a-verb operation. Now, instead of holding one copy of each file, it holds three.3
- If Git stops in the middle of a conflicted merge, it leaves various trace files, such as
MERGE_HEAD
, MERGE_MSG
, CHERRY_PICK_HEAD
, and so on. The git status
command knows to look for these and can tell you that you are in the middle of a conflicted merge, for instance, with files as yet unresolved, or with all conflicts resolved.
When you run git command --continue
or git commit
, Git picks up where it left off. (The --continue
variety acts as a sanity check, that there's that particular command to continue at this point.) When you run certain kinds of git reset
, or git command --abort
or git command --quit
, Git terminates the unfinished operation and either puts things back (--abort
) or doesn't (--quit
) by invoking the right kind of reset (--hard
or --soft
).
This means that, e.g., git merge --no-commit
can start the merge, run it as far as it can on its own—perhaps even to the point that there are no conflicts remaining—and then just stop and let you fiddle with Git's index and/or your working tree as much as you like. Your eventual git merge --continue
or git commit
will then finish the merge, using the files Git left behind when it stopped, plus any updates you made to the index (a so-called evil merge; see Evil merges in git?). Or, your git reset --hard
or git merge --abort
erases all the work that git merge
did, removes the merge-in-progress marker files, and leaves you set up as if you had not even started a git merge
command.4
Anyway, if you have gotten through to this part without getting lost, git merge --squash
becomes very easy to understand. It:
- starts the merge process, like
git merge
would;
- has an implied
--no-commit
, so that it stops before committing; and/but
- it does not create any "merge going on" files, so that the status after stopping is that
git merge --continue
is not allowed, and git commit
will make an ordinary commit, not a merge commit.
Since merges, and future merge bases, are determined by the commit graph—which is to say, the commits themselves including their parent linkages—and git merge --squash
does not put in the extra parent linkage, the final commit doesn't have the history you want. The solution, then, is to avoid git merge --squash
.
You might (quite legitimately) wonder what git merge --squash
is good for. The answer is: not all that much! There's one situation in which it definitely makes sense, though, and that is when you:
- create a branch for experimentation;
- do your experimenting by writing multiple commits;
- at the end of the experimenting, decide that the result is good, but it should just be one commit; and
- want to easily make that one commit.
To make that one commit, you go back to the branch from which you created the experimental branch, and run git merge --squash experiment
(or whatever name is appropriate here). You then write the desired commit message for the one commit, and then delete the experimental branch. It is now "dead": its commits have no further use. They are all trash, to be hauled away with the rest of the rubbish in a month or so when the garbage collector gets around to it.
If you don't intend to kill the branch, git merge --squash
is probably the wrong tool. (But see also matt's comment about using squash-merge with GitHub PRs.)
1A commit with more than two parents is an octopus merge. These are normally made with git merge -s octopus
, but the -s octopus
part is implied by giving git merge
two or more commit specifiers. They don't do anything you can't do with more typical two-parent merges. In fact, they specifically don't do things—namely, resolve conflicts—that you can do with two-parent merges, which is probably the main justification for having octopus merge in the first place: since an octopus merge is "weaker" than a normal merge, if you see one in a set of commits, you can be pretty sure it was one of these easy, conflict-free merge cases.
Overall, though, I still think octopus merges are mainly just for showing off.
2"Always" here is a little too strong: sometimes git cherry-pick
can just error out, for instance, and if the merge-as-a-verb part of the action stops with a merge conflict, you're left in the middle of the operation.
3More precisely, it holds up to three, from the three input commits to a merge operation: the merge base, the "ours" or "local" or HEAD
commit, and the "theirs" or "remote" or "other" commit. But if a file is missing from one of the three commits—for instance, if we modified file path/to/file.ext
and they removed it entirely—there might be fewer than three index entries for the file.
4Note that for this to work, the state that git reset --hard
writes—which is to say, the set of files that are in the HEAD
commit right now—must match the state that everything had when you first started the git merge
. Equivalently, git status
would have had to have said nothing to commit, working tree clean
(though perhaps with untracked files). That's why git merge
normally requires a "clean" state before it is willing to start. The internal git merge-recursive
command is not so careful, and it's possible to start a merge with index and/or working tree in states that cannot be recovered by stopping the merge after all, if you run git merge-recursive
—as, e.g., git stash apply
does.