I'm trying to understand why this happens:
I have branched mybranch
from master
and made several commits that touch files A
, B
, and C
. Meanwhile someone else has pushed to master
a commit that deletes files X
and Y
along with some other changes. When I merge master
into mybranch
, git status
tells me X
is “unmerged” and Y
is “deleted.” Why are these files treated differently?
I believe that to be a reduction of what's happening to me in real life on http://github.com/borglab/SwiftFusion,
[Edit: I was mistaken about that, so the description above is inaccurate. @torek's answer below is excellent and instructive, though, so I'm not going to try to delete this question]
with master
at the tag conflict-merge-source
and mybranch
at the tag conflict-merge-target
, but I'm posting the actual repo in case I've missed something. The two files are Sources/SwiftFusion/Core/FixedShapeTensor.swift
(conflicted) and Tests/SwiftFusionTests/Core/FixedShapeTensorTests.swift (deleted).
There is a modify/delete conflict on one of the two files, and no conflict at all on the other, as you noted. The reason why one of the two files has no conflict is that one "side" of the merge did not touch the file at all, but the other side did.
For the file with the conflict, Git leaves the modified file in your work-tree and the conflict in Git's index, where you must resolve it. How to resolve it is up to you, but if the resolution is "keep the deletion" you can use git rm
on that name, and if it is to keep the modified file you can use git add
. Either way, Git's index—the proposed next commit—is now updated, and this particular conflict is resolved.
I cloned the repository in question and found the following:
$ git merge-base --all conflict-merge-source conflict-merge-target
a07af749bdd416c5217c363b3f1da509c58d2d14
$ base=$(git merge-base --all conflict-merge-source conflict-merge-target)
$ git diff --find-renames --name-status $base conflict-merge-source
M Sources/BeeDataset/BeeFrames.swift
M Sources/SwiftFusion/Core/DataTypes.swift
D Sources/SwiftFusion/Core/FixedShapeTensor.swift
M Sources/SwiftFusion/Core/MathUtil.swift
A Sources/SwiftFusion/Core/TensorVector.swift
M Sources/SwiftFusion/Image/OrientedBoundingBox.swift
A Sources/SwiftFusion/Inference/AppearanceTrackingFactor.swift
M Sources/SwiftFusion/Inference/FactorGraph.swift
M Sources/SwiftFusion/Inference/FactorsStorage.swift
M Sources/SwiftFusion/Inference/JacobianFactor.swift
A Sources/SwiftFusion/Inference/PPCA.swift
M Sources/SwiftFusion/Inference/PPCATrackingFactor.swift
M Sources/SwiftFusion/Optimizers/CGLS.swift
M Sources/SwiftFusion/Optimizers/LM.swift
M Sources/SwiftFusionBenchmarks/Patch.swift
M Tests/BeeDatasetTests/BeeDatasetTests.swift
A Tests/BeeDatasetTests/BeePPCATests.swift
D Tests/SwiftFusionTests/Core/FixedShapeTensorTests.swift
M Tests/SwiftFusionTests/Image/PatchTests.swift
M Tests/SwiftFusionTests/Inference/FactorGraphTests.swift
R062 Tests/SwiftFusionTests/Inference/PPCATrackingFactorTests.swift
Tests/SwiftFusionTests/Inference/PPCATests.swift
M Tests/SwiftFusionTests/Inference/SwitchingMCMCTests.swift
M Tests/SwiftFusionTests/Optimizers/LMTests.swift
$ git diff --find-renames --name-status $base conflict-merge-target
M Sources/SwiftFusion/Core/FixedShapeTensor.swift
M Sources/SwiftFusion/Inference/AnyArrayBuffer+Differentiable.swift
R050 Sources/SwiftFusion/Inference/ValuesStorage.swift
Sources/SwiftFusion/Inference/AnyArrayBuffer+Vector.swift
M Sources/SwiftFusion/Inference/ArrayBuffer+Differentiable.swift
M Sources/SwiftFusion/Inference/ArrayBuffer+Vector.swift
M Sources/SwiftFusion/Inference/PenguinExtensions.swift
M Tests/SwiftFusionTests/Inference/AnyArrayBufferTests.swift
Note the R062
and R050
lines: Git has detected that, since merge base commit $base
(a07af749...
), these files were renamed in the two selected commits. This isn't really all that important here but they were long lines and I split them up for posting purposes.
I'm not entirely sure which of the two commits you had checked out as your branch and which one you selected with your git merge
command, but since the merge process is mostly symmetric,1 it doesn't really matter.
We can do the merge with a detached HEAD, but I like to have a branch name, so I created one now:
$ git checkout -b t1 conflict-merge-source
Switched to a new branch 't1'
$ git merge conflict-merge-target
CONFLICT (modify/delete): Sources/SwiftFusion/Core/FixedShapeTensor.swift
deleted in HEAD and modified in conflict-merge-target. Version
conflict-merge-target of Sources/SwiftFusion/Core/FixedShapeTensor.swift
left in tree.
Automatic merge failed; fix conflicts and then commit the result.
(Again, I split up a long line for posting purposes.)
1Any asymmetries are a result of viewing one commit as "ours" and the other as "theirs". For instance, the -X
extended-options themselves have to pick an ours vs theirs, and a rename/rename conflict, if you get one, has to pick which rename to take (if any). These tie-breakers introduce asymmetry.
git merge
merges changesIn all cases with git merge
, Git compares the merge base (which we found above) with each of the commits to be merged. This turns each of those two snapshots into changes-since-a-common-starting-point. We can use git diff
(see examples above) to find out which files contain what kinds of changes.
Having found these changes, the merge code's job is now to combine the changes. The combination of making some change, on either "side" of the merge, with making no change on the other "side", is to take the change wholesale. That's the case for most of these files—all 22 of the M
ones, for instance. So git merge
can just take the copy of the file from whichever side changed the file, in this case. This part of this merge was easy: sometimes, merge has to start with the base version of the file from the merge-base commit and add both sides' changes to that.
But we have eight other status (not M
) files listed, 7 on one side and 1 on the other:
There are four A
files (new files) from base to one side. These have no same-named operation on the other, so the merge operation just takes those new files as new.
There are two R
files. They are on different files that the other side did not touch, so Git just keeps the renamed copy: we get only the new name (with new content if the content was changed too); the original copy of the original file in the merge base commit is dropped entirely. (If the merge were harder, that might not be the case.)
There are two D
files, on one side only. These are:
D Sources/SwiftFusion/Core/FixedShapeTensor.swift
D Tests/SwiftFusionTests/Core/FixedShapeTensorTests.swift
(the same two you noted). Here's what we see on the other side, for these two files:
M Sources/SwiftFusion/Core/FixedShapeTensor.swift
That is, the second file does not appear at all: it wasn't touched.
How should Git combine "deleted" with "untouched"? Git's answer is "keep the deletion". How should Git combine "deleted" with "modified"? Git's answer is keep the modified file in the work-tree and mark the file as conflicted in the index. That's not always the right answer, but if it's not, you can manually massage the index content, e.g., to restore the deleted file from either HEAD
or MERGE_HEAD
.
The final merge commit, once you make it, will contain whatever files you have Git copy into Git's index. Your work-tree copies of files aren't really important to Git; they are just there so that you can work with them. The index copy of each file is in Git's own internal, compressed and de-duplicated form.