我会尝试回答这个问题,但不想陷入引用书籍的细节。我认为应该从“接收方”的角度来看待这个问题,即考虑当普通用户使用git并且有人向他们的存储库推送时会发生什么(相对于通常所做的,即从别人那里获取)。Git提供了许多机制,并提供了一个可以正常工作的默认策略:“拒绝”。 :-) Git的新版本(2.3+)添加了几个更多的策略,既提供安全性,又允许一些推送;你是否认为它们是“正确”或“完美”的更多是一种观点问题。(考虑如果有人开始在您的非裸露的“部署”主机上进行编辑,然后,比如说,睡着了,因此没有其他人可以将其推送到它,因为现在它“看起来脏”了git。)
首先要记住,一个仓库可以设置为“裸”(bare),这告诉Git不要寻找工作树。 (你可以将非裸仓库转换为裸仓库,反之亦然,但根据我的经验,大多数人在此过程中都会出错。使用
git clone --bare
最初设置裸克隆,并避免创建工作树,这意味着在这里没有混淆或错误的可能性.
1)给定一个没有工作树的仓库,没有人进入其工作树并进行任何工作,“push打乱正在进行的工作”的情况是不可能发生的。
有了这个想法,让我们来看看当我们是推送接收者时会发生什么,并且我们有一个非裸仓库,其中有一个实际的工作树。 我们还需要记住另外两件事:
因为我们有一个工作树,所以我们也有一个索引文件 (
.git/index
),它在其通常的双重角色中起作用,即“下一次提交的暂存区”和“加速
git status
等操作的缓存区”。
我们还有一个“当前”分支,存储在
HEAD
文件中
2,我们可以直接查看它,或者使用
git symbolic-ref HEAD
命令来读取(后者是正式批准的方法)。如果当前分支是
br
,则
HEAD
文件包含一个单独的行,内容为
ref: refs/heads/br
,例如,而
git symbolic-ref HEAD
命令则打印出
refs/heads/br
。
(如果我们处于“分离头指针”模式,则
HEAD
文件包含原始 SHA-1,而不是
ref: refs/heads/branch
。在这种情况下,接收推送不会破坏正在进行的工作,因此我们可以安全地忽略此情况。)
以下是接收推送的基本机制:
作为接收方,我们首先接收要添加到我们仓库的对象。3 即使我们最终会拒绝一个或多个引用更新,我们也会添加所有这些对象。
现在我们得到了一个提案列表,其一般形式如下:“请将refs/heads/X
设置为1234567...
”,“强制将refs/heads/Y
设置为fedcba9...
”等。这些对应于启动他们的push
的人使用的refspecs。(如果提供的SHA-1全是零,他们的git要求我们删除这些引用。)
我们逐个考虑每个参考更新请求,部分按照规则进行,部分整体进行,应用下面列出的子规则。对于通过的更新,我们将提供的参考设置为提供的SHA-1,并告诉另一个git“好的,完成了”;对于失败的更新,我们告诉另一个git“不行,拒绝了”,并提供更多的“原因”信息。(我们还将钩子的stderr输出和有时的stdout输出传递给另一个git。有一个小协议告诉他哪些是我们自己的答案,哪些只是传递的输出,以便他的git知道我们接受了哪些更新。)
一旦我们全部完成,我们会运行一个“post-receive hook”,并将成功的更新(与pre-receive hook的形式相同,但消除了被拒绝的更新)传递给它。
现在让我们(轻松地)介绍一下用于接受或拒绝单个更新和/或整体更新的规则。这些规则不一定按照实际内部顺序排列(我将忽略一些特殊情况,例如
receive.shallowUpdates
)。
一些更新必须通过各种内置测试,最常见的是“快进”测试,有时还会进行“永远不要更改此内容”测试。确切地说,哪些引用以及以哪种方式进行测试取决于我们使用的git版本、我们的配置以及此更新的force标志。有关详细信息,请参阅{{link1:git config
文档}},特别注意receive.denyDeletes
和receive.denyNonFastForwards
,请注意git曾经将快进规则应用于标记更新(但不适用于标记删除),直到git 1.8.2,当标记被更改为“永远不要更改”(但仍允许删除,除非设置receive.denyDeletes
)。
整个更新集发送到pre-receive
钩子中(作为标准输入的一系列行)。首先它们会附加一条更多的信息:与每个引用相关联的当前SHA-1或如果我们没有该引用,则为全零。如果该挂钩退出非零,则拒绝整个更新集——整个推送。(如果挂钩不存在,则认为此测试已通过。)
每个单独的更新都会发送到update
钩子中(作为参数)。如果该挂钩退出非零,则拒绝此特定更新,但我们会继续验证其他更新。(与之前一样,如果挂钩不存在,则测试将自动通过。)
最后,我们有“非裸库”规则,这是您在此处关心的规则,并且我将将其单独分成一个部分。
更新非裸库:允许哪些操作以及原因
(我看到VonC已经完成了这个, 但我会提供更详细的内容.)
在git 1.6.6、git 2.3和git 2.4中,新的配置项或值有:
receive.denyDeleteCurrent
: 这个选项实际上是在git 1.6.2中引入的,但直到git 1.6.6才真正起作用。在此之前,无论当前分支是什么,都会删除当前分支的引用。这样做时,HEAD
指向不存在的分支(修复它需要使用plumbing工具或直接修改文件)。在git 1.6.6中,默认情况下不再允许这样做。(我没有测试“坏HEAD”是否仍然发生。)
receive.denyCurrentBranch
: 这也是在1.6.2中引入的,并在1.6.6中启用(即默认的“拒绝”操作已生效)。但是,在git 2.3中增加了新的“updateInstead”值。
请注意,这两个命令都是针对“当前分支”而言的,即指代
HEAD
引用的单一参考
refs/heads/br
。并且需要强调的是,它们仅适用于未设置
core.bare
的情况。在这种情况下,存在一个工作树,其中充满了与文件有关的内容,这些内容以某种方式与存储在
refs/heads/br
中的 SHA-1 相关联。此外,还可能存在(或者不存在)索引文件,该文件可能已经添加、删除,并且如果您处于冲突合并的中间状态,则可能保持合并状态。
假设通过 receive.denyCurrentBranch
,您允许某人的 git push
更改您存储的 SHA-1 引用 refs/heads/br
。进一步假设您没有设置任何部署钩子,并且不使用新的(2.3+)功能。那么,在这种情况下,如果其他人更改了您的 refs/heads/br
,您自己的索引和工作树将完全不会受到影响。为了具体说明,假设 br
曾经指向提交 2222222...
,现在其他人——比如 Bob——刚刚成功推送并将其更改为 3333333...
。
如果您现在完成了自己的编辑/合并/等操作,请像往常一样使用
git add
添加结果,然后运行
git commit
,Git将从您当前的索引中创建一个新的提交,其中包括“来自提交
2222222...
的所有内容,但不包括您的
git add
和
git rm
”。Bob做的事情,即在
3333333...
中的更改,不在您的索引中。Git制作的新提交将以
3333333...
为其父级,同时使用基于
2222222...
的索引中获取的
内容。其效果是,您的提交会撤销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
在克隆过程结束时为他们检查出哪个分支,前提是他们没有指定特定的分支名称。
通常我们会将它们作为“薄包”获取,也就是说,这种包可以与我们已经拥有的对象进行增量压缩。为了实现这一点,在“接收对象”步骤之前,有一个步骤,我们告诉发送方我们拥有哪些SHA-1码。您可以通过在发送方使用
git ls-remote
来查看我们向发送方发送的内容。还有一些早期的协议协商步骤。这些对于低级细节很重要,但对于上述过程并不重要。
您可以删除
.git/index
,当git需要它时,git会重新构建它。我不特别建议删除它,但效果是失去所有存储的
git add
和
git rm
,以及如果您处于合并过程中,则失去所有合并信息。
并且其他附加测试通过(参见
VonC's answer)。其中一些附加测试不在最初尝试的
updateInstead
模式中,我认为是通过艰难的方式发现的。