git fetch 究竟是做什么的?

22

编辑:在提问之前我已经查阅了这个 What does FETCH_HEAD in Git mean?
抱歉原来的问题不够精确。

我的问题是fetch到底是如何工作的?fetch会删除当前的所有日志吗?

这是我的情况:我和我的团队正在使用只有一个分支的同一代码库。所以我们在推送任何内容之前都必须要先进行fetch操作。
我们通常是这样做的:

git status
git add .
git commit -m message1
git fetch origin
git reset head
git status
git add .
git commit -m message
git push

但是重置之后,我之前的提交(带有message1)好像消失了。

这是正常的吗?还是出了什么问题?
我怎样才能访问我的本地历史记录?
它们已同步但我的本地历史记录消失了。

旧的东西就不提了:最近我一直在学习 Git CLI。
有人告诉我输入“git fetch head”来跟踪远程分支。
但我想知道这个命令是做什么的?这个命令会覆盖我的本地日志吗?
"git fetch"和"git fetch head"之间有什么区别?


4
可能是 Git 中 FETCH_HEAD 的含义是什么? 的重复问题。 - Tim Biegeleisen
由于您正在使用命令行学习 Git 命令,请从 git help 开始;它知道所有的答案。尝试 git help fetch - axiac
谢谢,我会仔细检查这个。顺便说一下,我已经重新阐述了我的问题,非常抱歉之前的问题不够准确。 - HarryTheF
2个回答

56

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 (HEAD)

在这里,我们说分支名称master“指向”第三个提交,我已经标记为C,而不是使用Git的难以理解的哈希ID,如b06d364...。(名称HEAD指的是分支名称master。这就是Git如何将字符串HEAD转换为正确的哈希ID的原因:Git跟随HEADmaster,然后从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   <-- master (HEAD)

您可以从这个图表中看出Git的工作原理:给定一个名称,比如master,Git只需沿着箭头找到最新的提交。该提交有一个向后的箭头指向它更早或父级的提交,父级提交又有另一个向后的箭头指向它自己的父级,以此类推,直到所有祖先都指向根提交。
请注意,虽然子提交记住了它们的父级,但父级提交并不记住它们的子提交。这是因为任何提交的任何部分都永远不会改变:Git实际上不能将子提交添加到父级提交中,甚至不尝试。Git必须始终向后工作,从新到旧。提交箭头都自动指向后面,所以通常我甚至不画它们:
A--B--C--D   <-- master (HEAD)

分布式仓库:git fetch 做了什么

当我们使用 git fetch 时,我们有两个不同的 Git,拥有不同但相关的仓库。假设我们有两个位于不同计算机上的 Git 仓库,都从相同的三个提交开始:

A--B--C

因为它们起始于相同的提交,所以这三个提交也有相同的哈希ID。这一部分非常聪明,也是哈希ID的原因:哈希ID是提交内容的校验和2,因此任何两个完全相同的提交都具有相同的哈希ID。
现在,在你的Git和你的仓库中,你添加了一个新的提交D。同时,他们——不管他们是谁——可能已经添加了自己的新提交。我们将使用不同的字母,因为他们的提交必须具有不同的哈希值。我们还将主要从你(Harry)的角度来看待这个问题;我们称他们为"Sally"。我们将向我们关于你的仓库的图片中再加入一件事情:它现在看起来像这样:
A--B--C   <-- sally/master
       \
        D   <-- master (HEAD)

现在假设Sally进行了两次提交。在她的代码库中,她现在有这样的内容:
A--B--C--E--F   <-- master (HEAD)

或者(如果她从您那里获取,但尚未运行 git fetch ):

A--B--C   <-- harry/master
       \
        E--F   <-- master (HEAD)

当你运行git fetch命令时,你将自己的Git连接到Sally的Git,并询问她是否在C提交之后添加了任何新的提交到她的master分支。她确实有——她有了新的提交EF。所以你的Git从她那里获取这些提交,以及完成这些提交的快照所需的所有内容。然后你的Git将这些提交添加到你的代码库中,这样你现在就拥有了这些提交:
        E--F   <-- sally/master
       /
A--B--C
       \
        D   <-- master (HEAD)

如您所见,git fetch 对您做的事情是收集她所有的新提交并将其添加到您的代码仓库中。
为了记住她的master在哪里,现在您已经与她的Git交谈,您的Git将她的主分支复制到sally/master。 您自己的masterHEAD都不会发生任何变化。 只有这些“另一个Git存储库的内存”,Git称其为远程跟踪分支名称,会更改。
这个哈希值是一个加密哈希值,部分原因是为了防止Git被欺骗,另一部分原因是因为加密哈希值自然地适合于Git的目的。当前哈希使用SHA-1,它曾经是安全的,但现在已经遭受到暴力攻击,并且正在被弃用。Git可能会转向SHA2-256或SHA3-256或其他更大的哈希值。这将有一个过渡期,可能会有一些不愉快的事情发生。 :-)

现在你应该合并或变基——git reset通常是错误的

请注意,在从Sally获取之后,只有您的存储库拥有来自您和Sally的所有工作,而Sally仍没有您的新提交D

即使您的另一个Git被称为origin,这仍然是正确的。现在,您必须采取一些措施将您的新提交D与他们的最新提交F连接起来:

