当用户按下时,git 会锁定远程写入吗?

Wat*_* v2 4 git

如果两个或多个用户同时将他们的本地 repo 状态同时推送到同一个远程,git:

  1. 在完成一个用户的整批提交之前锁定远程提交/分支/存储库以进行写入?

  2. 或者它是否在他的 N 个提交批次中写入单个用户单个提交后释放它持有的提交/回购/分支上的锁?

第一个是有道理的,但我想我还是会问。

tor*_*rek 5

TL;博士:

该问题包含一个不正确的假设,因此两个选项都不正确。

存在原子性问题,但它们不是基于每个提交。它们基于每个参考

如果你只推送一个引用——例如,git push origin master——只有一个引用需要更新。更新要么成功要么失败,对于发送方来说,差不多就是这样(尽管有很多接收方的细节仍然很重要)。

如果你推送多个引用——例如git push origin develop master——有多个引用需要更新。如果您的 Git 支持它(两边都是 v2.4 或更高版本),请使用它git push --atomic来确保两个推送都成功,或者都不成功。

如果你不写 pre-push、pre-receive、update 和/或 post-receive 钩子,你可以停在这里。如果你做的写出来,请继续阅读。

锁定发生在接收方,而不是发送方(我希望这是显而易见的原因:-))。文档从不明确调用内部细节,即使它应该;但是有许多单独的锁和锁定步骤。特别是:

  • 每个包文件有一个锁。
  • 在浅存储库的情况下,有一个用于浅移植点的锁。
  • 打包引用后端数据存储有一个锁(涵盖所有打包引用)。
  • 每个引用名称都有一个锁。1
  • 索引只有一个锁(这在大多数情况下并不重要)。

读取引用不需要锁定;只有更新一个需要锁定它。这意味着纯读者可能会在转换期间看到旧值。然而,在内部,可以锁定一系列引用。请参阅下面的原子性注释。

获取锁包括使用原子“如果文件已经存在则创建或失败”操作来创建锁文件。这必须由底层操作系统提供。解锁是通过删除或重命名锁定文件来实现的:锁定文件通常包含锁定文件锁定的文件的新内容,因此要删除锁定而不更改内容,Git 只需删除锁定文件,并删除锁定文件锁定并更改文件的内容,作为单个原子操作,Git重命名锁定文件。原子重命名操作也必须由底层操作系统提供。

更新打包引用会将其转换为未打包(“松散”),从而获得每个引用锁。打包引用显然需要获得打包引用锁。删除引用是一种特殊情况,有两种方式:

  • 未打包的引用也可能出现在打包的 refs 文件中。(当松散副本存在时,会忽略打包副本。)在这种情况下,Git 还必须更新打包引用文件以删除两个副本。

  • 如果日志存在,删除引用会删除其引用日志。这大部分是不可见的,但这确实意味着引用更新代码想要提前知道这一个删除操作。


1值得注意的是:一些参考是per-worktree。最初这只是HEAD因为git worktree错误已经浮出水面,它现在包括 allrefs/bisect/refs/rewritten/refs。该refs/rewritten/文献本身是新的,与再现任意合并新票友互动变基介绍。拆分二等分引用是 Git 2.7.0 中的一个修复;见提交 ce414b33ec038

此外,一些引用被认为是“伪引用”。这些从不打包。伪引用是诸如ORIG_HEAD、等之类的东西MERGE_HEAD。这主要是一个内部细节,但它会影响可能应用哪些锁:refs/heads/master例如,常规引用可以被打包,在这种情况下打包引用锁适用,或者它可以被解包,在这种情况下解包引用锁适用。


推送顺序

由于您对推送期间的原子性感兴趣,我们必须查看该过程是如何工作的。

第一步取决于传输协议版本,但通常,发送方从接收方收集引用名称和值的列表。这里没有锁。这些引用名称和值将显示在发送方的 pre-push 挂钩中。

