Git pull vs fetch + merge,不先fetch而合并有什么意义?

Mar*_*kus 1 git git-merge git-fetch

git pull 是指 git fetch 然后是 git merge。但是,具体来说,为什么有人会在不先进行 fetch 的情况下进行 git merge 呢?即为什么可以将一个看似单一的操作分成两个子操作?

tor*_*rek 7

(我发现这已经被否决了,但作为一种哲学问题,我认为这是有道理的。尽管这一个很难的问题!)

\n
\n

git pull 是指 git fetch 然后是 git merge。

\n
\n

从技术上讲,它后面git fetch跟着第二个 Git 命令。您可以选择要运行的第二个命令。通常的两个是git mergegit rebase。(有时\xe2\x80\x94很少\xe2\x80\x94git pull最终会作为第二个命令运行git checkout,但这不是你可以通过选项选择的命令。如果你git pull在完全空的环境中运行,就会发生这种情况存储库。我还没有检查孤立分支上发生了什么,但这也是一个合理的git pull运行位置git checkout的合理位置,因为这与新的、完全空的存储库中的情况相同。)

\n
\n

但为什么有人会在不先进行 fetch 的情况下进行 git merge 呢?

\n
\n

为什么要写代码?为什么要做任何事情?从某种意义上说,答案只是因为我想。那么为什么要合并呢?因为你想要。但这只是将问题推向了另一个层次:你为什么要这样做?你为什么想跑步git merge

\n

要回答这个问题,我们应该看看git merge可以做什么。从高级概述\xe2\x80\x94中,运行git merge\xe2\x80\x94的结果是什么有四种可能性:

\n
    \n
  • 什么都没有:git merge表示没有任何内容可以合并,或者我们给出的参数无效,或者有错误,或者其他什么。这可能不是我们想要运行 的原因git merge,但它是一种可能性,所以我们必须将其保留在列表中。

    \n
  • \n
  • 我们得到了 Git 所说的快进合并,它实际上根本不是合并。这可能就是我们想要的!或者也可能不会。我不会在这里讨论任何细节,只是说“快进合并”实际上更多的是检查我们在合并命令中命名的提交,并将分支名称向前拖到此处操作。

    \n
  • \n
  • 我们遇到一个或多个合并冲突,并且合并在中间停止。这可能不是我们想要的,但它可能是获得我们想要的东西的路上的绊脚石。

    \n
  • \n
  • 我们得到一个合并提交,它与任何普通提交\xe2\x80\x94一样,都是添加到当前分支\xe2\x80\x94的新提交,只不过它有一个额外的父提交。这意味着,当我们查看存储库\xe2\x80\x94中的历史记录时,请记住,“存储库中的历史记录”由存储库\xe2\x80\x94中的提交组成,我们将在视图中看到一个分叉,这样我们就可以沿着这个分支的“主线”继续旅行到过去,或者我们可以沿着合并的另一条路旅行到过去

    \n
  • \n
\n

最后一个\xe2\x80\x94获得最终合并提交\xe2\x80\x94可能就是我们运行git merge. 事实上,如果我们想确保获得合并提交,即使git merge 可以进行快进,我们也应该运行git merge --no-ff.

\n

但有时,快进正是我们想要的。在这种情况下,我们应该使用git merge --ff-only. 如果无法快进,这将产生错误并使合并失败,而不是产生合并提交。

\n

这仍然没有回答问题

\n

和以前一样,这只是将问题推向了另一个层次:为什么我们想要或不想要合并提交? 至少,我们确实了解了什么是合并提交:历史上的分叉。请记住,当我们向前工作、进行新的提交时,Git却向后工作。因此,当我们将两段历史结合起来时,向后看就会将一段历史一分为二。

\n

要了解为什么我们可能想要这样分割历史,我们必须像 Git 一样向后思考。请记住,每个提交都保存一个快照,因此,要获取更改,我们必须将该快照与某个较早的快照进行比较。使用普通的非合并提交,这很容易:

