1

Is there any way to merge two branches while keeping "ours" changes for changed files (added, modified, deleted etc) and getting "theirs" version for all the rest (unchanged in my branch files).

What have i tried:

  1. "-X ours" works only for conflicts and if there is no conflict it tries to merge file contents and i want exactly "ours" version for such cases.
  2. "-s ours" takes "ours" version for every file, even if it was not changed and i want unchanged in my branch files to be overwritten.

There should be some combination of those two but im having a hard time finding it. Any help appreciated

ww_ho
  • 23
  • 3
  • Are you getting conflicts? – evolutionxbox Dec 23 '20 at 10:56
  • @evolutionxbox when i do it works ok with "-X ours", but if there is no conflict it merges two file versions recursively and i want "ours" version for such cases too. Only case when it's ok to get "theirs" is when file was not changed in my branch at all. – ww_ho Dec 23 '20 at 11:42

1 Answers1

1

There should be some combination of [the -s ours strategy, and the -X ours eXtended strategy option] ...

Whether or not there should be, there isn't. (You could write your own, but this would be a big task.) You may be able to get close enough with an ours driver, which has nothing to do with the ours strategy. Read the long description below to see what this means. See Whats the Proper usage of .gitattributes with merge=ours and tell git to use ours merge strategy on specific files for examples of this.

Long

It is, at this point, worth describing the mechanics behind git merge. You run:

git merge [-s <strategy>] [-X <extended-options> ...] <commit-specifier>

—there are other options but I'm ignoring them entirely for this particular answer—and the -s option, if specified, chooses the strategy to use, while the -X option(s), if specified, are passed on to this strategy. (That's why Git calls them strategy options, but to make sense of the -X flag, I call them extended strategy options or simply extended options.)

The commit specifier—often a branch name—is required here, though technically even that is optional (Git will merge the branch with its upstream if you don't name a particular commit), and for certain merge strategies, you can name more than one commit or branch here. Again, we aren't looking into those cases now; we're just concerned with the single-other-commit, "two branches get merged" type merge.

The way git merge itself starts out is pretty simple: it inspects the options you've supplied to make sure they make sense, makes some preliminary checks to make sure the merge can proceed, and then—this is just before things get complicated—it invokes the merge strategy you chose:

  • If you ran git merge -s ours, git merge runs git-merge-ours.
  • If you ran git merge -s resolve, git merge runs git-merge-resolve.
  • If you ran git merge -s recursive, or didn't specify a -s strategy, git merge runs git-merge-recursive.

Everything after this point is up to the merge strategy. Well, mostly everything: Git runs the strategy, and then the strategy signals back to git merge whether the merge worked and it should go ahead and make a merge commit. (This means the front end can implement the --no-commit and --squash options; the various strategies don't need to know about this stuff, saving work in the various strategy source codes.)

The -s ours strategy, as you have seen, is ridiculously simple: it completely ignores the other commit and makes a new merge commit whose contents match the current commit as seen on the current branch. For git merge -s ours otherbranch, this merges the branches while tossing out all of their work entirely.

The default recursive strategy, along with its sister strategy called resolve, do a lot more work. They:

  • find a merge base;
  • figure out what we changed on the current branch since that merge base;
  • figure out what they changed on their branch since that merge base;
  • combines their changes with our changes;
  • gets all the files from the merge base; and
  • applies the combined changes to those files.

If all goes well with the combining, the merge is done and Git makes a new merge commit as usual. If there are conflicts, the merge stops with the conflict, and makes you finish the job.

The extended arguments, if any, are passed to each of the various strategies. The recursive and resolve strategies use -X ours or -X theirs to automatically resolve some conflicts. These flags only apply to what I call low-level conflicts, i.e., conflicting changes within some file(s).

This is not what you are looking for. What you have asked for is a strategy that, upon discovering that some file is modified in both branch tips as compared to the merge base, will take the "ours" file (or lack thereof in case of a deletion) and completely ignore the changes they made to that file with respect to the shared merge base. But, if we did not touch the file and they did, you want this strategy to take their file.

The bad news is that you must write this merge strategy yourself.

The good news is that this strategy is simple enough that you could write it in, probably, a few dozen lines of shell script.

The bad news is that this strategy won't handle renames very well, though it's up to you to find the renames in the first place. (My guess is that handling renames well would at least triple or quadruple the number of shell-script lines required, or force you into using a more powerful programming language to write your strategy. You can write your strategy in any language you like.)

The really bad news, though, is that writing a strategy is hard. It's also quite undocumented: it's not entirely clear what the parameters to the strategy are. You'll need to look through the Git source code to figure it out, and it may depend on Git version.

Note that using a merge driver might, suffice for your particular use case, provided you don't really care about things like modify/delete and rename/rename (i.e., non-low-level) conflicts. But you explicitly mentioned "added, modified, deleted". Except for modify/modify, these are high-level or tree-level modifications; merge drivers only get called for modify/modify cases.

In the particular case where both branch-tips modified some file with respect to the version that appears in the merge base commit, the resolve and recursive strategies will invoke a defined merge driver. The default merge driver is essentially the one in the git merge-file command: the git merge-file command is basically a wrapper for the built-in merge driver. This is the one that produces conflict markers.

To define your own merge driver, you must:

  • define the driver itself in your .git/config or global gitconfig file;
  • declare that some particular set of files should use that driver, via your .gitattributes file(s).

At this point, -s resolve or the default -s recursive will use your merge driver for cases where the copy of the file in the merge base commit differs from the copy in the current commit and differs from the copy in the other commit. It won't invoke your merge driver for other cases, so it would suffice to set the result to be the "ours commit" file.

torek
  • 448,244
  • 59
  • 642
  • 775
  • I'll stick to identifying changed files (with diff by merge-base) and using "merge --no-commit" with "checkout filename" for every changed file. Thank you for the great answer! – ww_ho Dec 24 '20 at 11:37