git: how to revert multiple recent commits
Let's assume we have a history like this:
G1 - G2 - G3 - B1 - B2 - B3
Where G1-G3 are 'good' commits and B1-B3 are 'bad' commits that we want to revert.
Here is the shell script to create the revision history like the one above. You can use it to try and see the effect of different commands.
Create a new branch
This is a safe and simple method that does not require history rewrite: check out the last 'good' commit and create a new branch from it.
git checkout G3
git checkout -b my-new-branch
A variation of this is to create a new branch and then cherry-pick the commits you need into it:
git checkout master
git checkout -b my-new-branch
git cherry-pick G1
git cherry-pick G2
git cherry-pick G3
It is also possible to cherry-pick a range of commits with git cherry-pick G1^..G3.
Use git reset
A simple way to throw away a few recent commits is to use git reset.
It rewrites the commit history, so only use it when your changes are not public yet (you can do this locally or on your private branch).
The git reset command can be used to throw away recent commits (the
--hard flag will also remove any local changes that are not committed
yet):
Careful:
git resetrewrites history.Careful:
git reset --hardremoves non-committed local changes.
$ git reset --hard HEAD~3
Here we can refer to B3 as HEAD, B2 is HEAD~1, B1 is HEAD~2.
This way the last good commit G3 is HEAD~3:
G1 - G2 - G3 - B1 - B2 - B3
\ \ \ \-- HEAD
\ \ \------ HEAD~1
\ \---------- HEAD~2
\-------------- HEAD~3
Use git revert
If your changes are public already (for example, merged to master or
another public branch), then it is better to avoid history rewrites and
use git revert to generate an anti-commit for your changes.
The revert command takes the SHA1 of one or several commits and generates
the new change to reverse the effect of these commits.
Note for Mercurial users: in Mercurial, the
revertcommand works differently - it takes the revision identifier and reverts the state of the repository to that revision. So we can ask Mercurial torevert current state to the state of the revision G3. In git, thereverttakes one or multiple revision identifiers and reverts the effect of the specified revisions. When we are talking to git, we ask it torevert the effect of revisions B3, B2, B1.
Here is how we can use git revert:
$ git revert --no-commit HEAD~2^..HEAD
Or:
$ git revert --no-commit HEAD~3..HEAD
We need to revert a range of revisions from B1 to B3. The range
specified with two dots like <rev1>..<rev2> includes only commits
reachable from <rev2>, but not reachable from <rev1> (see
man 7 gitrevisions).
Since we need to include B1 (represented by HEAD~2), we use HEAD~2^ (its
parent) or HEAD~3 (also the parent of HEAD~2). The HEAD~2^ syntax is more
convenient if commit SHAs are used to name commits.
The --no-commit option tells git to do the revert, but not commit
it automatically.
So now we can review the repository state and commit it. After that we will get the history like this:
G1 - G2 - G3 - B1 - B2 - B3 - R'
Where R' is a revert commit which will return the repository state to the
commit G3. Run git diff to check this (the output should be empty):
$ git diff HEAD~4 HEAD
Another way to run git revert is to specify commits one by one from newest
to oldest:
$ git revert --no-commit HEAD HEAD~1 HEAD~2
In this case, there is no need to specify HEAD~3 since it is a good
commit we do not want to revert. This is very useful if we want to
revert some specific commits, for example, revert B3 and B1, but
keep B2:
$ git revert --no-commit HEAD HEAD~2
Revert the commit in the middle of the history
For example, we have a history like this:
G1 - G2 - G3 - B1 - G4 - G5
And we want to revert the B1 commit.
The safe way is to use the git revert B1-sha1 command, as described
above - it will generate an anti-commit for B1.
The other method is to use an interactive rebase to move the bad commit to
the top of the history and then remove it with git reset:
git rebase -i HEAD~3
pick B1-sha commit message
pick G4-sha commit message
pick G5-sha commit message
Here (in the file opened by git rebase) we can edit the order of the
commits and move the bad commit to the end of the commit history:
pick G4-sha commit message
pick G5-sha commit message
pick B1-sha commit message
Save the file and exit the editor. If everything is alright (the commit has
no dependencies), the command will complete successfully. Note:
otherwise (if you get conflict errors), it is probably better to run
git rebase --abort to get back to the previous state and reconsider
the update strategy.
Now the history should look like this:
G1 - G2 - G3 - G4 - G5 - B1
And we can remove the last commit with git reset --hard HEAD~1 (again,
careful: it will remove any local uncommitted changes and will rewrite
history, so do not use it on public branches).
Roll back to a specific revision
The git revert command reverts a range of specified revisions, but
sometimes we just want to restore the state of some specific revision
instead of reverting commits one by one.
This is also how hg revert works in Mercurial.
This is especially useful if we want to revert past the merge point,
where it can be quite difficult to use git revert because we will also
need to specify which parent to follow at the merge point (you'll see a
Commit XXX is a merge but no -m option was given. message).
Revert to a specific revision using git checkout
Having a history like this:
/-- master
/
G1 - G2 - G3 - B1 - B2 - B3
\--- branch
We can roll back the branch state to the G3 commit this way:
git checkout G3-sha
git branch -D branch # Delete existing branch
git checkout -b branch # Check out a branch with the same name again
git push -f origin HEAD # Force-push the new branch
So, basically, we are removing the existing branch and creating another
branch with the same name from G3.
Revert to a specific revision using git reset
The solution comes from the Revert to a commit by a SHA hash in Git? question.
Here we first hard reset the state of the repository to some previous revision and then soft reset back to the current state. The soft reset will keep file modifications, so it will bring the old state back on top of the current state:
Careful:
git reset --hardremoves non-committed changes.
$ git reset --hard 0682c06 # Use the SHA1 of the revision you want to revert to
HEAD is now at 0682c06 G3
$ git reset --soft HEAD@{1}
$ git commit -m "Reverting to the state of the project at 0682c06"
Roll back to a specific revision using git read-tree
This solution comes from the How to do hg revert --all with git? question:
git read-tree -um @ 0682c06 # Use the SHA1 of the revision you want to revert to
The -m option instructs read-tree to merge the specified state and
-u will update the work tree with the results of the merge.
Links
Stack Overflow: Revert multiple git commits
Stack Overflow: Revert a range of commits in git
Stack Overflow: Git diff .. ? What's the difference between having .. and no dots
Stack Overflow: What's the difference between HEAD^ and HEAD~ in Git?