git 遵循什么逻辑来确定远程包含本地存储库中不可用的工作

what logic does git follow to determine that remote contains work not available in local repository

当我将提交推送到远程时,它们经常被拒绝,因为远程包含我在本地没有的工作。我想知道 git 得出该结论的确切逻辑是什么?我试过谷歌搜索,但没有找到一个很好的过程说明。我可能会假设,为了将提交推送到远程分支,这个被推送的提交必须让远程分支上的所有提交都可用,因为它是祖先,是否正确?例如,存在以下内容:

A-B-C (remote master branch)
 \
  A-D (local master branch)

当我执行 git push origin master git 获取 origin/master 上可用的所有提交(可能从共同祖先的角度 - A 此处)并检查是否我尝试推送的最后一次提交(现在是提交 D)将它们作为其祖先。像这样的伪代码:

foreach (commitsInOriginMaster as commit) {
    if (!localCommitToPush.hasAncestor(commit)) {
        rejectPush();
    }
}

TL;DR 执行摘要

根据结果是否为 "fast-forward", 远程 允许或拒绝推送。当标签的新目标将该标签的旧目标作为祖先时,就会发生快进(正如您已经猜到的那样)。

长讨论版

这里有两点我认为你需要明白。首先是 git 基于 "references" 执行此操作,如果它是 "fast-forward",则允许​​非强制推送,因此您需要获得(在某种深刻的理解中) "fast-forward" 是什么。

第二点,也许在这种情况下更重要,是 远程 决定您的操作是否是 "fast forward"。这一点非常关键,因为 git 是分布式的,您的推送可能与其他人的推送同时发生1

什么是快进和什么不是快进的例子

让我们先处理其中的第一个,因为它更容易一些。正如您已经注意到的,"has ancestor" 是关键测试。让我像您一样绘制一些提交链,但我将唯一地标记每个节点并添加更多的分支和重新合并。而且,我将添加一些指向每个 "tip-most" 提交的标签:

    B
  /   \
A - C - D - E    <-- L1
  \
    F - G        <-- L2

现在,假设有另一个标签(例如 master)指向其中一个提交 AG。我们可以要求 git 将 any 标签从 any 提交到任何其他提交,使用 git update-ref(低级命令毫无疑问地服从我们,而不是检查这样做是否明智):

git update-ref <refname> <commit-id>

或者我们可以更安全,使用更智能、更高级的命令来检查某些操作是否是 "fast forward"。那么什么是快进?

好吧,想象一下我们的问题标签要提交 A。如果我们告诉 git 将其移动到任何其他提交,git 只会测试一件事:"is A an ancestor of the new commit?" 如果是这样,那就是 "fast forward"。如果不是,那就不是。由于 A 是(唯一的)根提交,它始终是所有其他提交的祖先,因此这始终是快进。

想象一下,我们的标签指向 B。如果我们要求 git 将其移动到 A,那是 而不是 快进。如果我们要求 git 将其移动到 FG,这些也不是快进操作。即使 C 也不是快进的,因为 C 的唯一祖先是 A。但是 DE 都有 B 作为祖先,所以将我们的标签从 B 移动到 E 是一个快进。

另一种(可能更简单)的查看方式是:从任何提交中,该提交历史中的所有提交都是可查找的 ("reachable") ,因为每个提交都指向其父项。 (在 D 的情况下,它指向 BC:它是一个合并提交并且有两个父项。)鉴于这个事实,快进标签移动是一个移动如果进行了,那么每个提交 previously 都可以从该标签访问,still 可以从该标签访问。非快进会使一些当前可找到的提交不再可访问——至少,不是从那个标签。 (其他一些标签,如 L1L2,可能会使其在更全局的意义上可以访问,但在定义快进性时这不算数。)

远程允许或拒绝推送

现在让我们看一下第二部分。当你 运行 git push <em>remote</em> <em>refspec</em>,2 你的 git 联系远程 git (或 git 模拟器)并与之进行一些对话: "what labels do you have, and what SHA-1s do they point to?" 然后你的 git找出哪些提交和其他对象(1)你有,(2)他们没有,以及(3)如果你的推送成功,他们将需要。你的git把这些打包3发过来。只有在之后 远程决定是否允许推送。

一旦遥控器上的一切就绪,遥控器就会查看您的 git 给它的内容,以及您的 git 告诉它设置的标签。它会查看那些标签现在在遥控器上的位置。如果标签不存在(例如,您正在创建新的分支或标签),那没关系(至少到目前为止)。4 如果标签 存在,标签可能要动了。

对于分支标签,遥控器将检查该移动在遥控器上是否是快进。如果是这样,则允许移动(同样,至少到目前为止)。否则,移动将被拒绝为 "non-fast-forward",除非您设置强制标志。5

(对于标记移动,较旧的 git 使用分支规则,而较新的则除非被迫才说不。)

最终,在遥控器查看了您提议的所有标签更改后,它要么允许部分或全部更改,要么拒绝部分或全部更改。然后,一旦完成,遥控器就会有一组新标签指向新的和更新的,或者相同的旧提交。

此时,如果还有其他人也在排队等待推送,则遥控器允许他们通过交换信息、打包对象和提议标签移动来开始推送。 (事实上​​ ,这通常由服务器上的单独进程处理,至少假设是标准 git 构建。如果启动了两个 "simultaneously",它们就会争先恐后地获得锁:一个获胜并继续首先,另一个等待直到锁被释放。如果启动了两个以上,再次有一个赢家,之后剩下的推手再次去锁,依此类推。)

这个简单但有效的规则的原因是,根据定义,快进永远不会 丢失 提交。它总是简单地向图表添加一些(可能为零)new 提交。所有可以到达的东西仍然可以到达,也许还有更多。

如果您想提前知道推送是否被允许,您可以连接到遥控器并询问它的标签现在在哪里。这正是 git fetch 所做的:它询问遥控器遥控器有什么,并将所有这些记录在您的存储库中。现在您可以 运行 测试您存储库中的内容——但此方法的缺陷是无论您得到什么答案,到您能够 运行 时它可能已经过时了git push。不过,这通常需要一个非常繁忙的存储库才能成为一个问题。


1Albert Einstein 尽管 :-) ...在这种情况下 "same time" 实际上是 "as decided by the remote",它在执行时锁定其他远程更新你的,反之亦然。

2在这种情况下,您的 refspec 是 master:master,即使您只是拼写它 master 一侧最重要的部分是你在左侧标识的提交;但是 遥控器 上最重要的部分是您在右侧提供的标签名称,因为这是您要求遥控器更改的标签名称。

3使用 "smart" 协议,这确实是真的:您的 git 构建了一个 "thin pack",然后将其发送过来。这就是 "counting objects" 和 "compressing" 的意义所在:构建包,然后投入大量 CPU 时间使其尽可能小,然后再通过网络发送。

4遥控器可以有 "git hooks" 可以施加额外的要求,例如 "only someone coming in as user release may move master" 或 "anyone can create a branch as long as its name starts with refs/heads/user, otherwise only blessed people may create them"。如今,大多数花哨的控件似乎都是用 gitolite 完成的。

5您可以使用 --force 全局设置此标志,或使用前导加号设置每个参考规范。