Coming from Subversion, I’ve definitely got some pre-conceived notions about how
reverting changes should work. For those that don’t know,
svn revert is
used to discard local changes in your working tree. It’s a command that I use
often. Either because I find my change heading down the wrong path, or I simply
was marking up the code while studying it’s functionality and I want to discard
When I first started using Git, reverting local changes was a big challenge.
git revert is meant to revert a changeset, and not local changes, which
did not match my mental model. I’ve been using Git everyday for a while now,
and I’m still not happy with the situation. There are just a few too many edges
in there, and it requires just a little too much thinking to pick out the right
tool and take care of the job.
The first time I saw this, I was confused. I kept asking myself “what does
checking out have to do with discarding changes?” When you think about what
git checkout does, it starts making a little more sense. From the
git checkout manpage, we have:
Updates files in the working tree to match the version in the index or the specified tree. If no paths are given, git checkout will also update HEAD to set the specified branch as the current branch.
The key here is “update files in the working tree to match the version in the
index.” Let me demonstrate. Let’s say you had a file called
we echoed some data to the end of it:
git status now shows us this:
To revert the change that we just made, we’d run:
Checking the status, we see:
So the change has been discarded. That’s because the version of the file in the index matches HEAD. We know that because we don’t have any staged changes. So the rule here is:
git checkout –
will discard unstaged changes in \<path\>.
Keep in mind, you can discard changes for your entire working copy with:
The former method has the nice effect of only discarding changes below your current working directory, if you’re further down in the tree… something that I did with Subversion quite often. Unlike Subversion, the command is recursive by default. Despite the risk of blowing away a tree’s worth of changes, I prefer git’s choice of being recursive by default.
Okay, so now we know how to discard unstaged changes, but what about staged
changes? That’s where
git reset comes into play. Unfortunately, it’s more
complicated than I care for. The man page for
git reset has this to say:
git reset [-q] [
] [--] ... git reset --patch [ ] [--] [ ...] git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [ ]
In the first and second form, copy entries from
to the index. In the third form, set the current branch head (HEAD) to , optionally modifying index and working tree to match. The defaults to HEAD in all forms.
The take away here is that
git reset changes the index to match a commit.
And depending on the form, it may update your working tree as well.
Let’s go back to the previous example, except this time I’m going to add the change to the index:
So you have a few choices at this point. Let’s walk through them.
Blow it all away
If you’re looking to discard all the changes, and get a clean working tree, simply run:
This would be the “third form” mentioned above1. This resets foo.txt in the index to match HEAD, and also updates the working tree to match. At this point, we have a nice clean working tree:
It’s a bit hard-core though. Chances are, if you have staged changes, you actually want to keep some of them. That’s when things get more interesting.
Selectively reverting staged changes
Let’s tweak our example just a little bit. Let’s say we have changes to
bar.txt staged, and we only want to revert the changes
foo.txt. Here’s the status of our working tree:
We don’t want to run
git reset --hard HEAD at this point, as it would
discard our changes to
bar.txt. So, we’re left with a two step process.
First, we unstage the change with a different form of
git reset, and then
we discard the unstaged change with
We can see that our changes too
foo.txt have been discarded. It just seems
like a little too much work to get there though.
But wait! That’s not all!
I ran across this situation a few times now. I’m getting ready to make an
initial commit, and I’ve added a file that I didn’t intend. My gut reaction is
git reset to unstage the change. However, I’m met promptly with an
The issue is that on a brand new repository, there is no HEAD. So the symbolic ref fails to resolve, and the command fails to execute. It feels like Git should set up HEAD to point to some default empty tree commit, but it doesn’t. So what do you do in this case? You remove it from the index:
So there you have it. Unfortunately, it takes three commands in Git to replicate what I could do with one command in Subversion. Am I ready to change back? No, the benefits of Git are just too great. Do I feel that there is still more room for Git to improve? You betcha. The nice part is that the community is genuinely concerned about making Git more friendly, and smoothing out the burrs. I hope this is one area that can be improved upon. Until things change, hopefully the above can serve as a reference for the various ways of reverting changes.
Anyone else find it amusing to read “hard head” in that command line? ↩