0

I have 2 branches: feature-branch-a and feature-branch-b I can go into elaborate git history details of this local repo if you want me to, but the summary is that feature-branch-a is an ancestral branch of feature-branch-b.

When I git checkout feature-branch-b and I git merge feature-branch-a or I git rebase feature-branch-a, nothing happens because it is Already up to date.

However, running git diff feature-branch-a from feature-branch-b clearly shows my edited changes.

Please help me translate this to git command:

Dear git, I have some changes in feature-branch-a that I want to add to feature-branch-b. I know this addition will result in a merge conflict. Kindly merge these changes together with those nice <<<===... conflict resolution markers

I just want to know how to merge any two branches which clearly have different content (according to git diff) without the spurious, funny Already up to date message. Thanks :)

Damilola Olowookere
  • 2,253
  • 2
  • 23
  • 33

2 Answers2

2

Git does not store changes. Git stores snapshots.

Think of this like daytime high temperatures. Suppose I tell you only that today is a few degrees warmer than yesterday. Can you tell me what the temperature was yesterday and today? If not, why not? If I tell you only that today was 50˚F, was it warmer today than last week? If you can't tell me, why not? What information will enable you to provide absolute temperatures for each day, if you are given only differences? Or, if you are given only absolute temperatures each day, how would you provide the difference between any two given days?

After thinking about this, you should have a clear notion of "change" (delta or difference: it is warmer today than yesterday, which we find by subtracting two snapshots) vs "snapshot" (it was X degrees yesterday and is Y today, which only lets us get a snapshot if we have a starting snapshot to add this to), and how to go from one to another. So now this next part will make sense: In order to go from snapshots to deltas, you must give Git two specific snapshots.

So now let's go back to your formulation:

Dear git, I have some changes in feature-branch-a ...

To have changes, you must pick two snapshots. Which two snapshots are you picking?

... that I want to add to feature-branch-b

Once you have picked the two snapshots in feature branch A, you can have Git turn them into changes (differences) and apply those to the snapshot at the tip of branch B. That's not too hard. But you'd like to do this automatically, and that's where you are running into problems:

... the summary is that feature-branch-a is an ancestral branch of feature-branch-b.

Branches aren't ancestors of branches, nor are they children, nor do they have any such relationship. Or, well, maybe they do: the problem here is that we have not defined quite what we—or you—mean by "branch". See What exactly do we mean by "branch"?

In Git, a branch name, like feature-branch-a, simply specifies one particular commit, by that one commit's unique hash ID. Commits have ancestor/descendant relationships, and based on your description and the failure when you run git merge, we can draw that relationship:

...--o--o--A   <-- feature-branch-a
            \
             o--o--...--B   <-- feature-branch-B

I picked out the two particularly interesting commits here: commit A, whose ancestors are those to the left of it—the unnamed round o nodes—and commit B, whose ancestors are the commits to the left of B, which includes commit A.

That is, the snapshots that are contained within feature-branch-A are A itself and the commits left of A. The snapshots that are contained within feature-branch-B are B itself and those left of that, including A and those left of A. Every commit "in" or "on" branch A is already in/on branch B.

If you run:

git checkout feature-branch-b
git merge feature-branch-a

the way git merge goes about figuring out what changes to combine is to find the best common ancestor between the tip of the current branch, i.e., commit B, and the tip of the branch you've named, i.e., commit A. The best ancestor is the one that's reachable from both commits but, in a sort of vague but obvious sense, nearest to them as well.

If what you had looked instead like this:

          o--...--A   <-- branch-A
         /
...--o--*
         \
          o--...--B   <-- branch-B

then the merge base would be the starred commit: it's not only on both branches, it is also as close as you can get to both A and B simultaneously while being on both branches. (Commits to the left of * are also on both branches, but are "further behind".)

So, Git can merge these on its own, by comparing—git diff—ing—* vs A to see what they did, and *-vs-B to see what you did. Git can then combine their improvements—*-vs-A—with yours, *-vs-B. Applying the combined changes to * gives you a snapshot with both sets of improvements, incorporating both lines of work into something that is ready to commit as a merge commit (a slightly special kind of commit: one that has two parents instead of just one, but is otherwise an ordinary snapshot, like any other commit).

