Abilash
Abilash

Reputation: 6089

Git: Wrong Merge, then reverted the merge. Now not able to merge the branch again

I'm in a little trouble with git.

Here is what i did.

  1. I merged latest master to my branch and pushed it
  2. Later realised that this merge is corrupted and reverted the merge
  3. Now I'm trying to merge master again, it says its up to date.

Can I know how I can force merge all the changes of master to my branch?

Thanks in advance.

Upvotes: 2

Views: 409

Answers (2)

Tim Biegeleisen
Tim Biegeleisen

Reputation: 522161

Let's say that your feature branch feature and master started off like this:

master:  A -> B -> C
feature: A -> D

After merging master into feature, this is how things looked:

master:  A -> B -> C
feature: A -> D -> M        # M is a merge commit

Next your reverted the merge in the feature branch by doing a git revert. This means that you told Git to add a new commit to undo the result of the merge. Now this is the state of the two branches:

master:  A -> B -> C
feature: A -> D -> M -> R   # R is a revert commit

When you try to pull master into feature, Git is telling you that you are already up to date, because you are! The revert commit functionally undid whatever happened during the merge, but now you have two new commits in your feature branch.

To get back to the state you were in before you made the erroneous merge in your feature branch, you can nuke the two merge and revert commits in feature. Follow these steps exactly:

git checkout feature        # switch to your feature branch
git reset --hard HEAD~2     # nuke the 'M' and 'R' commits

After this, you can try doing a merge with master again and all should be fine. Of course, make sure that you do the merge properly.

Note that this option involves rewriting the history of the feature branch, via nuking commits, and should probably not be used if the branch is shared and you have already pushed the branch with the merge commit. See the answer by @torek for other options if you fall into this category.

Upvotes: 2

torek
torek

Reputation: 489223

You have at least three options. See Tim Biegeleisen's answer for one of them: it's the simplest, and is the one to use if you have no additional commits and have not pushed the merge (or merge-and-revert).

If the rest is TL;DR, just use the other answer. :-)

Let me re-draw his master-and-feature graph this way, though:

A - B - C   <-- master
  \                         (pre-merge)
    D       <-- feature

and:

A - B - C     <-- master
  \      \                  (post-merge)
    D --- M   <-- feature

The thing that makes a merge commit be a merge commit is that it has two parents: the "main line" parent (D, in this case) and the commit that was merged (C).

How merge works

When you ask git to perform a merge, git starts by finding a common ancestor, in this case commit A. It then does two diffs: A vs D (what happened on the main line, feature) and A vs C (what happened on the branch being merged-in, master). It then combines these two sets-of-diffs, so that it can make a new commit that contains one copy of every change made in both lines.1 If all goes well, it commits the result automatically; if not, it stops with a merge conflict, makes you fix things up, and has you commit the final result. Either way, the final commit M lists both commits C and D as its (two) parent commits, rather than just one parent. That, in git, is the definition of a merge: a commit with two2 parents.

After a previous merge

For illustration purposes, let's add a new, ordinary commit on feature at this point:

A - B - C         <-- master
  \      \
    D --- M - E   <-- feature

What happens now if, while on branch feature, you ask git to merge master again?

Git must first find the most recent common ancestor of (the tips of) feature and master (aka the "merge base"). The ancestors of commit E, the tip of feature, are—in order of distance—E itself (zero steps back); M (one step back); C and D (both two steps back: both are parents of M); and A and B (both three steps back). (There's another way to get to A that's four steps back but we take the shorter route.) The ancestors of commit C, the tip of master, start with C itself (zero steps back). So this is the merge base, and to merge master, git should diff C (the merge base) against C ... but clearly there's no change there, so there is nothing to merge and you get the "up to date" message.

What about a revert, though? Well, as far as git is concerned, a revert is just an ordinary commit. What makes it a reversion is that the changes it applies "undo" the changes from a previous commit. That is, to make a revert, git essentially does a git diff (as for either side of a merge), but then wherever the diff says "add a line", git deletes that line instead, or where it says "delete a line", git puts back what got deleted. (This is a little bit trickier when you revert a merge since git has to avoid undoing something that, when you did the merge, had been done on both sides; but git does it correctly.)

Re-doing a merge

Back to the problem at hand, namely, re-doing the merge: you can use the suggested method, which is to (in effect) remove the undesired revert and the undesired merge. Using git reset --hard will do that, putting you back into the original pre-merge state:

A - B - C   <-- master
  \
    D       <-- feature

(The extra commits M and R are still stored in the repository, but are hidden from view and will eventually be garbage-collected.) Now you can re-do the merge, presumably getting it right this time.


When to do something fancier

One drawback to the simplest method is that when you make a new (corrected) merge, it will have a new and different underlying SHA-1 ID:

A - B - C      <-- master
  \      \
    D --- M2   <-- feature

In itself, this is necessary and even desirable. But, if you have pushed (or otherwise published) merge commit M somewhere (with or without pushing the revert R as well), someone else may have copies of that original bad merge. Pushing the new feature will require using --force to "rewrite history". Anyone who picked up the bad merge M may be depending on it, and rewriting history will make things difficult for them.

(You can of course choose to say "tough luck" to them and make them deal with your reset and re-merge. That's the easy way, and sometimes even the best way. But in case it isn't, I will press on.)

The other reason you might not want to do this happens if you've made some good commits since the bad one:

A - B - C                 <-- master
  \      \
    D --- M - E - R - F   <-- feature

