1

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.

onlinespending
  • 1,079
  • 2
  • 12
  • 20
  • What is your ultimate goal that requires this custom merge strategy per-file? I don't believe Git offers a way to do what you want, but if you explain your ultimate goal, we may be able to suggest a way of getting there. – bk2204 Mar 18 '22 at 22:49
  • the ultimate goal is defined in there. That is to do a true merge, just as git handles it normally by supplying 'git merge -X theirs' or 'git merge -X ours', but on a per file basis. Basically, auto resolve true conflicted lines with either 'theirs' or 'ours' without simply choosing the entire file from either 'theirs' or 'ours', thereby preserving unconflicted additions that were made to the file in either branches (such as that 'new = "line"' given in the example in my post) – onlinespending Mar 18 '22 at 22:52
  • You kinda lost me. You go from describing what is happening to "The common answer to this problem is to use". It seems that it should be obvious that you now have a problem, but what exactly **is** the problem at this point? Can you elaborate why the merge conflict resolution you chose, which chose the log line from "theirs" but also preserved the new line, isn't what you want? You even threw in "as it should be" in there. To me this sounds like you get what you wanted, but then "the solution to this **problem**". Again, what is the problem at this point? – Lasse V. Karlsen Mar 18 '22 at 22:57
  • the common answer to merging a single file doesn't actually do a merge, but simply chooses one version or the other. It's not a merge that uses the knowledge of a common parent commit between two divergent branches. Try following my example and see for yourself. NO...I first show what would happen if you simply did a 'git merge -X theirs', which is what I want. But I can't have every file auto resolved like that. Just this one file. And the solution to that is to use 'git checkout' which doesn't actually do a true merge of divergent changes – onlinespending Mar 18 '22 at 23:02
  • This statement, "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." implies that you could just resolve the conflicts in real time, with a good visual merge conflict editor. – TTT Mar 19 '22 at 03:10
  • Pretty sure you're looking for a post-checkout setup hook like [this one](https://stackoverflow.com/questions/20078756/making-git-retain-different-section-content-between-branches/20087076#20087076). Keep the branch-specific content in branch-specific files and if/since your environment can't tell it's got a git checkout and find what it needs, construct something simple in the few places it's smart enough to look. – jthill Mar 19 '22 at 05:17
  • @TTT usually I want to auto-resolve this one file with '-Xours' actually, but occasionally under certain conditions I need to auto-resolve with '-Xtheirs'. All other files I'd want to be manually resolved in case of a conflict. Perhaps I could accomplish this with a custom merge driver, but would rather just leverage git's built in 3-way merging with auto-resolution – onlinespending Mar 19 '22 at 16:30

2 Answers2

1

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.

onlinespending
  • 1,079
  • 2
  • 12
  • 20
  • Definitely roundabout, but accomplishes the goal. You could even repeat steps 1-3 with `ours` for the set of files you wish to resolve the other way. This would be easy to automate too. – TTT Mar 19 '22 at 16:15
  • good point. that may not apply in my case, but I'm sure others could benefit from a scheme like that – onlinespending Mar 19 '22 at 16:31
  • Right. It works for the general sense of doing a large merge, and as an example, today I want these 10 files to auto-resolve conflicts with `theirs`, and these other 14 files to auto-resolve conflicts with `ours`. – TTT Mar 19 '22 at 16:40
  • Note I can't imagine putting myself in a situation where I would want that, but I can envision scenarios where it's *possible* to want that. – TTT Mar 19 '22 at 16:41
0

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.

bk2204
  • 64,793
  • 6
  • 84
  • 100
  • obviously I know a 3-way merge occurs when doing a 'git merge'. The title says a single file, so thank you for bringing 'merge-one-file' to my attention. The standard answer to 'merge' a single file is to use 'git checkout' which does not perform a 3-way merge, but merely chooses 'ours' vs 'theirs', all-or-nothing. I don't need a custom merge strategy, just the ability to do the standard 3-way merge on a single file. Since .gitattributes can apply on a per-file basis, I figured that would be a natural area to look for a solution. I'll try 'git merge-one-file' and if it goes well I'll accept! – onlinespending Mar 19 '22 at 15:20
  • can you share a link to 'git merge-one-file'. I'm not getting any hits when searching for that. Thank you. OK I see it https://git-scm.com/docs/git-merge-one-file – onlinespending Mar 19 '22 at 15:22
  • You really need to see `git merge-one-file -h` because the docs don't tell you how to invoke it, but, yes, [there is a manual page](https://git-scm.com/docs/git-merge-one-file). – bk2204 Mar 19 '22 at 15:27
  • does it have to be issued after a 'git read-tree -m' or with 'git merge-index'? or can I simply use this after 'git merge' to resolve a single file between different branches? – onlinespending Mar 19 '22 at 15:35