\n
...--P--C--...\n
Run Code Online (Sandbox Code Playgroud)\n

C是父提交的子提交P。只有一条路可走,从C返回到P。的变化C是中的快照和 中的快照之间的差异。这就是将向我们展示的东西,或者将向我们展示的东西。PCgit show hash-of-Cgit log -p

\n

然而,在合并提交时,有两个父级:

\n
...--P1\n       \\\n        C--...\n       /\n...--P2\n
Run Code Online (Sandbox Code Playgroud)\n

为了了解P1和之间发生了什么C,我们让 Git 比较这两个快照。为了了解P2和之间发生了什么C,我们让 Git 比较两个快照。因此,如果我们希望能够看到两组变化,我们需要记录父母双方的情况。

\n

这是进行合并(合并提交)的一个可能动机这反过来又会激励我们运行git merge. 现在让我们看看另一个。

\n

Git 如何查找提交

\n

Git 中的提交是有编号的,但这些数字看起来是随机的。它们实际上根本不是随机的:每个数字都是提交内容的加密校验和。这样,每一个都将是独一无二的。1然后 Git 将这些提交\xe2\x80\x94 和其他内部 Git 对象存储在键值数据库中 ,这些对象的编号类似为2 \xe2\x80\x94 。因此,Git 查找提交的方式是通过其哈希 ID。如果您记住整个存储库中的每个哈希 ID,则只需提供哈希 ID 即可获取提交。

\n

问题是,没有人3愿意记住哈希 ID。幸运的是,没有必要这样做。每个提交都已经存储了其父级或多个父级的哈希 ID,因此我们\xe2\x80\x94 和 Git\xe2\x80\x94 需要的只是能够存储某个提交链中最后一次提交的哈希 ID。我们使用分支名称来执行此操作:

\n
... <-F <-G <-H   <-- branch\n
Run Code Online (Sandbox Code Playgroud)\n

分支名称存储链中最后一次提交的哈希 ID,在本例中由字母 表示H。我们说分支名称指向提交。同时 commitH存储其父级的哈希 ID G,因此我们说H指向G。类似地,G指向F,等等。

\n

这为我们提供了该分支的整个历史,从末端开始并向后追溯。只需一个名称\xe2\x80\x94a 分支名称\xe2\x80\x94,我们就可以获得整个提交链。我们只需要记住我们想要开始的提交的名称。而且,当我们使用 向链添加新提交时git commit,Git 会自动更新名称,以便:

\n
...--G--H   <-- branch\n
Run Code Online (Sandbox Code Playgroud)\n

变成:

\n
...--G--H--I   <-- branch\n
Run Code Online (Sandbox Code Playgroud)\n

事实上,现在有一个新的最后提交根本不需要打扰我们。

\n

这是分支名称的动机,而不是 \xe2\x80\x94 无论如何 \xe2\x80\x94 使用的动机git merge,但让我们记住这一点,然后继续。

\n
\n

1这假设每个提交的内容对于该提交都是唯一的。幸运的是\xe2\x80\x94和/或通过精心设计\xe2\x80\x94它是。它还假设鸽巢原理不适用。不幸的是,确实如此。幸运的是,这种情况在实践中不会意外发生。不幸的是,这可能是故意发生的发生的。幸运的是,这种已知的冲突不会影响 Git。与此同时,Git 正在转向一种新的哈希算法。

\n

2这些对象不必是唯一的:例如,无论出于何种原因,存储相同快照的两个提交实际上可以共享快照。文件内容也存储为内部对象,这会自动删除文件的重复项。

\n

3无论如何,我不认识任何人。我什至不想记住像空树那样重要的哈希 ID那样的重要哈希 ID 。

\n
\n

如果向后合并分裂历史,那么向前呢?

\n

假设我们有一条链结束于H这样结束的链:

