Is there a difference between git checkout HEAD —

2020-06-27 01:46发布

I have taken a look at this stackoverflow link but I think the subtle difference between what I am asking is the usage of HEAD within the checkout cmd as their suggestions don't' seem to be working :

Is there a difference between git reset --hard HEAD and git checkout .?

git checkout HEAD -- . cleans out my staging area as well. Additionally, the second answer, regarding deleted files added to staging area, seem to be coming back with git checkout HEAD -- .

Is there a situation where one would get different results?

标签: git
2条回答
劫难
2楼-- · 2020-06-27 02:29

Yes, there is a difference, besides that implied by . if you're not at the top level of your repository. I won't go so far as to claim that what follows is the only difference; it suffices to show one example, to see that they are different.

Set-up for illustrating a difference

Start by creating a new repository with at least one commit (here I have made two, as is my habit):

$ mkdir treset
$ cd treset
$ git init
Initialized empty Git repository in ...
$ echo 'for testing reset vs checkout' > README
$ git add README
$ git commit -m 'initial commit'
[master (root-commit) 058b755] initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 README
$ echo contents for file a > file-a
$ echo contents for file b > file-b
git commit -m 'add files'
[master f505609] add files
 2 files changed, 2 insertions(+)
 create mode 100644 file-a
 create mode 100644 file-b

At this point, HEAD and the index match—both contain the content from commit f505609—and the work-tree contains the (normal format) files that match that commit as well. Now let's add a new file, and copy it into the index:

$ echo 'uncommitted file' > foo
$ git add foo

Technically, the git add foo created blob object a9e2570d6af8c05b57e2cefecaebeedfabc98bf2 in the repository and then put that hash ID into the index:

$ git ls-files --stage
100644 e16f62b2e75cf86a6f54adcfddcfd77140f238b9 0       README
100644 881d9334f4593efc7bab0dd536348abf47efed5c 0       file-a
100644 fa438bc26ce6b7a8f574bad9e63b83c912a824b9 0       file-b
100644 a9e2570d6af8c05b57e2cefecaebeedfabc98bf2 0       foo

(The hash ID of this blob object is predictable due to the known content for file foo. That's true of the other three files as well, but they're actually committed, so those blob objects are permanent. The one for foo could be GCed, if we never actually commit it and instead remove the entry from the index.)

Using git checkout HEAD

If we use git checkout HEAD, we direct Git to copy from HEAD into the index and then expand them into normal work-tree files. HEAD contains three files (README, file-a, and file-b), so this does that and updates the three work-tree files with the contents they already have—so there's no observable effect.1

$ git checkout HEAD -- .; ls
file-a  file-b  foo README

Note that file foo remains, both in the index (run git ls-files again to see) and in the work-tree.


1Unless, that is, we inspect things like file modification times or system calls executed, via whatever OS-level tools we have available. In this case we can tell if Git really did overwrite the work-tree files or not. On my system, it actually didn't, because the index hashes matched the HEAD hashes and the stat data cached in the index matched the stat data from the work-tree files, so it didn't bother. But in principle Git copied HEAD to the index, and then the index to the work-tree, and if it were necessary based on hashes and/or stat data, Git would have actually touched the work-tree files here.


Using git reset --hard

If we tell Git to reset the index to match the current commit, and reset the work-tree to match changes to the index, the action is different. This time, Git examines the index and sees that file foo is present, while it's absent in the commit. So Git removes file foo from the index, and updates the work-tree accordingly:

$ git reset --hard HEAD; ls
HEAD is now at f505609 add files
file-a  file-b  README

File foo has vanished from the work-tree.

If we were to use git reset --mixed HEAD, Git would remove foo from the index, but not from the work-tree. (The default action for this kind of reset—there are many other kinds—is --mixed.)

Using git restore

With the new Git 2.23+ git restore command, we can control the index and work-tree separately. First we have to put foo back into the index and work-tree:

$ echo 'uncommitted file' > foo
$ git add foo

We can now choose whether to copy HEAD to the index or not, and whether to manage the work-tree similarly. Its documentation is a bit more explicit too:

If a path is tracked but does not exist in the restore source, it will be removed to match the source.

What it means for a path to be "tracked" is that the path is in the index. In this case, foo is in the index now (due to the git add) so it is tracked. If we restore the index from HEAD, foo will be removed from the index, just as with git reset --hard or git reset --mixed. So let's try VonC's command, but with . (the current directory and all sub-directories)2 as the pathname, here:

$ git restore --source HEAD --staged --worktree .
$ ls
file-a  file-b  README

So you can see that this had the same effect as git reset --hard. Unlike git reset, git restore has only one job—though with two parts to it—so we need not worry about other modes of operation.

(This is why both git switch and git restore were added: the mostly do the same things you can already do with git checkout and git reset, but they only have one job, even if it has several parts. By contrast, git checkout has anywhere from about three to about seven different jobs, depending on how you count, and git reset has anywhere from about three to about five.3)


2This particular repository has only the one top level directory, so we need not worry that you've done a cd subdir within the work-tree. If you had, though, . would mean apply this to the subdir/* files, so that checkout and reset would be even more different.

3For git checkout, consider:

  • switch branches
  • extract from index to work-tree (only, a la git checkout-index)
  • extract from specific commit to index and then on to work-tree
  • reproduce merge conflict (git checkout -m with a file name)
  • switch branches while merging (git checkout -m but with a branch name)

While that's just five, we can git checkout --ours and git checkout --theirs and some might wish to count these as separate from the usual "extract from index" flavor. We can get even more when you add create branch (git checkout -b) and forcibly reset branch (git checkout -B). Alas, git switch has the create and forcibly-reset options as well!

Some might, of course, lump some or all of these into one operation, just as git checkout does. That's why I say "depending on how you count".

For git reset, consider:

  • reset index and/or work-tree without moving HEAD
  • reset index and/or work-tree with moving HEAD
  • reset one or more specific paths (cannot move HEAD)
  • abort a merge or cherry-pick or revert (cannot move HEAD)
  • selectively patch with git reset -p (cannot move HEAD)

all of which are lumped under the one command git reset.

查看更多
一夜七次
3楼-- · 2020-06-27 02:39

To avoid the confusion, you should use the new experimental command git restore (Gti 2.23+, August 2019).

As I explained in "What is git restore Command ? What is the different between git restore and git reset?", you can specify what to restore with git restore.

  • the index
  • and/or the working tree.

This is more explicit than relying on HEAD or no HEAD in git checkout.

To do a reset --hard with git restore:

git restore --source=HEAD --staged --worktree hello.c

or the short form, which is more practical but less readable:

git restore -s@ -SW hello.c
查看更多
登录 后发表回答