0

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.

George Kastrinis
  • 4,924
  • 4
  • 29
  • 46

3 Answers3

7

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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Would you mind adding the visual diagram to some of the steps in the `Getting the merge we want` section, so that we can see what is going on? Would be much easier to follow along and visualise in our head..personally I got a bit lost there. – CyberMew Aug 16 '17 at 07:47
  • 1
    @CyberMew: see if those help. Graph-drawing can get difficult as the graph gets tangled... – torek Aug 16 '17 at 14:38
  • 1
    +1 Great explanation on why merges don't have a direction, and yet they do (in a sense). Really helpful graphs. – tjalling Aug 23 '17 at 09:08
  • Merge direction MATTERS! Look at the resulting merge-commit between "Merge A into B" (i.e. checkout B during merge) and "Merge B into A" (i.e. checkout A during merge). There are completely different, and they give you a sense of what's was the development flow. Of course, the contents of the merge-commit is the same, but the history that it read out will be different. – Ryuu Sep 01 '17 at 08:21
  • @Ryuu: it matters in terms of first-parent vs second-parent, and of course, which branch *name* Git updates once Git makes the commit. But we can change the commit hashes to which a branch *name* points at any time, which means we can, if we want, do the merge "backwards", then shuffle the names around. This leaves only the first vs second parent, which is what the answer is all about: in the end, we might want to use `git commit-tree` to copy the wrong-parent-order merge to a different corrected merge, while retaining the tree etc. – torek Sep 01 '17 at 14:17
  • @torek: yeah, I agree. I've been doing the reversals when other team mates messes with parenting, and this really clutters the graph; The only issue that I have with this answer is that the important part is buried in the middle, and titled as "The bad news" (not obvious). A skimmer who just read the first few paragraphs, will conclude as "this whole thing doesn't matter"; – Ryuu Sep 01 '17 at 15:01
  • @Ryuu: the problem is, some people really *don't* care. Others (including me) do. Note that `git pull` makes these same "backwards" merges (what some call a [foxtrot merge](https://stackoverflow.com/q/35962754/1256452)), although that may be less important than for feature branches. – torek Sep 01 '17 at 15:19
  • @torek: Yeah, you are right. I'm losing the battle trying to convince my teammates; Foxtrot: yeah, I'm facing that too. Thanks for pointing me to that link! I'm tempted to use that hook, but I am worried that some teammates might not be able to understand how to fix it. I'm looking at Git Flow as a potential solution. – Ryuu Sep 01 '17 at 15:49
4

Commit the merge on master, create a new branch on that commit, and reset the master branch to the previous commit.

git branch temp
git reset --hard HEAD~1

(This is assuming there's nothing else in your working copy you want to keep, you would lose it in the reset!)

You're back where you started (pre-merge) but your conflict resolutions are safe if you want them.

Now try the merge again, the right way round (master -> feature). If you're lucky there won't be as many conflicts this way and you can just get it done.

If that's no good for you, you could merge or cherry-pick your temp branch into feature. It won't look right on the graph but that's the price!

Remember to delete your extra branch when you're done.

brehonia
  • 66
  • 2
2

You can do several things:

  1. do it manually using git rebase -i and manually reverse the order of your commits.

  2. write a script that will loop over the git log --reverse and will update the commits in the order that you wish it to be.

  3. next time use the git rebase --onto and decide where you wish to start your commits

--onto <newbase>
Starting point at which to create the new commits. If the --onto option is not specified, the starting point is . May be any valid commit, and not just an existing branch name.

As a special case, you may use "A...B" as a shortcut for the merge base of A and B if there is exactly one merge base. You can leave out at most one of A and B, in which case it defaults to HEAD.

enter image description here

CodeWizard
  • 128,036
  • 21
  • 144
  • 167