\n
...--G--H   <-- branch1\n
Run Code Online (Sandbox Code Playgroud)\n

我们添加第二个分支名称 ,branch2它也指向 commit H。然后我们做出两个新的提交branch1,这样我们就有:

\n
          I--J   <-- branch1\n         /\n...--G--H   <-- branch2\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们做出两个新的提交branch2,这样我们就有了:

\n
          I--J   <-- branch1\n         /\n...--G--H\n         \\\n          K--L   <-- branch2\n
Run Code Online (Sandbox Code Playgroud)\n

当从两个“最后”提交和开始向后查看时,这两个历史记录在 commit 处连接。如果这看起来很重要或有利,那么现在您已经开始了解 Git 了。JLH

\n

为了为新的合并提交提供快照git merge,这两个提交中的aJL将会找到commit H。它使用这些向后指向的箭头,通过跟踪从JIH、从LK到 的历史记录来实现这一点H。现在已经git merge找到了最好的共同起点H,它可以将快照中的内容与快照中的内容进行比较J,以查看我们做了什么,并与L快照中的内容进行比较,以查看他们做了什么。

\n

此操作将合并基础提交与其他两个提交进行比较,是合并合并为 的动词部分git merge。事实上,Git 可以自行完成此任务,而且通常相当成功,这既令人惊奇又很有用4。正是这种合并作为动词动作才git merge真正有用。

\n
\n

4 Git 只是应用一些简单的差异组合规则,这些规则纯粹是文本的。Git 对它所组合的文本的语义没有概念。一旦你意识到这一点,就开始理解为什么 Git 可以组合多种源代码,但不能组合\xe2\x80\x94(例如 \xe2\x80\x94)大量 XML 文本。

\n
\n

我们还没有完全完成真正的合并

\n

git merge留下合并提交的能力对于历史目的很有用,但在某些方面,这只是合并作为动词操作的附属物,因为它留下了图形设置,以便下一个操作 git merge具有合理的合并根据。要了解其工作原理,请考虑如果您在两个独立的分支上工作,并且偶尔将一个分支合并到另一个分支中,您会得到什么。让我们从“之后”图片开始,然后获取导致此结果的“期间”图片:

\n
            o--o--o--o--o--o   <-- feature/short\n           /       \\\n...--A----F----M----N---P   <-- develop\n      \\       /        /\n       o--o--B--o--o--C--o--o   <-- feature/tall\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,我们有一个名为 的分支,develop我们可以在上面编写偶尔的修补程序提交(例如 commit F),但我们不会编写干扰系统实际使用的新功能。这是我们的第一张“期间”图片:

\n
            o--o   <-- feature/short\n           /\n...--A----F   <-- develop\n      \\\n       o--o--B   <-- feature/tall\n
Run Code Online (Sandbox Code Playgroud)\n

然而,当我们开发新功能时,我们意识到我们正在做的一些工作只是真正应该在开发线上进行的准备工作,或者已经准备好,或者其他什么。我们现在正在分支B上提交feature/tall,我们决定我们在 \xe2\x80\x94 上所做的一切都应该立即o-o-B进入。develop所以我们运行:

\n
git checkout develop\ngit merge feature/tall\n
Run Code Online (Sandbox Code Playgroud)\n

如果合并进展顺利,Git 会M自行进行新的合并提交:

\n
            o--o   <-- feature/short\n           /\n...--A----F----M   <-- develop\n      \\       /\n       o--o--B   <-- feature/tall\n
Run Code Online (Sandbox Code Playgroud)\n

Git 用来进行的合并基础提交是 commit ;输入提交是\xe2\x80\x94我们保留的修补程序\xe2\x80\x94和。现在提交已经完成,我们继续努力: MAFBMfeature/tall

\n
            o--o   <-- feature/short\n           /\n...--A----F----M   <-- develop\n      \\       /\n       o--o--B--o--o--C   <-- feature/tall\n
Run Code Online (Sandbox Code Playgroud)\n

