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 master
s 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 !O
s 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