Reputation:
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 :)
Upvotes: 1
Views: 8783
Reputation: 53
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
Upvotes: 0
Reputation: 488003
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:
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).
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.
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.
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.
1The 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, e.g., 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.
2Remember 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.
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:
--ours
copy of file F, from commit HEAD
.--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:
(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:
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.
Upvotes: 4
Reputation: 156
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
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
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
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
git merge
git
find conflict, git
pause the merge, git
mark the files both modified/both deleted/added by us/added by them
git
you have solve the conflict. git add <file name/folder name>
git merge --continue
Upvotes: 1