Using git reset

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:

git reset --soft 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.

git reset --mixed 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.

git reset --hard 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

OptionHEADIndexWorking directory
--softYes  
--mixedYesYes 
--hardYesYesYes

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.

Tip

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.217.40