【问题标题】:When should I use "git push --force-if-includes"我什么时候应该使用“git push --force-if-includes”
【发布时间】:2021-04-26 10:41:04
【问题描述】:

当我想强制推送时,我几乎总是使用--force-with-lease。今天升级到Git 2.30,发现了一个新选项:--force-if-includes

在阅读了updated documentation 之后,我仍然不完全清楚在什么情况下我会使用--force-if-includes 而不是像往常那样使用--force-with-lease

【问题讨论】:

    标签: git git-push


    【解决方案1】:

    --force-if-includes 选项,正如您所指出的,是新的。如果您以前从未需要它,那么您现在不需要它。因此,“我什么时候应该使用它”的最短答案是“从不”。 ?推荐的答案是(或者一旦它被证明就会是?)总是。 (我自己还没有以某种方式说服自己。)

    不过,“总是”或“从不”的毯子不是很有用。让我们看看您可能想在哪里使用它。严格来说,它从来没有必要,因为它所做的只是稍微修改--force-with-lease。因此,如果要使用--force-if-includes,我们已经有--force-with-lease 生效。1 在我们查看--force-with-includes 之前,我们应该了解--force-with-lease 的实际工作原理。我们要解决什么问题?什么是我们的“用例”或“用户故事”,或者当有人稍后阅读本文时可能出现的任何最新流行语?

    (注意:如果您已经熟悉所有这些,您可以搜索下一个 force-if-includes 字符串以跳过接下来的几个部分,或者直接跳到底部然后向上滚动到部分标题.)

    我们这里的基本问题是原子性。归根结底,Git 主要(或至少在很大程度上)是一个数据库,任何好的数据库都有四个属性,我们有这四个属性 ACID:原子性、一致性、隔离性和持久性。 Git 本身并不能完全实现任何或所有这些:例如,对于 Durability 属性,它(至少部分地)依赖于操作系统来提供它。但是其中三个(C、I 和 D)首先是 Git 存储库中的本地:如果您的计算机崩溃,您的数据库副本可能是完整的,也可能不是完整的、可恢复的,或其他任何东西,具体取决于您自己的硬件和操作系统的状态。

    然而,Git 不仅仅是一个本地数据库。它是一个分布式,通过复制进行分布,它的原子性单元——提交——分布在数据库的多个复制中。当我们在本地进行新的提交时,我们可以使用git push 将其发送到数据库的其他一个或多个副本。这些副本将尝试在那些计算机上提供自己的 ACID 行为。但我们希望在推送过程中保持原子性

    我们可以通过多种方式获得它。一种方法是从每个提交都有一个全局(或通用)唯一标识符的想法开始:GUID 或 UUID。2(我将在此处使用 UUID 形式。)我可以安全地给你只要我们都同意它获得我给它的 UUID,我已经做出了一个新的提交,而你没有。

    但是,虽然 Git 确实使用这些 UUID 来查找提交,但 Git 还需要有一个 name 用于提交——嗯,对于 last 在某个链中提交。这保证了任何使用存储库的人都有办法找到提交:名称在某个链中找到最后一个,我们从中找到同一链中所有较早的。

    如果我们都使用相同的 name,我们就有问题了。假设我们使用名称main 来查找提交b789abc,而他们使用它来查找提交a123456

    我们在这里使用git fetch 的解决方案很简单:我们为他们的Git 存储库分配一个名称,例如origin。然后,当我们从他们那里获得一些新的提交时,我们取 他们的 名称——即在某个链中找到这些提交中最后一个的那个,即——并 重命名 它。如果他们使用名称 main 来查找该提示提交,我们将其重命名为 origin/main。我们创建或更新我们自己的origin/main 以记住他们的 提交,它不会与我们自己的main 混淆。

    但是,当我们采用另一种方式时——将我们的提交推送给它们——Git 不会应用这个想法。相反,我们要求他们直接更新他们的main。例如,我们交出提交b789abc,然后要求他们将main 设置为b789abc。他们所做的是确保他们不会丢失他们的a123456 提交,确保a123456 是我们提交@987654344 的历史的一部分@:

      ... <-a123456 <-b789abc   <--main
    

    由于我们的main 指向b789abc,并且b789abca123456 作为其父级,因此他们更新他们的main 指向到b789abc 是“安全的”。为了真正安全,他们必须以原子方式替换他们的main,但我们只是将其留给他们。

    这种添加 提交到远程 Git 存储库的方法可以正常工作。 不起作用的是我们想删除他们的a123456。我们发现a123456 有问题或不好。我们没有进行简单的更正,b789abc,将 添加到到分支中,而是创建了 b789abc,以便它绕过错误的提交:

    ... <-something <-a123456   <--main
    

    变成:

    ... <-something <-b789abc   <--main
                   \
                    a123456   ??? [no name, hence abandoned]
    

    然后我们尝试将此提交发送给他们,他们拒绝了我们的尝试,并抱怨这不是“快进”。我们添加 --force 告诉他们无论如何都要进行替换,并且——如果我们有适当的权限3——他们的 Git 会服从。这有效地删除他们克隆的错误提交,就像我们从我们的克隆中删除它一样。4


    1作为您链接的文档,--force-if-includes 没有 --force-with-lease 将被忽略。也就是说,--force-if-includes 不会打开 --force-with-lease 你:你必须同时指定。

    2这些是哈希 ID,它们需要在所有会遇到并共享 ID 的 Git 中是唯一的,但在两个永远不会相遇的 Git 中则不需要。在那里,我们可以安全地拥有我称之为“doppelgängers”的东西:提交或其他具有相同哈希 ID 但内容不同的内部对象。不过,最好让它们真正独一无二。

    3Git,“开箱即用”,没有这种权限检查,但是像 GitHub 和 Bitbucket 这样的托管服务提供商添加了它,作为他们增值的一部分说服我们使用他们的托管系统。

    4无法找到的提交实际上并没有立即消失。取而代之的是,Git 将其留给以后的 git gc 操作。此外,从某个名称中删除提交可能仍然可以从其他名称或通过 Git 为每个名称保留的日志条目访问该提交。如果是这样,提交将持续更长时间,甚至可能永远存在。


    到目前为止还不错,但是...

    就目前而言,强制推送的概念很好,但这还不够。假设我们有一个存储库,托管在某个地方(GitHub 或其他),它接收git push 请求。进一步假设 我们不是唯一进行推送的人/组

    我们git push 一些新的提交,然后发现它很糟糕,并想立即用一个新的和改进的提交来替换它,所以我们需要几秒钟或几分钟——不管做出新的改进提交需要多长时间——然后得到就位并运行git push --force。具体来说,假设整个过程需要我们一分钟或 60 秒。

    这是 60 秒,在此期间 其他人 可能:5

    • 从托管系统获取我们的错误提交;
    • 添加自己的新提交;和
    • git push 结果。

    所以在这一点上,我们认为托管系统有:

    ...--F--G--H   <-- main
    

    commit H 不好,需要用我们新的和改进的H' 替换。但事实上,他们现在有:

    ...--F--G--H--I   <-- main
    

    commit I 来自另一个更快的提交者。同时,我们现在在 我们的 存储库中拥有序列:

    ...--F--G--H'  <-- main
             \
              H   ???
    

    H 是我们的错误提交,我们将要替换它。我们现在运行git push --force,因为我们被允许强制推送,所以托管服务提供商 Git 接受我们的新 H' 作为 他们的 main 中的最后一次提交,以便他们 现在有:

    ...--F--G--H'  <-- main
             \
              H--I   ???
    

    效果是我们的git push --force 不仅删除了我们不好的H,而且还删除了他们的(可能仍然不错,或者至少需要)I


    5在发现自己的git push 被阻止后,他们可能会通过重新设置他们已经做出的提交来做到这一点,因为他们最初的提交基于G。他们的 rebase 自动将他们的新提交复制到我们在这里调用的I,没有合并冲突,使他们能够在比我们进行固定提交H' 更少的秒内运行git push


    输入--force-with-lease

    --force-with-lease 选项(在 Git 内部称为“比较和交换”)允许我们向其他 Git 发送提交,然后让他们检查他们的分支名称——不管它是什么is—包含我们认为它包含的哈希 ID。

    让我们在我们自己的存储库的绘图中添加origin/* 名称。由于我们之前将提交 H 发送给托管服务提供商,并且他们接受了它,因此我们的存储库中实际上有 this

    ...--F--G--H'  <-- main
             \
              H   <-- origin/main
    

    当我们使用git push --force-with-lease 时,我们可以选择完全准确地控制这个--force-with-lease。这样做的完整语法是:

    git push --force-with-lease=refs/heads/main:<hash-of-H> origin <hash-of-H'>:refs/heads/main
    

    也就是说,我们将:

    • 发送到origin 提交以通过哈希 ID H' 找到的提交结尾;
    • 要求他们更新他们的名字refs/heads/main(他们的main 分支);和
    • 要求他们强制执行此更新,但如果他们的 refs/heads/main 当前包含提交 H 的哈希 ID。

    这让我们有机会捕捉到一些提交I 已添加到他们的main 的情况。他们使用--force-with-lease=refs/heads/main:&lt;hash&gt; 部分,检查他们的refs/heads/main。如果不是给定的&lt;hash&gt;,他们会拒绝整个事务,保持他们的数据库完整:他们保留提交IH,并将我们的新提交H' 放在地板上。6

    整个事务——他们的main 的强制租赁更新——已插入锁定,因此如果其他人现在试图推送一些提交(可能是I),其他人会被推迟到我们通过--force-with-lease 操作完成(失败或成功)。

    不过,我们通常不会把所有这些都写出来。通常我们会运行:

    git push --force-with-lease origin main
    

    这里,main 提供了我们要发送的最后一个提交的哈希 ID —H' — 以及我们希望它们更新的引用名称 (refs/heads/main,基于我们的 main 是一个分支名称)。 --force-with-lease 没有 = 部分,因此 Git 会填写其余部分:引用名称是我们希望他们更新的名称——refs/heads/main——而预期的提交是我们对应的 remote-tracking 名称中的那个,即我们自己的refs/remotes/origin/main

    这一切都是一样的:我们的origin/main 提供H 哈希,我们的main 提供H' 哈希和所有其他名称。它更短并且可以解决问题。


    6这取决于他们的 Git 是否具有“隔离”功能,但我认为任何拥有强制租赁的人都有此功能。隔离功能可以追溯到很久以前。缺少隔离功能的真正旧版本的 Git 可以保留推送的提交,直到 git gc 收集它们,即使它们从未被合并。


    这终于把我们带到了--force-if-includes

    上面带有--force-with-lease 的示例用例展示了我们如何替换一个错误的提交我们所做的,而我们自己解决了这个问题。我们所做的只是更换它并推动。但人们并非总是这样工作。

    假设我们做了一个错误的提交,就像以前一样。我们在自己的本地存储库中遇到这种情况:

    ...--F--G--H'  <-- main
             \
              H   <-- origin/main
    

    但是现在我们运行git fetch origin。也许我们正在努力做到尽职尽责;也许我们正承受压力并犯错误。不管发生了什么,我们现在得到:

    ...--F--G--H'  <-- main
             \
              H--I   <-- origin/main
    

    在我们自己的存储库中。

    如果我们使用git push --force-with-lease=main:&lt;hash-of-H&gt; origin main,推送将失败——就像它应该一样——因为我们明确声明我们希望来源的main 包含哈希ID H。正如我们从 git fetch 中看到的那样,它实际上具有哈希 ID I。如果我们使用更简单的:

    git push --force-with-lease origin main
    

    如果托管服务提供商 Git 将 I 作为最后一次提交,我们将要求他们将 main 替换为提交 H'。正如我们所看到的,他们做到了:我们将I 提交到我们的存储库中。我们只是忘了把它放进去。

    所以,我们的 force-with-lease 有效,我们在 origin 上清除了提交 I,这一切都是因为我们运行了 git fetch 并忘记检查结果。 --force-if-includes 选项旨在捕获这些情况。

    它的实际工作方式取决于 Git 的 reflogs。它会扫描您自己的 reflog 以查找您的 main 分支,并挑选出提交 H 而不是 I,用作 --force-with-lease 中的哈希 ID。这类似于git rebase 的分叉点模式(尽管该模式使用您的远程跟踪引用日志)。我自己并不是 100% 相信 --force-if-includes 选项在所有情况下都有效:例如,--fork-point 不适用。但它确实适用于大多数情况,我怀疑--force-if-includes 也可以。

    因此,您可以将其用于所有 --force-with-lease 推送来尝试一下。它所做的只是使用不同的算法——Git 人员希望会更可靠,考虑到人类的方式——为原子选择哈希 ID,“如果匹配,则交换你的分支名称"--force-with-lease 使用的操作。您可以通过提供--force-with-lease=&lt;refname&gt;:&lt;hash&gt; 部分手动执行此操作,但目标是自动执行此操作,比当前的自动方式更安全。

    【讨论】:

    • 在您的示例中,假设我已获取提交 I,在我的分支中检查了它,但我不喜欢它,所以我将 --hard 重置为 H 并强制推送出来。我的origin/main 现在位于H,但我的参考日志中已经包含I。现在我在本地添加了一个新的提交 J,与此同时,我真正喜欢 I 的同事注意到它已经消失并将其推回。我还没有获取,如果我用--force-with-lease 推送J 而不带参数,我预计它会失败,这很好。但是如果我这样做--force-if-includes 它可能会起作用,因为我已经提交了I? (人为但可能,我认为......)
    • 我还没有深入研究--force-if-includes 的实际代码,我需要这样做来弄清楚如何欺骗它。但这似乎是一种可能的可能性。
    • 这可能是一个权衡。我可能最终会留在--force-with-lease。但我听说有人在他们的机器上按计划在后台自动运行 fetch。我想对他们来说,--force-if-includes--force-with-lease 好得多。也许他们是推动此功能开始的人。 ;)
    • @TTT:我认为这很有可能,特别是因为还有一些其他新的即将推出的东西可以使自动背景获取功能更加实用。另外:我看到你在那里做了什么:-)
    【解决方案2】:

    为了避免意外覆盖其他开发人员的提交,我的最终最安全的解决方案是这样的,同时使用 2 个选项

    [alias]
    
        pushf = push --force-with-lease --force-if-includes
    

    参考:https://git-scm.com/docs/git-push

    【讨论】:

      猜你喜欢
      • 2011-04-10
      • 2011-01-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-07
      • 2012-09-22
      • 1970-01-01
      • 2012-12-23
      相关资源
      最近更新 更多