But given your setup:

...--o--o--A   <-- feature-branch-a
            \
             o--o--...--B   <-- feature-branch-B (HEAD)

Git will find that the merge base is commit A itself. That's on both branches, and is as close as possible to A and as close as possible to B. So Git will diff A vs A and find that there's nothing to merge!

That's clearly not what you want. But there's still nothing to merge: whoever made B started from A, and added improvements since then. That's all there is. If you want Git to remove those improvements, you can do that, but not with git merge.

If some commit, somewhere between A and B, is not an improvement after all, you can tell Git to back it out:

...--o--o--A   <-- feature-branch-a
            \
             o--X--...--B   <-- feature-branch-B (HEAD)

If commit X makes things worse, you can run git revert <hash-of-X>. Git will turn X into a change-set—by comparing it to its parent commit, just to the left of it in the drawing—and attempt to "un-change" everything in the current commit to undo exactly what was done in X. This may succeed on its own, or may fail with a merge conflict. (Internally, Git is really doing a git merge using X as the common starting point, the o to the left of it as the other branch tip, and B as your own branch tip. This is kind of confusing: it might be easier to think of it as Git just "reversing the changes". The merge is technically superior because it handles some harder cases, but usually that's the same thing.)

Ultimately, though, the key to understanding this is to realize that merge does not mean "make the same". It means combine changes since a common starting point. If you have no common starting point, that's difficult—modern Git doesn't use its "guess mode" unless you add --allow-unrelated-histories—and if the starting point is one of the two commits, there's nothing to merge.

torek
  • 448,244
  • 59
  • 642
  • 775
  • I get it to the point where you siad `That's clearly not what you want...`. However you mentioned that `...whoever made B started from A, and added improvements...If you want Git to remove those improvements...`. I made `B`, and as you rightly said, I started from `A`. I do not want "Git to remove those improvements" but rather have a `merge commit` of those new "improvements" in `B` —which obviously changed same lines in `A` (thus my expectation of a `merge commit`)—and the commit at the tip of `A` – Damilola Olowookere Feb 08 '19 at 03:55
  • Please also clarify this (so that I'll know maybe I have a wrong workflow) - while working on feature branch A above (branch A branched-off of master), I want to make some fix to master. But since I have a lots of changes in branch A, so much that it makes lots of sense to base the fix on branch A rather than on master branch, I created branch B from A (since I will eventually merge A into master when I am done with A). However, I still continue to develop branch A alongside branch B. Now merging branch B with A results in up to date. How is this best handled? – Damilola Olowookere Feb 08 '19 at 04:22
  • 1
    If you make new commits that are *only* on / contained-in A, `git merge` while on B, to pick up the new work you also did on A, will work. To bring the *name* `A` forward to point to the commit at the tip of B, do a fast-forward; to force a merge (a separate commit), use `git merge --no-ff feature-branch-b` while on `feature-branch-a`. To really understand this and the whole on/contained-in thing, work through the web site [Think Like (a) Git](http://think-like-a-git.net/). – torek Feb 08 '19 at 07:19
0

While @torek has given a very brilliant and thorough answer, I want to give a little up-the-sleeves slight. If you managed to screw-up your file versions by whatever means, and you just seem very uncomfortable with the dreaded "Already up to date" message even when diff shows clearly the differences (very annoying. Stop lying!!), you can simply leverage on Microsoft's Visual Studio Code editor.

There is this awesome extension called GitLens that truly supercharges git for you. So I resolve to using this when I mess-up my file versions, or when I simply want to manually cherry-pick changes in just a single or few files. The extension allows opening a file from different commits side-by-side, and it nicely highlights the diffs in the two version so changes can be easily made as necessary.

It is simply a time-saver (and hair-saver too :p))

Damilola Olowookere
  • 2,253
  • 2
  • 23
  • 33