George Kastrinis George Kastrinis - 6 months ago 54
Git Question

Change direction of git merge (after the merge is done)

I have a branch (e.g. Feature-X) in a git repo.

What I did was the following

git checkout master
git merge Feature-X


There were quite a few conflicts which I resolved. I haven't committed the changes yet.

But, it turns out what I wanted was to do the reverse merge (i.e. merge master into Feature-x) because the branch is probably still unstable.

Is there any command that can salvage the work I did in resolving the conflicts or do I need to do the resolution once more, this time in the branch Feature-X?

I'm thinking whether there is a way to get the current patch but apply it in branch Feature-X in the "reverse" order. E.g. when the patch says that line X changed to Y, this is relative to Feature-X going to master. If I want to do the opposite, the patch should say that line Y changed to X, right? This is just my thoughts. Any solution is OK.

Answer Source

TL;DR: see the end

There is a "recipe" at the end; scroll down to find it (and then up a bit to find the description and explanation and slightly safer method).

Good news and bad news

In a significant sense, merges don't exactly have a "direction". (The direction you "see" when you look at a merge is a product of your imagination, plus the merge commit's message, plus one more important thing that will be our "bad news"—but it's not fatally bad, in the end.)

Consider this diagram of a pair of branches with commit * as their merge base:

          o--o--...--o   <-- branch1
         /
...--o--*
         \
          o--o--...--o   <-- branch2

The process of merging ("merge as a verb") branch1 and branch2 is achieved by:

  • comparing, i.e., git diff, the merge base commit * to the tip commit of branch1;
  • comparing the merge base to the tip of branch2;
  • then, starting from the base, combining both sets of changes.

Hence the actual merge itself should look like this:

          o--o--...--o
         /            \
...--o--*              M
         \            /
          o--o--...--o

(I named the merge commit M since we don't know, or really care, what crazy Git hash ID deadbeef or badc0ffee or whatever it will have.) The eventual merge commit M is the merge ("merge as a noun"); it stores the final source tree, based on the merge-as-a-verb combining process.

Which "direction" is this merge? Well, that depends, at least in part, on what labels we stick on it, doesn't it? :-) Note that I carefully stripped away both branch1 and branch2. Let's put them back:

          o--o--...--o     <-- branch1?
         /            \
...--o--*              M   <-- branch1? branch2?
         \            /
          o--o--...--o     <-- branch2?

This might look confusing. It probably is confusing. It's a big part of the whole issue. The bad news is, it's not the whole thing. There's something we cannot quite capture in these drawings. It may not matter to you, and if it does, it's not that big a deal anyway; we'll fix it up by making one more merge commit. But for now, let's just press on.

Making the merge commit

Once we make the merge commit M itself, we have to choose—or have Git choose-which branch it's on. In a simple, unconflicted merge, we always end up having Git choose this. What Git does is simple: whatever branch we are on, when we start the git merge command, is the branch that gets the merge commit. So:

git checkout branch1
git merge branch2

means that the final picture is:

          o--o--...--o
         /            \
...--o--*              M   <-- branch1
         \            /
          o--o--...--o     <-- branch2

which we can re-draw as:

...--o--*--o--...--o--M   <-- branch1
         \           /
          o---...---o     <-- branch2

which makes it obvious that we merged branch2 into branch1, not vice versa. Nonetheless, the source code attached to commit M does not depend on this "merge direction".

Sidebar: Conflicted merges

Conflicted merges are just like unconflicted merges in terms of final result. It's just that Git can't do the merge on its own. It stops and makes you resolve the conflicts, and then run git commit to make the merge commit. That final git commit step makes merge commit M.

The new merge commit goes on the current branch, just like any other commit. So that's how M winds up on branch1. Note that the merge commit's message also says something about which branch name was merged into which other branch name—but you have a chance to edit this while you make the commit, so you can change that to suit whatever whim you may have. :-)

(In fact, this is no different from the unconflicted case: both run git commit to make the final merge commit, and both give you a chance to edit the message.)

