704

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.
Mus
  • 5,994
  • 19
  • 75
  • 112
e-satis
  • 515,820
  • 103
  • 283
  • 322
  • 18
    I think [A Visual Git Reference](http://marklodato.github.com/visual-git-guide/) gives good insight in what happens when using common git commands. –  Mar 27 '10 at 17:23
  • 3
    Working link to [A Visual Git Reference](https://marklodato.github.io/visual-git-guide/index-en.html) as of 24/Apr/2021. – Eric Mutta Apr 23 '21 at 17:30

7 Answers7

1032

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.

  • Git permanently deletes such commits after 30 days, but until then you can recover C by pointing a branch at it again (git checkout C; git branch <new branch name>).

Arguments

Paraphrasing the man page, most common usage is of the form git reset [<commit>] [paths...], which will reset the given paths to their state from the given commit. If the paths aren't provided, the entire tree is reset, and if the commit isn't provided, it's taken to be HEAD (the current commit). This is a common pattern across git commands (e.g. checkout, diff, log, though the exact semantics vary), so it shouldn't be too surprising.

For example, git reset other-branch path/to/foo resets everything in path/to/foo to its state in other-branch, git reset -- . resets the current directory to its state in HEAD, and a simple git reset resets everything to its state in HEAD.

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, i.e. git reset means git reset --mixed. 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.

Want to know more?

I do think man git reset is really 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 - they're conveying an awful lot of the above information in a very concise form.)

Strange notation

The "strange notation" (HEAD^ and HEAD~1) you mention is simply a shorthand for specifying commits, without having to use a hash name like 3ebe3f6. It's fully documented in the "specifying revisions" section of the man page for git-rev-parse, with lots of examples and related syntax. The caret and the tilde actually mean different things:

  • HEAD~ is short for HEAD~1 and means the commit's first parent. HEAD~2 means the commit's first parent's first parent. Think of HEAD~n as "n commits before HEAD" or "the nth generation ancestor of HEAD".
  • HEAD^ (or HEAD^1) also means the commit's first parent. HEAD^2 means the commit's second parent. Remember, a normal merge commit has two parents - the first parent is the merged-into commit, and the second parent is the commit that was merged. In general, merges can actually have arbitrarily many parents (octopus merges).
  • The ^ and ~ operators can be strung together, as in HEAD~3^2, the second parent of the third-generation ancestor of HEAD, HEAD^^2, the second parent of the first parent of HEAD, or even HEAD^^^, which is equivalent to HEAD~3.

caret and tilde

Cascabel
  • 422,485
  • 65
  • 357
  • 307
  • "you will use git reset to move it there." why don't you use git checkout to do so? – e-satis Mar 27 '10 at 17:04
  • 5
    @e-satis: git checkout will move HEAD, but leave the branch where it was. This is for when you want to move the branch. – Cascabel Mar 27 '10 at 17:06
  • So if I understand well, reset B would do : - A - B - C - B (master) while checkout B would do - A - B (master) ? – e-satis Mar 27 '10 at 17:11
  • @e-statis: No. Updated the answer to cover that. – Cascabel Mar 27 '10 at 17:16
  • 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 Mar 27 '10 at 17:21
  • 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 Mar 27 '10 at 17:23
  • @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. – Cascabel Mar 27 '10 at 17:29
  • @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 – Cascabel Mar 27 '10 at 17:30
  • I think `HEAD~3` is not necessarily always the same as `HEAD^^^`, particularly when HEAD has more than one parent. – hasen Mar 27 '10 at 18:08
  • @hasen j: It is. An excerpt from the documentation I linked: "A suffix ~ to a revision parameter means the commit object that is the 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." – Cascabel Mar 27 '10 at 18:33
  • 37
    the docs are good even though it takes forever to read them and they're very dense and it takes forever to verify that they say it works like you already know how it works. Doesn't sound like the docs are good to me... – Kirby Aug 23 '12 at 21:25
  • @Kirby It takes me even longer to read something like this. The docs are complete, correct, and concise, which means they're dense. It's nontrivial information; you can never convey it in a short amount of time without summarizing and approximating. – Cascabel Aug 24 '12 at 00:10
  • @RosePerrone I appreciate what you're going for with your edit, and the diagram is nice. Careful, though; you rewrote a lot of text, made some things more clear and some things less (in my opinion, anyway), and removed the link to the canonical documentation. I've edited back, but kept a lot of what you added. – Cascabel Jan 29 '13 at 03:30
  • 4
    A much simple and understandable explanation is given by this SO answer: stackoverflow.com/questions/3528245/whats-the-difference-between-git-reset-mixed-soft-and-hard – Nitin Bansal Feb 01 '13 at 15:17
  • Please also add information about these commands: `git reset` and `git reset -- .`. – Flimm Sep 20 '13 at 16:13
  • @Flimm Most of that was already in there, for example that the default behavior is --mixed, and when I originally answered this is it was fairly clear the OP didn't have trouble with the notion of commit/path arguments, but I've added an extra section about the arguments. – Cascabel Sep 20 '13 at 16:24
  • @jefromi: +1ed. One of the best article I have read on git reset. I don't how you manage to write this kind of detailed article within 4 mins from the Question posted :)You must be super fast :D – Ravi Feb 14 '14 at 13:03
  • Nice chart of HEAD using ~ and ^ – RayLuo Feb 11 '15 at 02:28
  • this is a very gr8 answer so much so that I was compelled to write this from someone who very rarely comments and who only understands half of the git puzzle. this alone helped me immensely understanding git. This also make me think that other commentors who did not show appreciation for this answer either are very lazy (I admit I am one but the answer is so gud that I had to write this) or really just do not understand git so much as to appreciate how gud this answer is. thanks for taking time to write this valuable answer. – dkjain Sep 08 '17 at 08:27
82

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
  • 281,997
  • 42
  • 326
  • 347
  • 2
    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. – Cascabel Mar 27 '10 at 17:14
  • @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 Mar 27 '10 at 17:16
  • 1
    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. – Cascabel Mar 27 '10 at 17:19
  • 3
    I found this answer the simplest and most helpful – Jazzepi Aug 07 '13 at 20:18
  • Please add information about these commands: `git reset` and `git reset -- .`. – Flimm Sep 20 '13 at 16:13
  • @John I did a git reset mixed to revert a commit but I really should have done git reset--soft. Now I cannot see the files in git status. Help? – user2441441 Aug 11 '16 at 23:11
35

The post Reset Demystified in the blog Pro Git gives a very no-brainer explanation on git reset and git checkout.

After all the helpful discussion at the top of that post, the author reduces the rules to the following simple three steps:

That is basically it. The reset command overwrites these three trees in a specific order, stopping when you tell it to.

  1. Move whatever branch HEAD points to (stop if --soft)
  2. THEN, make the Index look like that (stop here unless --hard)
  3. THEN, make the Working Directory look like that

There are also --merge and --keep options, but I would rather keep things simpler for now - that will be for another article.

suspectus
  • 14,884
  • 8
  • 41
  • 53
Daniel Hershcovich
  • 3,311
  • 2
  • 26
  • 35
27

When you commit something to git you first have to stage (add to the index) your changes. This means you have to git add all the files you want to have included in this commit before git considers them part of the commit. Let's first have a look over the image of a git repo: enter image description here

so, its simple now. We have to work in working directory, creating files, directories and all. These changes are untracked changes. To make them tracked, we need to add them to git index by using git add command. Once they are added to git index. We can now commit these changes, if we want to push it to git repository.

But suddenly we came to know while commiting that we have one extra file which we added in index is not required to push in git repository. It means we don't want that file in index. Now the question is how to remove that file from git index, Since we used git add to put them in the index it would be logical to use git rm? Wrong! git rm will simply delete the file and add the deletion to the index. So what to do now:

Use:-

git reset

It Clears your index, leaves your working directory untouched. (simply unstaging everything).

It can be used with number of options with it. 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. git reset --hard is something like svn revert .

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. If you make a commit locally but haven’t pushed the commit to the git server, you can reset to the previous commit, and recommit with a good commit message.

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. we use this sometimes if we committed more than we meant to with git commit -a, we can back out the commit with git reset --mixed, add the things that we want to commit and just commit those.

Difference between git revert and git reset :-


In simple words, git reset is a command to "fix-uncommited mistakes" and git revert is a command to "fix-commited mistake".

It means if we have made some error in some change and commited and pushed the same to git repo, then git revert is the solution. And if in case we have identified the same error before pushing/commiting, we can use git reset to fix the issue.

I hope it will help you to get rid of your confusion.

love
  • 2,823
  • 2
  • 14
  • 32
  • 2
    This is a nice plain english answer as asked by OP. – Episodex Feb 08 '17 at 13:09
  • 1
    Even though I might missed that in your answer. What is `git reset HEAD` by default? `--hard`,`--soft` or `--mixed` ? Great answer btw. – giannis christofakis Jan 26 '18 at 12:12
  • 1
    Great answer, but I would make it clearer that `git reset --hard` will cause you to lose some data. And there's a point which could be wrong (though I'm not 100% sure... Still learning!): talking about `--mixed` you say that "we use this sometimes if we committed more than we meant to with git commit -a". Did you mean: "if we *staged* more than we meant to with `git stage .`"? If you really committed it, I think it's too late (as you say at the end, git reset is a command to "fix-uncommited mistakes") – Fabio says Reinstate Monica Apr 26 '18 at 16:07
6

TL;DR

git reset resets Staging to the last commit. Use --hard to also reset files in your Working directory to the last commit.

LONGER VERSION

But that's obviously simplistic hence the many rather verbose answers. It made more sense for me to read up on git reset in the context of undoing changes. E.g. see this:

If git revert is a “safe” way to undo changes, you can think of git reset as the dangerous method. When you undo with git reset(and the commits are no longer referenced by any ref or the reflog), there is no way to retrieve the original copy—it is a permanent undo. Care must be taken when using this tool, as it’s one of the only Git commands that has the potential to lose your work.

From https://www.atlassian.com/git/tutorials/undoing-changes/git-reset

and this

On the commit-level, resetting is a way to move the tip of a branch to a different commit. This can be used to remove commits from the current branch.

From https://www.atlassian.com/git/tutorials/resetting-checking-out-and-reverting/commit-level-operations

Snowcrash
  • 66,400
  • 60
  • 203
  • 323
2

Please be aware, this is a simplified explanation intended as a first step in seeking to understand this complex functionality.

May be helpful for visual learners who want to visualise what their project state looks like after each of these commands:


For those who use Terminal with colour turned on (git config --global color.ui auto):

git reset --soft A and you will see B and C's stuff in green (staged and ready to commit)

git reset --mixed A (or git reset A) and you will see B and C's stuff in red (unstaged and ready to be staged (green) and then committed)

git reset --hard A and you will no longer see B and C's changes anywhere (will be as if they never existed)


Or for those who use a GUI program like 'Tower' or 'SourceTree'

git reset --soft A and you will see B and C's stuff in the 'staged files' area ready to commit

git reset --mixed A (or git reset A) and you will see B and C's stuff in the 'unstaged files' area ready to be moved to staged and then committed

git reset --hard A and you will no longer see B and C's changes anywhere (will be as if they never existed)

timhc22
  • 6,059
  • 5
  • 39
  • 56
1

Checkout points the head at a specific commit.

Reset points a branch at a specific commit. (A branch is a pointer to a commit.)

Incidentally, if your head doesn’t point to a commit that’s also pointed to by a branch then you have a detached head. (turned out to be wrong. See comments...)

Neuron
  • 3,776
  • 3
  • 24
  • 44
Ian Warburton
  • 13,336
  • 19
  • 85
  • 164
  • 1
    Not to nitpick, but (yes, in fact it *is* nitpicking but let's add it for completion) your 3rd sentence is technically false. Let's say your HEAD is pointing at branch B which is in turn pointing to commit abc123. If you now checkout commit abc123, your HEAD and branch B are both pointing to commit abc123 AND your HEAD is detached. Committing at this point will *not* update branch B's position. You could have said "if your head doesn’t point to a branch then you have a detached head" – RomainValeri Nov 07 '18 at 15:38
  • @RomainValeri What will committing do in that circumstance? – Ian Warburton Nov 07 '18 at 16:30
  • 1
    Committing would create commits which aren't referenced by any branch, and branch B would keep on pointing to the same commit abc123 even after you committed multiple times after that. It implies that these commits would become candidates for garbage collection when HEAD stops pointing at the last commit in this 'wild' series of commits. – RomainValeri Nov 08 '18 at 07:39