同时,我们\xe2\x80\x94或某人,无论如何,在feature/short\xe2\x80\x94上工作已经发现他们应该进行合并提交N,这给了我们:

\n
            o--o--o   <-- feature/short\n           /       \\\n...--A----F----M----N   <-- develop\n      \\       /\n       o--o--B--o--o--C   <-- feature/tall\n
Run Code Online (Sandbox Code Playgroud)\n

当我们合并feature/tall\xe2\x80\x94 或更准确地说,将 \xe2\x80\x94 提交到(commit )C的尖端提交时,Git 将从to向后工作,然后到以及两者。换句话说,提交是分支上,通过合并提交。Git 还将从 向后工作,通过两次提交,到,因此这是最好的共享提交。因此,下一个“合并为动词”过程只需将我们的更改放入,同时保留更改(\ 的“更改”直接通过-vs-比较反映出来:它们基本上只是保留修补程序)。developNNMFBBdevelopCoBo-o-CdevelopM-NMBN

\n

一旦我们完成了一项功能,我们就会将其最后一次合并,然后,因为最终合并RD一个父项,所以我们可以完全删除该名称 feature/tall。如果我们需要找到 commit D,我们将通过查看 commit 的第二个父级来做到这一点R

\n
            o--o--o--o--o--o   <-- feature/short\n           /       \\\n...--A----F----M----N---P--------R   <-- develop\n      \\       /        /        /\n       o--o--B--o--o--C--o--o--D\n
Run Code Online (Sandbox Code Playgroud)\n

这一切都运行得很好(或者说效果很好),这就是我们使用git merge. 差异组合加上一些基本的图论让我们走得很远。

\n

我可能已经说服你合并了,但是呢git fetch

\n

如果我们都认为这git merge是有用的,为什么我们不总是git fetch先运行呢?好吧,在回答这个问题之前,我们需要问为什么要跑步git fetch更不用说在跑步之前了git merge要理解这一点,我们需要考虑 Git 是一个分布式版本控制系统这一事实。

\n

我们获得某个存储库的副本,并在我们的副本中工作并进行新的提交。其他人可能会控制原始存储库\xe2\x80\x94(我们复制的\xe2\x80\x94),并且可能会在那里进行新的提交。或者,也许原始存储库托管在集中式服务器类型站点(例如 GitHub 或 Bitbucket 等)上,并且一个或多个人正在向它发送新的提交。

\n

如果我们正在处理这种情况,那么与其他用户协调就有意义了。也就是说,如果我们协作使用 Git并且想要获取其他人的新提交,git fetch这是一个很好的方法。

\n

然而,一旦我们引入这个额外的存储库,我们就会陷入很多复杂的情况。特别是,提交是共享的:它们具有全宇宙唯一的哈希 ID,因此任何两个 Git 在任何时候都可以暂时相互连接,并且只向对方显示它们的哈希 ID。如果一个 Git 具有哈希 ID,而另一个 Git 没有,则该 Git 具有另一个 Git 所缺少的提交或其他内部 Git 对象。和git fetch命令git push为我们提供了连接一对 Git 并让它们相互传输提交的方法。因此,提交总是具有唯一的编号,因此可以通过这种方式轻松共享。但问题是:分支名称根本不以这种方式共享。

\n

要了解为什么不共享分支名称,只需想象一下您和朋友或同事都在工作并计划进行协作。让我们给这里涉及的两个人起个名字吧。我们将使用标准名称Alice 和 Bob,并且我们将讨论具有三个存储库的设置:

\n
    \n
  • Alice拥有Alice的存储库;
  • \n
  • Bob 拥有 Bob 的存储库;和
  • \n
  • 它们都使用 GitHub 或 Bitbucket 等中央站点第三存储库相互共享。
  • \n
\n