The bad news

The bad news is that these diagrams omit something that may matter to you. Git has a notion of first parent: a merge commit like M has two parents, so one of these is the first parent and one is not.1 That first parent is how you tell which branch you were on when you made the merge.

Hence, while these diagrams, and the merge-as-a-verb process, show that there isn't exactly a "merge direction", this first-parent notion proves that there is. If you will ever use this first-parent notion, you will care about this, and want to get it right. Fortunately, there is a solution. (In fact, there are multiple solutions, but I'll show the klunky-but-straightforward ones first.)


1Obviously, the other one is the second parent: there are only two parents. Git allows, however, merge commits with three or more parents. You can enumerate all parents whenever you have to; but Git considers the first especially important, and has --first-parent flags to various commands, so as to follow "the original branch".


Getting the merge we want

Let's go ahead and make the "wrong" merge commit:

# git add ...    (if needed)
git commit

Now we have:

          o--o--...--o     <-- [old branch1 tip]
         /            \
...--o--*              M   <-- branch1
         \            /
          o--o--...--o     <-- branch2

where M's first parent is the old branch1 tip.

What we want now is to make a new merge commit (with different ID) that's the same as M except that:

  • branch2 points to it
  • its first parent is the tip of branch2
  • its second parent is the previous tip of branch1

The trick is to save commit M somehow—that's easy enough, we'll use a temporary name so we don't have to copy down M's hash ID—and then reset branch1:

git branch temp          # or git tag temp
git reset --hard HEAD~1  # move branch1 back, dropping M

(note that this is the same as brehonia's answer, so far). We now have this graph, which is quite unchanged except for the labels attached to each commit:

          o--o--...--o     <-- branch1
         /            \
...--o--*              M   <-- temp
         \            /
          o--o--...--o     <-- branch2

Now check out branch2 and run the merge over again; it will fail with conflicts as before:

git checkout branch2
git merge branch1        # use --no-commit if Git would commit the merge

The commit we have not yet made is the same, in a sense, as the merge commit M—but once we make it, its first parent will be the current tip of branch2, and its second parent will be the current tip of branch1. We just need to get the right source to go with this commit we're going to make.

Now we use the merge result we saved with the name temp. This assumes you're in the top level of your tree, so that . names everything:

git rm -rf -- .          # remove EVERYTHING
git checkout temp -- .   # get it all back from temp

We are now using the merge result we committed earlier. We do not even have to git add anything as this form of git checkout updates the index, so:

git commit

and we have our new merge M2, on branch branch2, as desired:

          o--o--...--o__   <-- branch1
         /            \ \
...--o--*              M \ <-- temp
         \            /   \
          o--o--...--o-----M2   <-- branch2

Now we can delete the temporary branch or tag:

git branch -D temp   # or git tag -d temp

and we are all done. With the temporary name gone, we can't see the original merge M any more.

Sneaky plumbing method

There's actually a shorter way to do all this, using Git's "plumbing" commands, but it's a bit tricky. Once we have everything git add-ed and have run git commit, we have the right tree, we just have a commit that has the wrong first and second parent IDs. You may wish to edit the commit message as well, so that it looks like you're merging branch1 into branch2 in the message. Then:

# git add ...    (if / as needed, as above)
# git commit     (make the merge)
git branch -f branch2 $(git log --pretty=format:%B --no-walk HEAD |
    git commit-tree -p HEAD^2 -p HEAD^1 -F -)
git reset --hard HEAD^1

The git commit-tree step makes a new merge commit that's a copy of the one we just made, but with the two parents swapped. We then make branch2 point to this commit, using git branch -f. Be sure you get the name (branch2) right at this step. The HEAD^1 and HEAD^2 names refer to the two (first and second) parents of the current commit, which is of course the merge commit we just made. That has the right tree, and the right commit message text; it just has the wrong first and second parent hashes.

Once we have the new commit safely added to branch2, we simply reset our current (branch1) branch back to remove the merge commit we made.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download