Normally you revert a commit using git revert. This git command reverts the whole commit. So all changes contained in this commit will be reverted. This works well if commits have a high cohesion in matters of the tasks they implement. In other words… if commits are fine-grained it is very likely that the changes you want to revert are only contained in one commit. But this isn’t always the case.
So what should you do if you only want to revert a part of a big commit in a safe manner? Fortunately git provides tools you need to do this. In the next sections I will show you how to do it.
First imagine the following situation.
As you can see the git repository contains 3 commits with the ids A, B, C. Commit B contains changes to the files file1, file2, file3, file4. Commit C is based on commit B and is already pushed to the remote repository, because origin/master and master refer to the same commit.
Now if you want to revert the changes to file2 and file4 of commit B but not the other changes you can’t simply do a git revert B. But if you can isolate the changes into a single commit you can use git revert. So the question is how to isolate changes into an own commit or split a commit in multiple commits?
Since the commits B and C are already pushed we should not rewrite them, but we don’t need to rewrite them as I will show you now.
- Checkout the commit that contains the changes you want to revert in detached HEAD state.
$ git checkout B
- Reset the index to the previous commit
git reset HEAD~
Now the index’s state is the state of the previous commit, but the working directory still contains the files of commit B. This means that git status
will show you all changes introduced in commit B as unstaged changes. That gives us the opportunity to create a new commit that only contains the changes we want to revert on top of commit C. - Commit the changes you want to revert.
Since the working directory contains the state of commit B and the index is resetted to A a git status will show you the changes that commit C contains but are not committed yet.$ git status HEAD detached from 74b388e Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: file1 modified: someDir/file2 modified: someDir/file3 modified: someDir/someOtherDir/file4 no changes added to commit (use "git add" and/or "git commit -a")
Create a commit that only contains the changes you want to revert on top of commit C.
$ git add someDir/file2 someDir/someOtherDir/file4 $ git commit -m 'Changes to file2 and file4' [detached HEAD 823bd88] Changes to file2 and file4 2 file changed, 5 insertions(+), 2 deletion(-)
- Revert the isolated changes contained in commit B’ on top of C
First make a copy the commit id of B’, we need it soon. In this example the commit B‘s id is 823bd88 as you can see in the commit message above (point 3).
Now switch back to master in forced mode and revert the isolated changes.
git checkout -f master git revert B'
Now we are done and you can safely push the reverted changes contained in commit D. That was easy wasn’t it? Thanks to git and it’s great and flexible design.
I hope this blog will make your developer life a bit easier.
Great article and incredibly helpful, thanks!
Didn’t you mess up the commit names in bullet 2 and 3? Should it not B and A instead of C and B?
Thanks very helpful.
Thank you! This just saved me hours. The key revelation was that you can revert a commit that isn’t an ancestor of the commit you’re applying it to.
This was very helpful! Thanks!
Seems easier to just do the revert, then do
git reset –mixed HEAD~1
now you’ve essentially uncommitted the revert but have the full reversion in your working tree.
Just stage the parts you want, and commit.
Then cleanup by dumping the remaining working tree changes one way or another. Git stash is easy, if coarse, option.
Thanks a lot ! A very logical approach!