Undo git command - git checkout --

こ雲淡風輕ζ 提交于 2019-12-19 11:16:52

问题


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 not staged or committed. That command didn't seem to do that, instead changing some other files.

Update:

There's a difference between untracked filed and unstaged files. In this case, I would like to find a way to discard both untracked and unstaged files. When I ran git checkout -- ., I thought it would get rid of untracked files, but it only got rid of unstaged files, as far as I can tell.


回答1:


git checkout -- path copies from index / staging-area, to work-tree

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

The (up to) three copies of each 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.)

The work-tree copies are ordinary files

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-area

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

The HEAD commit copy of each file is read-only; the index copy is not

Nothing 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 commit

What 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!

Untracked files

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-tree

Here, 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 alone

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

Summary

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 index
  • git checkout -- path: copy from index to work-tree
  • git checkout commit -- path: copy from commit to index and work-tree
  • git reset [commit] -- path: copy from commit (default HEAD) to index

The primary operations that remove files are:

  • git add path: remove from index, if path is in the index but is missing from the work-tree
  • git rm --cached path: remove from index but not work-tree
  • git rm path: remove from index and work-tree
  • git 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.



来源:https://stackoverflow.com/questions/51352460/undo-git-command-git-checkout

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!