Search code examples
gitgit-resetworking-directorygit-stage

git reset resulting in changes to both working directory and stage


I have a commit with many files in it.

One of the files in the commit has several changes, one of which I would like to undo.

So, I was thinking I could reset that particular file back to HEAD~ leaving the outstanding changes I had made in the working-directory. Then, I could use git add -p and only stage the desired changes, followed by git commit --amend.

First, I made sure that I had no outstanding changes in either my working-directory or stage.

Then, I ran this command:

git reset HEAD~ -- path/to/file

But, to my surprise, git status now shows changes to both my working-directory and stage. Further inspection using gitk shows that the changes in my working-directory and stage are identical.

Where am I going wrong here?


Solution

  • This kind of reset:

    • does not affect HEAD itself or the current branch (so it's not the same as any of git reset --hard, git reset --mixed, or git reset --soft);
    • does affect the contents of the index;
    • does not affect the contents of the work-tree.

    Hence this:

    So, I was thinking I could reset that particular file back to HEAD~ leaving the outstanding changes I had made in the working-directory. Then, I could use git add -p and only stage the desired changes, followed by git commit --amend.

    ... is a fine plan! But as you saw, the output of git status becomes a bit surprising:

    git status now shows changes to both my working-directory and stage.

    The reason is that what git status does includes running two git diffs. If you could name the index and the work-tree as arguments to git diff (you can't), that might be:

    • git diff --name-status HEAD <index>: this is "changes to be committed".
    • git diff --name-status <index> <work-tree>: this is "changes not staged for commit".

    Since the index version of path/to/file now matches the HEAD~1 version of path/to/file, which is different from the HEAD version, the first git diff shows that "staged changes" exist.

    Likewise, the work-tree version does not match the index version, so git status says that there unstaged changes also exist.

    You can in fact now run git add -p. Git will extract the index version to a temporary file, diff the index version against the work-tree version, and let you add changes to the index version (from the work-tree version).

    There was a different route you could have gone

    If you wish to be able to see what you are doing, though, it might have been better to, at the start, do a git reset --soft HEAD~1. This kind of reset:

    • does affect the current branch, using HEAD to find out what that branch is and adjust it;
    • does not affect the contents of the index; and
    • does not affect the contents of the work-tree.

    You would now be in a similar position as before, but now you would need to:

    git reset HEAD path/to/file
    

    to make the index version match the HEAD version, now that HEAD names the desired commit. The commit you made that you don't want—that you intended to --amend—is now "one past the end" of the branch tip:

                 X  [bad commit, now shoved out of the way]
                /
    ...--o--o--@   <-- branch
    

    Now git status makes sense, because it's comparing @ to index instead of X to index. (As before, git status also compares index to work-tree.) Now you can git add -p as you intended, and then git commit without the --amend part.

    You do not have to do it this way; you can just continue with your current plan. If you like the git status result here better, though, you can convert your current plan to this other plan right now, by doing the git reset --soft HEAD~1. This will change the current branch name so that it stops pointing to X and starts pointing to @, and change nothing else at all.