The first questions that keep coming up during the first months using Git probably look like these: How do I undo git add? How do I undo git commit? How do I undo xxx?

These days I realized that there is really no “undoing” of most commands in Git. Essentially, you are always moving forward.

Take git add for example. The popular ways to undo git add are:

  • git reset HEAD <filename> (and its variations, git reset, git reset . etc.) Since from 1.8.2 Git decides to throw in an empty index for HEAD instead of just erroring, this command is applicable even with a repository with no previous commit. You may need to run git gc --prune=now to remove all loose objects but it is fine to leave them there (they would not interfere with your work and the memories would eventually be reclaimed).
  • git rm --cached <filename> Adding --cached will let your git rm command remove paths only from the index while leaving the working tree files intact, whether modified previously or not.

However, both are not perfect.

git rm --cached <filename>, as the documentation goes, simply removes a file path from the index. This is not ideal. For example, in the case where your file has a few correct git add or/and git commit before you modified the file and made an erroneous git add (which you want to undo), the git rm --cached command will remove and unstage this file entirely and discarding all previous commits. This is probably not what you want.

In other words, git add have two use cases: track a new file, or update content of a previously tracked file. git rm --cached <filename> works as a undo for the first case but not the second.

git reset HEAD <filename> is generally more preferable as it seems to work for both cases. However, it is still not an undo, since all it does it simply resetting the repository to the HEAD commit. If your file has a few correct git add (for updating content) before a erroneous git add, those previous git add will not survive the git reset either.

In a way, all consecutive git adds are “combined”. You could only commit them all or reset them all.

Example:

mkdir test
cd test
git init
vim dummy # type in 0 and exit
git add dummy
git status # dummy has staged but uncommitted changes: 0
git commit -m "0"
git status # working tree clean
vim dummy # type in 1 and exit
git status # dummy has unstaged changes: 1
git add dummy
git status # dummy has staged but uncommitted changes: 1
vim dummy # type in 2 and exit
git status # dummy has unstaged changes: 2; staged but uncommitted changes: 1
git add dummy
git status # dummy has staged but uncommitted changes: 12
vim dummy # type in "wrong" and exit
git status # dummy has unstaged changes: 12; staged but uncommitted changes: wrong
git add dummy
git status # dummy has staged but uncommitted changes: 12wrong
git reset HEAD dummy # Or `git restore --staged dummy`; they behave the same here
git status # dummy has unstaged changes: 12wrong
cat dummy #prints 012wrong
# Notice the correct changes of 1 and 2 also gets unstaged
# Also, working directory changes are still there, just unstaged
# If want to remove those working directory changes:
git checkout -- dummy #Or `git restore dummy`; they behave the same here
git status # working tree clean. So all working directory changes are gone now
cat dummy #prints 0

Hence there is really no perfect “undo command” for git add. That is probably why most command names in Git are not symmetric (add and unadd, for example), because the processes are indeed not perfectly symmetric.

Hence, be mindful when you try to invent alias for undo commands. When you follow an online tutorial and start to do something like these:

git config --global alias.unadd 'reset HEAD --'
git config --global alias.unstage 'reset HEAD --'

Always keep in mind what your command actually do.

Update: Might git restore --staged (new in 2.35.1) work as a better option?
Git used to recommend git reset in its bash terminal but now it decides to recommend git restore instead.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   xxxxx.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        xxxxx.txt

I have not looked into this command in detail. Some basic testings show that they behave the same.