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 the case sometimes.
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 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.
Checkout the commit that contains the changesShell1$ git checkout B
- Reset the index to the previous commit
Rebase interactiveShell1git reset HEAD~
Now the index’s state is the state of the previous commit, but the working directory still contains the files of the last commit (master). This means that git status
will show you all changes introduced in commit C 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 C and the index is resetted to B a git status will show you the changes that commit C contains but are not committed yet.
git statusShell123456789101112$ git statusHEAD detached from 74b388eChanges 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: file1modified: someDir/file2modified: someDir/file3modified: someDir/someOtherDir/file4no 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.
Java12345$ git add someDir/file2 someDir/someOtherDir/file4$ git commit -m 'Changes to file2 and file4'[detached HEAD 823bd88] Changes to file2 and file42 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.
Revert the isolated changesShell123git checkout -f mastergit 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.