简体   繁体   中英

How to resolve this git conflict?

I'm really having hard time understanding what went wrong with this merge. I've created a feature branch and restructured some parts of the project. When i tried to merge it in master, things got messy and the conflict messages are confusing me. I have the same file at the same location in both branches

"legacy/css/menu.css"

. Unmerged conflict paths shows the following message:

added by us - "legacy/css/menu.css"

First of all I don't get why a conflict may arise from an "added by us" situation (it does not say "both added" so what's the problem?).

Second of all when I try to check out "their version" of the file it says: "legacy/css/menu.css" does not have their version. Ok?

The file does not have any conflict markers in it.

I'm assuming that git does not see any relation between the two files except that they have the same name and happens to be at the same location (though I don't know why I don't think I've moved that part of the project "/legacy/**" at all actually).

Can someone please give me some insight about the possible cause of this situation ? and how to solve this conflict. I need "their" version to be in the merge's commit's snapshot. Thanks in advance for your time and effort :)

I've created a feature branch and restructured some parts of the project .

The bold text here (bolding being mine) is the key, I think, and YeXiaoRain's answer shows how to set up a conflict like the one you're seeing. (We don't know enough about your repository—about all the commits in it—to be able to set up the exact same conflict.)

When you have some branch's tip commit cleanly checked out, either as a result of:

git checkout somebranch

or:

git checkout somebranch
... do some work, git add, etc ...
git commit

so that all is ready for a merge, and then you run:

git merge othercommit

you instruct your Git to:

  1. Find the merge base between the current ( HEAD ) commit and the other commit you specified. Let's call this merge base commit B (for base).

  2. Run two git diff --find-renames operations:

     git diff --find-renames B HEAD git diff --find-renames B othercommit 

    This converts the snapshots in commits HEAD and othercommit into change-sets. Both change-sets apply to commit B , so now Git can combine these two change-sets. This leads to step 3.

  3. Combine the two change-sets. Apply the combined changes to the snapshot in commit B . If some of the change-set changes conflict with each other, declare a merge conflict.

  4. If no merge conflicts occur, automatically commit the result as a merge commit. If some conflicts do occur, stop with a conflict, leaving the conflicted merge state in the index and work-tree, with everything else set up so that the next successful git commit will make a merge commit instead of an ordinary single-parent commit.

You are now stuck between steps 3 and 4, because a merge conflict did occur. Clearly, you are familiar with the more common cases of merge conflicts, where the left and right sides ( HEAD and otherbranch ) both direct Git to, for instance, change line 42 of file path/to/file.ext , but the left and right sides say to change that line to different final text. That's what I call a low-level conflict: a conflict within some file. But that is not the only possible kind of conflict.

When Git did the two diffs in step 2, it used --find-renames . There are many things to know about the rename-finding that Git does, and I won't be able to cover all of them here, but the first and most important is this: Git's rename detection is approximate . It can be fooled. It depends on a simple fact: git diff is comparing two snapshots, each of which has some set of files.

Suppose that Git is comparing commit B (the merge base) to commit HEAD (your current branch tip). Suppose further that there is a file named path/to/old in B , but there is no such file in HEAD . Meanwhile there is a file named new/path/to/file in HEAD , and no such file in B . This pair of files is added to a rename candidates queue . The queue holds every file that seems to have gone missing in B and/or been newly created in HEAD .

Git then does a (slightly cheat-y variant of a) massive matrix-fill-in job with every file that has gone missing or been created, to see how close each possible pairing of files is. Does missing file O1 (old-file-number-1) match newly created file N7 (new-file-number-7)? Any exact matches get taken quickly, because Git is good at finding exact matches quickly. These proposed matches are paired up and taken out of the matrix. All remaining not-exact-match potential pairings get fed through a fast-but-not-as-thorough diff-like algorithm—not regular git diff , which is too slow for this—to determine a similarity index between the two files.

The result is a big matrix full of similarity index values:

file    N0    N1     N2    N3    ...
  O0   20%    92%    55%   37%   ...
  O1   17%    41%    22%   93%   ...
  ...  ...    ...    ...   ...   ...

The above suggests that file O0 has become file N1, and O1 has become N3, for instance. Git takes any value that's at least 50%—or some other threshold you specify: 50% is the default—as "can be paired up", and uses the highest similarity for doing the actual pairing-up, after which both the old and new files are removed from the queue. 1

In the end, after running through the rename-finding queue, Git has some files paired up. The rest are treated as deleted or totally new .

In this particular case, Git may have identified some renames on your side and/or some renames on their side. This may have led to a rename/rename conflict . This conflict is printed during the git merge operation, but not recorded properly in the index, so that a subsequent git status generally sees only added by us and/or added by them . Let me say that again in bold: What Git leaves behind in the index, for later analysis, is not sufficient in general to reconstruct the original conflict. There are other kinds of conflicts, such as add/add , modify/delete , and rename/delete . An add/add conflict, for instance, occurs when B has no file named path/to/file but both HEAD and othercommit do have a file named path/to/file .

All of these are what I call high level conflicts. The origin of such a conflict is not recorded in the index. You only get to see, from the index, that a conflict has occurred. Because these are not conflicts with changes within a file—they're conflicts that have to do instead with the file's names —there are no conflict markers in the work-tree copy of the file. 2

Your job at this point is not so much to fix up the work-tree—though you'll probably have to do that, to complete the job that Git leaves to you—but rather to clean up the mess that Git has left behind in the index .


1 The precise algorithm here could change, and the Git authors have been fiddling with it to try to improve it by treating path names as possibly containing directory names to see if there's a been a directory rename. Note that most parts of Git just think of a file has having a full name, eg, path/to/file.ext is not two-directories-and-a-file. There are no folders in Git: the file just has the long-string-y name path/to/file.ext . But real systems do have directories / folders, so a real comparison probably ought to take that into account.

2 Remember that the work-tree copy of any file is, generally, not all that important to Git. Git does leave marked-up merge-conflict work-tree files during git merge , but these are secondary to the files that Git really does care about, which are the index copies.


How to clean up the index during a conflicted merge

First, it's important to realize what's in the index. If you have a very small set of files, you can run:

git ls-files --stage

to see, directly, what's in the index. You can do this any time, whether or not you have any merge conflict to resolve: git ls-files is just a diagnostic tool to look at the index, and/or compare the index vs the work-tree, depending on flags and other arguments. With --stage , you tell Git: dump out the contents of the index, listing the file hashes and stage numbers.

When there are no conflicts (or you're not in the middle of a merge at all), all the stage numbers are always zero. That's not very interesting, and you could and should wonder why there are stage numbers at all.

The stage numbers are the real key to a conflicted merge. Consider the more typical case of a merge conflict, where some file F in commit B is changed in both HEAD and othercommit . Moreover, both changes are to line 42, and the two changes differ. If you open the work-tree copy of the file, what was line 42 is now surrounded by conflict markers—but that's just so that Git can show you the conflict. To Git, the important part is that there are now three copies of file F in the index, in three numbered staging slots:

  • Slot #1 holds the merge base copy of file F .
  • Slot #2 holds the --ours copy of file F , from commit HEAD .
  • Slot #3 holds the --theirs copy of file F , from commit othercommit .

As always, Git has whole snapshots of whole files . In this case, since there are three snapshots, they're in the three staging slots.

For high-level conflicts, however, things get a bit weird. Suppose you have a simple add/add conflict: the merge base didn't have file F , and you and they both created file F . Then:

  • Slot #1 is empty.
  • Slot #2 has your F .
  • Slot #3 has their F .

(The work-tree has one of the two copies of F , but not the other.)

Suppose instead that you have a rename/delete conflict. You renamed F to F2 and they just deleted F entirely. Then:

  • There is a slot-1 F , but no slot-1 F2 at all.
  • There is a slot-2 F2 but no slot-2 F .
  • There is no slot-3 F and no slot-3 F2 .

Again, this makes perfect sense from Git's point of view: there is an F in the merge base, so there is an F-slot-1 . There is no F2 in the merge base, so there's no F2-slot-1 . There is an F2 in your commit, so there is an F2-slot-2 , and so on.

In all cases, your job is to put the correct copies of whole files into slot zero, and erase all the other slot-numbered entries. That's what it means to resolve a merge conflict. Regardless of how the merge conflict occurred—whether it was an ordinary low-level conflict, or one of these weirder high-level conflicts—you have some files in some nonzero staging slots right now . You must erase those slots and fill in the zero-numbered slots.

In general, the way to fill in a slot zero is to use git add . What git add does is to copy a work-tree file into the index. While doing this copying, it removes any high-numbered slots. So if there are three F s in the three possible slots 1, 2, and 3, and you git add F , this removes the other three F s while copying the work-tree F into slot #0. If you do this git add after fixing up the marked-up work-tree file, you've resolved file F correctly and you can move on.

If some file in some non-zero staging slot should just be eliminated, you can use either git rm or git add . I like to use git rm myself, even though it will complain a little bit, because this makes more sense to me. Suppose after one of these high-level conflicts, you decide that the correct result is to have the merge commit contain a file with a whole new name you make up right now: G . You just want to get rid of F1 and F2 entirely. So, you put the right file into your work-tree and call it G , and then you run:

git add G
git rm F1 F2

The add step puts a copy of G into the index at staging slot zero. Whether there was some G there before, in any staging slots at all, is irrelevant: there's now a G in slot zero. The rm step removes all copies of F1 and F2 in all index slots, and complains if/when there's no F1 and/or F2 in the work-tree. The complaint is not important: what matters is that, now, there is no F1 and no F2 in the index.

You can, weirdly, use git add to remove entries from the index. Suppose there's an F2 in the index, at any staging slots—the number or numbers do not matter—but there's no F2 in your work-tree. You can git add F2 to tell Git: *remove F2 from all of its staging slots. This generates no complaints because git add was able to do something, even though the thing it did was remove! It works, and maybe is how you're supposed to use Git, but it just feels terribly wrong to me, so I use git rm even though it complains.

In any case, your job, to complete this conflicted merge, is simply to arrange all the files into the index at their staging-slot-zero "normal, not conflicted" position. How you do this is not important to Git: the only thing that matters is that you do it. The easy way to do it, though, is to work with your work-tree, and then git add and/or git rm to update the index.

Once you have cleaned up the mess in the index—and usually the work-tree too, as a side effect—you just run git merge --continue or git commit to make the final merge commit. Git makes this commit from whatever is in the index , as it always does for any ordinary commit. It uses the other extra information that git merge left behind, so as to make the new commit be a merge commit, with othercommit as its second parent.

High level conflicts really should be recorded in the index, so that git status can tell you why some file is marked unmerged, and so that programs like git mergetool can behave better when working with complicated merge conflicts. This could be fixed in a future Git: the index file format has version numbers, so even if this sort of extra information doesn't fit in today's index files, it could be put into tomorrow's by going to a new index file format. But it's not recorded today, so if you spot some high-level merge conflicts when you run git merge , it may be wise to save that information somewhere until you've finished the merge.

What may cause

maybe some like below

# init git resposi
mkdir /tmp/demo && cd /tmp/demo
git init
echo "aaaa" > A
git add . && git commit -m "commit"

# move file A to A1 in branch b1
git checkout -b b1
mv A A1
git add . && git commit -m "commit"

# move file A to A2 in branch master
git checkout master
mv A A2
git add . && git commit -m "commit"

# try to merge branch b1 to master
git merge b1

and now, type git status you will see

    both deleted:    A
    added by them:   A1
    added by us:     A2

What you should do

you should tell git about what the code you want by git add or git checkout

if you want to keep the A1 , you can use

git add A1
git rm --cached A A2
git merge --continue

or

git checkout b1 A1
git rm --cached A A2
git merge --continue

More Detail

from the perspective of git

when git start merging

he will try merge all the code which he is able to infer the final version.


for example, file X in source branch is

line1
line2
line3
line4
line5

and now branch A and branch B are both come from source branch

maybe the branch graph is like bellow

source branch ---*---*---*--- branch A
                  \
                   \
                    *---*--- branch B

and now , git merge branch A with branch B


Case 1

file X only change in one branch

git can infer the final version of file X will be then changed one

Case 2

both branch edit file X but, the changed line has no conflict

for example, branch A change the line1 to newline1

and

branch B change the line5 to newline5

in this case, git also know the final version of file X will be

newline1
line2
line3
line4
newline5

case 3

when the change is conflict in branchs

this time, git has no idea how to merge, it will mark the file as both modified

and when you open the file you will see something like below:

line1
line2
<<<<<< some branch name
line3 change in branch A
======
line3 change in branch B
<<<<<< another branch name
line4
line5

this time, it's your turn, you should modify the file to what you want, and tell git you have solved the merge conflict, and then let git do the git merge --continue


Conclusion

  • if git know how to merge (haven't found any conflict), git can only using git merge to merge two branch

  • if git find some conflict. So the Complete the steps is as bellow

    1. you call git merge
    2. git find conflict, git pause the merge, git mark the files both modified/both deleted/added by us/added by them
    3. you modify the files to what you want with any editor you like
    4. you tell the git you have solve the conflict. git add <file name/folder name>
    5. call the git merge --continue

You can download the 3rd party merge tool called P4Merge, which can help you resolve the conflicts by comparing the local | Base | Remote version of the modified code. Download the P4Merge from: https://www.perforce.com/products/helix-core-apps/merge-diff-tool-p4merge

Also I recommend you using a Git GUI tool such as - Sourcetree

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM