The git reset command changes your
repository and working directory to a known state. Specifically,
git reset adjusts the HEAD
ref to
a given commit, and by default, updates the index to match that commit.
If desired, git reset can also modify your working
directory to mirror the revision of your project represented by the
given commit.
You might
construe git reset as “destructive”
because it can overwrite and destroy changes in your working directory.
Indeed, data can be lost. Even if you have a backup of your files, you
might not be able to recover your work. However, the whole point of this
command is to establish and recover known states for the
HEAD
, index, and working directory.
The git reset command has three main options:
commit
The --soft
changes the HEAD
ref to point to
the given commit. The contents of your index and working directory
are left unchanged. This version of the command has the least
effect, changing only the state of a symbolic reference so it
points to a new commit.
commit
--mixed
changes HEAD
to point to the
given commit. Your index contents are also modified to align with
the tree structure named by commit, but your working directory
contents are left unchanged. This version of the command leaves
your index as if you had just staged all the changes represented
by commit, and it tells you what remains modified in your working
directory.
Note that --mixed
is the default mode for
git reset.
commit
This variant changes the HEAD
ref
to point to the given commit. The contents of your index are also
modified to agree with the tree structure named by the named
commit. Furthermore, your working directory contents are changed
to reflect the state of the tree represented by the given
commit.
When changing your working directory, the complete directory
structure is altered to correspond to the given
commit
. Modifications are lost and new
files are removed. Files that are in the given commit but no
longer exist in your working directory are reinstated.
These effects are summarized in Table 10-1.
Table 10-1. git reset option effects
Option | HEAD | Index | Working directory |
---|---|---|---|
--soft | Yes | ||
--mixed | Yes | Yes | |
--hard | Yes | Yes | Yes |
The git reset command also saves the original
HEAD
value in the ref ORIG_HEAD
.
This is useful, for example, if you wish to use that original
HEAD
’s commit log message as the basis for some
follow-up commit.
In terms of the object model, git reset moves
the current branch HEAD
within the commit graph to a
specific commit. If you specify --hard
, your working
directory is transformed as well.
Let’s look at some examples of how git reset operates.
In the following example, git status reveals that file foo.c has been accidentally staged in the index. To avoid committing the file, use git reset HEAD to unstage it:
$git add foo.c
# Oops! Didn't mean to add foo.c! $git ls-files
foo.c main.c $git reset HEAD foo.c
$git ls-files
main.c
In the commit represented by HEAD
, there is no
pathname foo.c (or else git add
foo.c would be superfluous). Here, git
reset on HEAD
for
foo.c might be paraphrased as “With respect
to file foo.c, make my index look like it did in
HEAD
, where it wasn’t present.” Or, in other
words, “Remove foo.c from the
index.”
Another common use for git reset is to simply redo or eliminate the top-most commit on a branch. As an example, let’s set up a branch with two commits on it:
$git init
Initialized empty Git repository in /tmp/reset/.git/ $echo foo >> master_file
$git add master_file
$git commit
Created initial commit e719b1f: Add master_file to master branch. 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 master_file $echo "more foo" >> master_file
$git commit master_file
Created commit 0f61a54: Add more foo. 1 files changed, 1 insertions(+), 0 deletions(-) $git show-branch --more=5
[master] Add more foo. [master^] Add master_file to master branch.
Suppose you now realize that the second commit is wrong and you
want to go back and do it differently. This is a classic application of
git reset --mixed HEAD^. Recall from Identifying Commits that HEAD^
references the commit parent of the current master
HEAD
and represents the state immediately prior to completing
the second, faulty commit.
# --mixed is the default $git reset HEAD^
master_file: locally modified $git show-branch --more=5
[master] Add master_file to master branch. $cat master_file
foo more foo
After git reset HEAD^, Git has left the new state of the master_file and the entire working directory just as it was immediately prior to making the “Add more foo.” commit.
Because the --mixed
option resets the index, you
must restage any changes you want in the new commit. This gives you the
opportunity to reedit master_file, add other files,
or perform other changes before making a new commit.
$echo "even more foo" >> master_file
$git commit master_file
Created commit 04289da: Updated foo. 1 files changed, 2 insertions(+), 0 deletions(-) $git show-branch --more=5
[master] Updated foo. [master^] Add master_file to master branch.
Now only two commits have been made on the
master
branch, not three.
Similarly, if you have no need to change the index (because
everything was staged correctly) but you want to adjust the commit
message, you can use --soft
instead:
$ git reset --soft HEAD^
$ git commit
The git reset --soft HEAD^ command moves you back to the prior place in the commit graph but keeps the index exactly the same. Everything is staged just as it was prior to the git reset command. You just get another shot at the commit message.
But now that you understand that command, don’t use it. Instead, read about git commit --amend below!
Suppose, however, that you want to eliminate the second commit
entirely and don’t care about its content. In this case, use the
--hard
option:
$git reset --hard HEAD^
HEAD is now at e719b1f Add master_file to master branch. $git show-branch --more=5
[master] Add master_file to master branch.
Just as with git reset --mixed HEAD^, the
--hard
option has the effect of pulling the
master
branch back to its immediately prior state. It
also modifies the working directory to mirror the prior,
HEAD^
, state as well. Specifically, the state of the
master_file in your working directory is modified
to again contain just the one, original line:
$ cat master_file
foo
Although these examples all use HEAD
in some
form, you can apply git reset to any commit in the
repository. For example, to eliminate several commits on your current
branch, you could use git reset --hard HEAD~3 or even
git reset --hard master~3.
But be careful. Just because you can name other commits using a branch name, this is not the same as checking the branch out. Throughout the git reset operation, you remain on the same branch. You can alter your working directory to look like the head of a different branch, but you are still on your original branch.
To illustrate the use of git reset with other
branches, let’s add a second branch called dev
and
add a new file to it:
# Should already be on master, but be sure. $git checkout master
Already on "master" $git checkout -b dev
$echo bar >> dev_file
$git add dev_file
$git commit
Created commit 7ecdc78: Add dev_file to dev branch 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 dev_file
Back on the master
branch, there is only one
file:
$git checkout master
Switched to branch "master" $git rev-parse HEAD
e719b1fe81035c0bb5e1daaa6cd81c7350b73976 $git rev-parse master
e719b1fe81035c0bb5e1daaa6cd81c7350b73976 $ls
master_file
By using --soft
, only the HEAD
reference is changed:
# Change HEAD to point to the dev commit $git reset --soft dev
$git rev-parse HEAD
7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f $ls
master_file $git show-branch
! [dev] Add dev_file to dev branch * [master] Add dev_file to dev branch -- +* [dev] Add dev_file to dev branch
It certainly seems as if the master
branch and
the dev
branch are at the same commit. And, to a
limited extent, they are (you’re still on the master
branch, and that’s good), but this operation leaves things in a peculiar
state. To wit, if you made a commit now, what would happen? The
HEAD
points to a commit that has the file
dev_file in it, but that file isn’t in the
master
branch:
$echo "Funny" >> new
$git add new
$git commit -m "Which commit parent?"
Created commit f48bb36: Which commit parent? 2 files changed, 1 insertions(+), 1 deletions(-) delete mode 100644 dev_file create mode 100644 new $git show-branch
! [dev] Add dev_file to dev branch * [master] Which commit parent? -- * [master] Which commit parent? +* [dev] Add dev_file to dev branch
Git correctly added new and has evidently
determined that dev_file isn’t present in this
commit. But why did Git remove this
dev_file? Git is correct that
dev_file isn’t part of this commit, but it’s
misleading to say that it was removed because it was never there in the
first place! So why did Git elect to remove the file? The answer is that
Git uses the commit to which HEAD
points at the time
a new commit is made. Let’s see what that was:
$ git cat-file -p HEAD
tree 948ed823483a0504756c2da81d2e6d8d3cd95059
parent 7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f
author Jon Loeliger <[email protected]> 1229631494 -0600
committer Jon Loeliger <[email protected]> 1229631494 -0600
Which commit parent?
The parent of this commit is 7ecdc7
, which you
can see is the tip of the dev
branch and not
master
. But this commit was made while on the
master
branch. The mix-up shouldn’t come as a
surprise, as master HEAD
was changed to point at the
dev HEAD
!
At this point, you might conclude that the last commit is totally bogus and should be removed entirely. And well you should. It is a confused state that shouldn’t be allowed to remain in the repository.
Just as the earlier example showed, this seems like an excellent opportunity for the git reset --hard HEAD^ command. But now things are in a bit of a pickle.
The obvious approach to get to the “previous version”
of the master HEAD
is simply to use
HEAD^
, like this:
# Make sure we're on the master branch first $git checkout master
# BAD EXAMPLE! # Reset back to master's prior state $git reset --hard HEAD^
So what’s the problem? You just saw that HEAD
’s
parent points to dev
and not to the prior commit on
the original master
branch:
# Yep, HEAD^ points to the dev HEAD. Darn.
$ git rev-parse HEAD^
7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f
There are several ways of determining the commit to which the
master
branch should, in fact, be reset:
$ git log
commit f48bb36016e9709ccdd54488a0aae1487863b937
Author: Jon Loeliger <[email protected]>
Date: Thu Dec 18 14:18:14 2008 -0600
Which commit parent?
commit 7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f
Author: Jon Loeliger <[email protected]>
Date: Thu Dec 18 13:05:08 2008 -0600
Add dev_file to dev branch
commit e719b1fe81035c0bb5e1daaa6cd81c7350b73976
Author: Jon Loeliger <[email protected]>
Date: Thu Dec 18 11:44:45 2008 -0600
Add master_file to master branch.
The last commit (e719b1f
) is the correct
one.
Another method uses the reflog, which is a history of changes to refs within your repository:
$ git reflog f48bb36... HEAD@{0}: commit: Which commit parent? 7ecdc78... HEAD@{1}: dev: updating HEAD e719b1f... HEAD@{2}: checkout: moving from dev to master 7ecdc78... HEAD@{3}: commit: Add dev_file to dev branch e719b1f... HEAD@{4}: checkout: moving from master to dev e719b1f... HEAD@{5}: checkout: moving from master to master e719b1f... HEAD@{6}: HEAD^: updating HEAD 04289da... HEAD@{7}: commit: Updated foo. e719b1f... HEAD@{8}: HEAD^: updating HEAD 72c001c... HEAD@{9}: commit: Add more foo. e719b1f... HEAD@{10}: HEAD^: updating HEAD 0f61a54... HEAD@{11}: commit: Add more foo.
Reading through this list, the third line down records a switch
from the dev
branch to the master
branch. At that time, e719b1f
was the master
HEAD
. So, once again, you could directly use
e719b1f
or you could use the symbolic name
HEAD@{2}
:
$git rev-parse HEAD@{2}
e719b1fe81035c0bb5e1daaa6cd81c7350b73976 $git reset --hard HEAD@{2}
HEAD is now at e719b1f Add master_file to master branch. $git show-branch
! [dev] Add dev_file to dev branch * [master] Add master_file to master branch. -- + [dev] Add dev_file to dev branch +* [master] Add master_file to master branch.
As just shown, the reflog can frequently be used to help locate prior state information for refs, such as branch names.
Similarly, it is wrong to try and change branches using git reset --hard:
$git reset --hard dev
HEAD is now at 7ecdc78 Add dev_file to dev branch $ls
dev_file master_file
Again, this appears to be correct. In this
case, the working directory has even been populated with the correct
files from the dev
branch. But it didn’t really work.
The master
branch remains current:
$ $ git branch dev * master
Just as in the previous example, a commit at this point would cause the graph to be confused. And as before, the proper action is to determine the correct state and reset to that:
$ git reset --hard e719b1f
Or, possibly, even to:
$ git reset --soft e719b1f
Using --soft
, the working directory is not
modified, which means that your working directory now represents the
total content (files and directories) present in the tip of the
dev
branch. Furthermore, since
HEAD
now correctly points to the original tip of the
master
branch as it used to, a commit at
this point would yield a valid graph with the new
master
state exactly the same as the tip of the
dev
branch.
That may or may not be what you want, of course. But you can do it.
3.143.217.40