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.
CodePudding user response:
Git already performs a true three-way merge on all files that are merged. This is standard and built-in and happens automatically.
However, if you are asking whether there is a way to perform a custom merge strategy per-file from the command line, then there is not. Git does provide a way to merge a single file using git merge-one-file
, which performs a three-way merge, but it doesn't support custom merge strategies: it literally just does a standard three-way merge.
In general, however, the goal of trying to merge a config file alternately with the equivalent of --ours
or --theirs
indicates an anti-pattern in the way you're storing configuration files. Usually this is because people have a single config file with different versions on different branches (e.g., for dev, staging, and prod). The easiest way to solve this problem is either to generate the file based on a template using a script (e.g., reading the right choice from the environment or a command-line option), or just storing all three files in every branch and copying or symlinking the right one into place.
People have asked for functionality similar to what you're requesting as a .gitattributes
option on the Git list in the past, for similar reasons, and have generally been given the advice above.
CodePudding user response:
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 mergegit merge --no-commit main
to start the merge without auto-resolution. Naturally those particular files will have conflictscopy 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. Orgit 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.