Is there a way to undo the following git command:
git checkout -- .
I was trying to remove files that were added to the repo directory but
git checkout -- path
copies from index / staging-area, to work-treeIf the path
you name is a directory, Git copies all the files it knows about (as found in the index / staging-area) into the work-tree, starting from that directory and including any sub-directories.
To make use of this answer, though, you need to know about the three active copies of every file.
Keep in mind that in Git, there are up to three versions of each file active at all times. For instance, suppose that in your repository, you have committed files named README.txt
and a.ext
. There are three copies of README.txt
, and three copies of a.ext
, available to you at any given time. Two of the three copies are in a special Git-only format.
If we use the syntax that git show
uses to access the Git-only format files, we can describe these three copies this way:
HEAD index work-tree
--------------- ----------- ----------
HEAD:README.txt :README.txt README.txt
HEAD:a.ext :a.ext a.ext
If you now create a new untracked file b.dat
, you have this:
HEAD index work-tree
--------------- ----------- ----------
HEAD:README.txt :README.txt README.txt
HEAD:a.ext :a.ext a.ext
b.dat
There is as yet no copy of b.dat
in the index / staging-area. There are two logically-separate copies of the other two files, even though they are the same in both HEAD
and the index. (When they are the same like this, Git shares the underlying copy automatically, so there is no extra space needed.)
Any copy of any file stored in your work-tree is just an ordinary file. You can do anything with it that your computer will let you do. Git does not care what you do to such files. Git will tell you that the work-tree copy differs from the index copy, if you ask Git what's different.
git add path
copies from work-tree, to staging-areaSuppose at this point that you modify README.txt
using whatever editor you like, which edits the work-tree copy (it can't use or touch the index copy at all unless it knows Git rather intimately). The work-tree copy now differs from the index copy. The index copy is in the special Git-only format, ready to go into the next commit.
You now will need to run git add README.txt
, to copy the updated work-tree file into the index. When you have done that, the old version of README.txt
is still in the HEAD
commit, also in the special Git-only format, but now HEAD:README.txt
is different from :README.txt
, while :README.txt
is the same as README.txt
.
HEAD
commit copy of each file is read-only; the index copy is notNothing in any commit can ever be changed. Hence the copy of README.txt
that you committed, and the copy of a.ext
that you committed, are safely saved away forever1 in your repository. The copy in the index / staging-area, which may or may not be the same as the one on the HEAD
commit, can be overwritten at any time. It starts out the same as the one in the HEAD
commit,2 but git add
copies from the work-tree, to the index.
1If you abandon or delete a commit (typically via git reset
or git rebase -i
), you can cause Git to lose the frozen copy: the frozen copy only lasts as long as the commit(s) that contain it. However, most of Git is built around the idea of adding new commits, without removing old ones.
2If you Checkout another branch when there are uncommitted changes on the current branch you can defeat the normal case of "HEAD and index match up after checkout or commit". There is not enough room in this answer to go into these details.
git commit
freezes all the index copies into a new commitWhat git commit
does is to take every file that is in the index at that time, in the form it has in the index at that time, and freeze it into a read-only committed copy. This committed set of files becomes the new HEAD
commit. Once git commit
has finished running and you have your prompt back, you have a new commit and your HEAD
commit and your index match—because Git made the new commit from the index!
Git defines an untracked file very simply: it's a file that is not in the index. That's it—that's all there is to it—but it has a strong consequence: if b.dat
is not in the index, git commit
won't put it in the new commit. Moreover, git checkout --
cannot find b.dat
, because it's not in the index, so it cannot overwrite the work-tree copy.
Note that just because some file exists in some commit within the repository does not mean the file is tracked! The file is tracked if and only if that file is in the index right now. If you run a git checkout
of a commit that does contain the file, then—at that time—Git will copy the file from the commit, into the index, and on into the work-tree. At that time the file will be tracked. If you then explicitly remove the file from the index, at that time the file will cease to be tracked. So you must always keep in mind, or test, whether there is a copy of some particular file in your index, to know if it is tracked.
git checkout commit -- path
copies from commit, to index and work-treeHere, and with git reset
, Git gets overly complicated, by cramming multiple different things into one command. When you use git checkout
with paths, but without a commit or tree specifier, Git copies from the index to the work-tree. When you use git checkout
with both paths and a commit-or-tree, Git copies into both the index and the work-tree.
git reset -- path
copies from commit, to index, leaving work-tree aloneThis particular form of git reset
, used with paths, copies from a commit into the index / staging-area. Remember, the index already has copies of all tracked files, so this just overwrites those copies with other copies. By default, the commit that git reset
uses to get the files is the HEAD
commit—so this copies from the active HEAD
copy into the index.
The work-tree copy of any file is left alone. The fact that the file exists in the HEAD
commit implies that the file is probably tracked: the only way the file could be untracked is if you checked out the commit, but then explicitly removed the index copy. In this case, git reset -- path
puts the file back into the index, so that it is once again tracked.
Note, however, that you can use git reset commit -- path
to copy a file from some specific commit. If that file isn't in the HEAD
commit, that file may well be untracked (not in the index) before your git reset
operation, but tracked (in the index) afterward. This all depends on what changes you already made to the index.
Running git status
does two comparisons:
The first comparison is HEAD
vs index. Whatever is different here is staged for commit.
The files that are in HEAD
are likely also in the index (and hence all of those files are tracked). If the copy that's in the index differs from the copy that is in HEAD
, Git calls that staged for commit. If the copy that's in the index is the same as what's in HEAD
, the file itself is still tracked and staged—it's just that git status
doesn't bother to mention it.
The key idea here is that, even though every commit is a complete snapshot of all files, what we tend to want to know about a commit is: What is different about this commit than its predecessor? So git status
tells us what will be different if we take the current staging area—the proposed new commit—and actually turn it into a new commit.
The second comparison is index vs work-tree. Whatever is different here is not staged for commit.
The files that are in the index are tracked. For those that match what's in the work-tree, git status
just doesn't bother to mention them. For those that differ, git status
mentions them, as not staged for commit. For files that are not in the index at all, but are in the work-tree, Git whines about them being untracked.
Once again, the general idea here is that we care about what's actually different in each commit, as compared to its predecessor. If we have unstaged or even untracked files that differ from their staged copies—or lack of staged copy—we could use git add
to copy them into the staging area. If we have an work-tree a.ext
that's exactly the same as the staged a.ext
that's exactly the same as HEAD:a.ext
, we probably don't care about that, so we simply don't see it at all.
To shut Git up about untracked files that definitely should not be committed, you can list those untracked files (by name or glob pattern such as *.o
or *.pyc
) in a .gitignore
directive. This keeps Git from automatically adding the file in an en-masse git add .
or git add --all
, and keeps git status
from whining. Note, however, that if some file is already in the index—however it may have gotten there—listing that file's name or pattern in a .gitignore
has no effect.
The primary operations that copy files are:
git add path
: copy from work-tree to indexgit checkout -- path
: copy from index to work-treegit checkout commit -- path
: copy from commit
to index and work-treegit reset [commit] -- path
: copy from commit
(default HEAD
) to indexThe primary operations that remove files are:
git add path
: remove from index, if path
is in the index but is missing from the work-treegit rm --cached path
: remove from index but not work-treegit rm path
: remove from index and work-treegit reset [commit] -- path
: remove from index, if path
is not contained in commit
(default HEAD
)Besides these, there are some special cases you can trigger with git commit --only paths
or git commit --include paths
, but these are essentially equivalent to doing git add
on those paths first. Always keep the index in mind, and be aware that git status
summarizes the index's differences, rather than listing the index's contents, so that if you have a big 30,000 file project, you see only the few interesting files, not all 30,000 files.