为了正确理解差异,让我们从这些定义开始:
-
Git 存储库包含commits,它们是目录中一些文件树的快照(tree)以及关联的元数据,通常包括一个父提交(但允许任意数量的父提交 ID)。这些提交由丑陋的大哈希 ID 来标识,deadc0defeedbeefac0ffee...
等等。因为这些丑陋的大哈希 ID 又大又丑,而且看起来是随机的,所以我们使用名称(主要是分支名称和标签名称)来跟踪它们。
存储在存储库中的文件具有仅对 Git 本身有用的形式。普通命令无法读取它们。 (除了 Git 之外没有什么可以write这些存储的对象,并且它们具有一次写入的形式:一旦写入,它们就永远不能更改。所有四种 Git 对象都是如此,但是not分支名称,它们是supposed更改:每次向分支添加新提交时,存储在分支名称下的哈希 ID 都会更改。)
-
每个存储库还有一个index。索引(请注意,有一个特殊的、显着的“the”索引)是您构建next您打算做出的承诺。它开始匹配一些现有的提交。索引有多种作用,包括让 Git 运行得更快(它的cache角色),并处理合并并安排下一次提交(其暂存区角色)。但它开始匹配一些现有的提交。
索引本质上是一棵扁平树,因此它就像以特定方式提交的内容。与内部 Git 对象一样,它的形式仅对 Git 有用。
-
大多数存储库还有一个(单一的、可区分的)工作树,这是您处理文件的地方。与索引一样,树开始时匹配某些提交。不过,由于这些是计算机其余部分可以处理的普通文件,因此可以写入和读取它们,或者您可以添加新文件或删除文件。
如果更改工作树中的文件并想要提交新版本,则必须将该文件从工作树复制到索引中。这是什么git add
作用:它将一个文件复制到索引中,要么替换现有文件(如果文件的路径名之前在该索引中),要么存储一个全新的文件(如果路径名是新的)。
同样,要从索引中删除文件,您必须运行git rm
。默认情况下,这将从中删除文件both索引and工作树(但您可以告诉它保留工作树版本)。
A repository always1 has one current commit. This is called the HEAD commit. The way .git/HEAD
(it's an ordinary file, in the .git
directory) really works is that it normally just contains the current branch name. The actual commit ID is stored under the branch name. However, Git has what is called "detached HEAD" mode, where HEAD
contains a hash ID instead. This all matters in a moment for both git checkout
and git reset
.
所以,有一个当前提交,通常来自当前分支名称,加上分支名称实际上只是查找名称到 ID 映射以查找提交的一种方式。我们称之为HEAD
,它是:“嘿,Git,去阅读.git/HEAD
,查找当前分支名称,并使用它来查找当前提交。或者,如果我处于分离 HEAD 模式,请阅读.git/HEAD
和以前一样,但是看到它有一个哈希 ID,然后that'是当前提交。”
现在,您在这里看到的两个不同的命令是git reset
and git checkout
。这些都有很大不同goals:
git reset
is (mostly2) about changing the current branch name's name-to-ID mapping in some way, with the option of also changing the index too, or even changing all three: the current commit, the index, and the work-tree.
git checkout
(主要)是关于改变当前分支是哪个分支,通过将新的分支名称写入 HEAD,或通过“分离”HEAD:将提交哈希 ID 写入 HEAD。正在进行中,git checkout
will更改索引和工作树。
这就是事情变得有点复杂的地方。 :-) 既然你正在使用--hard
, git reset
will更新索引和工作树。所以现在听起来更像是git checkout
,在某些方面确实如此。但有几个关键的区别:
-
Using git reset
提交当前分支指向的更改。 Using git checkout
变化哪个提交是当前的。
请记住,我们上面说过,像这样的名字master
and develop
是我们如何让 Git 记住又大又难看的哈希 ID。其效果是这些名称就像标签一样,粘贴到或指向特定的提交。这git reset
命令让你move标签:改变它指向的位置;将其从一个特定的提交上剥离并将其粘贴到另一个提交上。相比之下,git checkout
does not移动标签。相反,它改变了哪个分行名称存储在.git/HEAD
. You git checkout master
切换到分支master
, 进而git checkout develop
切换到分支develop
。标签保持在原来的位置,但 Git 改变了name存储在.git/HEAD
.
所有这些都适用于normal情况,当你“在树枝上”时。如果你处于“分离的 HEAD”情况,git reset
仍然将你分离的 HEAD 从一个提交移动到另一个提交 - 但因为没有分支name涉及,这不会更改任何现有的分支名称。同样,如果你git checkout
不是分支名称的东西,git checkout
通过将原始提交 ID 写入“分离你的 HEAD”.git/HEAD
.
注意git reset
永远不会分离 HEAD,也永远不会重新连接 HEAD。那就是git checkout
做。同时,git checkout
从不更改任何分支标签,但是git reset
does.
-
The git checkout
命令尝试非破坏性(无论如何,在这种模式下;再次参见脚注 2)。这git reset
命令愉快地销毁未保存的工作.
实际上,这意味着如果您对索引和/或工作树进行了更改,git checkout
不会覆盖它们。这变得特别复杂,因为它will如果可以的话,切换分支或提交。它通过将未保存的工作保留在索引和/或工作树中(如果可以的话)来实现这一点。如果不能,它就会出错。
相比之下,git reset --hard
将丢弃这些未保存的更改:--hard
意味着覆盖索引和工作树。
在评论中,您添加了:
我想忽略任何人可能在服务器上所做的任何提交(尽管这不太可能)
但“忽略”与“丢弃”不同,并且这些都不能解决对索引和/或工作树所做的未提交的更改。
通常,使用这样的集中式服务器的正确做法是设置一个没有工作树,即,是一个--bare
存储库。没有工作树的存储库不能在其中完成任何工作。首先没有什么可以保留、可以忽略或可以丢弃的东西。如果您使用这种方法,所有这些区别都会消失。这是你最好的选择。
即使您不能或不会使用--bare
存储库,让我们看看剩余的项目。除了上述所有考虑因素之外,请记住,集中式存储库是某人可以访问的存储库git push
es new首先提交——仍然有一个当前(HEAD)提交,如果它没有分离——命名一个当前分支。它仍然有一个索引,如果不是空的,还有一个工作树。 Git 通常会拒绝推送到工作树的当前分支:请参阅receive.denyCurrentBranch
in the git config文档 https://www.kernel.org/pub/software/scm/git/docs/git-config.html。由于裸存储库没有工作树,因此对于裸存储库来说,这个特定的 Git 问题也消失了。
(不过,您的描述听起来好像您没有将其设置为其他人直接推送到的中央服务器,而是作为另一台服务器的客户端,您在其中使用git fetch
在客户端上。如果是这样的话,下面的几项就不再是问题了。)
因此,假设您有一个带有工作树的非裸存储库。为了让推送成功,您可能需要使用分离的 HEAD,否则 Git 将拒绝推送到当前分支。 (较新的 Git 有能力接受它们,但您必须非常小心地使用它;有关详细信息,请参阅链接的文档。)分离的 HEAD 将为您提供没有当前分支的当前提交,并且将使git reset
and git checkout
比在树枝上更亲近的表兄弟。
(它还破坏了客户端在克隆中央存储库时看到的一个功能:客户端将检查服务器上当前的哪个分支,即客户端询问服务器“你的 HEAD 分支是什么?”并自动检查出来。此功能有一些可疑的价值,所以你可能不会错过它,但值得注意。)
如果您使用这个分离的 HEAD,那么,之间的主要区别git reset --hard origin/master
and git checkout origin/master
是未提交的索引和/或工作树修改发生的情况。和git reset --hard
,他们将在没有警告的情况下被消灭。任何在服务器上积极工作的人都将SOL http://www.urbandictionary.com/define.php?term=SOL。索引和工作树将被重新设置以匹配新的提交。
如果你是not使用分离的 HEAD,git reset --hard origin/master
将写入提交 IDorigin/master
到当前分支名称。像之前一样,git reset
也会更新索引和工作树。这不仅消除了积极的工作,而且扔掉通过从旧提交上剥离分支标签并将其粘贴到新提交上,在当前分支上进行提交。
With git checkout
, well, this is also a bit complicated. Since origin/master
is a so-called remote-tracking branch, checking out this name gives you a detached HEAD, with the commit hash stored directly in .git/HEAD
. This is true regardless of whether HEAD was attached or detached before. So HEAD
now is detached, provided that the checkout succeeds. If there are no uncommitted changes in the index and work-tree, the checkout should3 succeed. If there are uncommitted changes, the checkout will carry them to the new detached HEAD if possible, or fail if switching to that commit cannot be done without wiping out the uncommitted work.
因此,如果您选择沿着这条道路前进,这些就是您的选择。由于 Git 是一个大型工具集,而不是一个单一的固定解决方案,因此您可以选择一些other路径(如果更合适)。
(注意git clean -f -x
将删除未跟踪和忽略的文件,即不在索引中的文件。没有-d
但是,它不会删除未跟踪的目录。)
1There is an exception to this "always a current commit" rule, used mainly for the case of a new, empty repository, when there are no commits at all. You won't encounter this exception unless you are working in an empty repository, or use git checkout --orphan
to create a new but unborn branch, as Git calls it.
2Both git reset
and git checkout
have additional usages that deviate, anywhere from a little bit to a lot, from their main function. I am describing only their main function here.
3A git checkout
can still fail due to, e.g., running out of disk space.