如果两个或多个用户同时将其本地存储库状态推送到同一远程,则执行git:
锁定远程提交/分支/存储库以进行写入,然后再提交一个用户的整批提交,然后再提交另一用户?
还是在它从N个提交中写入单个用户的单个提交后,释放它持有的提交/存储/分支的锁?
第一个是有道理的,但我想我还是会问。
答案 0 :(得分:2)
该问题包含一个错误的假设,因此这两个选项都不正确。
存在原子性问题,但不是基于每次提交。它们基于每个参考。
如果您仅推送一个引用(例如git push origin master
),则只有一个引用需要更新。更新是成功还是失败,对于发送方来说,更新就差不多了(尽管仍然有很多接收方细节)。
如果您推送多个 引用(例如git push origin develop master
),则有多个引用需要更新。如果您的Git支持它(两面都是v2.4或更高版本),请使用git push --atomic
来确保两个推送都成功,或者都不成功。
如果您不编写推送前,接收前,更新和/或接收后挂钩,则可以在此处停止。如果您做写它们,请继续阅读。
锁定发生在接收器中,而不是发送者中(出于我希望是显而易见的原因:-))。该文档永远不会显式地调用内部细节,即使应该这样做也是如此。但是有许多单独的锁和锁定步骤。特别是:
读取参考不会要求锁定;仅更新一个 requires 对其进行锁定。这意味着纯读者可能会在过渡期间看到旧值。但是,在内部可以锁定一系列引用。请参阅下面的原子性注释。
进行锁定包括使用原子性的“如果文件已存在,则创建或失败”操作来创建锁定文件。这必须由基础操作系统提供。解锁是通过删除或重命名锁定文件来实现的:锁定文件通常包含锁定文件锁定的文件的新内容,因此要放弃锁定而不更改内容,Git只需删除锁定文件,然后通过单个原子操作Git 重命名来释放锁定并更改文件的内容。基本操作系统还必须提供原子重命名操作。
更新压缩的引用会将其转换为解压缩(“松散”),从而获得每引用锁定。打包引用显然需要获得packed-refs锁。不过,删除引用是一种特殊情况,有两种方式:
解压缩的引用也可能出现在打包的引用文件中。 (当存在散装副本时,打包副本将被忽略。)在这种情况下,Git还必须更新打包引用文件以删除两个副本。
如果存在引用,则删除引用会删除其引用日志。这在大多数情况下是不可见的,但这确实意味着参考更新代码想提前知道此是删除操作。
1 值得注意的是:有些参考是每个工作树。最初只是HEAD
,但是随着git worktree
错误的浮出水面,现在它包含了所有refs/bisect/
和refs/rewritten/
引用。 refs/rewritten/
引用本身是新的,引入了新的更新颖的交互式rebase,可重新创建任意合并。拆分bisect引用是Git 2.7.0中的一个修复。参见commit ce414b33ec038。
此外,某些引用也被视为“伪引用”。这些从来没有打包。伪引用是诸如ORIG_HEAD
,MERGE_HEAD
之类的东西。这主要是一个内部细节,但会影响哪些锁可能适用:例如,常规引用,例如refs/heads/master
,可以打包(在这种情况下适用打包的引用锁),也可以拆开(在这种情况下)未包装的参考锁适用。
由于您对推送期间的原子性感兴趣,因此我们必须查看该过程的工作方式。
第一步取决于传输协议的版本,但是通常,发送方从接收方收集参考名称和值的列表。这里没有锁。这些参考名称和值将显示在发送者的预推钩中。
接下来,接收者让发送者收集对象并打包并发送它们(或发送单个对象,但是今天这种情况很少见)。这里也没有锁,这可能需要很多时间。在此过程中,接收器的参考值可能会更改。 含义:在打包前的钩子中,您对发送方所做的任何检查都不能保证在打包文件到达完好且接收方开始处理它之前,接收方的引用是相同的。打包文件本身一旦完成就被锁定。
在这一点上,如有必要,浅嫁接文件会被锁定(我认为-这并不完全明显;稍后可能会发生)。
接下来,发送方发送一系列更新请求(带有可选的强制标志)。接收器现在有机会查找并有选择地锁定每个要更新的参考。但是实际上,这里也没有发生锁定。接收器在没有锁定的情况下运行预接收挂钩。如果预接收钩拒绝了推送,则此时整个推送都将中止,因此没有任何变化。接收前挂钩审核整个更新后,如果您使用的是Git 2.11或更高版本(引入隔离),则也将打包文件(或单个对象)也从隔离区移出。
接下来,接收器运行所有更新。这是原子性变得特别有趣的地方。 自Git 2.4.0版以来,git push
具有新的标志--atomic
。这取决于接收方发布原子更新。。有一个配置值receive.advertiseAtomic
,您可以在接收方上设置为禁用原子更新。如果:
git push
)都了解原子更新功能,并且--atomic
然后,接收者将在更新任何参考之前锁定所有要更新的参考。如果这些锁中的任何一个失败,则此处将放弃整个推送。如果它们全部成功,则接收器将在应用任何更新之前一次运行一次每个更新挂钩,以验证每个更新。如果任何更新挂钩失败,则整个推送将中止。如果所有更新挂钩都接受每个更新,则通过重命名释放每个锁,原子地提交整个参考更新系列。 2
另一方面,如果发送方未选择--atomic
, 3 ,则接收方将一次更新每个参考。它运行更新钩子,如果更新钩子说要继续,则用lock-update-unlock序列更新一个引用。因此,每个单独的更新都可以成功或失败。
含义:无论是否有--atomic
,更新钩子都不应轻描淡写。此时,其他操作仍处于暂停状态。由于推送可能没有--atomic
进行,即使您无法确定将要更新哪些引用,您也不能假定其他任何引用在这里也是稳定的。
无论如何,在更新所有可更新的引用之后,Git会放弃所有 锁。正如我们在顶部所指出的,参考锁是通过更新它们的操作来删除的,但是如果需要,在更新了浅的嫁接点之后,Git现在也删除了浅锁和打包锁。然后,在未持有任何锁的情况下,Git运行后接收挂钩。 含义:接收后挂钩不能假定任何引用的 current 值与其标准输入中的值匹配。要查看已更新的内容,您必须阅读stdin;要查看当前值,必须重新阅读参考;这两个可能不同步。
2 虽然单个重命名是原子重命名,但是当其他较早的重命名成功时,某些重命名可能会失败。目前尚不清楚在这种情况下会发生什么。
3 如果接收方配置说不发布原子,并且发送方使用--atomic
,则发送方本人将取消其交易。就是说,如果您运行git push --atomic
且接收者尚未发布原子支持-要么是因为接收者太旧而无法使用原子支持,要么是因为该接收者被配置成这种方式- your Git停止了在此刻。实际上,在这种情况下,您不能选择原子推送。
从发送者的角度看,这看起来很简单:如果您没有在预推钩中进行假设(或者首先没有预推钩),则可以使用git push --atomic
使所有参考更新都是原子性的-整个推送将成功或失败-或不成功,在这种情况下,每个参考更新将单独成功或失败。每个参考更新都包含以下其中一项:
ref
设置为 hash
(常规/非---force
推送)ref
设置为 hash
! (git push --force
或git push ... +master:master
)ref
= old-hash
,请将其设置为 hash
! (git push --force-with-lease
),每个都可能被单独拒绝,但是--atomic
表示如果任何一个被拒绝,则没有会发生。
在接收方,您可以编写三种钩子,这很复杂。