0

How is this even possible?

These two branches are clearly different, yet a PR says it can find no differences.

I am stumped. And blocked. I can't continue until I've got my code into dev, so this is not merely curiosity.

I even tried merging MarketReseach2 into dev. It tells me it's already up-to-date.

Yet, I can flip back and forth between them in Visual Studio and see the differences.

enter image description here

DaveC426913
  • 2,012
  • 6
  • 35
  • 63

1 Answers1

-1

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.

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
  • I'm not sure how any of this addresses my problem. I have some code in one branch that's different from my other branch. I want the changes in MarketResearch in dev. I can't merge them because it can't find any changes. That's a direct contradiction *within* git and TFS. – DaveC426913 Jan 10 '17 at 01:20
  • One of your branches points to a commit that is an ancestor of the commit to which the other branch points. You didn't post the actual commit graph, so that's all I can say. – torek Jan 10 '17 at 01:22
  • And that means what to me? (Sorry, I don't mean to be cranky. No other tool on Earth is so labrynthine.) I still don't have a solution. – DaveC426913 Jan 10 '17 at 01:24
  • I don't know your GUI, but from the command line, you could run `git log --all --decorate --oneline --graph`. The key is not whether the commits differ, but rather how the graph *connects* the differing commits. – torek Jan 10 '17 at 01:32
  • I prefer command line. I hate it only slightly less than I hate the obfuscation of Visual Studio's machinations. ;) But I have no idea what to make of the output. – DaveC426913 Jan 10 '17 at 01:43
  • Well, the ASCII-fied graph is rarely great, but it does let you view the commit graph. (`gitk` draws it with graphic lines, which is sometimes easier to view, and certainly more colorful :-) although the colors are not as meaningful as one might like.) This should at least help in terms of figuring out what to merge to what, to get what you want. – torek Jan 10 '17 at 01:47
  • @DanielMann: The question seemed to be "how can this happen", so that's what I answered: how it can happen. – torek Jan 10 '17 at 02:16