Merge doesn't mean what you think it means
The problem is simple: merge (as a verb, i.e., as an action to take) doesn't mean "make identical". It means "combine changes".
Let's step back for a minute though. What exactly do we mean by "changes"? What's in a commit anyway? And: What exactly do we mean by "branch"?
You mention that:
I can flip back and forth between them ... and see ... differences
Let's get a good grip on what "them" means first. See the linked question about what we mean by "branch" as well, but based on your images, "they" are the names MarketResearchCampaign2
and dev
. Those two names actually translate into two specific commit IDs: two big ugly SHA-1 hashes that your IDE helpfully (cough :-) ) hides so that you can't see them, but in fact they look like 7b19d86e69d...
or some such.
Those hash IDs, hidden behind names because hash IDs really are not very useful to humans, represent specific commits. Each commit has its own unique hash ID. But what exactly is a commit?
Well, when you do flip back and forth between these two commits, you see a whole tree of files: a snapshot of a work-tree. That's part of what a commit is. A commit gives you access to a snapshot of a work-tree: you can check out any historical commit and retrieve the saved work-tree that goes with that commit.
The rest of a commit is a bunch of metadata, including the person who made the commit, and a log message, and—something that really matters a lot to Git—the parent commit, i.e., the commit that was in place just before that particular commit was made.
Parents and branches
Most commits have one parent. These parents form, in a backwards fashion, the history of the project—or some part of the project. This lets us (and Git) start from the most recent commit and work backwards:
... <- o <- o <- o <-- dev
The name dev
points to the most recent commit, which points back to a previous commit, which points back to another, and so on. Except for the key fact that dev
names the latest such commit, we mostly don't have to care that the arrows are all backwards, so it's nicer to draw these like this instead, though:
...--*--o--o <-- dev
\
o--o <-- ziggy
This lets me draw in the branches more easily. Now dev
points to the most recent dev
commit, and ziggy
points to the most recent ziggy
commit. The names dev
and ziggy
point to one specific commit, and those commits point back to older commits.
Note that I've marked one commit with *
, where the two sets of backwards-connecting arrows join up. This is one of the key items to understanding merges.
Comparing commits
When you flip back and forth between your two specific commits, you see different files. Git can compare these two commits for you. From the command line, you would simply run:
git diff dev MarketResearchCampaign2
to compare these two commits. Your IDE should have a way to do this as well.
More often, though, you might want to compare, not two different branch names, but rather a commit somewhere on some branch, against one of its parent commits. That is, given something like dev
and ziggy
above, we might compare the tip of dev
to the commit one back from the tip of dev
: the two commits right after commit *
. That would show you what you changed when you made the second of those two commits.
We might also compare the first one of those two against commit *
itself. That would show you what you changed from *
to the first of those two commits.
And, of course, we could compare *
against the tip of dev
. That would show you everything you did in all the commits going from *
to the tip of dev
.
Meanwhile, we can compare the tip of ziggy
to previous commits, in just the same way. If we compare the last commit on ziggy
vs its immediate predecessor, that shows what we did there. If we compare the last commit on ziggy
to commit *
, though: well, that shows us everything we did on branch ziggy
, as compared to commit *
.
But look at where commit *
is
The interesting thing about commit *
is that it's on both branches: it's the most recent commit where both dev
and ziggy
were still together. Since then, dev
has diverged, with several new commits; and ziggy
has diverged as well, with several new commits. If we decide it's a good idea, we can now rejoin these two lines: we can have Git find "everything we did since *
" in dev
, and "everything we did since *
" in ziggy
, and make a new commit that re-combines those two.
That's what merge does: it finds changes since some common point.
Note that any commits before commit *
were also on both branches. The thing that makes *
special here is not just that it's on both branches, but that it's the most recent commit that is on both branches.1
1Technically, it's the "Lowest Common Ancestor" or LCA in the DAG or Directed Acyclic Graph. This technicality only matters when there are multiple LCAs. One or another of the LCAs might actually be newer, but actually it's the topology, not the dates, that matter. Git and some other VCSes handle the multiple-LCA case differently from, e.g., Mercurial. Git actually offers you a choice of merge strategies in this case: -s recursive
does one thing and -s resolve
does another. But that's a separate, advanced topic.
Merges make new common-commit points
Let's say we make that merge, and do it on branch ziggy
:
...--o--o--o <-- dev
\ \
o--o--M <-- ziggy
There's something peculiar about this new merge commit M
: it points back, not to one previous commit, but to two. The first "previous commit" is the old tip of ziggy
, the rightmost o
on the bottom row, and the second "previous commit" is—still!—the tip of dev
, the rightmost o
on the top row.
Now let's make some more commits on ziggy
, by changing some things. I'm also going to mark the tip-most commit of dev
with *
:
...--o--o--* <-- dev
\ \
o--o--M--o--o--o <-- ziggy
Let's compare the tip of dev
, i.e., commit *
, with the tip of ziggy
. They certainly won't match: while we brought stuff in from dev
into ziggy
with the merge, they won't even match at *
-vs-M
, because we combined the dev
changes with the previous ziggy
changes. Now they'll probably match even less.
If we now ask Git to merge dev
into ziggy
again, though, what happens? Well, Git first goes to find the most recent point where the two branches were together ... and that's commit *
. So Git goes to compare *
with the tip of dev
to see what we changed on dev
, to combine it with whatever we've changed on ziggy
.
But *
is the tip of dev
. There's nothing to merge! In fact, the entire top row of commits is on both branches, and *
is once again simply the most recent such commit (or, again, technically it's the LCA node).
When Git tells you that there is nothing to merge, it means the two particular commits you have selected have, as their most recent common ancestor commit, one of those two commits. (Or, in the latest Git versions, you get a different error if there are no common commits; older Git versions simply diff both commits against an empty tree, and combine the resulting diffs as usual.) Since "merge" means "combine changes as viewed against some common base commit", there must be some changes, at both end-points, against some common base commit: the common base commit can't be one of the two end-points.
A side note about "fast-forward merges"
In a situation like this:
...--o--o--* <-- dev
\ \
o--o--M--o--o--o <-- ziggy
you can't merge dev
into ziggy
, but you can merge ziggy
into dev
. There are in fact two ways to do it. One is with a real merge:
...--o--o--o------------N <-- dev
\ \ /
o--o--M--o--o--o <-- ziggy
This will combine the changes from *
to *
(i.e., no changes) with the changes from *
to tip-of-ziggy
, resulting in the same files as in the tip of ziggy
. That is, it combines "no changes" with "some changes". The result is clearly going to be "match the tip of ziggy
", so in this case, it does wind up making a new commit N
whose files exactly match those in the commit at the tip of ziggy
. (And, the tip of ziggy
becomes the new *
: the new most-recent-common-commit.)
However, Git normally doesn't actually make a merge commit for this case. Instead, it realizes that combining nothing with something automatically produces the "something", i.e., that the new commit would exactly match the tip of ziggy
. So there's no reason to bother making commit N
after all.2 Instead, it just moves the branch labels so that they both point to the same commit:
...--o--o--o
\ \
o--o--M--o--o--* <-- dev, ziggy
This is a so-called "fast forward" operation (or "fast forward merge", although there is no actual merging going on). The drawback here is that this does not create a new merge commit at all; instead, both branch labels wind up pointing to the same commit, until you make a new commit on one or both of the two branches. For instance, suppose you make one new commit on dev
and two on ziggy
:
...--o--o--o o <-- dev
\ \ /
o--o--M--o--o--*--o--o <-- ziggy
Because there was never an actual merge commit, it's no longer possible to follow the two lines independently. Had you used git merge --no-ff
to force a real commit, you'd get commit N
and end up with this instead:
...--o--o--o------------N--o <-- dev
\ \ /
o--o--M--o--o--o--o--o <-- ziggy
and now it's possible, using the correct (--first-parent
) links in each commit, to follow exactly where each phase of development happened.
Does that matter? You decide!
2Except, of course, that there is a reason, as the rest of the section describes. Sometimes that's an important reason, and sometimes it's not; it is up to you to decide whether it is important, and if so, to force Git to make a real merge commit.