Git在用户推送代码时是否会锁定远程仓库以进行写入操作?

4
如果两个或多个用户同时将他们的本地repo状态推送到同一远程位置,git会怎么做?
  1. 它会在完成一个用户的整个提交批次之前锁定写入远程提交/分支/repo 吗?
  2. 还是在写出单个用户的N次提交中的一个提交后,释放它所持有的关于提交/ repo /分支的锁呢?
第一种情况听起来很合理,不过我还是想问一下。

3
我理解,Git 操作基本上保证是原子性的,这意味着整个操作完成/失败或者什么都不会发生。在这种情况下,应该执行以上第一版的操作。我没有参考资料。 - Tim Biegeleisen
@TimBiegeleisen 谢谢。 - Water Cooler v2
@TimBiegeleisen:不幸的是,这种方法在重要方面是错误的。要获得原子推送,您需要使用git push --atomic(Git 2.4中新增)。但是,就OP的意图而言,这种方法可能是正确的。正在努力回答... - torek
1个回答

7

TL;DR: mu

这个问题含有错误的假设,因此两个选项都不正确。

虽然存在原子性问题,但它们并不是基于每一次提交的。它们是基于每一个引用的。

如果你只推送一个引用,例如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使用重命名锁文件。底层操作系统也必须提供原子重命名操作。

更新打包的引用将其转换为未打包(“松散”),获取每个引用锁。显然,打包引用需要获取打包引用锁。删除引用有两种特殊情况:

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

  • 如果存在日志,则删除引用会删除其引用日志。 这在大多数情况下是不可见的,但它确实意味着引用更新代码想要事先知道这是一个删除操作。


1值得注意的是:某些引用是per-worktree。 最初只有HEAD,但是由于出现了git worktree错误,现在它同样包括所有的refs/bisect/refs/rewritten/引用。 refs/rewritten/引用本身是新的,是通过新的更高级交互式变基引入的,它重新创建任意合并。分离 bisect 引用是 Git 2.7.0 中的修复程序;请参见commit ce414b33ec038

此外,有些引用被视为“伪引用”。这些永远不会被打包。 伪引用是像ORIG_HEADMERGE_HEAD等东西。这主要是一个内部细节,但它影响可能适用的锁:例如,一个常规引用refs/heads/master可以是打包的,在这种情况下,应用打包引用锁,或者可以是未打包的,在这种情况下,应用未打包引用锁。


推送序列

既然您对推送期间的原子性感兴趣,我们就必须看看该过程的工作方式。

第一步取决于传输协议版本,但通常,发送方从接收方收集引用名称和值列表。 这里没有保持任何锁。这些引用名称和值将出现在发件人的预推挂钩中。

接下来,接收方让发送方收集对象并打包和发送它们(或发送单个对象,但今天这相当罕见)。这里也没有保持任何锁,这可能需要很长时间。在此过程中,接收方的引用值可能会更改。影响:在 pre-push挂钩中对发件人进行的任何检查都不能保证当打包文件完整到达并且接收方开始处理它时,接收方的引用仍然相同。 但是一旦完成打包文件本身就会被锁定。

在此时,如果需要,浅插件文件会被锁定(我认为——这并不完全明显;它可能会在稍后发生)。

接下来,发送方发送一系列更新请求(带有可选的强制标志)。接收方现在有机会查找每个要更新的引用,并可选择锁定它们。但实际上,在这里也没有发生任何锁定。接收方在没有任何锁定的情况下运行预接收挂钩。如果预接收挂钩拒绝了推送,则整个推送在此时被中止,因此没有任何更改。在预接收挂钩对更新进行全面审核之后,如果您使用Git 2.11或更高版本(引入隔离区),则打包文件(或单个对象)也将从隔离区移动。

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

  • 接收方广告原子更新功能(默认为true),并且
  • 发送方(运行git push的人)了解原子更新功能,并且
  • 发送方选择--atomic

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

另一方面,如果发送方没有选择--atomic3则接收方将逐个更新每个引用。它运行更新挂钩,如果更新挂钩指示继续,则使用锁定-更新-解锁序列更新一个引用。因此,每个单独的更新可能会成功或失败。

含义:无论有没有--atomic,更新挂钩都不应该拖延。此时正在进行其他操作。由于可以在没有--atomic的情况下进行推送——即使有也不能确定将更新哪些引用——因此,在此处也不能假定任何其他引用是稳定的。

无论如何,在更新所有可更新的引用之后,Git会删除所有锁定。引用锁定是通过更新它们来释放的,正如我们在顶部所述,但是在更新浅层移植点(如果需要)后,Git还会删除浅层和包锁定。然后,在没有保持任何锁定的情况下,Git运行post-receive钩子。含义:post-receive钩子不能假设任何引用的当前值与其标准输入中的值匹配。要查看已更新的内容,必须读取stdin;要查看当前值,必须重新读取引用;这两者可能不同步。

2虽然单个重命名是原子的,但某些重命名可能会失败,而其他较早的重命名成功。在这种情况下,情况并不完全清楚。

3如果接收器配置指示不广告原子性,并且发送方使用--atomic,则发送方自己取消了事务。也就是说,如果您运行git push --atomic,并且接收方没有广告原子支持-因为接收方太老而无法拥有它,或者因为接收方被配置为这样-您的 Git在此时停止。实际上,在这种情况下,您不能选择原子推送。


结论

从发送方的角度来看,它看起来相当简单:如果您不在pre-push钩子中做出假设(或者根本没有pre-push钩子),则可以使用git push --atomic使所有引用更新成为原子操作-整个推送将成功或失败-或者不使用,其中每个引用更新将自行成功或失败。每个引用更新由以下之一组成:

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

每个引用更新可能会被单独拒绝,但是--atomic意味着如果任何一个被拒绝,则都不会发生

从接收方的角度来看,您可以编写三种钩子,情况会比较复杂。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接