I have two branches in my git repo. There is a config file which should be different in two branches. I added config merge=ours
to .gitattributes to preserve the config file when merging.
The config file does not get merged when merging from B branch to A branch. See the below graph
*---*---*---*--*(A)(HEAD) //config file in branch A remains
\ /
*---*---*(B)
However, when I merge from A branch to B, my config file also gets merge
*---*---*---*--*(A)
\ / \
*---*---*----*(B)(HEAD) //config file in B gets merge with A
Can someone tell me what is the reason? And how I can preserve the file when merging from both directions? Even though I found other discussions to preserve file when merge only one direction, I couldn't find a discussion about this particular scenario.
A merge driver as defined in a .gitattributes
file is used only when all three input files differ.
This means that setting merge=ours
(and defining an ours
merge driver) will often fail. I recommend not bothering with this: just define your own process by which you'll handle these.
Remember first that git merge
doesn't always do a real merge:
-s
/ --squash
directs it to not make a real merge at all, but it will still use the merge-as-a-verb action (and prohibits fast-forwarding).
--no-ff
will prohibit a fast-forward not-a-merge operation, forcing a real merge for this case.
--ff-only
will cause the command to fail (and not merge) if a fast-forward not-a-merge is not possible.
Keep these in mind when looking at the rest of this:
We're considering only the resolve and recursive strategies here; octopus merges and -s ours
are quite different.
One of the input commits is always HEAD
. Another is the commit you name on the command line. The git merge
command will locate the merge base commit on its own; this is the third input commit. If there is more than one merge base commit, -s resolve
will pick one at random and -s recursive
will first merge the merge bases—this is the inner recursive merge—and then make a new commit to use as the merge base for the outer merge.
If the merge base commit is the HEAD
commit, a fast-forward is possible. If allowed, Git will not do a merge, and will instead just check out the other commit named on the command line, adjusting the current branch to point to that commit.
If the merge base commit is the other commit, no merge is required: the current branch is up to date and nothing happens.
This leaves the real merge cases: there are three different commits involved. Each of those commits has a full snapshot of however many files, and Git will:
HEAD
to see what we changed;To combine these changes, Git will use a low-level merge driver. This is the kind of driver you can define with a merge=name
.gitattributes
setting. (Note: the recursive/resolve merge code will do its own high-level merging to handle newly-created files, deleted files, and renamed files, before reaching down to the low-level merge handler. This high-level merge may produce a conflict, which will cause the merge to stop, regardless of whatever the low-level driver might do.)
This is where the problem comes in though: the high-level merge code, which handles any high-level conflicts, doesn't bother to run the low-level merge code at all if it can skip it. This high-level code skips the low-level code whenever the raw hash IDs of the to-be-merged file(s) match across multiple commits.
That is, suppose we changed file F
and they did not. Then the hash of F
in the merge base matches the hash of F
in their commit, but the hash of F
in our commit differs. Git assumes that the correct action is to take our file: it never even runs the low-level merge driver. With the default low-level driver, that's fine. With an ours
merge driver, that's still fine: Git took our F
.
But suppose we didn't change F
and they did. Then the hash of F
in the merge base matches the hash of F
in our commit, but not the hash in theirs. Git assumes that the correct action is to take their file. It never runs the low-level driver, even if it is an ours
merge driver.
If Git had a .gitattributes
setting that forced it to run the low-level driver for each of these files, that would fix the problem. But it does not.