I'm looking to be able to use a different merge strategy on an individual file with git that actually performs a merge using the knowledge of a common ancestor commit that a typical merge handles. Unfortunately, the widespread answer to this problem is to use git checkout
which doesn't actually perform a real 3-way merge, but simply chooses 'ours' or 'theirs'. Let's look at an example.
There's a lot of misinformation being spread on SO and on blogs about using git checkout --ours
or git checkout --theirs
to merge a single file. This will only perform a simple patch effectively, choosing one or the other. This is not a true 3-way merge, using the common ancestor as a base file! Non-conflicting changes will be lost from the side not chosen!
I have a config file (config.toml) at the root of my repo that needs to be auto resolved by choosing 'theirs' (or sometimes 'ours' depending on another condition). Starting on a 'main' branch, this first entry becomes the common commit.
path = "some/path"
description = "this is a description"
owner = "username"
foo = "bar"
log = "some stuff"
I then checkout a new branch, say 'newbranch', off of this commit and add one line and change the log line.
path = "some/path"
description = "this is a description"
owner = "username"
new = "line"
foo = "bar"
log = "new branch"
I then go back to the 'main' branch I started on and change only the log line.
path = "some/path"
description = "this is a description"
owner = "username"
foo = "bar"
log = "main updates"
I go back to the 'newbranch' branch. If I do a git merge -Xtheirs main
, everything works as you'd expect.
path = "some/path"
description = "this is a description"
owner = "username"
new = "line"
foo = "bar"
log = "main updates"
The only true conflict was the log line, which was auto resolved by choosing 'theirs' (the one from the 'main' branch). The added line new = "line"
is preserved as it should be, given the knowledge of the common ancestor commit where the branches diverged.
But this is applied to all files obviously, and I can't have every conflict auto-resolved in that manner. I want it applied to just this one file.
The common answer to this problem is to use
git checkout --theirs main -- config.toml
This doesn't actually perform a 3-way merge, but simply chooses their version of the file. And the added new = "line"
is deleted. Similarly,
git checkout --patch main -- config.toml
also doesn't use the knowledge of the common ancestor commit and again views the new = "line"
entry as something that should be deleted.
I've seen .gitattributes
suggested as a solution to providing per file merge strategies, but I haven't had any luck getting that to actually work. And it would seem that it's a bit rigid, no ability to choose whether I want 'theirs' or 'ours' at the time of the merge.
Is there a way to perform a true 3-way merge on a single file that considers the common ancestor? I'm somewhat shocked there's not a more obvious answer to this. Thank you!
UPDATE: I suppose one solution would be to checkout the common ancestor, the 'main' version of the file, and the 'newbranch' into a temporary directory. Then manually do a 3-way merge using 'diff3', overwriting the 'newbranch' version, before committing that.
UPDATE2: Doesn't appear I can actually auto resolve using 'diff3' so that really isn't a viable option. Seems the easiest thing to do is to first do a git merge -Xtheirs --no-commit main
copy the individual files that I wanted to merge with git merge -Xtheirs
into a tmp directory. git merge --abort
to back out. git merge --no-commit main
to do the merge. Naturally there will be conflicts with those files. Copy those individual files back into the repo. git add <those_files>
. Up to this point I do this entirely programmatically. If there are other files that need to be resolved, they would be done manually by the user. Otherwise git commit -m "merged 'main' into 'newbranch'
to complete the merge.
Although a bit circuitous, it seems the easiest thing to do is the following.
git merge -Xtheirs --no-commit main
This gets me the 3-way merge I want on those files with auto-resolving conflicting lines by using 'theirs'
copy those files I want 3-way merged with 'theirs' auto-resolution into a /tmp dir
git merge --abort
to back out of the merge
git merge --no-commit main
to start the merge without auto-resolution. Naturally those particular files will have conflicts
copy the merged versions from the /tmp dir back into the repo
git add <those_files>
if any other files had conflicts, those would need to be resolved manually by the user
git commit -m "merged 'main' into 'newbranch'"
to complete the merge. Or git merge --continue
can be used.
A bummer git doesn't offer a more direct facility to 3-way merge individual files, but this will do.