git fetch
本身非常简单。复杂的部分在前后。
这里需要知道的第一件事是,Git存储提交。实际上,这基本上就是Git的主要功能:它管理着一系列的提交。这个集合很少会缩小:大多数情况下,你对这个提交集合所做的唯一操作就是添加新的提交。
提交、索引和工作树
每个提交都有几个信息,例如作者的姓名和电子邮件地址以及时间戳。每个提交还保存了您告诉它的所有文件的完整快照:这些文件存储在您运行git commit
时在您的索引(也称为暂存区)中的文件。对于从其他人那里获得的提交也是如此:它们保存了其他用户在运行git commit
时其索引中的文件。
请注意,每个Git存储库最初只有一个索引。该索引与一个工作树相关联。在较新的Git版本中,您可以使用git worktree add添加额外的工作树。每个新的工作树都带有一个新的索引/暂存区。该索引的目的是充当中间文件持有者,位于“当前提交”(也称为HEAD)和工作树之间。最初,HEAD提交和索引通常匹配:它们包含所有已提交文件的相同版本。Git将文件从HEAD复制到索引,然后从索引复制到工作树。
很容易看到工作树:它以普通格式显示您的文件,您可以使用计算机上的所有常规工具查看和编辑它们。如果您编写Java或Python代码,或为Web服务器编写HTML,则编译器、解释器或Web服务器可以使用工作树文件。存储在索引中并存储在每个Git提交中的文件不具有此形式,并且不能被编译器、解释器、Web服务器等使用。
关于提交(commit)的另一件事情需要记住的是,一旦文件被提交(commit),它就不能被更改。任何提交(commit)的部分都不会改变。因此,提交(commit)是永久性的——或者至少,在移除提交(commit)之前是永久性的(虽然可以移除,但难度很大,通常也不希望这样做)。然而,索引(index)和工作树(work-tree)中的内容可以随时修改。这就是它们存在的原因:索引(index)几乎是一个“可修改的提交(commit)”(除了在运行git commit
之前它不会被保存),而工作树(work-tree)保留了计算机其余部分可以使用的文件形式。1
1拥有索引和工作树两者并非必须。版本控制系统可以将工作树视为“可修改提交”。这就是Mercurial的做法,也是Mercurial不需要索引的原因。这种设计可能更好,但Git的工作方式不同,因此在使用Git时需要索引。索引的存在是使Git如此快速的一个重要因素:没有它,Mercurial必须格外聪明,并且仍然不如Git快。
提交记录记住它们的父级;新的提交记录是子级
当您通过运行git commit
进行新的提交时,Git会获取索引内容并在此时将其中的所有内容永久快照。 (这就是为什么您必须使用git add
命令:将其从您的工作目录中更改后的文件复制回到索引中,以便准备为新快照“拍照”)Git还收集提交消息,并且当然使用您的名称、电子邮件地址和当前时间来创建新提交。
但是,Git还将当前提交的哈希ID存储在新提交中。 我们说新提交“指向”当前提交。 例如,考虑这个简单的三个提交记录的存储库:
A <-B <-C <
在这里,我们说
分支名称master
“指向”第三个提交,我已经标记为
C
,而不是使用Git的难以理解的哈希ID,如
b06d364...
。(名称
HEAD
指的是分支名称
master
。这就是Git如何将字符串
HEAD
转换为正确的哈希ID的原因:Git跟随
HEAD
到
master
,然后从
master
读取哈希ID。)实际上,提交
C
本身“指向”-保留了提交
B
的哈希ID;而提交
B
指向提交
A
。(由于提交
A
是有史以来的第一个提交,因此没有更早的提交可以指向它,因此它根本不指向任何地方,这使它有点特殊。这称为
根提交。)
为了创建一个新的提交,Git将索引打包成一个快照,并保存您的名称、电子邮件地址等信息,同时包括提交C的散列ID,以创建具有新散列ID的新提交。由于我们不知道新的散列ID是什么,因此我们将使用D代替它。请注意保留HTML标签。
A <-B <-C <-D
注意
D
指向
C
。现在,
D
存在后,Git会更改存储在名称
master
下的哈希ID,以存储
D
的哈希ID而不是
C
的。存储在
HEAD
中的名称根本不会改变:它仍然是
master
。因此,我们现在有了这个:
A <-B <-C <-D <
您可以从这个图表中看出Git的工作原理:给定一个名称,比如
master
,Git只需沿着箭头找到最新的提交。该提交有一个向后的箭头指向它更早或父级的提交,父级提交又有另一个向后的箭头指向它自己的父级,以此类推,直到所有祖先都指向根提交。
请注意,虽然子提交记住了它们的父级,但父级提交并不记住它们的子提交。这是因为
任何提交的任何部分都永远不会改变:Git实际上
不能将子提交添加到父级提交中,甚至不尝试。Git必须始终向后工作,从新到旧。提交箭头都自动指向后面,所以通常我甚至不画它们:
A
分布式仓库:git fetch 做了什么
当我们使用 git fetch
时,我们有两个不同的 Git,拥有不同但相关的仓库。假设我们有两个位于不同计算机上的 Git 仓库,都从相同的三个提交开始:
A--B--C
因为它们起始于相同的提交,所以这三个提交也有相同的哈希ID。这一部分非常聪明,也是哈希ID的原因:哈希ID是提交内容的校验和
2,因此任何两个完全相同的提交都具有相同的哈希ID。
现在,在你的Git和你的仓库中,你添加了一个新的提交
D
。同时,他们——不管他们是谁——可能已经添加了自己的新提交。我们将使用不同的字母,因为他们的提交必须具有不同的哈希值。我们还将主要从你(Harry)的角度来看待这个问题;我们称他们为
"Sally"。我们将向我们关于
你的仓库的图片中再加入一件事情:它现在看起来像这样:
A
\
D <
现在假设Sally进行了两次提交。在她的代码库中,她现在有这样的内容:
A
或者(如果她从您那里获取,但尚未运行 git fetch
):
A
\
E
当你运行
git fetch
命令时,你将自己的Git连接到Sally的Git,并询问她是否在
C
提交之后添加了任何新的提交到她的
master
分支。她确实有——她有了新的提交
E
和
F
。所以你的Git从她那里获取这些提交,以及完成这些提交的快照所需的所有内容。然后你的Git将这些提交添加到你的代码库中,这样你现在就拥有了这些提交:
E
/
A
\
D <
如您所见,
git fetch
对您做的事情是收集她所有的新提交并将其添加到您的代码仓库中。
为了记住她的
master
在哪里,现在您已经与她的Git交谈,您的Git将
她的主分支复制到
sally/master
。 您自己的
master
和
HEAD
都不会发生任何变化。 只有这些“另一个Git存储库的内存”,Git称其为
远程跟踪分支名称,会更改。
这个哈希值是一个加密哈希值,部分原因是为了防止Git被欺骗,另一部分原因是因为加密哈希值自然地适合于Git的目的。当前哈希使用SHA-1,它曾经是安全的,但现在已经遭受到暴力攻击,并且正在被弃用。Git可能会转向SHA2-256或SHA3-256或其他更大的哈希值。这将有一个过渡期,可能会有一些不愉快的事情发生。 :-)
现在你应该合并或变基——git reset
通常是错误的
请注意,在从Sally获取之后,只有您的存储库拥有来自您和Sally的所有工作,而Sally仍没有您的新提交D
。
即使您的另一个Git被称为origin
,这仍然是正确的。现在,您必须采取一些措施将您的新提交D
与他们的最新提交F
连接起来:
A
\
E
我将 D
移到了顶部,是为了绘图的原因,但这与之前的图形相同。
在这里,你主要有两个选择:使用 git merge
或 git rebase
。(还有其他方法,但这两种方法是最值得学习的。)
合并实际上更简单,因为 git rebase
做的事情涉及到合并的动词形式——合并。而 git merge
所做的是运行合并的动词形式,然后将结果作为一个新的提交进行提交,该提交称为合并提交或简称 "合并",这是合并的名词形式。我们可以用以下方式绘制新的合并提交 G
:
A
\ /
E
与普通提交不同,
合并提交有
两个父级。
3它连接到用于进行合并的两个早期提交。这使得将您的新提交
G
推送到
origin
成为可能:
G
带有您的
D
,但也连接回他们的
F
,因此他们的Git可以接受这个新更新。
这种合并与合并两个分支得到的合并相同。实际上,在这里,您已经合并了您的
master
与Sally的(或
origin
的)
master
。
使用
git rebase
通常很容易,但它所做的事情更加复杂。它不是将您的提交
D
与他们的提交
F
合并以创建一个新的合并提交
G
,而是
复制您的每个提交,使得这些新的
副本(即新的、不同的提交)在您的
上游最新提交之后。
在这里,您的上游是origin/master
,而您拥有的他们没有的提交只有您的一个提交D
。因此,git rebase
会创建D'
的一个副本,我将其称为D'
,将该副本放置在他们的提交F
之后,以便D'
的父提交是F
。中间图像如下:5
A
\
E
\
D' <-- HEAD
复制过程使用与
git merge
相同的合并代码来执行动词形式
合并,从提交
D
中合并您的更改。
4 然而,一旦复制完成,rebase代码会看到没有更多提交可以复制,所以它会将您的
master
分支
更改为指向最终复制的提交
D'
。
A
\
E
\
D' <-- master (HEAD)
这意味着我们放弃了原始提交
D
.
6,因此我们可以停止绘制它,现在我们得到:
A
\
D' <-- master (HEAD)
现在,你可以轻松地通过
git push
将新提交的代码
D'
推送回
origin
。
3在Git中(但不是Mercurial),合并提交可以有多个父提交。这并没有做什么你不能通过重复合并来完成的事情,所以它主要是为了炫耀。
4技术上讲,至少对于这种情况,合并基础提交是提交C
,而两个尖端提交是D
和F
,因此在这种情况下它完全相同。如果您重新定位超过一个提交,则会变得有点复杂,但原则上仍然很简单。
5这种中间状态,其中HEAD
与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 fetch
和您选择的第二个命令-如何工作之后才使用
git pull
。
git help
开始;它知道所有的答案。尝试git help fetch
。 - axiac