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:
--hard
, --soft
and --merge
;HEAD
such as HEAD^
and HEAD~1
;HEAD
and your global stress level.This contains the changes you'll commit next to the repository. If you've staged a version of a file and later decide you don't want to include it in the next commit, you'll want to unstage it first. You can use the git reset command to unstage a staged file: git reset HEAD path/to/file.
Running git reset --hard ORIG_HEAD will let you go back to where you were, but it will discard your local changes, which you do not want.
Running git reset will typically delete files and commits. And without those, you don't have a project! This is why it's critical to plan ahead when using it, so you don't end up deleting elements that are critical for your project.
reset --soft : History changed, HEAD changed, Working directory is not changed. reset --mixed : History changed, HEAD changed, Working directory changed with unstaged data. reset --hard : History changed, HEAD changed, Working directory is changed with lost data. It is always safe to go with Git --soft.
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.
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.
--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>
).
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.
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.
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.)
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).^
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
.Remember that in git
you have:
HEAD
pointer, which tells you what commit you're working onPlease 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;
--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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With