A--B--C--D   <-- master (HEAD)
       \
        E--F   <-- origin/master

我将 D 移到了顶部,是为了绘图的原因,但这与之前的图形相同。

在这里,你主要有两个选择:使用 git mergegit rebase。(还有其他方法,但这两种方法是最值得学习的。)

合并实际上更简单,因为 git rebase 做的事情涉及到合并的动词形式——合并。而 git merge 所做的是运行合并的动词形式,然后将结果作为一个新的提交进行提交,该提交称为合并提交或简称 "合并",这是合并的名词形式。我们可以用以下方式绘制新的合并提交 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通常很容易,但它所做的事情更加复杂。它不是将您的提交D与他们的提交F合并以创建一个新的合并提交G,而是复制您的每个提交,使得这些新的副本(即新的、不同的提交)在您的上游最新提交之后。

在这里,您的上游是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,而两个尖端提交是DF,因此在这种情况下它完全相同。如果您重新定位超过一个提交,则会变得有点复杂,但原则上仍然很简单。

5这种中间状态,其中HEADmaster分离,通常是不可见的。只有在动词形式的合并过程中出现问题,使Git停止并需要您帮助完成合并操作时,才会看到它。当在重新定位期间发生合并冲突时,重要的是要知道Git处于这种“分离HEAD”状态,但只要重新定位自己完成,您就不必太关心这个问题。

6原始提交链暂时通过Git的reflogs和名称ORIG_HEAD保留。 ORIG_HEAD的值将被下一个进行“大更改”的操作覆盖,reflog条目最终会过期,通常在30天后。此后,git gc将真正删除原始提交链。


git pull 命令只是运行了 git fetch 然后再运行第二个命令

请注意,在运行 git fetch 之后,通常需要运行第二个 Git 命令,即 git mergegit rebase

如果您事先知道您肯定会立即使用其中的一个命令,则可以使用 git pull,它将运行 git fetch,然后运行其中的一个命令。您可以通过设置 pull.rebase 或提供 --rebase 作为命令行选项来选择要运行的第二个命令。

在你对git mergegit rebase的工作原理非常熟悉之前,我建议不要使用git pull,因为有时git mergegit rebase无法自行完成。在这种情况下,你必须知道如何处理此故障。你必须知道你实际运行了哪个命令。如果你自己运行该命令,则会知道你运行了哪个命令以及在必要时可以寻求帮助的位置。如果你运行git pull,甚至可能不知道你运行了哪个第二个命令!

除此之外,有时您可能需要在运行第二个命令之前先“查看”。git fetch带来了多少次提交?执行合并与重新设置所需的工作量有多大?现在合并比重新设置好,还是重新设置比合并好?要回答这些问题中的任何一个,您必须git fetch 步骤与第二个命令分开。如果使用git pull,您必须事先决定要运行哪个命令,甚至在您知道哪个命令要使用之前就要做出决定。
简而言之,在熟悉它的两个部分-git fetch和您选择的第二个命令-如何工作之后才使用git pull

2
真的吗?真的吗? 我是说,当然,为努力加1分,但在这种情况下,拉取--rebase,解决冲突,推送并完成它 ;) - VonC
1
@VonC:在您了解rebase的工作方式之后再使用rebase来拉取代码:) 这里真正的大问题是找到使用“git reset”命令的人(他们真的想要一个squash吗?)。 - torek
1
OP会理解rebase的作用:这就是我小小提交图表的用途。 - VonC
2
因为详尽、准确、适当地使用脚注提供额外细节,通常写得很好且幽默,所以我点赞了。你考虑过从事技术写作吗?特别是那个与提问者名字相关的IMDB参考让我笑了。 - Jules Kerssemakers

4

您不需要进行两个单独的提交,git fetch 不会删除任何日志。

 --o--o--o (origin/master)
          \
           x--x (master: my local commits)

您需要做的是将本地提交(commit)变基(rebase)到由git fetch命令获取的任何新提交(commit)之上:
git fetch

--o--o--o--O--O (origin/master updated)
         \
          x--x (master)

git rebase origin/master

--o--o--o--O--O (origin/master updated)
               \
                x'--x' (master rebased)

git push

--o--o--o--O--O--x'--x' (origin/master, master)

更简单的方法是,自Git 2.6起,我会使用以下配置:(参考链接)
git config pull.rebase true
git config rebase.autoStash true

然后简单的git pull命令会自动在origin/master顶部重放您本地的提交。然后您可以使用git push命令。


作为一个 Git Rebase 学习者,你是否需要像这个例子一样进行快进合并?https://i.imgur.com/FfMJ77V.jpg - Royi Namir
1
@RoyiNamir 不适用于变基情况:变基不是合并,而是将一个分支的提交重放在另一个分支之上。 - VonC
我知道。只是这张图片代表了一个rebase的结果,然后为了移动分支名称,他们进行了合并。https://git-scm.com/book/en/v2/Git-Branching-Rebasing。而你在这里没有这样做。因此我才问的。 - Royi Namir
在我的情况下,另一个分支不是本地分支(您需要在合并之后重新衍合以进行快进式更新)。它是一个远程跟踪分支 "origin/xxx",您不需要将其合并。您可以通过推送到远程来更新它。 - VonC
所以基本上它允许您稍后执行 git merge master 而不是 git merge origin/master - Royi Namir
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接