1

Pro Git explains git reset like this:

Recap

The reset command overwrites these three trees in a specific order, stopping when you tell it to:

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

The way I understood is that, if I do git reset --hard, then both my index and my working directory would become EXACTLY like my HEAD. So I went ahead and did this:

# make a git repo
mkdir mygitrepo
cd mygitrepo
git init

# init commit
touch old_file
git commit -a

# stage a file
touch staged
git add staged

# create file that is not staged
touch unstaged

So far my repo looks like this:

  • HEAD old_file
  • index old_file + staged
  • working dir old_file + staged + unstaged

Now if I run git reset --hard then I expect my repo to become:

  • HEAD old_file
  • index old_file
  • working dir old_file

But I would get this instead:

  • HEAD old_file
  • index old_file
  • working dir old_file + unstaged

I did similar test by explicitly passing target argument, like git reset --hard target, and I got similar result: staged files are all gone, but unstaged files are still present after git reset --hard.

Could some one explain if I misunderstood anything about git reset?

Jay Somedon
  • 1,068
  • 11
  • 27

2 Answers2

2

There is one important rule that Git will always follow (unless explicitly told not to do it): Do not touch untracked file.

Using the default functionality, Git will never perform any action that will end up in a data loss of untracked files. That is because information from untracked files can never be recovered by Git: It does not “know” about them, so it will not store their content anywhere, making any operation on them very dangerous. So it will simply not touch them.

This is also the reason why Git will not let you switch to a branch with an untracked file if that branch contains a tracked file with the same path. Because doing so would overwrite your untracked file, replacing it with the file from that branch, but you could never recover that untracked file. So Git will simply not switch the branch, asking you to take care of the untracked file first (which could mean renaming it, adding it to Git, or even deleting it).

So when you do git reset, you will only affect the index. And git reset --hard will affect the index and the working directory. And for Git, the working directory only includes tracked files. That’s why git reset --hard will neither remove untracked files nor files that are in your .gitignore. Git does not “know” about them, so it will not touch them.

If you want to remove untracked files, there is one command which is responsible for that: git clean. The command comes with different combinations of arguments which all perform various different jobs.

The default case, git clean -f, will delete untracked files but keep ignored files intact. If you also want to delete ignored files, you can use git clean -x -f. Note that you will usually always have to specify -f (or --force) for git clean to do anything. This is just an additional security mechanism to make sure that you know what you do. You can use -n instead to perform a dry run and only show which files Git would delete. That makes it easy to avoid problems.

poke
  • 369,085
  • 72
  • 557
  • 602
1

As mentioned in "Undoing Changes"

The git clean command is often executed in conjunction with git reset --hard.
Remember that resetting only affects tracked files, so a separate command is required for cleaning up untracked ones.

Combined, these two commands let you return the working directory to the exact state of a particular commit.

The --hard option is documented as:

Resets the index and working tree.
Any changes to tracked files in the working tree since <commit> are discarded.


This was already discussed back in 2008.
Simply put by Linux Torvalds:

If you're used to doing "git checkout -f" or "git reset --hard", both of those checks(*) are just ignored. After all, you asked for a forced switch.

(* checks = dirty file or untracked files)

And at least in the second case, what I think happens is that git won't remove the file it doesn't know about, so you'll have a "turd" left around.


Added files though... even if there were never committed (being brand new), are still deleted by a git reset --hard, as seen also in 2008:

I actually accidentally deleted hundred of newly added files yesterday doing just this.

My question is why "git reset --hard" can't make a special case for newly added tracked files?
After all, "git status" knows that they're "new files", and "git reset --hard" could realize that wiping them off the face of the earth isn't the most helpful thing possible.

As a suggestion, git read-tree -m HEAD or git rm --cached <file list> before a git reset --hard would help keeping this new files around (removing them from the index)

Junio C. Hamano stil argues:

you would want "reset --hard" to remove that path when a path does not exist in the HEAD but in index in other cases.
And it is my experience that far more often than not it is what is desirable.

(like getting rid of crufts from a conflicted merge)

Note that in this instance, git fsck can still help recover those deleted new files.

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250