tags:

views:

530

answers:

3

I have seen interesting posts explaining subtleties about git reset.

Unfortunately, the more I read about it, the more it appears that I don't understand it fully. I come from a SVN background and Git is a whole new paradigm. I got mercurial easily, but Git is much more technical.

I think git reset is close to hg revert, but it seems there are differences.

So what exactly does git reset do? Please include detailed explanations about:

  • the options --hard, --soft and --merge;
  • the strange notation you use with HEAD such as HEAD^ and HEAD~1;
  • concrete use cases and work flows;
  • consequences on the working copy, the HEAD and your global stress level.

Holly blessing and tons of chocolate/beer/name_your_stuff to the guy who makes a no-brainer answer :-)

+36  A: 

I do think the docs really are quite good for this - perhaps you do need a bit of a sense of the way git works for them to really sink in though. In particular, if you take the time to carefully read them, those tables detailing states of files in index and work tree for all the various options and cases are very very helpful. (But yes, they're very dense - takes me a while just to read them and confirm they say what I already know.)

In general, git reset's function is to take the current branch and reset it to point somewhere else, and possibly bring the index and work tree along. More concretely, if your master branch (currently checked out) is like this:

- A - B - C (HEAD, master)

and you realize you want master to point to B, not C, you will use git reset B to move it there:

- A - B (HEAD, master)      # - C is still here, but there's no branch pointing to it anymore

Digression: This is different from a checkout. If you'd run git checkout B, you'd get this:

- A - B (HEAD) - C (master)

You've ended up in a detached HEAD state. HEAD, work tree, index all match B, but the master branch was left behind at C. If you make a new commit D at this point, you'll get this, which is probably not what you want:

- A - B - C (master)
       \
        D (HEAD)

Remember, reset doesn't make commits, it just updates a branch (which is a pointer to a commit) to point to a different commit. The rest is just details of what happens to your index and work tree.

Use cases

I cover many of the main use cases for git reset within my descriptions of the various options in the next section. It can really be used for a wide variety of things; the common thread is that all of them involve resetting the branch, index, and/or work tree to point to/match a given commit.

Things to be careful of

  • --hard can cause you to really lose work. It modifies your work tree.

  • git reset [options] commit can cause you to (sort of) lose commits. In the toy example above, we lost commit C. It's still in the repo, and you can find it by looking at git reflog show HEAD or git reflog show master, but it's not actually accessible from any branch anymore.

The main work tree and index options

There are four main options to control what happens to your work tree and index during the reset.

Remember, the index is git's "staging area" - it's where things go when you say git add in preparation to commit.

  • --hard makes everything match the commit you've reset to. This is the easiest to understand, probably. All of your local changes get clobbered. One primary use is blowing away your work but not switching commits: git reset --hard means git reset --hard HEAD, i.e. don't change the branch but get rid of all local changes. The other is simply moving a branch from one place to another, and keeping index/work tree in sync. This is the one that can really make you lose work, because it modifies your work tree. Be very very sure you want to throw away local work before you run any reset --hard.

  • --mixed is the default. It resets the index, but not the work tree. This means all your files are intact, but any differences between the original commit and the one you reset to will show up as local modifications (or untracked files) with git status. Use this when you realize you made some bad commits, but you want to keep all the work you've done so you can fix it up and recommit. In order to commit, you'll have to add files to the index again (git add ...).

  • --soft doesn't touch the index or work tree. All your files are intact as with --mixed, but all the changes show up as changes to be committed with git status (i.e. checked in in preparation for committing). Use this when you realize you've made some bad commits, but the work's all good - all you need to do is recommit it differently. The index is untouched, so you can commit immediately if you want - the resulting commit will have all the same content as where you were before you reset.

  • --merge was added recently, and is intended to help you abort a failed merge. This is necessary because git merge will actually let you attempt a merge with a dirty work tree (one with local modifications) as long as those modifications are in files unaffected by the merge. git reset --merge resets the index (like --mixed - all changes show up as local modifications), and resets the files affected by the merge, but leaves the others alone. This will hopefully restore everything to how it was before the bad merge. You'll usually use it as git reset --merge (meaning git reset --merge HEAD) because you only want to reset away the merge, not actually move the branch. (HEAD hasn't been updated yet, since the merge failed)

    To be more concrete, suppose you've modified files A and B, and you attempt to merge in a branch which modified files C and D. The merge fails for some reason, and you decide to abort it. You use git reset --merge. It brings C and D back to how they were in HEAD, but leaves your modifications to A and B alone, since they weren't part of the attempted merge.

Strange notation

