imnothardcore
imnothardcore

Reputation: 271

Merging two branches, how do I accept one branch for all conflicts

Im merging two branches together, lets say branchA and branchB. They have about 100 files that conflict.

branchB has the accepted work for everything and is 100% what I need. I don't want to force push branchB or anything.

Is there a way I can merge the two and just say for any conflict accept whats on branchB so I don't have to open the files or "theirs" or "ours" every single file.

was trying a recursive merge from branchB pulling branchA with the -s and -x option in but that didnt seem to work as expected

thanks

Upvotes: 18

Views: 19015

Answers (1)

torek
torek

Reputation: 489888

Given what you asked, the answer is the simple sequence:

git checkout branchA
git merge -X theirs branchB

Before you blindly apply this, make sure you know what you are doing! You also say that you used -s and -x, but git merge does not have a -x option. (Perhaps you meant -X. Showing the actual commands you used, and at least some of the results, would probably have helped.) If you got what I call high level conflicts, the -X option won't help and you'll need to do some manual cleanup, but you can automate some or even all of it.

Merge essentials

Remember that what git merge does is to combine changes. To do this, it needs to find a common starting point. Let's say you're either Alice or Adam, working on branchA, or Bob or Barbara, working on branchB. User "A" started from some commit * and made some more commits o ending in commit A:

          o--o--A   <-- branchA
         /
...--o--*

Meanwhile, user "B" started from the exact same commit, the one labeled *, and made some commits:

...--o--*
         \
          o--o--B   <-- branchB

What you have now, in your repository, after running git fetch for instance, is the complete set of commits:

          o--o--A   <-- branchA
         /
...--o--*
         \
          o--o--B   <-- branchB

When you run git checkout brachA; git merge branchB you have your Git find commit * for you—this is the one you both started from, however far back it may be in history—and then you have your Git run two diff commands:

git diff --find-renames <hash-of-*> <hash-of-A>
git diff --find-renames <hash-of-*> <hash-of-B>

Git will now attempt, if it can, to combine both sets of changes, applying them to whatever was in commit *. If Git can do all of this on its own, it makes a final merge commit—an almost-ordinary commit, except that it has two parent pointers, pointing back to both commit A and commit B. This changes the name branchA to point to the new commit as well, giving:

          o--o--A
         /       \
...--o--*         M   <-- branchA (HEAD)
         \       /
          o--o--B   <-- branchB

The snapshot associated with new commit M is the result of Git's combination of your changes—* vs A—with their changes, * vs B, applied to *. (Any intermediate commits are completely ignored during this process. Git is only interested in applying to * the union of your two change-sets.)

When some change(s) to some file(s) in *-vs-A conflict with some change(s) to some file(s) in *-vs-B, Git will typically stop with a merge conflict. The uppercase -X option lets you tell Git that, in the event of a conflict, it should prefer either "our" (*-vs-A) or "their" (*-vs-B) change. However, if there isn't a conflict, Git will go ahead and take both changes. This applies within a single file. An example may help.

Suppose that the *-vs-A change-set includes these instructions for file README.txt:

  • Change the word "yellow" to "brown" on line 3. (It actually sees the whole line as changed, but let's go with "word" here.)

  • Change the words "the yellow cow" to "a brown cow" on line 20.

Suppose that the *-vs-B change-set includes these instructions for file README.txt:

  • Change the words "the yellow cow" to "a large yellow cow" on line 20.

Because both change-sets try to change line 20, you will get a conflict here. Because only one change-set changes line 3, that change will not conflict, so Git will apply it. With -X theirs, Git will prefer their change on line 20, which will now have the words "the large yellow cow" even though it changed line 3 to "brown".

Given -X theirs, Git thinks this is the correct resolution: take your unconflicted change to line 3, and their conflicted change to line 20. It's quite likely wrong, but that's what Git will do. It's sometimes better to let the conflict happen, and then go inspect things yourself to see what the correct resolution is; but if you have good tests, you should catch most problems that escape Git anyway, so -X theirs (or -X ours) is a useful tool.

High level conflicts

If you did already use -X theirs and still have conflicts, what you have are what I call high level conflicts. Note that the two git diff commands above, to find the change-sets from * to A, and from * to B, both use --find-renames. This means that between * and A, Git may decide that you (or user A anyway) renamed README.txt to README.rst. User B renamed it as well, but to read-me.rst. Git does not know which name to use, and -X theirs does not tell it: use their name. Git just stops with a conflict.

Similar high-level conflicts occur if you both added a new file with the same name, or if you modified some file and they deleted it or vice versa. The former is an add/add conflict, and the latter is a modify/delete conflict. There are several more such conflicts. All of them mean that Git either doesn't know which file(s) to keep or which name to use.

In all of these cases, Git will stop with a merge conflict, just as it would if you had not used -X. When Git does stop this way, it leaves all the files in the index / staging-area, using stage numbers to identify which one is which. For instance, if there are conflicts in README.txt, there are now three copies of README.txt in the index:

  • :1:README.txt is the version from the base commit, commit *.
  • :2:README.txt is the version from the HEAD commit, commit A.
  • :3:README.txt is the version from their commit, commit B.

For add/add conflicts, version 1 does not exist (the file was not in the base). For modify/delete conflicts, either of versions 2 or 3 does not exist (the file was deleted in A or B).

You can use git show on these names: git show :1:README.txt to see the base version, for instance.

The work-tree has one copy of README.txt, which is typically Git's best-effort at combining the three inputs, perhaps with conflict markers.

Your task is now to create a stage-zero entry, :0:README.txt, for the file, wiping out the stages 1-2-3 entries. The easiest way to do that is often to edit the work-tree file and run git add README.txt. The git add command copies whatever is in the work-tree into stage zero, removing stages 1-3 if they exist.

You can also run:

git checkout --ours README.txt

or:

git checkout --theirs README.txt

which will copy version 2 or version 3 respectively, from the index, to the work-tree. Note that this does not affect the index contents! It only extracts from the index—remember, this is also called the staging area, but with three entries in it none of them are staged yet—to the work-tree.

You can also run:

git checkout branchA -- README.txt

or:

git checkout branchB -- README.txt

These copy the committed file from commit A or commit B, into the index at stage zero, and then from the index to the work-tree. The copying into stage-zero makes the file ready to be committed, as it has wiped out the slot 1-3 entries.

You can also run:

git rm README.txt

which will remove the file from all stages and from the work-tree. (You should do this only if the correct merge result should lack the file README.txt.)

Note that you can do any of these actions from a script that you write. You can use git ls-files --stage to examine the index. During a merge, the conflicted files and their nonzero stage numbers will show up here. Or, you can use git ls-files -u to show only the unmerged (stage greater than zero) entries.

Hence, if you run git ls-files -u after getting merge conflicts while using -X theirs, you will see the set of files that still have issues, all of which are by definition due to high level conflicts. You can then decide from this whether it is OK to do an en-masse operation to extract their version of all such files. If so, simply write a short command or script to take all those file names and pass them to git checkout branchB, e.g.:

git ls-files -u | cut -d$'\t' -f 2 | uniq | xargs git checkout branchB --

(as long as no file names have white-space in them).

Upvotes: 28

Related Questions