Search code examples
gitgit-reset

git reset --hard doesn't make the working tree look like index?


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?


Solution

  • 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.