Alice 和 Bob 都以 开头。这为他们提供了一个包含一些提交的存储库。现在,这里有一个秘密5:当他们还没有完全完成时,Alice 和 Bob 都没有任何分支。相反,它们具有 Git 所谓的远程跟踪分支名称,我将其称为远程跟踪名称,因为它们实际上不是分支名称。git clone urlgit clone

\n

如果您(或 Alice 或 Bob)运行,那么您所做的就是指示 Git作为其克隆操作的最后一步来运行。这是实际创建分支名称的最后一步。如果省略该选项,您的 Git 会询问位于 \xe2\x80\x94 的另一个 Git \xe2\x80\x94 使用什么分支名称。目前通常的默认值是. 6git clone -b branch urlgit checkout branch-burlmaster

\n

这是最后一步,实际上在您的存储库中创建了您自己的分支。所以 Alice 得到 a master,Bob 得到 a master,他们的两个masters 都基于他们的,origin/master而后者本身又基于master中央服务器上的第三个 Git 中的 。

\n
\n

5这并不是真正的秘密,但在 Git 介绍中通常会被忽视。

\n

6 GitHub 计划main很快将其更改为新存储库;现有的存储库仍然会推荐他们推荐的任何内容。GitHub 提供了一个 Web 界面,让您可以调整您控制的任何存储库。

\n
\n

让我们画出爱丽丝和鲍勃的情况

\n

我们现在有三个存储库,我们\xe2\x80\x94作为某种全知的神可以以某种方式一直知道所有的事情\xe2\x80\x94所以我们将画出所有三个存储库的图片:

\n
central-server:  ...--G--H   <-- master\n\nalice: ...--G--H   <-- master, origin/master\n\nbob:   ...--G--H   <-- master, origin/master\n
Run Code Online (Sandbox Code Playgroud)\n

现在,Alice 比 Bob 更快地进行了新的提交:

\n
                 I   <-- master\n                /\nalice: ...--G--H   <-- origin/master\n
Run Code Online (Sandbox Code Playgroud)\n

鲍勃第二次做出承诺;由于提交具有唯一的 ID,我们将调用他的J

\n
bob:   ...--G--H   <-- origin/master\n                \\\n                 J   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n

既然我们有了这种无所不知的概述,那么让我们画出如果将所有提交合并到一个存储库中会发生什么:

\n
                 I\n                /\n[all]  ...--G--H\n                \\\n                 J\n
Run Code Online (Sandbox Code Playgroud)\n

我们应该使用什么名称来查找提交IJ?一个名字,比如master,只允许记住一次提交。所以这里没有合适的名称可以使用。

\n

为了让 Alice 和 Bob 合作,其中一人必须使用git push. 该git push命令并不像 那样聪明git fetch7 它的工作原理是向某个服务器发送提交,8然后要求服务器的 Git 设置其 xe2x80x94 服务器的 Git 的 xe2x80x94 分支名称以记住该提交。因此,如果我们假设爱丽丝再次首先到达那里,我们就有了这样的全局视图:

\n
central-server:  ...--G--H--I   <-- master\n\nalice: ...--G--H--I   <-- master, origin/master\n\nbob:   ...--G--H   <-- origin/master\n                \\\n                 J\n
Run Code Online (Sandbox Code Playgroud)\n

(我对绘图进行了一些线性化以节省空间:放在I的右侧H)。

\n

鲍勃只是还没有提交I。他必须跑去git fetchI服务器。之后,他得到:

\n
bob:   ...--G--H--I   <-- origin/master\n                \\\n                 J   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n

也就是说,他的Git 现在知道origin/master应该识别 commit I,并且他的提交J仅在他的master.

\n

如果 Bob 尝试git push提交J,他会要求服务器将 master设置为指向J。他们会拒绝,因为如果他们这样做,他们将丢失commit副本I。无论 Bob 是否知道提交I是否存在,都会发生这种情况:中央服务器知道,并且Git正在执行检查的计算机正在执行检查。

\n