The "strange notation" you refer to is simply a bunch of ways of specifying commits. The specifying revisions section of the git-rev-parse man page goes into detail. The two you mention:

  • HEAD^ simply means "the commit before HEAD". The ^ means "commit before", and if the commit is a merge commit, it means first parent (i.e. if you merge topic into master, master^ is the previous commit on master)

  • HEAD~1 means exactly the same thing as HEAD^. The general case HEAD~n means "n commits before HEAD" (this also follows first parents of merges).

Jefromi
"you will use git reset to move it there." why don't you use git checkout to do so?
e-satis
@e-satis: git checkout will move HEAD, but leave the branch where it was. This is for when you want to move the branch.
Jefromi
I wish I could vote your post twice.
e-satis
So if I understand well, reset B would do : - A - B - C - B (master) while checkout B would do - A - B (master) ?
e-satis
@e-statis: No. Updated the answer to cover that.
Jefromi
Okayyyyyy. I just got something very important. You said "there is no branch pointing to it" and it bugged me. Now I get it. A branch is not a list a changes, it's just a pointer to somewhere in the history, isn't it? That's why SVN guy don't get it, we don't see it the proper way. Very useful post, hope you'll got plenty of rep from it.
e-satis
Oh, and there is only one HEAD, which has nothing to do with branches. Your HEAD is just the last commit, what ever branch in is into. You don't have one HEAD per branch. Or am I mistaken?
e-satis
@e-satis: Yes. A branch is merely a pointer to a specific commit - look at a file in `.git/refs/heads` and you'll see its contents is just an SHA1.
Jefromi
@e-satis: Yes, there is only one HEAD. It can be either a symbolic reference to the current branch (this is the normal state) or a direct reference to a given commit (by SHA1, like branches do - this is detached HEAD). More verbose explanation of this at your other question: http://stackoverflow.com/questions/2529971/what-is-the-head-in-git
Jefromi
I think `HEAD~3` is not necessarily always the same as `HEAD^^^`, particularly when HEAD has more than one parent.
hasen j
@hasen j: It is. An excerpt from the documentation I linked: "A suffix ~<n> to a revision parameter means the commit object that is the <n>th generation grand-parent of the named commit object, following only the first parent. I.e. rev~3 is equivalent to rev^^^ which is equivalent to rev^1^1^1."
Jefromi
+9  A: 

Remember that in git you have:

  • the HEAD pointer, which tells you what commit you're working on
  • the working tree, which represents the state of the files on your system
  • the staging area (also called the index), which "stages" changes so that they can later be committed together

Please include detailed explanations about:

--hard, --soft and --merge;

In increasing order of dangerous-ness:

  • --soft moves HEAD but doesn't touch the staging area or the working tree.
  • --mixed moves HEAD and updates the staging area, but not the working tree.
  • --merge moves HEAD, resets the staging area, and tries to move all the changes in your working tree into the new working tree.
  • --hard moves HEAD and adjusts your staging area and working tree to the new HEAD, throwing away everything.

concrete use cases and workflows;

  • Use --soft when you want to move to another commit and patch things up without "losing your place". It's pretty rare that you need this.

--

# git reset --soft example
touch foo                            // Add a file, make some changes.
git add foo                          // 
git commit -m "bad commit message"   // Commit... D'oh, that was a mistake!
git reset --soft HEAD^               // Go back one commit and fix things.
git commit -m "good commit"          // There, now it's right.

--

  • Use --mixed (which is the default) when you want to see what things look like at another commit, but you don't want to lose any changes you already have.

  • Use --merge when you want to move to a new spot but incorporate the changes you already have into that the working tree.

  • Use --hard to wipe everything out and start a fresh slate at the new commit.

John Feminella
That's not the intended use case for `reset --merge`. It doesn't perform a three-way merge. It's really only for resetting out of conflicted merges, as described in the docs. You'll want to use `checkout --merge` to do what you're talking about. If you want to move the branch as well, I think the only way is to follow up with some checkout/reset to drag it along.
Jefromi
@Jefromi » Yes, I didn't phrase that very well. By "a new spot" I meant "a fresh place where you don't have the conflicted merge".
John Feminella
@John: Ah, I see. I think the important thing here is that unless you really know what you're doing, you probably don't ever want to use `reset --merge` with any target besides (the default) `HEAD`, because in cases besides aborting a conflicted merge, it's going to throw away information that you could otherwise save.
Jefromi
+8  A: 

I think A Visual Git Reference gives good insight in what happens when using common git commands.

unbeknown
Thanks pale, that's going to help.
e-satis
+1 oooh pretty pretty pictures. I love resources like this.
Jefromi