Search code examples
gitgithubmergediffpatch

git merge a single file....and a TRUE 3-WAY MERGE, not just a checkout of ours vs theirs which doesn't consider common ancestor


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.


Solution

  • Although a bit circuitous, it seems the easiest thing to do is the following.

    1. 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'

    2. copy those files I want 3-way merged with 'theirs' auto-resolution into a /tmp dir

    3. git merge --abort to back out of the merge

    4. git merge --no-commit main to start the merge without auto-resolution. Naturally those particular files will have conflicts

    5. copy the merged versions from the /tmp dir back into the repo

    6. git add <those_files>

    7. if any other files had conflicts, those would need to be resolved manually by the user

    8. 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.