在这一点上,我会尝试回答,但我不想陷入你引用的书所说的细节中。
我认为看待这个问题的方式是从“接收方”,即考虑当你是一个使用 git 的普通人并且有人推送到你的存储库时会发生什么(与你通常所做的相比,这是从他们那里取来的)。 Git为此提供了很多机制,以及一个可以正常工作的默认策略,即:“只是说不”。 :-) 较新版本的 git (2.3+) 增加了一些提供安全的策略,同时允许一些这些推送;您是否会称它们为“正确”或“完美”更多的是见仁见智。 (考虑一下如果有人开始在您的非裸“已部署”主机上进行编辑,然后,比如说,睡着了,这样其他人就无法推送它,因为它现在对 git “看起来很脏”。)
首先请记住,可以将存储库设置为“裸”,这告诉 git 不要寻找工作树。 (您可以将非裸仓库转换为裸仓库,反之亦然,但根据我的经验,大多数人都会在此过程中搞砸一些事情。使用git clone --bare 最初会设置一个裸克隆,并避免创建工作树,这意味着这里没有混淆或错误的可能性。1) 给定一个没有工作树的存储库,没有人会进入它的工作树并做任何工作,并且“推送搞砸了”进度工作”的情况是不可能的。
考虑到这一点,让我们看看当我们是推送的接收者时会发生什么,并且我们有一个非裸存储库,它有一个实际的工作树。我们还要记住两点:
-
因为我们有一个工作树,所以我们还有一个索引文件 (.git/index),它通常扮演着“放置下一次提交的位置”和“缓存以加速 git status 和类似的双重角色”操作”。
-
我们还有一个“当前”分支,保存在HEAD文件中,2我们可以直接查看,也可以使用git symbolic-ref HEAD读取(后者是正式认可的)方法)。如果当前分支是br,则HEAD 文件包含一行读取ref: refs/heads/br,例如,git symbolic-ref HEAD 打印refs/heads/br。
(如果我们处于“分离 HEAD”模式,HEAD 文件包含原始 SHA-1,而不是 ref: refs/heads/<em>branch</em>。在这种情况下,接收推送不会搞砸正在进行的工作,所以我们可以放心地忽略这种情况。)
这是接收推送背后的底层机制:
-
作为接收者,我们首先接收要添加到存储库的对象。3我们添加所有这些对象,即使我们最终会拒绝一个或多个引用更新。
-
现在我们得到一个提案列表,其一般形式如下:“请将refs/heads/X 设置为1234567...”,“强制将refs/heads/Y 设置为fedcba9...”,等等。这些对应于启动push 正在使用的参考规范。 (如果提供的 SHA-1 是全零,他们的 git 要求我们删除这些引用。)
我们会考虑这些参考更新请求中的每一个,部分是一次一个,部分是整个 gulps,应用下面列出的子规则。对于通过的更新,我们将提供的引用设置为提供的 SHA-1,并告诉另一个 git “ok,done”;对于失败的更新,我们告诉另一个 git “不,拒绝”并提供更多“原因”消息。 (我们还将钩子的 stderr 输出,有时是 stdout 输出传递给另一个 git。有一个小协议告诉他哪些是我们自己的答案,哪些只是传递的输出,以便他的 git 知道哪些更新我们接受了。)
-
一旦我们都完成了,我们会运行一个“post-receive hook”并将成功的更新传递给它(与 pre-receive hook 的形式相同,但会消除被拒绝的更新)。
现在让我们(简单地)介绍一下用于接受或拒绝个别更新和/或整体更新的规则。这些不一定是实际的内部顺序(我将忽略一些特殊情况,例如receive.shallowUpdates):
-
某些更新必须通过各种内置测试,最常见的是“快进”测试,有时还有“永不更改”测试。究竟哪些 refs 以哪种方式进行测试取决于我们的 git 版本、我们的配置和此更新的 force 标志。有关这些的详细信息,请参阅the git config documentation,特别注意receive.denyDeletes 和receive.denyNonFastForwards,并注意在 git 1.8.2 之前,git 用于将快进规则应用于标记更新(但不是标记删除),当标记已更改为“从不更改”(但仍允许删除,除非设置了 receive.denyDeletes)。
-
整个更新集被发送到pre-receive 钩子(在其标准输入上,作为一系列行)。它们首先增加了一点信息:与每个引用关联的当前 SHA-1,或者如果我们还没有该引用,则为全零。如果该钩子退出非零,则 整个更新集 - 整个推送 - 被拒绝。 (如果钩子不存在,我们认为这个测试已经通过了。)
-
每个单独的更新都发送到update 挂钩(作为参数)。如果该钩子退出非零,则此特定更新被拒绝,但我们继续验证其他更新。 (和以前一样,如果钩子不存在,则测试自动通过。)
-
最后,我们有“非裸存储库”规则,这是您在这里关心的,我将分开到他们自己的部分。
更新非裸存储库:允许的内容和原因
(我看到VonC beat me to this,但我会继续详细说明。)
git 1.6.6、git 2.3 和 git 2.4 中的新配置条目或值是:
-
receive.denyDeleteCurrent:这个选项实际上是在 git 1.6.2 中引入的,但直到 git 1.6.6 才真正做任何事情。在此之前,无论当前分支是什么,接收到推送删除,都会删除当前分支的引用。当它这样做时,HEAD 指向一个不存在的分支(要修复它,您必须使用管道工具或直接修改文件)。在 git 1.6.6 中,默认情况下不允许这样做。 (我还没有测试过“坏 HEAD”的事情是否仍然发生。)
-
receive.denyCurrentBranch:这也在 1.6.2 中出现,并在 1.6.6 中启用(即默认的“拒绝”操作生效)。但是,它在 git 2.3 中增加了新的 'updateInstead' 值。
请注意,这两个都是特定于“当前分支”的,即HEAD 所指的(单个)引用refs/heads/<em>br</em>。同样,它们仅适用于未设置 core.bare 的情况。在这种情况下,有一个工作树,其中包含在某种程度上与refs/heads/<em>br</em> 中归档的任何 SHA-1 相关的文件。还有(可能4)一个索引文件,它可能有也可能没有add-ed、rm-ed,如果你在中间,它会保持合并状态有冲突的合并。
假设通过receive.denyCurrentBranch,您允许某人的git push 更改您的存储库中为refs/heads/br 存储的SHA-1。进一步假设您没有设置任何部署挂钩,并且没有使用新的(2.3+)功能。然后,在这种情况下,如果其他人更改了您的 refs/heads/br,您自己的索引和工作树将完全保持不变。具体来说,假设 br 曾经指向提交 2222222...,而其他人(例如 Bob)刚刚成功推送并将其更改为 3333333...。
如果您现在完成自己的编辑/合并/任何事情,git add 照常运行结果,然后运行git commit,git 将从您当前的索引进行一个新的提交,其中包括“提交2222222... 中的所有内容,除了你的git add 和git rms”。 Bob 在3333333... 中所做的事情不在 在您的索引中。不过,git 所做的新提交将以 3333333... 作为其父级,同时使用从基于 2222222... 的索引中获取的 contents。结果是您的提交在添加所有更改的同时还原了 Bob 的所有更改:将您的新提交与 2222222... 进行比较将显示您所做的,而将您的新提交与其父项进行比较将显示您退出所有 Bob 的工作,同时保留您的自己的作品。
如果您确实有一个钩子可以进行一些部署,那么您的索引和/或工作树的内容将完全取决于该钩子的作用。例如,如果它执行 git checkout -f,则 Bob 更改的所有内容都将替换您放入索引和工作树中的内容。
这些结果都不是任何人真正想要的。
新的updateInstead 设置更接近人们有时想要的:在允许参考更新之前(Bob 将refs/heads/br 从2222222... 更改为3333333...),git 检查您的索引和工作树是否匹配提交2222222...。如果他们这样做了,5 git 允许 Bob 的推送 并且 将该更新应用于您的索引和工作树,就好像您以某种方式发现 Bob 的推送并完成了 git checkout br,或者任何相当于使所有内容都更新的东西。
这里仍然存在一些潜在的危险。例如,假设您已经打开 README 来处理它。您花了一段时间在编辑器中查找一些参考 URL 并输入它们,但没有在任何地方写出结果。同时,Bob 已经修复了README 并运行了他的git push。您的 git 看到您的工作树是“干净的”并且更新是“安全的”,因此它会更新您的 README。
根据您的编辑器有多聪明,当您写出您的README 时,您可能会覆盖 Bob 的更改,或者您的编辑器可能会说“嘿,README 已更改,我将获取新的”并且丢失您的工作等。有人可能会争辩说这是该编辑的不良行为(我会接受该论点),但这仍然是一个潜在的问题-不仅限于编辑;您可能正在运行一些缓慢的计算过程来写入您保持源代码控制的文件,这可能会产生相同类型的问题。
Git 不会尝试决定如何处理这一切。 Git 只是为您提供配置选项(更多机制)并将最终的策略留给您。我想说 git 的默认设置是正确的;更高级的updateInstead 模式不是默认模式,因为“正确”策略不清楚。
1还有其他可能的错误,这取决于您是否想要简单的 ssh 推送的组写入模式和共享。在以前的工作场所,我们最终制定了使用脚本配置可推送存储库的策略:您可以将希望在其中看到的内容设置为私有存储库,然后您自己运行脚本或让管理员运行它,给它您的私人仓库的 URL,通过克隆创建公共和共享仓库。在那之后,我们不在乎你对那个私人仓库做了什么,但这里的重点是我们将使用--bare 进行克隆,而不必让某人——通常是我——去修复所有的损坏位。 :-)
2即使是一个裸仓库也有一个HEAD 文件,因此有一个当前分支。它也有一个索引,但没有工作目录,索引通常是无关紧要的。 (一些部署脚本最终使用了裸仓库的索引,这会导致其中一些部署脚本出现错误,但这完全是另一个问题。)当前分支有点相关:它会影响其他人的 git clone 将检查哪个分支它们在克隆过程结束时,只要它们没有指定特定的分支名称。
3通常我们将这些作为“瘦包”,也就是说,可以对我们已有的对象进行增量压缩的包。为了实现这一点,在“接收对象”步骤之前有一个步骤,我们告诉发件人我们拥有哪些 SHA-1。您可以在发件人上使用git ls-remote 查看我们告诉发件人的内容。还有一些早期的协议协商步骤。这些对于较低级别的细节很重要,但对于上述过程不重要。
4你可以删除.git/index,git会在以后需要的时候重新构建它。我不特别建议删除它,但如果您处于合并过程中,则会丢失所有存储的 git add 和 git rms,以及任何合并信息。
5另外的测试也通过了(见VonC's answer)。其中一些额外的测试并没有在updateInstead 模式下进行初始测试,而且我认为是很难找到的。