接下来,接收者让发送者收集对象并打包并发送它们(或发送单个对象,但今天这种情况非常罕见)。这里也没有锁,这可能需要很多时间。在此过程中,接收器的参考值可能会发生变化。 含义:您在预推送钩子中对发送方进行的任何检查都不能保证在包文件完整到达并且接收方开始处理它时接收方的引用是相同的。 但是打包文件本身一旦完成就会被锁定。

在这一点上,如果有必要,浅移植文件被锁定(我认为——这并不完全明显;它可能会在以后发生)。

接下来,发送方发送一系列更新请求(带有可选的强制标志)。接收器现在有机会查找并可选择锁定每个更新引用。然而,事实上,这里也没有发生锁定。接收器在没有锁定的情况下运行预接收钩。如果 pre-receive hook 拒绝推送,则整个推送在此时中止,因此没有任何改变。如果您有 Git 2.11 或更高版本(引入了隔离),则在 pre-receive hook 将更新作为一个整体进行审查后,pack 文件(或单个对象)也会从隔离区中移出。

接下来,接收器运行所有更新。这是原子性变得特别有趣的地方。 从 Git 版本 2.4.0 开始,git push有了一个新标志,--atomic. 这依赖于接收者通告原子更新。 有一个配置值,receive.advertiseAtomic您可以在接收器上设置以禁用原子更新。如果:

  • 接收者通告原子更新能力(默认为真),以及
  • 发送者(运行者git push)了解原子更新能力,并且
  • 发件人选择 --atomic

然后接收器将锁定全部引用将要更新的现在,更新其中的任何之前。如果这些锁中的任何一个失败,则整个推送将在此处中止。如果它们都成功,接收器将运行每个更新挂钩,一次一个,以在应用任何更新之前验证每个更新。如果任何更新挂钩失败,则整个推送将中止。如果所有更新挂钩都接受每个更新,则通过重命名释放每个锁,以原子方式提交整个系列的引用更新。2

在另一方面,如果发送者没有选择--atomic3接收机将更新每次打开一个参考之一。它运行更新钩子,如果更新钩子说要继续,就用锁定-更新-解锁序列更新一个引用。因此,每个单独的更新都可能成功或失败。

含义:无论有没有--atomic,更新钩子都不应该磨蹭。 此时其他操作正在暂停。由于推送可能会在没有的情况下进行--atomic——即使你无法确定哪些引用将被更新——你也不能假设任何其他引用在这里都是稳定的。

无论如何,在更新所有可更新的引用后,Git 会删除所有锁。正如我们在顶部指出的那样,引用锁通过更新它们的行为被删除,但是 Git 现在也会删除浅锁和包锁,如果需要的话,在更新浅嫁接点之后。然后,在没有锁的情况下,Git 运行 post-receive 钩子。 含义:post-receive hooks 不能假设任何引用的当前值与其标准输入中的值匹配。 要查看更新的内容,您必须阅读 stdin;要查看当前值,您必须重新阅读参考;这两个可能不同步。


2虽然个别重命名是原子性的,但当其他较早的重命名成功时,某些重命名可能会失败。目前尚不清楚在这种情况下会发生什么。

3如果接收方配置说不做广告原子,而发送方使用--atomic,则发送方自己取消他的交易。也就是说,如果你运行git push --atomic并且接收器没有公布原子支持——要么是因为接收器太旧而无法拥有它,要么因为接收器是这样配置的——你的Git 会在此时停止。实际上,在这种情况下您不能选择原子推送。


结论

从发送者的角度来看,它看起来相当简单:如果你不在 pre-push 钩子中做假设(或者首先没有 pre-push 钩子),你可以使用git push --atomic使你所有的引用更新原子化——整个推送要么成功要么失败——在这种情况下,每个引用更新要么成功要么失败。每个参考更新都包含以下之一:

  • 请设置refhash(常规/非--force推送)
  • 设置refhash!(git push --forcegit push ... +master:master)
  • 如果ref= old-hash,则将其设置为hash! ( git push --force-with-lease)

每个都可以单独拒绝,但这--atomic意味着如果任何一个被拒绝,不会发生。

从接收方来看,你可以写三种钩子,这很复杂。