git fetch 本身真的很简单。复杂的部分在前后。
首先要知道的是 Git 存储 commits。事实上,这本质上就是 Git 的意义所在:它管理提交的集合。这个集合很少收缩:在大多数情况下,你对这个提交集合所做的唯一事情就是添加新的提交。
提交、索引和工作树
每个提交都有几条信息,例如作者的姓名和电子邮件地址以及时间戳。每次提交还会保存您告诉它的所有文件的完整快照:这些是您当时存储在您的索引(也称为您的暂存区)中的文件跑了git commit。这也适用于您从其他人获得的提交:他们保存在其他用户运行 git commit 时位于其他用户索引中的文件。
请注意,每个 Git 存储库只有一个索引,至少在最初是这样。该索引与一个工作树链接。在较新的 Git 版本中,您可以使用 git worktree add 添加其他工作树;每个新的工作树都带有一个新的索引/暂存区。该索引的重点是充当中间文件持有者,位于“当前提交”(又名HEAD)和工作树之间。最初,HEAD 提交和索引通常匹配:它们包含所有已提交文件的相同版本。 Git 将文件从HEAD 复制到索引中,然后从索引中复制到工作树中。
很容易看到工作树:它以普通格式保存您的文件,您可以在其中使用计算机上的所有常规工具查看和编辑它们。如果您为 Web 服务器编写 Java 或 Python 代码或 HTML,则编译器或解释器或 Web 服务器可以使用工作树文件。存储在索引中并存储在每个 Git 提交中的文件不具有这种形式,并且不可供编译器、解释器、网络服务器等使用.
关于提交要记住的另一件事是,一旦文件处于提交中,它就无法更改。任何提交的任何部分都不能改变。因此,提交是永久性的——或者至少是永久性的,除非它被删除(可以这样做,但很困难,而且通常是不可取的)。但是,可以随时修改索引和工作树中的内容。这就是它们存在的原因:索引几乎是一个“可修改的提交”(除非它在您运行 git commit 之前不会保存),并且工作树以计算机其余部分可以使用的形式保存文件。1
1没有必要同时索引和工作树。 VCS 可以将工作树视为“可修改提交”。这就是 Mercurial 所做的;这就是 Mercurial 不需要索引的原因。这可以说是一个更好的设计——但这不是 Git 的工作方式,所以在使用 Git 时,你有一个索引。索引的存在是 Git 如此快速的很大一部分原因:没有它,Mercurial 必须非常聪明,而且仍然没有 Git 快。
提交记住他们的父母;新提交是子项
当您通过运行git commit 进行新 提交时,Git 会获取索引内容并对此时其中的所有内容进行永久快照。 (这就是为什么您必须git add 文件:您将它们从您更改过它们的工作树复制回您的索引,以便它们准备好为新快照“拍照”。)Git 还收集提交消息,当然还会使用您的姓名和电子邮件地址以及当前时间来进行新的提交。
但 Git 还在新提交中存储当前提交的 哈希 ID。我们说新提交“指向”当前提交。例如,考虑这个简单的三提交存储库:
A <-B <-C <-- master (HEAD)
这里我们说分支名称master“指向”第三次提交,我将其标记为C,而不是使用像b06d364... 这样的Git 难以理解的哈希ID 之一。 (名称HEAD 指的是分支名称master。这就是Git 如何将字符串HEAD 转换为正确的哈希ID:Git 跟随HEAD 到master,然后从其中读取哈希ID master。)不过,提交C 本身“指向”——保留了提交B 的哈希ID;并提交B 指向提交A。 (由于提交 A 是有史以来的第一次提交,它没有指向更早的提交,所以它根本不指向任何地方,这使它有点特别。这称为 root 提交。)
为了进行 新 提交,Git 将索引打包成一个快照,并用你的姓名和电子邮件地址等保存,和 包括哈希 ID commit C,使用新的哈希 ID 进行新的提交。我们将使用 D 代替新的哈希 ID,因为我们不知道新的哈希 ID 是什么:
A <-B <-C <-D
注意D 如何指向C。现在 D 存在,Git 更改 存储在名称 master 下的哈希 ID,以存储 D 的哈希 ID 而不是 C 的哈希 ID。存储在HEAD 中的名称本身并没有改变:它仍然是master。所以现在我们有了这个:
A <-B <-C <-D <-- master (HEAD)
您可以从此图中看到 Git 的工作原理:给定一个名称,例如 master,Git 只需按照箭头查找 最新 提交。该提交有一个指向其较早或 父 提交的向后箭头,该提交有另一个指向其自己的父提交的向后箭头,依此类推,贯穿其所有祖先,返回到根提交。
请注意,虽然孩子们记得他们的父母,但父母的承诺不记得他们的孩子。这是因为任何提交的任何部分都不能改变: Git 从字面上不能将子级添加到父级,它甚至不会尝试。 Git 必须总是向后工作,从新到旧。提交箭头都自动向后指向,所以通常我什至不画它们:
A--B--C--D <-- master (HEAD)
分布式存储库:git fetch 的作用
当我们使用git fetch 时,我们有两个不同的 Gits,具有不同但相关的存储库。假设我们在两台不同的计算机上有两个 Git 存储库,它们都以相同的三个提交开始:
A--B--C
因为它们以完全相同的提交开始,所以这三个提交也具有相同的哈希 ID。这部分非常聪明,这也是哈希 ID 如此的原因:哈希 ID 是提交的 contents 的校验和2,因此任何两个完全相同的提交总是具有 same 哈希 ID。
现在,你在你的 Git 和存储库中添加了一个新的提交 D。与此同时,他们——无论他们是谁——可能已经添加了他们自己的新提交。我们将使用不同的字母,因为它们的提交必然有不同的哈希值。我们还将主要从您(哈利)的角度来看待这个问题;我们会打电话给他们"Sally"。我们将在我们的你的存储库图片中再添加一件事:它现在看起来像这样:
A--B--C <-- sally/master
\
D <-- master (HEAD)
现在让我们假设 Sally 做了两次提交。在 her 存储库中,she 现在有这个:
A--B--C--E--F <-- master (HEAD)
或者也许(如果她从您那里获取,但尚未运行 git fetch):
A--B--C <-- harry/master
\
E--F <-- master (HEAD)
当你运行git fetch时,你将你的Git连接到Sally的Git,并询问她是否有任何新的提交添加到她master自从提交@ 987654371@。她做到了——她有她的新提交 E 和 F。所以你的 Git 从她那里得到这些提交,以及完成这些提交的快照所需的一切。然后,您的 Git 将这些提交添加到 您的 存储库,这样您现在就拥有了:
E--F <-- sally/master
/
A--B--C
\
D <-- master (HEAD)
如您所见,git fetch 为您所做的是收集她的所有新 提交并将它们添加到您的存储库。
为了记住 她 master 在哪里,现在您已经与她的 Git 进行了交谈,您的 Git 将 her master 复制到 您的sally/master。你自己的master,和你自己的HEAD,一点都没有改变。只有这些“另一个 Git 存储库的内存”名称(Git 称之为 远程跟踪分支名称)会改变。
2这个散列是一个加密散列,部分是因为它很难欺骗 Git,部分是因为加密散列对于 Git 的目的来说自然表现得很好。当前的哈希使用 SHA-1,它是安全的,但已经看到暴力攻击,现在正被放弃用于加密。 Git 可能会迁移到 SHA2-256 或 SHA3-256 或其他更大的哈希值。会有一段不愉快的过渡期。 :-)
您现在应该合并或变基——git reset 通常是错误的
请注意,在您从 Sally 获取之后,它是您的存储库,并且只有您的存储库拥有你们俩的所有工作。 Sally 仍然没有你的新提交 D。
即使您的另一个 Git 被称为 origin 而不是“Sally”,这仍然是正确的。既然您同时拥有master 和origin/master,您必须做一些事情来将您的新提交D 与他们的最新提交F 联系起来:
A--B--C--D <-- master (HEAD)
\
E--F <-- origin/master
(出于绘图的原因,我将D 移到顶部,但这与以前的图表相同,
您在这里的两个主要选择是使用git merge 或git rebase。 (还有其他方法可以做到这一点,但这是要学习的两种方法。)
合并实际上更简单,因为git rebase 做了一些涉及合并的动词形式,合并。 git merge 所做的是运行合并的动词形式,然后将结果提交为 new 提交,称为 merge 提交 或简称为“合并”,这是merging的名词形式。我们可以这样绘制新的合并提交G:
A--B--C--D---G <-- master (HEAD)
\ /
E--F <-- origin/master
与常规提交不同,合并提交有两个父级。3它连接回之前使用的两个提交进行合并。这使得将你的新提交 G 推送到 origin 成为可能:G 会带上你的 D,但也可以连接回他们的 F,所以他们的 Git 可以接受这个新的更新。
此合并与合并两个分支所获得的合并类型相同。事实上,您确实在这里合并了两个分支:您将 master 与 Sally 的(或 origin 的)master 合并。
使用git rebase 通常很简单,但它的作用更复杂。而不是 merge 你的提交 D 与他们的提交 F 以进行新的合并提交 G,git rebase 所做的是复制 您的每个提交,以便新的 副本,它们是新的和不同的提交,在您的 上游 上的最新提交之后。
在这里,您的上游是origin/master,而您拥有但没有提交的提交只是您的一个提交D。所以git rebase 制作了D 的副本,我将其称为D',将副本放在他们提交F 之后,这样D' 的父级就是F。中间图如下所示:5
A--B--C--D <-- master
\
E--F <-- origin/master
\
D' <-- HEAD
复制过程使用git merge 用于执行从提交D 更改的动词形式合并 的相同合并代码。4 一次复制已完成,但是,rebase 代码发现没有更多要复制的提交,因此它更改您的master 分支以指向最终复制的提交D':
A--B--C--D [abandoned]
\
E--F <-- origin/master
\
D' <-- master (HEAD)
这放弃了原始提交D。6这意味着我们也可以停止绘制它,所以现在我们得到:
A--B--C--E--F <-- origin/master
\
D' <-- master (HEAD)
现在很容易将git push 新提交D' 回origin。
3在 Git(但不是 Mercurial)中,合并提交可以有两个以上的父级。这并没有做任何你通过重复合并做不到的事情,所以它主要是为了炫耀。 :-)
4从技术上讲,合并基础提交,至少在这种情况下,是提交 C,而两个提示提交是 D 和 F,所以在这种情况下,它实际上是相同。如果你对多个提交进行 rebase,它会变得有点复杂,但原则上它仍然很简单。
5HEAD 与 master 分离的这种中间状态通常是不可见的。只有在动词形式的合并过程中出现问题时,您才会看到它,因此 Git 会停止并必须从您那里获得帮助才能完成合并操作。但是,当 确实 发生时——当变基期间发生合并冲突时——重要的是要知道 Git 处于这种“分离的 HEAD”状态,但只要变基自行完成,你这个不用太在意。
6通过 Git 的 reflogs 和名称 ORIG_HEAD 临时保留原始提交链。 ORIG_HEAD 值被下一个进行“大更改”的操作覆盖,并且 reflog 条目最终到期,通常在此条目的 30 天后。之后,git gc 将真正删除原始提交链。
git pull 命令只运行git fetch,然后运行第二个命令
请注意,在git fetch 之后,您通常需要运行第二个 Git 命令,git merge 或 git rebase。
如果您事先知道肯定会立即使用这两个命令之一,您可以使用git pull,它运行git fetch,然后运行这两个命令之一。您可以通过设置pull.rebase 或提供--rebase 作为命令行选项来选择要运行的第二个 命令。
在您非常熟悉 git merge 和 git rebase 的工作原理之前,我建议不要使用 git pull,因为有时 git merge 和 git rebase 无法完成它们的自己的。在这种情况下,您必须知道如何处理这种故障。您必须知道您实际运行的是哪个命令。如果您自己运行命令,您将知道您运行的是哪个命令,以及在必要时在哪里寻求帮助。如果你运行git pull,你可能甚至不知道你运行了第二条命令!
除此之外,有时您可能希望在运行第二个命令之前查看。 git fetch 带来了多少次提交?进行合并与变基需要多少工作?现在是合并比变基更好,还是变基比合并更好?要回答任何这些问题,您必须将git fetch 步骤与第二个命令分开。如果您使用git pull,您必须提前决定要运行哪个命令,甚至在您知道要使用哪个命令之前。
简而言之,只有在您熟悉了git pull 的两个部分(git fetch)和您选择的第二个命令真正起作用之后,才能使用它。