2

I've a little problem. I need to cancel a merge on my remote repository without cancel commits done after. See below my model and my expectation.

Currently, I've this model:

enter image description here

I would like this :

enter image description here

I've check the docs and search on the web but I didn't find any smart solution. Do you have an idea for me ? I would take care about my merge on the future !

Thanks you very much !

YesThatIsMyName
  • 1,585
  • 3
  • 23
  • 30
areuseriouus.eth
  • 383
  • 2
  • 3
  • 16
  • Are you happy to do a force push, which will require getting anyone else with local copies of that branch to update their local copies before they can push? – DaveyDaveDave Sep 04 '18 at 09:56
  • Possible duplicate of [git remove merge commit from history](https://stackoverflow.com/questions/17577409/git-remove-merge-commit-from-history) – phd Sep 04 '18 at 13:58
  • https://stackoverflow.com/search?q=%5Bgit%5D+remove+merge+commit – phd Sep 04 '18 at 13:58

3 Answers3

3

You can either rewrite branch history, or revert the merge. Each has pros and cons.

First let's start with a slightly modified copy of you current-state diagram, that reflects a bit more of what's going on.

A -- B -- C <--(branch>
           \
  M -- N -- O -- P -- Q <--(master)

You didn't show any refs, so I'm assuming master is pointed at Q. If you don't have a branch at branch, you should probably create one (unless you're permanently discarding the changes from A, B, and C). Also, a minor point of notation, but I've switched all commits to letters (as this can sometimes be clearer).


History Rewrite

All of the usual warnings about history rewrites apply to this approach. Mostly that means, if master has been pushed such that anyone else has already "seen" commit O as a part of masters history, then you will have to coordinate with them to successfully do a history rewrite. The rewrite will put their copy of master in a bad state from which they'll have to recover, and if they do this the wrong way - as they might if you haven't communicated what's happening - then your work could be undone. See "Recovering from upstream rebase" in the git rebase docs for more information, as that is the applicable condition whether or not you actually use the rebase command to perform the rewrite.

If you do want to do a rewrite, rebase is the simplest way. You'll need either the IDs of commits N and O, or expressions that resolve to commits N and O. In this example we can use master~3 for N and master~2 for O.

git rebase --onto master~3 master~2 master

This will take the changes reachable from master, but not reachable from O, and replay them over N; and then move master to the rewritten commits.

A -- B -- C <--(branch>
           \
  M -- N -- O -- P -- Q <--(master@{1})
        \
         P' -- Q` <--(master)

The old commits still exist (and, as I've shown here, the reflog could still reach them - locally for the time being). Because most tools don't follow the reflog, you'll likely see something more like

A -- B -- C <--(branch>

M -- N -- P' -- Q` <--(master)

And in fact, after the reflog expires that's exactly what will remain (if you don't do something to preserve the old commits in the meantime). At this point, to push master you would have to do a force push. The safest way to do that is

git push --force-with-lease

It is common for people to recommend simply the -f option, but this is less safe as it could clobber commits you don't know about on the remote master. In any event, after a force-push is the point where anyone else with copies of master will have to recover from the "upstream rebase" condition.

Other ways of doing the rewrite (such as by resetting and then cherry-picking) are functionally equivalent (barring a few weird edge cases), but they are more manual and therefore more error-prone. Worth reiterating, even though such alternatives might not use the rebase command, the "upstream rebase" situation would still apply in exactly the same way.


Without a Rewrite

If a history rewrite is not feasible - as is often the case in widely-shared repos - the alternative is to revert the merge commit. This creates a new commit that "undoes" the changes introduced by the merge. To use revert on a merge commit, you have to give the -m option (which tells revert which parent to revert to; if you're trying to undo the effect of a merge this is usually -m 1).

Again you need the ID of, or an expression that resolves to, O; we'll use master~2 in the example.

git checkout master
git revert -m 1 master~2

Now you have

A -- B -- C <--(branch>
           \
  M -- N -- O -- P -- Q -- !O <--(master)

where !O reverses the changes that O applied to N.

As noted elsewhere, git sees branch as "already accounted for" - it doesn't track that !Os changes were intended as a revert/rollback of O or anything like that. So if you later want to say git merge branch, it will skip over commits A, B, and C.

One way to fix that is with rebase -f. For example, after the revert you could say

git rebase -f master~3 branch

and all commits reachable from branch, but not reachable from master prior to the merge at O, would be rewritten. Of course, this is a rewrite of branch. Since you might've been using the revert approach to avoid rewriting master, you might also not want to rewrite branch. (If you do rewrite branch, and if branch is shared with other repos, then you'd have to push --force-with-lease and other users would have to recover from an upstream rebase.)

Another option, at the point where you want to merge branch back into master, is to "revert the revert". Let's suppose some time has passed since you reverted the merge, and you have

A -- B -- C -- D -- E <--(branch>
           \
  M -- N -- O -- P -- Q -- !O -- R -- S <--(master)

Now to merge branch to master you could say

git checkout master
git revert master~2
git merge branch
Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • Thanks you so much for your excellent answer, I've learned a lot ! I've used the `git revert` solution :) – areuseriouus.eth Sep 05 '18 at 16:01
  • A question about the revert snippets: Initially, `master~2` was reverted using `git revert -m 1 master~2` (1st revert). But to revert that revert (2nd revert), we should again refer to the same commit `master~2`, running `git revert master~2`? shouldn't we refer to the 1st revert commit in some way, e.g. running `get revert master~3`, assuming `master~3` is the 1st revert? – OfirD Aug 08 '23 at 08:51
2

One way is to reset hard on the last commit before O which doesn't belong to the branch 5 - 6 - 7 i.e. N in this case. Then cherry pick all the required commits i.e. P & Q. Example below:

git reset --hard N         #This resets the branch to N & removes all commits of merged branch `5 - 6 - 7`
git cherry-pick P          #Manually add the 2 commits you want back.
git cherry-pick Q

Another way is to revert the merge commit with the following command:

git revert -m 1 O .     #this is alphabet O - merge commit id. Not Numeric zero.

This will add a new commit on top of Q - Let's name it as O', where

  • O' is the reverse commit of O

caveat: If you try to do some changes in the 5 - 6 - 7 branch in future and merge it again - it won't merge commits 5, 6, 7 because those commit ids are already in this branch & there also exists a reverse commit of these commits on top of them.

This means you won't be ever able to merge commits 5, 6, 7.

Though there are mechanisms by which you can change the commit ids by doing either a rebase or making a trivial change just for the sake of changing the ids, that would incur a merge conflict on identical changes. Personally, I wouldn't recommend this approach.

Pankaj Singhal
  • 15,283
  • 9
  • 47
  • 86
  • The concerns about reverting the merge are vastly overstated, and this concerns about rewriting (possibly published) history are entirely ignored. Someone's personal preferences are showing through. – Mark Adelsberger Sep 04 '18 at 12:53
  • I might have missed writing about histry rewrite but which part of the answer overstates anything and doesn't talk about facts? – Pankaj Singhal Sep 04 '18 at 15:11
  • "mechanisms by which you can change the commit ids ... would incur a merge conflict". Not generally true. – Mark Adelsberger Sep 04 '18 at 15:35
  • I think the possibility of occurring is more than not occurring. If the changes are same in 2 commits but diff IDs, why do you think there wouldn't be any conflicts if you merge one on top of another? – Pankaj Singhal Sep 04 '18 at 15:45
  • You may think the possibility of occurring is more than not occurring, but you're mistaken. The reasoning behind how such a merge works is complex enough that I'm not going to try to spell it out in a comment, but (1) search on the topic, and you'll find questions and answers that do spell it out, or (2) try it yourself. – Mark Adelsberger Sep 04 '18 at 17:38
  • Thank you for your answer @PankajSinghal ! I didn't know the `git cherry-pick`. I've used the `git revert` solution :) – areuseriouus.eth Sep 05 '18 at 16:03
  • Glad I could help. Please accept the answer if it helped. – Pankaj Singhal Sep 05 '18 at 16:13
1

Since you don't mention anything about the branches you currently have, I suggest you start by creating some branches (b7 and bkp) on the existing commits to keep them visible after the change:

        +----- b7
        v
5 - 6 - 7       +-------- bkp
        |       v
M - N - O - P - Q
                ^
                +-------- bQ

Since you probably have a branch that points to Q you can use it instead of bQ in the commands below.

Then git rebase the commits P and Q on top of commit N to get the structure you desire.

The commands are:

# Preparations
# Create the branch `b7` to keep the commit `7` reachable after the change
git branch b7 7
# Create the branch `bkp` for backup (the commit `Q` will be discarded)
git branch bkp Q

# The operation
# Checkout `bQ` to start the change
git checkout bQ
# Move the `P` and `Q` commits on top of `N`
# Change "2" and "3" in the command below with the actual number of commits
# 2 == two commits: P and Q, 3 == 2 + 1
# Or you can use commit hashes (git rebase --onto N O)
git rebase --onto HEAD~3 HEAD~2

Now the repository looks like this:

        +----- b7
        v
5 - 6 - 7       +-------- bkp
        |       v
M - N - O - P - Q
    |
    P'- Q'
        ^
        +-------- bQ

The old O, P and Q commits are still there and they are still reachable as long as the branch bkp or any other branches that point to Q still exists. If you are happy with the change you can remove bkp and any other branches that point to Q you have.

The command is:

git branch -D bkp

You probably don't want to remove b7 unless you already have another branch that points to commit 7 already. In this case you do not even need to create b7.

After this operation, the repo looks like this:

        +----- b7
        v
5 - 6 - 7

M - N - P'- Q'
            ^
            +-------- bQ

Please note that the commits P' and Q' are different than the original commits P and Q.

Warning

If the commits O, P and Q were already pushed to a remote repository (through the bQ branch), pushing bQ again will fail. The pushing can be forced (git push --force origin bQ) but this action will confuse your coworkers that already fetched the current position of the bQ branch (it contains the O, P and Q commits).

If you really need to perform this stunt, make sure you inform everybody about this change.

A better approach in this situation is to git revert -m the commit O. This creates a new commit on top of Q that introduces changes that cancel the changes introduced by commit O. It is ugly but it is the safest solution.

axiac
  • 68,258
  • 9
  • 99
  • 134