A short (hah) essay about parallel branches and merges
The situation you're experiencing is a result of the Git flow you are using, where there are two (in your case, three) persistent parallel branches. That flow only works if you merge consistently and fully in one direction from one branch to the other. If you subvert that topology, you are letting yourself in for potential conflict.
Why the topology works (when it does work)
To understand how this topology works, you have to ask yourself: What is a merge? It is an attempt to reconstruct and perform multiple sets of changes simultaneously (usually, exactly two sets of changes).
Let's demonstrate. Here I've prepared a repo in the standard topology I just described. There is just one file in the working tree (and in the repo), and it has just one line, which I am changing on every commit. At the start, that line says "a":
* 086cbed (HEAD -> master) merged once again
|
| * 82abd8a (br) now it is d
* | ec0397a merged branch again
|
| |/
| * 2546392 changed it to c
* | f6535d4 merged branch
|
| |/
| * c9fcf16 changed a to b
|/
* a014c37 start
I have two branches, master
and br
. I keep working on br
and merging br
into master
. When I merge, I say --no-ff
so as to get a true merge commit each time; that's important.
Now, I am changing the same line of the same one-line file over and over, but there is no merge conflict. Why? Because master
is not making any contribution to the changes.
Let's put ourselves inside Git's head when we ask to merge br
into master
. Git looks at what happened on br
and sees that the first line of the file changed; and it also looks at what happened on master
and sees that nothing changed! So Git says to itself: "Well, it's easy to enact both of those changes simultaneously; stuff-done-on-br plus nothing equals stuff-done-on-br alone."
So Git enacts both of those change sets simultaneously by doing on master
what was done on br
, namely, it changes that one line of the file to match what it is on br
.
The magic meeting point
Now, before I go on, I should say something about what "changed" means. In particular, changed since when? This is actually crucial and is the key to the entire story.
When you ask Git to do a merge, Git immediately starts thinking about the changes from the point where the branches last met (or last diverged, depending on which way you look at it). This is called the merge base — and in fact, Git has a merge-base
command that tells you where it is.
For example, looking at the chart above, let's reconstruct the logic of the last merge (086cbed). Go back one step. Suppose we are at 82abd8a (on br
) and ec0397a (on master
) and we ask to merge br
into master
. Git works out that those branches last met (or diverged) at 2546392. That's the merge base.
And looking at the graph, you can see how Git knows this. ec0397a is a true merge commit, so has 2546392 as one of its parents; and meanwhile 82abd8a is a normal commit, and it has 2546392 as its ancestor (its direct parent, in this graph).
So that is the value of this topology. Providing you keep merging with true merges, so that there is a merge commit every time, and provided the target branch (here, master
) never makes any other contribution to the history, you will always be able to merge without conflict.
Perverting the topology
Okay, so now let's say you pervert the topology by making an independent commit directly on master
. Meanwhile you go on working on br
, and eventually you try to make your usual merge of br
into master
. Now there is a likelihood of conflict, because both branches are making a contributory change, and those contributions can conflict.
And that is exactly what you did by cherry-picking. Cherry-pick just creates a new commit out of nowhere. Well, not nowhere, but the point is, it is not a merge — even though you keep calling it a merge; that does not make it a merge. It is just a new totally independent commit on this branch. So you started a conflict — and now you want to know how to get out of it.
A perverted topology remains perverted
Now, you can get out of this situation and complete the merge, obviously, by resolving the conflict. But the trouble is, this does not necessarily solve the whole difficulty for future merges.
Think of it this way. How exactly are we going to resolve the conflict? If we don't choose the contribution from master
, then what was the independent commit on master
for in the first place? But if we do choose the contribution from master
, then master
is still making an independent contribution to the story, and we're still likely to get a conflict again the next time we merge!
So for example, here we are after resolving the conflict and merging br
into master
, and then making a further commit on br
:
* 5bad46b (HEAD -> master) merged after resolving conflict
|
* | a981153 changed it to z!
* | 086cbed merged once again
|
* ec0397a merged branch again
|
* f6535d4 merged branch
|
| | | | | * 7dc8475 (br) now it is f
| | | | |/
| | | | * f6a9ed5 now it is e
| | | |/
| | | * 82abd8a now it is d
| | |/
| | * 2546392 changed it to c
| |/
| * c9fcf16 changed a to b
|/
* a014c37 start
What's happened in that chart is that we independently changed the file to contain "z" on master
(a981153), perverting the topology. Meanwhile, we kept on working on br
(f6a9ed5), changing the file to "e". We then tried to merge — and got a conflict. Okay, so we resolved the conflict (5bad46b): we chose "z", because otherwise, what was the point of changing the file to contain "z" in the first place?
Okay, but now we go on working on br
(7dc8475) and let's say we now propose to merge into master
again. But what's going to happen when we switch to master
and try merge br
into master
? We're going to get another conflict!
Why? Well, the point of last meeting (divergence), the merge base, is f6a9ed5. What change was made on each branch since then? On br
, we changed "e" to "f". But on master
, we changed "e" to "z" — because that's how we resolved the conflict! (In other words, we made that change in the conflict resolution itself.) So that's a conflict if we try to merge at this moment.
So it looks like we are going to get merge conflicts forever going forward! It's a vicious cycle. The only way to get out of the cycle is to eventually let br
win when we resolve the conflict. And that is going to mean that the independent change we made on master
in a981153 is going to have to be undone somehow.
So, as we say, you can pay me now or you can pay me later, but sooner or later you must undo the perversion of the topology that was caused by the independent change on master
.
Backwards merge
The question is, what's a good way to do that? The answer is that we need to change the point at which master
and br
diverge. We need to move the merge base.
In particular, we need to move the merge base to the end of master
, because that way, when we merge br
into master
, we are back to the situation where master
is making no contribution; if the merge base is the end of master
, then master
is making no new contribution since the merge base. So from that point of divergence going forward, master
will not make any independent contribution, but br
will, and we will be back to our normal topology once again!
And that is why we merge backward — merge master
into br
. After doing that, the problem is solved; we can move forward on br
and eventually merge forward from br
into master
, and it will work. Here's a chart that demonstrates:
* 3e9b302 (HEAD -> master) Merge branch 'br'
|
| * df3d049 (br) Merge branch 'master' using 'ours' strategy
| |
| |/
|/|
* | 5bad46b merged after resolving conflict
|
* | | a981153 changed it to z!
* | | 086cbed merged once again
|
* ec0397a merged branch again
|
* f6535d4 merged branch
|
| | | | | * 7dc8475 now it is f
| | | | |/
| | | | * f6a9ed5 now it is e
| | | |/
| | | * 82abd8a now it is d
| | |/
| | * 2546392 changed it to c
| |/
| * c9fcf16 changed a to b
|/
* a014c37 start
In that chart, we merged backward at df3d049. As we did so, we resolved any conflicts in favor of br
. In fact, we resolved everything in favor of br
; we used the ours
strategy, which basically means: "Disregard the contribution of the incoming branch entirely, and just make a merge commit consisting entirely of the state of the current branch."
And now when we merge br
forward into master
, it works fine. The reason why the forward merge works now is that at that moment the point of meeting (divergence), the merge base, is 5bad46b — the previous merge commit into master
. And what's happened since then? Well, on master
, nothing has happened; the merge base is the last commit on master
before we make our forward merge. So on master
, our file was "z" and it is still "z". Meanwhile, on br
, we have changed "z" to "f". So when we merge forward, "z" is changed to "f" on master
, and all is well from now on.
Note, however, that "z" is changed to "f" on master
. Our solution has thrown away the contribution that was created at a981153. In that commit, we changed "a" to "z"; now that "z" is gone. But that is the price we have to pay if we're going to get things back on track. Creating a981153 was wrong, and could only operate as a temporary measure; now its time is over.