"git checkout -- ." 和 "git reset HEAD --hard" 有什么区别?

What is the difference between "git checkout -- ." and "git reset HEAD --hard"?

这不是关于“--”的作用的一般性问题,如在标记的重复项中那样。这是一个特定于 git 的问题,要求澄清上述命令之间的操作差异。

如果我想在不存储或提交的情况下清理当前目录,我通常使用这些命令:

git reset HEAD --hard
git clean -fd

一位同事也提到使用这个命令:

git checkout -- .

google 是一个困难的命令,从 git 文档中我不清楚这个命令的实际作用。好像是手册后面提到的用法之一

我猜它复制了 git reset HEAD --hard,但与我已经在使用的命令相比,它到底做了什么?
它复制了一个或两个命令,还是相似但略有不同?

首先,让我们解决双连字符或双破折号的问题,以解决问题(特别是因为这个问题不再有明显的重复)。

Git 主要在 the POSIX-approved fashion (see Guideline 10) 中使用它来指示选项参数和非选项参数之间的分界线。由于 git checkout 接受分支名称,如 git checkout master,以及文件(路径)名称,如 git checkout README.txt,您可以使用 -- 强制 Git将 -- 之后的任何内容解释为文件名,即使它本来是有效的分支名称。也就是说,如果你有一个 branch 和一个 file 命名为 master:

git checkout master

将检查 分支 ,但是:

git checkout -- master

将检查 文件 (令人困惑的是,从当前的 索引 )。

Branches, index, and files, oh my

接下来,我们需要解决 git checkout 的一个怪癖。从 the documentation, there are many "modes" of git checkout (the documentation lists six separate invocations in the synposis!). There are various rants (of varying quality: Steve Bennet's 可以看出它实际上是有用的,在我看来,虽然我自然不会 100% 同意它 :-) ) 关于 Git 糟糕的“用户体验”模型,包括事实git checkout 的操作模式太多了。

特别是,您可以git checkout一个分支(切换分支),或者git checkout一个或多个文件。后者从特定提交或索引中提取文件。当 Git 从提交中提取文件时,它首先将它们复制 索引,然后从 索引复制它们, 工作树。

这个序列有一个潜在的实现原因,但它完全显示出来的事实是一个关键因素。我们需要了解很多关于 Git 的索引,因为 git checkoutgit reset 都使用它,有时使用的方式不同。

我认为,绘制一个三向图或 table 说明当前(或 HEAD)提交、索引和工作树是个好主意。假设:

  • 有两个普通的已提交文件 README.mdfile.txt
  • 有一个新的 git add-ed 但未提交 new.txt;
  • 有一个名为 rmd.txt 的文件已被 git rm 编辑但尚未提交;
  • 并且有一个名为 untr.txt.
  • 的未跟踪文件

每个实体——HEAD 提交、索引和工作树——现在拥有三个文件,但每个实体拥有一组 不同的文件集 。整个状态的 table 看起来像这样:

  HEAD       index    work-tree
-------------------------------
README.md  README.md  README.md
file.txt   file.txt   file.txt
           new.txt    new.txt
rmd.txt
                      untr.txt

可能的状态远不止这些:事实上,对于每个文件名,“in/not-in”HEAD、索引有种可能的组合, 和工作树(第八个组合是“不在所有三个中”,在这种情况下,我们首先讨论的是什么文件?!)。

checkoutreset 命令

您询问的两个命令,git checkoutgit reset,都可以做很多事情。然而,每个的具体调用将“完成的事情”减少为两个之一,我将添加更多:

  • git checkout -- .:从索引复制工作树,仅
  • git checkout HEAD -- .:从 HEAD复制索引,然后 工作树
  • git reset --mixed:从 HEAD 重置索引(然后单独留下工作树)
  • git reset --hard:从 HEAD 重置索引,然后从索引重置工作树

这些重叠很多,但有几个关键的不同部分。

让我们特别考虑上面名为 new.txt 的文件。它现在在索引中,所以如果我们从索引复制工作树,我们将工作树副本替换为索引副本。例如,git checkout -- new.txt 就是这样做的。

相反,如果我们从 HEAD 复制到索引,则索引中的 new.txt 没有任何变化:new.txt 存在HEAD 中。因此,显式 git checkout HEAD -- new.txt 只会失败,而 git checkout HEAD -- . 会复制 HEAD 中的文件,并保持两个现有的 new.txt 版本不受干扰。

文件rmd.txt从索引中消失,所以如果我们git checkout -- .、Git看不到它并且什么也不做它。但是如果我们 git checkout HEAD -- ., Git 从 HEAD 复制 rmd.txt 到索引中(现在它回来了)然后从索引到工作树(现在它又回来了) ,也是)。

git reset 命令在没有路径名参数的情况下有一个关键的区别。在这里,它从字面上重新设置索引以匹配提交。这意味着对于 new.txt,它注意到该文件不在 HEAD 中,因此它 删除 索引条目。如果与 --hard 一起使用,它因此也会 删除 工作树条目。同时 rmd.txt HEAD 中的 ,因此它将其复制回索引,并使用 --hard 复制到工作树。

如果有未暂存的,即仅工作树,则更改其他两个文件 README.mdfile.txtgit checkout--hard 的两种形式git reset 的形式消除了这些变化。

如果对这些文件进行了 暂存 更改(已复制到索引中的更改),则 git reset 取消暂存它们。 git checkout 的变体也是如此,您将其命名为 HEAD。但是,将索引文件复制回工作树的 git checkout 变体会保留这些阶段性更改!

顶级与当前目录

最后,值得注意的是 .,意思是 当前目录 ,可能随时不同于“Git 存储库的顶部”:

$ git rev-parse --show-toplevel
/home/torek/src/kernel.org/git
$ pwd
/home/torek/src/kernel.org/git/Documentation
$ git rev-parse --show-cdup
../

在这里,我在顶级目录gitDocumentation子目录中,所以.表示Documentation中的所有内容并且它的子目录。例如,使用 git checkout -- . 将检出(从索引中)所有 DocumentationDocumentation/RelNotes 文件,但不会检出任何 ../builtin 文件。但是 git reset,当不使用路径名时,将重置 所有 条目,包括 ..../builtin.

的条目