Recently, while showing someone at work a useful Git ‘trick’, I was asked “how many ways are there to undo a bad change in Git?”. This got me thinking, and I came up with a walkthrough similar to the ones I use in my book to help embed key Git concepts and principles.
There’s many ways to achieve the result you might want, so this can be a pretty instructive and fertile question to answer.
If you want to follow along, run these commands to set up a simple series of changes to a git repository:
cd $(mktemp -d) git init for i in $(seq 1 10) do echo "Change $i" >> file git add file git commit -m "C${i}" done
Now you are in a fresh folder with a git repository that has 10 simple changes to a single file (called ‘file'
) in it. If you run:
git log --oneline --graph --all
at any point in this walkthrough you should be able to see what’s going on.
See here if you want to know more about this git log
command.
1) Manual Reversion
The simplest way (conceptually, at least) to undo a change is to add a new commit that just reverses the change you last made.
First, you need to see exactly what the change was. The ‘git show
‘ command can give you this:
git show HEAD
The output should show you a diff of what changed in the last entry. The HEAD
tag always points to a specific commit. If you haven’t done any funny business on your repo then it points to the last commit on the branch you are working on.
Next, you apply the changes by hand, or you can run this command (which effectively removes the last line of the file) to achieve the same result in this particular context only:
head -9 file > file2 mv file2 file
and then commit the change:
git commit -am 'Revert C10 manually'
2) git revert
Sometimes the manual approach is not easy to achieve, or you want to revert a specific commit (ie not the previous one on your branch). Let’s say we want to reverse the last-but-two commit on our branch (ie the one that added ‘Change 9
‘ to the file).
First we use the git rev-list
command to list the previous changes in reverse order, and capture the commit ID we want to the LASTBUTONE
variable using pipes to head
and tail
:
COMMITID=$(git rev-list HEAD | head -3 | tail -1)
Now check that that change is the one you want:
git show ${COMMITID}
which should output:
commit 77c8261da4646d8850b1ac1df16346fbdcd0b074 Author: ian ian.miell@gmail.com Date: Mon Sep 7 13:38:42 2020 +0100 C9 diff --git a/file b/file index 5fc3c46..0f3aaf4 100644 --- a/file +++ b/file @@ -6,3 +6,4 @@ Change 5 Change 6 Change 7 Change 8 +Change 9
Now, to reverse that change, run:
git revert ${COMMITID}
and follow the instructions to commit the change. The file should now have reverted the entry for Change 9
and the last line should be Change 8
. This operation is easy in this trivial example, but can get complicated if the changes are many and varied.
3) Re-point Your Branch
This method makes an assumption that you can force-push changes to remote branches. This is because it changes the history of the repository to effectively ‘forget’ about the change you just made.
In this walkthrough we don’t have a remote to push to, so it doesn’t apply.
Briefly, we’re going to:
- check out the specific commit we want to return to, and
- point our branch at that commit
The ‘bad commit’ is still there in our local Git repository, but it has no branch associated with it, so it ‘dangles’ off a branch until we do something with it. We’re actually going to maintain a ‘rejected-1
‘ branch for that bad commit, because it’s neater.
Let’s first push a bad change we want to forget about:
echo Bad change > file git commit -am 'Bad change committed'
Now we realise that that change was a bad one. First we make a branch from where we are, so we can more easily get back to the bad commit if we need to:
git branch rejected-1
Now let’s check out the commit before the bad one we just committed:
git checkout HEAD^
Right now you have checked out the commit you would like your master branch to be pointed at. But you likely got the scary detached HEAD
message:
Note: switching to 'HEAD^'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example:
What this means is that you are on the commit you want to be on, but are not on a branch. master
is still pointed at the ‘bad’ commit. You can check this with a quick log command:
$ git log --oneline --graph --all --decorate 8f08673 (rejected-1, master) Bad change committed cbc9576 (HEAD) Revert "C9" ef86963 Revert C10 manually 067bb17 C10 77c8261 C9 6a10d2b C8 1125bde C7 a058fa5 C6 a8392e9 C5 dca0013 C4 46a0b18 C3 3df2db8 C2 84a9d7a C1
Your ‘HEAD
‘ (ie, where your Git repository is right now) is pointed at the commit before the ‘bad’ one. The rejected-1
and master
branches are still pointed at the ‘bad’ commit.
We want the master
branch to point to where we are right now (HEAD
). To do this, use git branch
, but force the branching to override the error we would get because the branch already exists. This is where we start to change the Git repo’s history.
git branch -f master
The log should now show we are on the master branch:
8f08673 (rejected-1) Bad change committed cbc9576 (HEAD, master) Revert "C9" ef86963 Revert C10 manually 067bb17 C10 77c8261 C9 6a10d2b C8 1125bde C7 a058fa5 C6 a8392e9 C5 dca0013 C4 46a0b18 C3 3df2db8 C2 84a9d7a C1
You should be able to see now why we branched off the rejected-1
branch earlier. If we want to get back to the ‘bad’ commit, it’s easy to check out that branch. Also, the branch provides an annotation for what the commit is (ie a mistake).
We’re not finished yet, though! The commit you have checked out is now the same as the commit the master
branch is on, but you still need to tell Git that you want to be on the master
branch:
git checkout master
Now you have effectively un-done your change. The ‘bad’ change is safely on the rejected-1
branch, and you can continue your work as if it never happened.
Remember that if you have a remote, then you will need to force-push this change with a git push -f
. In this walkthrough we don’t have a remote, so we won’t do that.
If you like this, you might like my book Learn Git the Hard Way
4) git reset
There’s a more direct way to revert your local repository to a specific commit. This also changes history, as it re-sets the branch you are one back some steps.
Let’s say we want to go back to ‘Change 8’ (with the commit message ‘C8
‘).
COMMITID=$(git rev-list HEAD | head -5 | tail -1) echo $COMMITID
Check this is the commit you want by looking at the history:
git log --oneline --graph --all
Finally, use the git reset
command to . The --hard
flag tells git that you don’t mind changing the history of the repository by moving the branch tip backwards.
git reset --hard "${COMMITID}"
Now your HEAD
pointer and master
branch are pointed at the change you wanted.
5) git rebase
This time we’re going to use git rebase
to go back to ‘Change 6’. As before, you first get the relevant commit ID. Then you use the git rebase
command with the -i
(interactive) flag to ‘drop’ the relevant commits from your branch.
COMMITID=$(git rev-list HEAD | head -3 | tail -1) git rebase -i "${COMMITID}"
At this point you’re prompted to decide what to do with the previous commits before continuing. Put a ‘d
‘ next to the commits you want to forget about.
If you run the git log
command again:
git log --oneline --graph --all
You’ll see that the commits are still there, but the master
branch has been moved back to the commit you wanted:
8f08673 (rejected-1) Bad change committed cbc9576 Revert "C9" ef86963 Revert C10 manually 067bb17 C10 77c8261 C9 6a10d2b C8 1125bde C7 a058fa5 (HEAD -> master) C6 a8392e9 C5 dca0013 C4 46a0b18 C3 3df2db8 C2 84a9d7a C1
This trick can also get you to return your branch to the initial commit without losing the other commits, which is sometimes useful:
git rebase -i $(git rev-list --max-parents=0 HEAD)
This uses the git rev-list
command and --max-parents
flag to give you the first commit ID in the history. Dropping all the above commits by putting ‘d
‘ next to all the commits takes your branch back to the initial commit.
Other git posts
Five Things I Wish I’d Known About Git
If you like this, you might like one of my books:
Learn Bash the Hard Way
Learn Git the Hard Way
Learn Terraform the Hard Way

2 thoughts on “Five Ways to Undo a Commit in Git”