There are multiple ways to do it. Which you should use depends on (a) what you want the history to look like, and possibly (b) whether the costs of a "history rewrite" make sense and (c) whether your answer to (a) or (b) is more important to you. So let's look at some options...
No matter what else you do, you may want to start by committing your new changes. This ensures that no amount of fiddling around will cause you to lose them.
git checkout -b temp
git commit
Now those new changes become G
, and you have
A -- B -- C -- D -- E -- F <--(master)(origin/master)
\
G <--(temp)
When you're done, you'll delete temp
, and if G
isn't part of the actual solution state then it will get cleaned up eventually, so no harm in taking this first step as a safety net.
Now if you know you'll never care about B
through F
again, then the simplest solution with the cleanest result is to simply move master
to G
. This is a history rewrite, in that it removes commits from the history of a ref. When doing history rewrites, you have to coordinate with everyone else who uses the repo / might have fetched the ref when the "removed" commits were in its history. Things that would make a history rewrite more difficult to execute:
- Other work has been done "on top of"
F
- Lots of developers use the repo
- Some developers who use the repo are in limited communication and might not be immediately aware of what's happening
If you have any of those conditions, you might want to settle for a different solution. The ideal case for a history rewrite would be when you can coordinate with all repo users to
- push all changes
- discard all local repos
- perform the rewrite
- everyone re-clones
If you decide rewriting the history is the right thing to do:
First move any ref(s) pointed at F
(e.g. master
in the above example) to G
git checkout temp
git branch -f master
If other branches have been based on F
, they need to be rebased to G
. For example if you have
A -- B -- C -- D -- E -- F <--(origin/master)
\ \
G <--(temp)(master) H <--(feature_b)
then you could say
git rebase --onto temp origin/master feature_b
yielding
A -- B -- C -- D -- E -- F <--(origin/master)
\
G <--(temp)(master)
\
H' <--(feature_b)
If you have work based on B
, D
, or E
(but not F
), that could be more challenging, though rebasing that may also be ok.
Finally, you "force push" any branches you moved or rebased. e.g.
git checkout master
git push -f
At this point, all other users need to get back in sync. If, as I suggested above, everyone threw away their local repos, they can just re-clone and they're back in sync. Otherwise, any local changes they have, based on any commit you've replaced, need to be rebased; and their refs have to be updated. See "Recover from upstream rebase" in the git rebase
docs for more detail.
Note that if all of this sounds good, except you want to keep the original commits B
through F
"off to the side" for future reference, then before moving master
in the above procedure you can tag F
.
git checkout master
git tag old_master
(In truth you can do it after moving master
, but at that point it's a little harder to find F
.)
If a history rewrite isn't good for your situation, then B
through F
have to remain in the history. You can then add one or more commits to "undo" B
through F
and apply G
. This is still easiest if there aren't a bunch of branches all already based on F
. In the simplest case, you can
git checkout master
git revert HEAD~4..HEAD
git rebase master temp
git checkout master
git merge temp
giving you something like
A -- B -- C -- D -- E -- F -- ~F -- ~E -- ~D -- ~C -- ~B -- G' <--(master)
If you want to jump straight from F
to G'
(without explicit commit(s) to revert changes), you could instead just re-parent G
onto F
(see git filter-branch
docs). Or another way to do the same thing
git checkout temp
git reset --soft master
git commit
git checkout master
git merge temp
With any of these solutions, B
through F
still appear in the history (e.g. git log
). On a repo-by-repo basis, if a developer wants to hide this they can supply A
as a "replacement" for the commit before G'
(i.e. G'^
). See the git replace
docs for details.
One additional option would be to merge G
into master
(presumably after a revert commit). This yields something like
A -- B -- C -- D -- E -- F -- ~FEDCB -- M <--(master)
\ /
---------------- G ------------------
which is ok, but makes it harder to "paper over" the history with replacements. Note that in this case you should not combine the revert commit(S) with the merge commit, as this would produce an "evil merge" and could cause trouble down the line.