I'd like to move the last several commits I've committed to master to a new branch and take master back to before those commits were made. Unfortunately, my Git-fu is not strong enough yet, any help?
I.e. How can I go from this
master A - B - C - D - E
to this?
newbranch C - D - E
/
master A - B
For those wondering why it works (as I was at first):
You want to go back to C, and move D and E to the new branch. Here's what it looks like at first:
After
git branch newBranch
:After
git reset --hard HEAD~2
:Since a branch is just a pointer, master pointed to the last commit. When you made newBranch, you simply made a new pointer to the last commit. Then using
git reset
you moved the master pointer back two commits. But since you didn't move newBranch, it still points to the commit it originally did.Much simpler solution using git stash
If:
master
, andThen the following is far simpler (starting on branch
master
that has three mistaken commits):What this does, by line number
master
, yet leaves all working files intactmaster
working tree exactly equal to the HEAD~3 statenewbranch
You can now use
git add
andgit commit
as you normally would. All new commits will be added tonewbranch
.What this doesn't do
Goals
The OP stated the goal was to "take master back to before those commits were made" without losing changes and this solution does that.
I do this at least once a week when I accidentally make new commits to
master
instead ofdevelop
. Usually I have only one commit to rollback in which case usinggit reset HEAD^
on line 1 is a simpler way to rollback just one commit.Don't do this if you pushed master's changes upstream
Someone else may have pulled those changes. If you are only rewriting your local master there's no impact when it's pushed upstream, but pushing a rewritten history to collaborators can cause headaches.
1) Create a new branch, which moves all your changes to new_branch.
2) Then go back to old branch.
3) Do git rebase
4) Then the opened editor contains last 3 commit information.
5) Change
pick
todrop
in all those 3 commits. Then save and close the editor.6) Now last 3 commits are removed from current branch (
master
). Now push the branch forcefully, with+
sign before branch name.Most previous answers are dangerously wrong!
Do NOT do this:
As the next time you run
git rebase
(orgit pull --rebase
) those 3 commits would be silently discarded fromnewbranch
! (see explanation below)Instead do this:
--keep
is like--hard
, but safer, as fails rather than throw away uncommitted changes).newbranch
.newbranch
. Since they're no longer referenced by a branch, it does that by using git's reflog:HEAD@{2}
is the commit thatHEAD
used to refer to 2 operations ago, i.e. before we 1. checked outnewbranch
and 2. usedgit reset
to discard the 3 commits.Warning: the reflog is enabled by default, but if you've manually disabled it (e.g. by using a "bare" git repository), you won't be able to get the 3 commits back after running
git reset --keep HEAD~3
.An alternative that doesn't rely on the reflog is:
(if you prefer you can write
@{-1}
- the previously checked out branch - instead ofoldbranch
).Technical explanation
Why would
git rebase
discard the 3 commits after the first example? It's becausegit rebase
with no arguments enables the--fork-point
option by default, which uses the local reflog to try to be robust against the upstream branch being force-pushed.Suppose you branched off origin/master when it contained commits M1, M2, M3, then made three commits yourself:
but then someone rewrites history by force-pushing origin/master to remove M2:
Using your local reflog,
git rebase
can see that you forked from an earlier incarnation of the origin/master branch, and hence that the M2 and M3 commits are not really part of your topic branch. Hence it reasonably assumes that since M2 was removed from the upstream branch, you no longer want it in your topic branch either once the topic branch is rebased:This behavior makes sense, and is generally the right thing to do when rebasing.
So the reason that the following commands fail:
is because they leave the reflog in the wrong state. Git sees
newbranch
as having forked off the upstream branch at a revision that includes the 3 commits, then thereset --hard
rewrites the upstream's history to remove the commits, and so next time you rungit rebase
it discards them like any other commit that has been removed from the upstream.But in this particular case we want those 3 commits to be considered as part of the topic branch. To achieve that, we need to fork off the upstream at the earlier revision that doesn't include the 3 commits. That's what my suggested solutions do, hence they both leave the reflog in the correct state.
For more details, see the definition of
--fork-point
in the git rebase and git merge-base docs.To do this without rewriting history (i.e. if you've already pushed the commits):
Both branches can then be pushed without force!