Felix
Felix

Reputation: 5619

git merge issues - force merge

I have two branches in git.

Branch dev and my feature branch 1300.

Branch 1300 is a complete refactoring of branch dev. Now I'm finished with 1300 and want to merge it into the dev branch.

How can I do a merge force 1300 into dev.

When I do a normal merge and accept theirs in intellij (theris is 1300) the files I deleted in 1300 are still in dev.

Any hints for a force merge?

Thanks in advance

Upvotes: 1

Views: 118

Answers (1)

torek
torek

Reputation: 487725

There is no such thing as a "force merge" in Git. Luckily, that's not what you want anyway. What you want is git merge -s theirs. Unluckily, that doesn't exist. Luckily, there are ways to fake it. See also Is there a "theirs" version of "git merge -s ours"? I could close this as a duplicate, but instead I'm going to write up a long-form answer.

Git commits, including merge commits

The word merge has two or more meanings, depending on whether you're using it as a verb, to merge, or an adjective-or-noun: a merge commit (adjective) or a merge (the adjective becomes a noun, as adjectives are wont to do in English: this is called nominalization). The noun version, a merge, simply is there in your repository, as a commit, just like any other commit, except that instead of the usual single parent, it has two.1

Remember that a commit, in Git, is composed of two things: a snapshot—a saved set of files—and some metadata: information about the commit, such as who made it, when, and why. In the metadata, Git stores the hash ID(s) of some earlier commit(s). These are the parents of the commit in question. Most ordinary commits have a single parent hash ID; what makes a commit a merge commit is that it has two parents, instead of the usual one. But it has only one snapshot. That snapshot is the merge result.

I find pictures to be very helpful here. First, let's draw a picture of a simple string of commits—a "branch", if you will—in Git:

... <-F <-G <-H

Here, H stands in for the actual hash ID of the last commit in some chain of commits. Since H is a commit, it has both data—a snapshot—and metadata. The snapshot holds all of the files as of the form they had when you, or whoever, made the commit. The metadata for H contains the actual raw hash ID of earlier commit G. We say that H points to G.

G, of course, is also a commit: a snapshot (of the files as of then) and metadata. This metadata points back to an even-earlier commit F. F, like G, has a snapshot and metadata, so it points on back.

What all this means is that as long as we can find the last commit, we can find all the commits in the chain. So that's what a Git branch name does: it lets us find the last commit in the chain. It just holds the hash ID of that commit. Whatever hash ID the name holds, that's the last commit in that chain. So this might look like:

...--F--G--H   <-- main
            \
             I--J   <-- feature

Here, branch name feature points to commit J, which points back to commit I, which points back to commit H, which points back to G, and so on. The fact that there's another name, main, that points to H doesn't stop all the commits I just listed from being on both branches. The fact that main points to H, however, does stop commits I and J from being on that branch. The commits up through and including H are on both branches, while the last two are only on feature.

We can add and remove any names we like at any time. There are just a few constraints. For instance, the names must meet certain tests: main and feature are fine, but hello..world is not because double-dots are forbidden. The names also need to point to actual commits.2 I won't cover everything, but you should get the idea: we can create and destroy any names within certain (reasonable) limits. So we can add a name dev to the picture above, like this:

...--F--G--H   <-- dev, main
            \
             I--J   <-- feature

If we now git checkout dev or git switch dev, so that this becomes the current name—we'll get commit H out as the current commit in this process—and then make a few new commits, we will need to redraw the graph a bit. Here I've put feature up top and dev down below:

             I--J   <-- feature
            /
...--F--G--H   <-- main
            \
             K--L   <-- dev (HEAD)

This drawing shows us that we have two commits (I-J) that are only on feature and two that are only on dev. The (HEAD) notation shows that we are currently using the name dev, and therefore using commit L, which is one of the ones only on dev.


1Technically, any commit with two or more parents is a merge commit, but two suffices, and having two is typical.