由于 Alice 击败了 Bob,现在 Bob 的工作就是决定如何处理 Alice 的提交I和 Bob 的提交之间的历史分叉J

\n
\n

7早期 Git 中不存在远程跟踪名称的概念,额外的聪明才被添加到 中,因为它实际上git fetch没有意义git push:服务器存储库通常没有一直在看管它们,所以没有人可以利用它。

\n

8要接收git push,站点必须提供某种身份验证和访问控制,因为 Git 不提供。提供身份验证和访问控制就足以将该系统称为服务器。你可以git fetch从一个没有所有这些东西的客户端,Alice 和 Bob 可以做点对点的 Git,根本不需要打扰中央服务器,如果他们愿意的话,通过使用git fetch从一个没有所有这些东西的客户端,Alice 和 Bob 可以通过相互通信但这要求他们提供只读服务,让其他人无需首先进行身份验证即可获取提交。或者,如果他们有足够好的、足够安全的系统,他们可以直接在自己的系统上提供 ssh 或 web 服务。不过这有点痛苦,这就是 GitHub 这样的服务如此受欢迎的原因。

\n
\n

现在我们可以明白为什么鲍勃想要git fetch

\n

鲍勃现在想跑步,git fetch因为:

\n
    \n
  • Bob想要与Alice合作,而不是对她粗暴;
  • \n
  • Bob 发现了git push故障,或者 Bob 已先发制人地运行git fetch以避免看到git push故障。不管怎样,鲍勃现在知道提交I存在。
  • \n
\n

Bob 现在可以运行git merge、 或git rebase或任何其他 Git 命令来安排对他的提交执行某些操作,J以便它更适合 Alice 的提交I

\n

但这给了我们获取并合并的动机!

\n

正是这样:鲍勃的情况向我们展示了为什么鲍勃会运行git fetch然后然后运行,而你的问题更多的是为什么鲍勃可能会在没有先运行git merge的情况下运行。他为什么要这么做?git merge git fetch

\n

最终,如果他真的这么做了,我们就得问他为什么。但这里有一些可能性:

\n
    \n
  • 爱丽丝不存在。没有中央服务器存储库。鲍勃正在独自工作。

    \n
  • \n
  • 或者,无论 Alice 是否存在,Bob 自己创建了多个分支,做出了一些他从未向其他人提供过的承诺。没有其他人拥有这些提交,因此没有其他人可能将这些提交用于任何目的。Bob 可以安全地合并这些提交,而无需考虑其他人可能做了什么,因为没有人可以任何事情

    \n
  • \n
  • 或者,Bob 得到了 Alice 的提交,但这是错误的。鲍勃不想要爱丽丝的提交。

    \n
  • \n
  • 或者,Bob 得到了 Alice 的提交,这是正确的,Bob 意识到他应该创建一个feature/tall分支。他现在可以这样做,而不是合并。

    \n
  • \n
\n

最后一个是我避免的动机之一git pull

\n

最后一种可能性是不使用git pull. git fetch我喜欢按照步骤 1运行,然后看看发生了什么

\n

根据git fetch获取的内容,我可能决定运行:

\n
    \n
  1. 无事,因为无事可做;
  2. \n
  3. git merge --ff-only,以确保我可以进行快进而不是合并;
  4. \n
  5. git log(也许是-p),看看新东西;
  6. \n
  7. git rebase,因为我在步骤 3 中看到的新内容很好,而且我的内容也很好地添加了;
  8. \n
  9. git checkout -b建立一个新的分支;
  10. \n
\n

或者我忘记列出的其他东西(除了git merge,我几乎从不想要)。所以我经常git fetch先运行,然后运行git merge --ff-only,但我经常在两个操作之间插入一些其他命令。

\n

其他人有不同的动机。这里的要点是,虽然获取和合并或获取和重新设置序列非常常见,并且也许应该有一个像git pull这样的方便命令来为您做这件事,但这并不是唯一的工作方式。

\n