CONFLICT (modify/delete): ...
There are different kinds of merge conflicts. The ones you mention here:
Usually ... I can open the files, and the conflicts are pointed out
are what I call low-level conflicts as they're generated by Git's ll-merge.c
code, where ll
stands for "low level". A low-level conflict means that some part of some file has two conflicting changes: the --ours
changes, and the --theirs
changes. In this case, the work-tree copy of the file shows both versions of these lines. They will include a third, merge base, version if you select the diff3
style of conflict using Git's merge.conflictStyle
setting.
We must contrast this with the other kind of conflict, a high-level one:
A high level change occurs when something that isn't a source code line has changed about a file. For instance, its name might be changed: perhaps, in the merge base commit, some file was named thing.py
, but now, in --ours
or --theirs
, it is renamed something.py
.
If one side of the diff, such as base-vs-ours, has made a high level name change, and the other side (base vs theirs) has not changed anything, Git will take the one change.
The same is true if both sides made the same change. If we renamed thing.py
to something.py
, and they also renamed the same file the same way, Git can combine these changes.
But if both sides made conflicting changes—if we named it things.py
and they named it something.py
, for instance—Git does not know which change(s) to keep and which one(s) to discard.
There is no way to record the high level conflict as a line change in the file, so Git records it somewhere else: in Git's index.1 Remember that when Git makes a new commit, the snapshot it uses consists of the copies of the files that are in the index, not the copies that are in the work-tree.2
In this case, the conflict was that one side—theirs, in this case—completely deleted a file, while another side—yours, in this case—modified the file. Git does not know whether to keep the file with your change made, or to delete the file entirely. You must choose the correct resolution, which might not be just "keep" or just "delete": perhaps some additional file(s) must change too.
As simon-pearson answered, some commands, such as git mergetool
, will offer you the opportunity to delete the file entirely (their change) or keep it with your changes (your change). If one of those two actions is the correct one, that's fine. If not, your job, as the person completing the merge for Git, is to take whatever actions are correct, and record them in Git's index.
Git's index holds copies of all the files that will go into the next commit. The copies of files that you see in your work-tree are there for you to work with. Do whatever you like with those, then use git add
and/or git rm
to update the index copies of each file. Running git add
or git rm
marks the conflict as resolved.
1There is a bit of a flaw in the way Git saves this information. In in this case, though, you'll have two index copies of the file instead of three of them, and if there are no other high level conflicts, that's sufficient to know what happened.
2Technically, the index holds references rather than copies, but the effect is pretty much the same.
More about the index
Git's index, which Git also calls the staging area, is a bit complicated. It has more than one role. Its main role, most of the time, is that it's where you build the next commit you'll make.
Normally, when you're not merging, you do:
git checkout somebranch # or git switch somebranch, in Git 2.23 or later
and then work in your work-tree, run git add
on updated files, and git commit
. The part that you don't see is that the initial git checkout
created, in Git's index, the entire set of files from the most recent commit for somebranch
. These files then got copied into your work-tree so that you can see and edit them.3
What git add
does is copy the work-tree file—the one you edited—back into the index, replacing the previous copy that was in the index. This is why you have to git add
a file every time you edit it: it's not because it wasn't there in the index—it was there all along—but rather because the copy in the index is now out of date, and you must replace it.
Note that git status
didn't list the file as staged
before the git add
. After the git add
, it does list the file as staged
. This doesn't mean the file wasn't there before and is now. What it means is that the copy that was there before, matched the copy in the commit. Now, after the git add
, the copy that is in the index no longer matches the copy in the commit.
In other words, there are, at all times, three copies of each file:
HEAD index work-tree
-------------------- ------------------- -------------------
README.md README.md README.md
.../nlue_service.py .../nlue_service.py .../nlue_service.py
Initially, all three copies match. Then you change one—the work-tree one—and now HEAD
and index match, but index and work-tree differ. Then you git add
, which copies the work-tree copy into the index: now HEAD
and index differ, but index and work-tree match.
If you make all three copies different—which you can do by editing the work-tree copy, using git add
, and then editing the work-tree copy again—you'll see that the file you did this to is both staged for commit
and not staged for commit
. These two messages just mean the HEAD
and index copy are different (staged
) and the index and work-tree copy are different (not staged
).
3There are a lot of details that this mental picture glosses over, but it's suitable as a starting point. In fact, as noted in footnote 2, the index doesn't hold copies of each file, but rather references to Git blob objects. Moreover, when switching commits, Git takes a lot of care to only replace files that really need replacing. This makes the operation a lot faster, and also gives you the ability to switch branches while holding uncommitted changes in your index and/or work-tree ... sometimes, anyway. For much more about that, see Checkout another branch when there are uncommitted changes on the current branch.
The index expands during merges
As noted above, the index normally has a copy of each file, so that there are three active copies at all times:
One copy is committed, in HEAD
. Nothing can change this copy! It's committed, so it's frozen for all time. (You can change which commit the name HEAD
selects, but not the committed copy of the file.)
Another copy is in the index. It's in the same format as a committed copy, but you can overwrite it with a new copy. When it's the only copy in the index, that's pretty straightforward, but let's note here that this is in "slot zero".
The last copy is the only one you can see and work with. It's in your work-tree.
When you do a merge, Git expands the index. Instead of just one copy of each file, it holds (up to) three copies of each file. These copies go in numbered slots. Each of these three copies comes from a commit.4 So if we add these three copies to the HEAD
and work-tree copy, we now have five copies of each file!
When you run git merge
, Git locates three commits:
The merge base is the common commit. Git puts this in slot #1, but we can't really talk about it until we talk about the other two commits.
The current or --ours
or HEAD
commit is the one you have checked out. Git puts this in slot #2.
The other or --theirs
commit is the one you name. When you run git merge other-branch
, the name other-branch
selects one specific commit, the same way git checkout somebranch
checked out one specific commit. Git puts the files from this commit in slot #3.
The merge base commit is the best common commit that's on both your branch and the other branch. (Remember, Git commits are often on many branches at the same time.) Git finds this commit on its own, using the commit graph, which we won't go into here.
So, at this point—just starting a git merge
—Git drops all the slot-zero entries and instead fills in slots 1, 2, and 3 with each file from each of the three commits. Now that the index has three copies of README.md
and three—or maybe two—copies of .../nlue_service.py
, the merge can begin:
If all three copies of the file are the same, Git knows that the merge result is: just use any copy. Git discards all of the high-numbered slots and puts one copy in slot zero.
If the copy in slot 1 (the merge base) matches the copy in slot 3 (the other commit), then only you changed the file. Git discards the slot-1-and-3 copies and moves the slot-2 copy-your version of the file—to slot zero.
If the copy in slot 1 matches the copy in slot 2, then only they changed the file. Git discards the slot-1-and-2 copies and moves the slot-3 copy to slot zero.
If all three copies are different, Git runs the low-level merge.
The low-level merge attempts to combine the changes and write the result. If it can combine the changes, it writes the result to slot zero and to your work-tree copy, and removes the slot 1-2-3 copies: this file is now merged.
If the low-level merge can't combine the changes on its own, it writes a conflicted file to your work-tree copy and leaves all three slots occupied. Your job is to fix things up, write the correct file to your work-tree, and use git add
. This will erase the slots 1-2-3 copies and write your work-tree version into slot zero, and the file is now merged.
In this case, though, what happened is that the merge base copy and your copy were different and their copy is just missing entirely. So there's a .../nlue_service.py
in slots 1 and 2, and slot 3 is empty. The slot 1 and 2 copies are different. When Git stops with the merge conflict, you have two different .../nlue_service.py
files in slots 1 and 2, and one of those in your work-tree:
Version HEAD of backEnd/nlue-service/nlue_service.py left in tree.
So the work-tree copy matches the slot-2 copy.
If you decide that the correct thing is to keep this version, without making any further changes, you can just run:
git add backEnd/nlue-service/nlue_service.py
This copies the work-tree copy of the file into slot zero, erasing slots 1-2-3 (there's nothing in slot 3 at that time but that's OK). The file is now resolved, and the index copy matches the work-tree copy.
If you decide that the correct thing is to make more changes to this file and/or other files, you can make those changes, then run git add
on each such file. The git add
will copy the work-tree copy of each added file to slot zero. If there was a file in slot zero, it's now been replaced. If there were files in slots 1-2-3, they're now gone.
4There's a special case for some merges where the --ours
copy comes from what's already in the index. Or, another way to put this is that git merge
itself demands that the index already match HEAD
, so that each slot #2 entry always comes from the default slot-zero entry.
Conclusion
Your job, when Git can't complete a merge on its own, is to complete the merge. You do this by adjusting the set of files in Git's index, using git add
and/or git rm
. The git add
command copies files from the work-tree to the index. The git rm
command removes files from the work-tree and from the index (both).
The work-tree copies of files are ordinary files. You can use any program you like to work with them. The index copies are Git-only; you must use Git to see or update them. To view the copy of a file that's in slot 1, use:
git show :1:backEnd/nlue-service/nlue_service.py
for instance. This :number:path
syntax means read the file that's in the numbered slot in the index.
Once you've put all the correct files to slot-zero, you can finish the merge with git merge --continue
or git commit
.