2There's no technical reason for this, and Git is acquiring a feature in which this constraint will be relaxed for certain names, but it's currently always required for all names, and will still be required for branch names.


The mechanics of merge-as-a-verb

When you choose to make a merge commit—usually, by running git merge—you select the two (or, per footnote 1, more) commits that you are tying together. One of the two is the current commit, which you select before running git merge, usually with git checkout branch or git switch branch. The other is the commit that you name on the command line. Hence we might run:

git checkout dev
git merge feature

for instance.3 Git will then go off and find a third commit on its own. This third commit is the merge base, and the merge base provides the starting point for the action of merging: the to merge process, or merge as a verb.

The problem you have is that if your picture looks like this:

             I--J   <-- feature
            /
...--F--G--H
            \
             K--L   <-- dev (HEAD)

—I've erased the name main to keep it from cluttering up the picture; it doesn't really matter if it's there or not—the git merge action, the merge as a verb process, will work by:

  • locating the best commit that's on both branches, which in this case is obviously commit H;
  • comparing the snapshot in H to the one in L (dev) to see what we changed;
  • comparing the snapshot in H to the one in J (feature) to see what they changed;
  • combine those changes and apply the combined changes to the snapshot in H; and
  • make its final merge result by using those combined changes.

The result will look like this:

             I--J   <-- feature
            /    \
...--F--G--H      M   <-- dev (HEAD)
            \    /
             K--L

Note how the branch name dev now points to the new commit, M, as a result of doing this merge process.


3You can add git merge --no-ff to prevent Git from doing a fast-forward instead of a merge. I suppose one could view this as forcing a merge—but it's still not what you want. :-)


There is an ours strategy

The strategy thing that Tim Biegeleisen refers to in a comment is how Git actually implements the merge-as-a-verb process. When you run git merge, this does a bunch of preliminary tests and handles various command line options. One of the allowed options is -s, and -s takes a strategy argument.

The strategies that Git has built in, out of the box, are:

  • -s recursive: this is the usual default one. It does the combining we talked about above.
  • -s resolve: this is a variant of -s recursive; it too does this combining. We don't need to go into the technical difference between them here because your issue is that you'd like to avoid the combining.
  • -s octopus: this is a kind of special purpose algorithm for merging more than two commits. It isn't the answer you want here.
  • -s ours: this is another special purpose algorithm. It's almost the one you want!

What the ours strategy does is simple: in order to make the new snapshot, it completely ignores the other commit(s) you specify with your git merge command. It then says that the new snapshot should exactly match the current commit. So if we're on dev and we run git merge feature, we get:

             I--J   <-- feature
            /    \
...--F--G--H      M   <-- dev (HEAD)
            \    /
             K--L

This looks exactly the same as before, until we look at the snapshot in commit M. Before, the snapshot in M was the result of combining work on two branches and applying the combined changes to the merge base snapshot from H. Now, the snapshot in M is exactly the same as the snapshot in commit L.

This is oh so close but still not what you want, because what you said you want is to make M as usual, but use the snapshot from commit J. If Git had a way to do that out-of-the-box, it would be spelled -s theirs. Apparently Git once did have -s theirs (this must have been pre-Git-1.6), but it does not have -s theirs now.

There are a bunch of ways to fake it. See the linked answer (should be a duplicate) for these various ways. Be sure that you really do want to fake it. You will end up with a merge commit M whose snapshot matches whichever "side"'s files you choose.

As several answers there note, be wary of -X ours and -X theirs, which have a very different meaning. These options—I like to call them eXtended options, to make it clear that they're not the strategy (-s) option—are passed to the strategy to do with as it wishes. The default recursive/resolve code uses this to resolve conflicts automatically. It does not completely ignore one side's changes. Instead, it takes either side's changes, but when those changes have conflicts, resolves the conflicts using one side's changes, throwing out the other's for that particular diff-hunk only.

Upvotes: 2

Related Questions