Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
390 views
in Technique[技术] by (71.8m points)

git - How to normalize a merge from a pull request after doing a manual merge

There are three branches: dev, master and release.
The flow is to merge from dev to master and from master to release.
The release branch is referenced in production.
(Basically, the source is reflected in production by git pull.)

I'm using Gitea, which is similar to Github.
Basically, I'm merging into each branch with a pull request.

One day, I was asked to reflect only a particular commit on the release branch.
At that time, I used cherry-pick to merge specific commits from the master branch to the release branch.
At this time, I manually merged from the command line ...

And released.

Then, a few days later, I merged the master branch into the release branch to release all the commits.
At this time, I manually merged from the command line ...


And the current issue is what to do if the pull request doesn't pass.
I can merge from dev to master from pull request.
But there are some conflicts for merging from master to release by pull request.

Last time, I manually resolved and merged the conflicts in release branch, so the dev, master, and release branches had the same content.
After that, development was committed to dev.
After that, I was able to merge from a pull request from dev to master. (No conflict)
And merging from a pull request from master to release will result in conflicts.

I think the cause is the effect of the previous manual merge in release branch.
How to normalize a merge from a pull request after doing a manual merge?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...