Lorenzo Belli
Lorenzo Belli

Reputation: 1847

Undo pushed git merge

I'm in this situation

        \ /
         C
         |     |
         B     F
         |     |
    [dev]A     D
          \   /
           \ /
            M [Master]

Master was at commit D and this mistake happened: the branch at commit A has been mistakenly merged into master and pushed. Moreover the commit M does not contain all the changes in A,B,C,... because has been modified by hand before committing. All these commits have already been pushed.

I need to create a new branch with the same changes as in [dev] and merge it later into master, at the moment I can't because those commits have already been merged. Also all the changes in [dev] comes from various merges from different branches.

Important limitation: my git server does not allow me to force push into master branch, so I cannot reset master branch.

Is there a way to put changes in [dev] into [master] after a bad merge [M]?

EDIT: I am allowed to force push in branches which are not master.

Upvotes: 0

Views: 295

Answers (4)

Mark Adelsberger
Mark Adelsberger

Reputation: 45819

So it sounds like the issues are

1) The merge should not yet have happened

2) The merge was edited in a way that means M doesn't apply all dev changes over D

3) Eventually you will want to merge dev (including A through C) into master

4) Force pushes are not allowed on master

We're left with options that aren't great.

To address (1) and (2), you just have to revert M. Because of (4) you can't rewrite the history of master, so all methods of returning master to a state that matches D are equivalent to git revert. That resolves (1) and makes (2) moot.

But now (3) is a problem. Most solutions involve a force-push to dev; while you can't force-push master, you haven't indicated either way whether rewriting dev history is allowed. (If not, bear with me anyway, because I'll be building a solution for that case on top of this...)

If rewriting dev history is ok, then the question is simply how detailed a history you want in the replacement branch (remembering that the original history will still appear, whether you want it or not, before the commit where you revert M).

The simplest option I can think of (but the one with the least historical fidelity on the new branch) would look like

git checkout D
git branch -f dev
git merge --squash C
git commit -m "Replacement dev branch"
git push -f

Note that if you did want to manually edit the result of the "squash merge", you could do it after the merge command but before the commit command; but again, my understanding is that the edits done previously were part of the problem...

So this creates a single new commit rooted at D, introducing the same changes relative to D as a merge of C, but without the "2nd parent" relationship of a real merge - which means git will "forget" that these changes are the same as the changes you already merged into master.

     \ /
      C
      |     |
      B     F
      |     |
      A     D
       \   / \
        \ /   \
         M    CBA [Dev]
         |
         W [Master]

In this diagram, the TREE (content) at W matches D, and CBA matches what M would've been if it hadn't been incorrectly edited. You could have rooted CBA at the original branch point of dev instead of at D (that's controlled by the checkout command); only difference is whether existing conflicts should be resolved now or later, given that the history is going to be "off" either way.

If you have more specific requirements for what the "new history" of dev should look like, let us know and I can suggest a procedure that might come closer to meeting them.

In any event, because this rewrites history on dev, it is best done when nobody has unpushed changes on dev (otherwise they'll have to be rebased to the new dev branch). And even then, everybody will have to force the update of their local dev branch. See "recovering from upstream rebase" in the git rebase docs, because that's essentially what has to happen.

Which brings me to the other possibility: if you can't (or don't want to) rewrite dev history either, then extra steps are needed to ensure that dev is always moving in a "normal" way (as far as the remote sees).

git checkout D
git branch -f dev
git merge --squash C
git commit -m "Replacement dev branch"
git rebase master
git push

Note that although we move the dev branch, we later rebase it back to a descendant of origin/dev; so no force-push is needed. If this seems unnerving, you can create a temp branch for the squash merge and then do a simple ff merge to advance dev after the rebase, but the result is the same.

     \ /
      C
      |     |
      B     F
      |     |
      A     D
       \   / \
        \ /   \
         M    CBA
         |
         W [Master]
        /
       /
     CBA' [Dev]

Note that CBA is unreachable and will eventually be garbage-collected; I show it here for no particular reason. The new dev is at CBA' - a rewrite of CBA.

Again, if you have specific requirements for how the history should look between W and CBA' let us know and we can try to work out a procedure - but again, remember that the original history is still visible before M anyway.

Upvotes: 1

barnski
barnski

Reputation: 1732

You could revert merge, than revert merge's revert commit, than reset last commit, and choose manually which patches to apply.

git revert -m 1 <commit id> 
git revert HEAD
git reset HEAD~1 

Than you would have all unstaged changes from merge commit available to edit with GUI or with:

git add -i 

With this you could interactively apply or reset patches. Or just edit by hand in files.

Upvotes: 0

Kmg Kumar
Kmg Kumar

Reputation: 616

After merge when you checkout into your branch and give the below mentioned comment into you putty, where you last correct commit id put into the #commit_id place. Warning - once you will back into the previous code you cannot retrieve last merge code.

git revert #commit id

Upvotes: 0

Niobos
Niobos

Reputation: 930

A mangled merge-commit is always difficult to fix, since you can't (fully) revert a merge to start over. (You can revert the changes it brought in, but re-merging will not re-introduce these changes). Usually, the easiest solution is to rewrite history and start over, but I understand that that isn't possible.

The next best thing is to create a fixup commit that brings the state to the way you want it. If I understood your question correctly, the problem is that the merge was not "fully" done (because of edits in the merge-phase), and you actually want the full merge to be in master.

In that case, I would:

# Undo the merge, locally
git checkout master
git reset --hard D  # the commitID from your graph, just before the merge

# Re-do the merge correctly, locally
git merge dev
N=$(git rev-parse HEAD); echo $N   # print as reference, call this commit N

# restore to upstream state
git reset --hard origin/master

Now you need to create a new commit on top of master M', that brings M to the desired state N.

I have a solution for that part of the problem, but I'm not particularly proud of it. There are probably better ways to do this, but here it goes:

# Figure out the tree-ID of the wanted state:
TREE_ID=$( git cat-file -p $N | grep '^tree' | sed 's/tree //' ); echo $TREE_ID

# Generate a commit with that tree ID. I.e. all files will be exactly as
# they are in commit N: everything you did in M to mangle the merge will be undone
COMMIT_ID=$( echo "Fixup merge" | git commit-tree $TREE_ID -p HEAD ); echo $COMMIT_ID
#                   ^^^^ this will be the commit message; change if needed

# pull the generated commit in to the master branch
git reset --hard $COMMIT_ID

Upvotes: 0

Related Questions