2

My commit tree looks something like

---A---B---C   master
    \
     D-----E     dev

I want to merge E onto master so that the leading master commit looks exactly like E:

---A---B--C--F   master
    \       /
     D-----E     dev

So F should be exactly the same as E.

What I did was:

> git checkout master
> git merge -X theirs E

F is produced without an error message, but there are several differences between E and F. They are not identical, like I need them to be.

I know I could merge and fix the diffs by hand. But, 1) I really do need E to completely overwrite C and 2) I can't mess this up, so some automatic method for doing this is much preferred to doing it by hand.

What did I do wrong? What do I need to do differently?

UPDATE:

> git merge -X theirs dev

doesn't help either. Same result.

bob.sacamento
  • 5,295
  • 7
  • 40
  • 95

3 Answers3

2

First, a word of caution: The result you want is an example of an "evil merge", and it may cause certain issues down the road. If it's what you need, it's what you need; but just be aware that merges whose result differs from the default (especially when the default doesn't conflict) can be misinterpreted during certain rebase operations (and maybe other things).

That said: There's some confusion because the default merge strategy takes -X parameters "ours" or "theirs" that tell it how to resolve conflicts; but this doesn't change that the merge operation takes non-conflicting changes from both sides. What's more confusing is that there is also a merge strategy called "ours" which completely ignores the changes from the other branch - but no corresponding "theirs" strategy.

So you can do a few things. One option would be

git checkout dev
git merge -s ours master
git checkout master
git merge dev

(You want that last merge to be a fast-forward; by default it will be, but if your setup changes that default you might need an --ff option.)

This isn't exactly the same as just merging dev into master, in that the parents will be reversed; but if that doesn't matter for you, then this is possibly the simplest way.

Another option is to manually edit the merge.

git checkout master
git merge --no-commit dev

Now you need to make your worktree and index look just like commit E. I would think

git checkout dev -- .
git clean

And of course you can use git diff to validate.

This approach keeps the order of parents "as expected" on the merge - but don't forget, it's still an evil merge.

Mark Adelsberger
  • 32,904
  • 2
  • 24
  • 41
2

TL;DR: you need -s theirs which does not exist

There are several substitutes, though. See the other answers to this question, search StackOverflow for "git merge strategy theirs", and/or read Is there a "theirs" version of "git merge -s ours"? carefully (since the original querent was actually looking for -X theirs, which I describe below). Or use the following rather magic bit of sh/bash script code:

git merge --ff-only $(git commit-tree -p HEAD -p dev dev^{tree} -F /tmp/commitmsg)

after putting the commit message you want into the file /tmp/commitmsg. Be very sure this is really what you want to do, because it almost never is.

Description

The -X (eXtended arguments) ours and theirs do not mean use our version or use their version. They mean, instead, when there is a conflict, use our change or use their change.

What this means

In your example, the tip of master is commit C and the tip of dev is commit E. So if the current branch is master, the current commit is C. It does not matter at this point whether you pass the name dev, or the hash ID of commit E, to git merge: both select commit E as the commit to merge.

In any case, commit A is the merge base, because tracing backwards from both C and E simultaneously to the first point where they meet, arrives at commit A.

Therefore, git merge will:

  • diff commit A against commit C: this is what changed in your current branch, i.e., ours.
  • diff commit A against commit E: this is what changed in theirs.
  • Attempt to combine these two diffs.

Suppose that one of the files is named animals.txt, in all three commits. In commit A we find some lines about cats and some about elephants. There are more lines (about aardvarks, bats, dogs, finches, gerbils, hamsters, iguanas, jackrabbits, and so on—something for every letter of the alphabet!) but we'll concentrate on Cats and Elephants for the moment ... and also on Shrews, which are Shared.

In commit C compared to A, there is a new or different claim about cats. So the diff from A to C shows this change about Cats (C). There is also a change to Shrews:

$ git diff <hash-of-A> <hash-of-C>
...
@@ ... @@
 blah blah
-something about Cats
+something different about Cats
 blah blah
@@ ... @@
 etc
-the Shrew flies through the air, carefree
+the Shrew swims the ocean, carefree
 etc

In commit E compared to A, there is a new or different claim about elephants. So the diff of A-to-E shows this change about Elephants. There is a different, conflicting change to Shrews—we'll omit everything but that here:

@@ ... @@
 etc
-the Shrew flies through the air, carefree
+the Shrew eats rhinos, though it causes indigestion
 etc

When Git goes to merge these changes, it will pick up the change to Cats from A-to-C and the change to Elephants from A-to-E. These changes do not conflict so there is no problem combining them. But it will also see the changes to the lies about shrews, and these changes conflict and cannot be combined.

The -X options direct Git to choose one side or the other of a conflict

Because there is a conflict here, Git needs help resolving the merge. By default, Git just declares "there is a conflict", writes both changes into the work-tree, and stops with a conflict, leaving you to clean up the mess yourself. (There's much more done behind the scenes to help you with conflict resolution, but none of that matters right now.)

With one of these -X options, though, Git is told to choose ours, or choose theirs, and just plow on ahead. But this only affects conflicting changes. Git will still combine non-conflicting changes as usual: in the example above, regardless of which -X you pick, Git will take both the Cat and Elephant changes, and -X will simply choose which Shrew change to pick.

Merge strategies are different

There is a Git merge strategy called ours, invoked as git merge -s ours, that is very different from the -X ours option. This strategy tells Git not to bother with a merge base after all, and just to keep the source tree from the "ours" commit. Git will make the merge commit, after doing no work at all to produce a proper merge. The effect is to "kill off" the other branch while preserving its commits; this allows you to delete the other branch name, keeping the commits in case you want to view them sometime, while declaring that you intend never to use them again (but obviously aren't quite sure about it :-) ).

If git merge -s theirs existed, it would do much the same, except that it would "kill off" the current branch in favor of the new one. There may be times when you want this, and when they come up, it might be nice if Git had git merge -s theirs, but for whatever reason, it has only git merge -s ours.

The substitutes work, and the trick with git commit-tree allows you to tie together arbitrarily many commits using any arbitrary existing source tree attached to any existing commit—hence it covers both -s ours (use HEAD^{tree}) and -s theirs. The resulting commit object has no name, but is an immediate descendant of the HEAD commit, so git merge --ff-only will add it to the current branch.

torek
  • 330,127
  • 43
  • 437
  • 552
1

The -X theirs option of git merge tells it how to resolve the merge conflicts when it uses the default strategy (-s recursive). The changes that can be merged clean are not affected. The merged files will contain the changes operated on both branches.

The ours strategy can be used to create a merge commit that ignores the changes on the merged branch(es). In order to make F look the same as E you have to merge master into dev but this is not what you asked for.

A possible solution to your request (I didn't tested it) could be like this:

  • do the merge as you do it now but add the --no-commit option to the command line; this way, Git prepares the merge commit but lets you review (and possibly change) it;
  • (forcibly) check out the worktree files to match the status of the repo on the E commit;
  • add everything to the index and commit.

The commands are:

git checkout master
git merge -X theirs --no-commit dev
git checkout --force E -- .
git add .
git commit
axiac
  • 56,918
  • 8
  • 77
  • 110