Andy Delcambre
Software Engineer at Engine Yard. Ruby programmer. Photographer.

git reset In depth12 Mar 2008

This is the second part of a series of articles on Git. This post is primarily going to focus on the git reset command and how to track multiple remote subversion branches.

First, a bit of background on git reset. In my opinion it is one of the cooler git commands that I use regularly, it’s function is a bit odd, there is no equivalent in subversion to compare to. What git reset does at a very high level is move a tag in the git graph that makes up the revision history.

So lets take the following sequence of commands as an example.

git init
touch README
git add README
git commit -a -m "initial import"
git checkout -b new_branch
vi README
git commit -a -m "modified README"
git checkout master
vi README
git commit -a -m "updated README"
touch blah
git add blah
git commit -a -m "added blah"

So we create a git repository, make a commit, branch from the commit. Make a change in the branch, and make two changes back in master. The git directed graph now looks like this:

You can see that there are two branches. Right now we are in the master branch so the current HEAD tag points to the 31281c1 commit. (This is important, git reset moves the current HEAD.)

adelcambre@hiro:/tmp/blah% cat .git/refs/heads/master
31281c194e505bf000f1d67c07b76255ac9370e9
adelcambre@hiro:/tmp/blah% cat .git/refs/heads/new_branch
0b6f5c586c257820f2ce94981f71a860107184ed

So now, lets say we want to make master follow the new_branch branch. (This is a bit contrived, bear with me.) So you use git reset --hard new_branch

adelcambre@hiro:/tmp/blah% git reset --hard new_branch
HEAD is now at 0b6f5c5... modified README

So the current HEAD in the master branch is pointing at the same commit as the new_branch branch. Let’s make a commit in each branch and see what happens.

vi README
git commit -a -m "changed README"
git checkout new_branch
touch new_file
git add new_file
git commit -a -m "added new file"

Which gets us this graph:

So, you basically just moved the master branch to be a branch of the new_branch branch. But what happened to those commits against the old master. Well, they aren’t reachable, so would get garbage collected if you did a repack (more on that in a later edition). But for now, they are still there, just not reachable from a tag. We happen to know the commit-id of the old master branch, but if you didn’t, you could use git lost-found.

adelcambre@hiro:/tmp/blah% git lost-found
[31281c194e505bf000f1d67c07b76255ac9370e9] added blah

So it found the old master branch! Let’s merge our current master back into the new_branch, and move master back to the old master.

git checkout new_branch
git merge master
git checkout master
git reset --hard 31281c1

Which results in the graph looking like:

So you can see that we recovered the unreachable commit, and merged back the changes we made on the master branch while it followed the new_branch.

Now, there are no unreachable commits, git lost-found doesn’t return anything, and we are good to go.

git reset options

There are three main options to use with git reset: --hard, --soft and --mixed. These affect what get’s reset in addition to the HEAD pointer when you reset.

First, --hard resets everything. Your current directory would be exactly as it would if you had been following that branch all along. The working directory and the index are changed to that commit. This is the version that I use most often. This is what we used in the above examples. It just says make the current HEAD and working directory exactly like commit “x”.

Next, the complete opposite, —soft, does not reset the working tree nor the index. It only moves the HEAD pointer. This leaves your current state with any changes different than the commit you are switching to in place in your directory, and “staged” for committing. I use this for only every once in a while, and mostly for correcting a commit message. If you make a commit locally but haven’t pushed the commit to the git server or subversion server, you can reset to the previous commit, and recommit with a good commit message. This would look something like:

touch test
git add test
git commit -m "bad commit"
git reset --soft HEAD^
git commit -m "good commit"

So, because git reset --soft doesn’t reset the index nor the working tree, you can just re-commit without having to add anything.

Finally, --mixed resets the index, but not the working tree. So the changes are all still there, but are “unstaged” and would need to be git add’ed or git commit -a. I use this sometimes if I committed more than I meant to with git commit -a, I can back out the commit with git reset --mixed, add the things that I want to commit and just commit those.

The place that I really use git reset a fair amount is with remote branches. If I have a branch that I want to track a specific remote subversion branch, I can simply git reset --hard svn_branch_name and then git svn does the right thing. I have seen issues where for some reason the git master branch ended up following a subversion tag rather than trunk. A quick git reset --hard trunk cleaned everything up.

I really started liking this command once I realized what was happening. You really need to be aware of the nature of git’s directed graph to take full advantage of git reset, but once you do, you are really able to exploit git quite a lot further.

This is part of a series on git, other articles in the series are:

Do you have any git reset success stories? Horror stories? Lost work? Saved work? Let me know in the comments.

Update: Changed the terminology to only refer to the current branch as “HEAD”, per comment from Bob.