There are basically two ways to go about this. Each has pros and cons.
So let's draw up a simple approximate current state picture. I'm inferring, based on your question title, that the work is done on branches and master
consists of merge commits from those branches. If so it might look a bit like this:
A -- B C D -- E
/ \ / \ / \
O ------ M1 - M2 ------ M3 <--(master)
^ ^ ^
v1 v2 v3
Now you want to update master
so that it doesn't contain the changes from C
, but you want to keep those changes around in case you need them in the future. Note that if changes in C
overlap with changes in later commits, then every approach will involve some amount of conflict resolution.
With rebase
One option is to rewrite the history. That is the option that will involve a --force
(or, preferably, --force-with-lease
) push over master
, and that in turn will require clean-up of everybody's local clones to avoid undoing the rewrite. Also, doing it is complicated by the presence of merge commits.
The first step is to create the new branch; easiest to do that before anything else is changed. So check out commit C
. You'll need an expression that resolves to commit C
- the commit ID is the most universal thing you could use, but in our example you could also use something like master^^2
(the 2nd parent of the 1st parent of the master
tip commit).
git checkout master^^2
git branch v2-archive
Now you have
C <--(v2-archive)
/ \
A -- B / \ D -- E
/ \ / \ / \
O ------ M1 ----- M2 ------ M3 <--(master)
^ ^ ^
v1 v2 v3
and what's left is to rewrite master
. The idea here is to rebase
, but rebase
doesn't mix well with merge commits.
You can try to use the --prseerve-merges
option. If M3
(or, more generally, every merge being rewritten) was completed successfully using default merge settings, then this will probably be ok. The command would look like
git rebase --preserve-merges --onto master~2 master^ master
where master~2
, master^
, and master
stand in as expressions that resolve to M1
, M2
, and M3
in our example. If it works, this will leave you with
A -- B /- D' -- E'
/ \ / \
O ------ M1 ---------- M3' <--(master)
^ \
v1 \-- C <--(v2-archive)
\ \
\-- M2 -- D -- E
^ \ \
v2 \------- M2
^
v3
where D'
and E'
are the result of replaying changes from D
and E
onto the v1
commit, and M3'
merges that back to master
(essentially a rewrite of M3
). Note that all the original commits are still present, and in particular the tags still point where the always pointed.
Now you have to decide what to do about tag v3
. Normal convention is that tags don't move. My advice is to create a new version number and tag M3'
accordingly. You might then choose to delete the v2
and v3
tags, or just leave them for archival purposes.
If that doesn't go smoothly, the other option (starting back at
C <--(v2-archive)
/ \
A -- B / \ D -- E
/ \ / \ / \
O ------ M1 ----- M2 ------ M3 <--(master)
^ ^ ^
v1 v2 v3
again), would be to create a temporary branch at the E
commit
git checkout master^2
git branch temp
then rebase the temporary branch
git rebase --onto master~2 master^ temp
so you have
C <--(v2-archive)
/ \
A -- B / \ D -- E
/ \ / \ / \
O ------ M1 ----- M2 ------ M3 <--(master)
^ \ ^ ^
v1 \ v2 v3
\
D' -- E' <--(temp)
Next you have to push master
back to M1
, and then merge in the temp
branch
git checkout master
git reset --hard master~2
git merge temp
git branch -D temp
taking care to account for any non-standard merge behaviors that were originally in M3
. The end result is basically the same; it's just a more manual way of ensuring the merge is handled correctly. You still need to decide what to do about the tags, just as before.
Either way, at this point you have completed the rewrite. D'
, E'
, and M3'
are all untested states of the code, and ideally they should all be validated to be sure they pass automated tests. Of course M3'
must be re-tested, but if you want debugging tools like bisect
to work well in the future, then you should also make sure the rewritten intermediate states are "clean".
And then you can do the force push and begin the clean-up.
With revert
The other option is to use git revert
. This keeps the history as it is; so if the real goal is to have the next version of the product exclude v2
features, this is perhaps fine; but if the actual removal of the changes from history is a literal requirement, this isn't the way.
The big advantage is, no history rewrite means no force push and no recovery from upstream rebase for everyone on the team. Generally this should be the simpler procedure. The down-side is, you have to take an extra step to allow for future re-integration of the changes from v2
.
So we're back at our initial state
A -- B C D -- E
/ \ / \ / \
O ------ M1 - M2 ------ M3 <--(master)
^ ^ ^
v1 v2 v3
Again we put a branch at C
.
git checkout master^^2
git branch v2-archive
But then we proceed with
git revert -m1 master^
Note the -m1
option; this tells git to undo merge M2
from the perspective of its "first parent" - i.e. undo the changes from C
. (In the example, you could just revert C
instead of M2
; but if the reverted branch really has many commits, then reverting the merge is more straightforward.)
So now you have
C <--(v2-archive)
/ \
A -- B / \ D -- E
/ \ / \ / \
O ------ M1 ----- M2 ------ M3 -- !M2 <--(master)
^ ^ ^
v1 v2 v3
where !M2
reverses all the changes that were introduced into master
by M2
. As with the history rewrite, you need to decide how you're going to handle tags. In this case maybe it's a little more clear why v2
and v3
might be best left alone (or just deleted), so you might want to create a new version number for !M2
.
But now, if you try to merge v2-archive
into master
, git will find that everything is already up to date because C
- the commit you're trying to re-integrate - is already "reachable" from master
(by parent pointers). For this reason, the git docs say that reverting a merge means you're permanently discarding the changes from the branch; but there is a work-around.
git rebase -f v2-archive^ v2-archive
This creates a new copy of C
that's basically identical to C
, but it's not reachable from master
C' <--(v2-archive)
/
A -- B /- C D -- E
/ \ / \ / \
O ------ M1 ---- M2 ------ M3 -- !M2 <--(master)
^ ^ ^
v1 v2 v3