Let's say that commit M is the bad merge, R is its reversion, but E and F are both good and desirable. If you use git reset --hard to wipe out M, you also wipe out E and F. So now what?

Fancier things to do

Git being git, there are many ways to handle this. You must choose:

  • whether you want a "fast-forward-able" history
  • whether you want to completely re-do the merge, or just fix it up
  • how you want to preserve your "good" commits

Let's address the last one first. You can save these commits as patches, using git format-patch. That lets you wipe them out (with git reset --hard) and then just re-apply them (with git am). Or, you can leave them in the repository, so that you can use git cherry-pick to copy them, or leave them in the original git commit chain, so that you can get a fast-forward-able history.

Let's look at the fast-forward-able item next. This basically means that every published commit (every commit you've ever pushed, or made available to someone else via git pull, or whatever) must remain unchanged: you must only add on to such history. That's really all there is to it (of course it means leaving your mistakes out there for others to view).

Finally, let's look at the middle item: do you really want to re-do the merge, or just take what's there and fix it? There's actually a third option, which seems a bit stupid at first: you can completely re-do the merge and then (using that one) fix the broken one. The advantage to this method is that it lets you produce, pretty easily, a fast-forward-able history.

Re-doing the merge without resetting

There are actually several ways to do this, but I'll just show one in particular. You want git to see the same setup it saw "pre-merge" even though there's a merge. I'll draw the same graph again, but stretch it out a bit:

A - B - C                   <-- master
  \      \
    D     \
     \     \
      ----- M - E - R - F   <-- feature

Now let's do something git does easily, namely, check out commit D. We can find its ID (78cefa3 or whatever), or count back commits on feature: F is 0 back, R is 1 back, E is 2 back, M is 3 back, D ... well, it's slightly tricky: both D and C are 4 back, but D is on the "main line", so feature~4 will name commit D.

Either way we check it out ... but we make a new branch label for it, let's call this feature-alt:

$ git checkout -b feature-alt 78cefa3   # by ID

Now we have this graph:

A - B - C                   <-- master
  \      \
    D ....\................ <-- feature-alt
     \     \
      ----- M - E - R - F   <-- feature

(I put in the .s to show that feature-alt points directly to commit D). Now, even though there's a feature branch, let's simply ignore it for a while, and draw the graph one more time:

A - B - C   <-- master
  \
    D       <-- feature-alt

This should look very familiar: it's the graph we had when we made the bad merge. So now, while we're on feature-alt, we simply run:

$ git merge master

and this time do the merge correctly (use git merge --no-commit if you like, to prevent git from committing an automatic merge result, so you can fix it up first).

Once the merge finishes and is committed, we'll have this:

A - B - C___                <-- master
  \      \  \
    D ----\-- M2            <-- HEAD=feature-alt
     \     \
      ----- M - E - R - F   <-- feature

I added the HEAD= here to show that we're still on feature-alt, with new merge commit M2. Note that M has parents D and C (in that order), and so does M2, but the contents of M2 are (presumably) different (the merge being correct this time).

At this point, you can simply git cherry-pick commits E and F, then delete branch feature and rename feature-alt to feature, and push the result with --force. That gives you a clean history with a correct merge and your two good commits. It's not fast-forward-able, but it preserves the changes you made in E and F.

Or, if M2 is the correct merge result, and you want a fast-forward-able history, what you need now is to apply the changes in M2 to the tip of existing branch feature. Since R reverted M, you can now do this:

$ git checkout feature
$ git cherry-pick -m 1 feature-alt

This is like any other cherry-pick except that you must specify the main-line of the merge (just like with git revert). This says, in essence, "compare D (the first parent) to M2 and apply those changes to the tip of the current branch feature": that is, put the correct merge in, instead of the wrong one.

If all goes well, you now have this:

A - B - C___                     <-- master
  \      \  \
    D ----\-- M2                 <-- feature-alt
     \     \
      ----- M - E - R - F - M3   <-- HEAD=feature

where M3 is a copy of M2 (but is a regular non-merge commit).

If all has gone well, you can now simply delete the feature-alt branch entirely. The bad merge is preserved in history, as is its reversion, and the corrected version of the merge appears as if by magic as an ordinary commit.


If all of this is too complicated and you don't want to repeat the merge, you can simply revert your revert of the merge, bringing back the bad merge:

A - B - C                 <-- master
  \      \
    D --- M - E - R - F   <-- feature

becomes:

A - B - C                      <-- master
  \      \
    D --- M - E - R - F - R2   <-- feature

where R2 reverses R. Now you can make whatever fixes are needed and git commit --amend to adjust R2 (or even just git commit without --amend to just add another fix on top, and then maybe use git rebase -i to squash it in later before pushing). What this does is make R2 be exactly the same as what you'd have gotten with M3, the cherry-picked copy of M2, the corrected merge. You avoid re-doing the merge, but of course, you have to figure out what to do to fix the bad merge.

In any case, the final result of all this can be pushed without -f as it is simply new commits added atop all previous ones.


1For instance, if A-to-D adds a line of text to README.txt and A-to-C adds a different line of text to main.c, the new commit M will have each line added to each file. However, if A-to-C adds the same line to README.txt, in the same place, git will know to just add one copy of that new line, not one from each set of changes. The new README.txt will have just the one line added, without repeating it.

2Really two or more, but multi-armed "octopus" merges basically work the same way.

Upvotes: 2

Related Questions