我应该什么时候使用 "git push --force-if-includes"

When should I use "git push --force-if-includes"

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

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

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

笼统地说“总是”或“从不”并不是很有用。让我们看看您可能想在哪里使用它。严格来说,它从来 是必需的 因为它所做的只是稍微修改 --force-with-lease 。因此,如果要使用 --force-if-includes,我们已经使 --force-with-lease 生效。1 在我们查看 --force-with-includes 之前,我们应该介绍如何 --force-with-lease 确实有效。我们要解决什么问题?我们的“用例”或“用户故事”是什么,或者当有人稍后阅读时最新的流行语是什么?

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

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

然而,

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

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

但是,虽然 Git 确实使用这些 UUID 来 查找 提交,Git 还需要有一个 name 用于提交——好吧,对于某个链中的 last 提交。这个 gua运行tees 任何使用存储库的人都有办法找到提交:名称在某个链中找到 last 一个,我们从中找到所有更早的在同一条链上。

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

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

混淆

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

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

因为我们的main指向b789abc,而b789abc的parent有a123456,那么有他们 更新 他们的 main 指向 b789abc 是“安全的”。为了真正安全,他们 必须自动替换他们的 main,但我们只是让他们自己决定。

这种 添加 提交到某个远程 Git 存储库的方法工作正常。 没有的是我们想要删除他们的a123456的情况。我们发现 a123456 有问题或不好。我们没有进行简单的更正,b789abc,即 添加到 b运行ch,我们制作 b789abc 以便它绕过 错误提交:

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

变为:

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

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


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

2这些是 哈希 ID,它们在所有 Git 中必须是唯一的并共享 ID,但不会跨越两个从未相遇的 Git。在那里,我们可以安全地拥有我所说的“doppelgängers”:提交或其他内部 objects 具有相同的哈希 ID,但内容不同。不过,最好让它们真正独一无二。

3Git 因为它是“开箱即用”的,没有这种权限检查,但是主机提供商像 Git Hub 和 Bitbucket 添加它,作为他们 value-adding 说服我们使用他们的托管系统的一部分。

4un-find-able 提交实际上并没有立即消失。相反,Git 将此留给以后的内务处理 git gc 操作。此外,从某个名称删除提交可能仍然可以从其他名称或通过 Git 为每个名称保留的日志条目访问该提交。如果是这样,提交将持续更长时间,甚至可能永远存在。


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

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

我们 git push 一些新的提交,然后发现它很糟糕,并希望立即用新的和改进的提交替换它,所以我们需要几秒钟或几分钟——无论制作新的改进需要多长时间提交——并将其落实到位 运行 git push --force。具体来说,假设这整个过程需要我们一分钟或 60 秒。

那是六十秒,期间 其他人 可能:5

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

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

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

其中提交 H 是错误的,需要替换为我们的 new-and-improved H'。但事实上,他们现在有:

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

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

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

其中 H 是我们即将替换的错误提交。我们现在 运行 git push --force 并且由于我们被允许 force-push,托管服务提供商 Git 接受我们的新 H' 作为 中的最后一次提交他们 main,所以他们现在有:

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

效果是我们的 git push --force 不仅去除了我们的坏 H,而且去除了它们的(大概仍然是好的,或者至少,想要的)I


5他们可能会在发现自己的 git push 因为他们的提交基于 [=78 而被阻止后,通过变基他们已经做出的提交来做到这一点=]原来。他们的 rebase 自动将他们的新提交复制到我们在这里称为 I 的提交,没有合并冲突,使他们能够 运行 git push 在比我们 运行 git push =402=] 提交 H'.


输入--force-with-lease

--force-with-lease 选项在内部 Git 调用“比较和交换”,允许我们将提交发送给其他 Git,然后 让他们检查 他们的 b运行ch 名称——不管它是什么——包含我们认为它包含的哈希 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 b运行ch);和
  • 要求他们强制更新,但是 只有 如果他们的 refs/heads/main 当前有提交的哈希 ID H.

这让我们有机会发现一些提交 I 已添加到他们的情况main。他们使用 --force-with-lease=refs/heads/main:<hash> 部分 检查 他们的 refs/heads/main。如果不是给定的 <hash>,他们拒绝整个 t运行saction,保持他们的数据库完整:他们保留提交 IH,并删除我们的新提交 H'在地板上。6

整个 t运行saction——他们 main 的 forced-with-lease 更新——插入了锁定,这样如果其他人试图推送一些提交(也许 I) 现在,someone-else 被推迟,直到我们完成(失败或成功)我们的 --force-with-lease 操作。

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

git push --force-with-lease origin main

在这里,main 提供了我们希望发送的最后一次提交的哈希 ID——H'——以及我们希望他们更新的 ref-name(refs/heads/main,基于我们的 main 是一个 b运行ch 名称这一事实)。 --force-with-lease 没有 = 部分,因此 Git 填写其余部分:ref name 是我们希望他们更新的那个——refs/heads/main——而预期的提交就是那个在我们相应的 remote-tracking 名称 中,即我们自己的 refs/remotes/origin/main.

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


6这取决于他们的 Git 是否具有“qua运行tine”功能,但任何拥有 force-with-lease 的人都拥有这个功能,我觉得。 qua运行tine 功能可以追溯到很久以前。 Really-old 版本的 Git 缺少 qua运行tine 功能可以保留推送的提交,直到 git gc 收集它们,即使它们从未被合并。


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

上面 --force-with-lease 的示例用例显示了我们如何替换错误的提交 我们所做的,当 我们自己弄清楚了[=323] =].我们所做的只是替换它并推动它。但这并不是人们一贯的工作方式。

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

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

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

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

在我们自己的存储库中。

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

git push --force-with-lease origin main

我们将要求 hosting-provider Git 将他们的 main 换成提交 H' 如果他们将提交 I 作为他们的最后一次提交。正如我们所见,他们做到了:我们将 I 提交到我们的存储库中。我们只是忘记把它放进去了。

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

它的实际工作原理是它取决于 Git 的 reflogs。它扫描你自己的 reflog 以查找你的 main b运行ch,并挑选出提交 H 而不是 I,用作 --force-with-lease 中的哈希 ID .这类似于 git rebase 的 fork-point 模式(尽管那个使用你的 remote-tracking reflog)。我自己并不是 100% 相信这个 --force-if-includes 选项在所有情况下都有效:例如,--fork-point 不会。但它在 大多数 情况下确实有效,我怀疑 --force-if-includes 也会如此。

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

为了避免意外覆盖其他开发人员的提交,我的最终最安全解决方案是这样的,同时使用 2 个选项。 git config --global alias.pushf 'push --force-with-lease --force-if-includes'

[alias]

    pushf = push --force-with-lease --force-if-includes

Alternatively, specifying --force-if-includes as an ancillary option along with --force-with-lease[=<refname>] (i.e., without saying what exact commit the ref on the remote side must be pointing at, or which refs on the remote side are being protected) at the time of "push" will verify if updates from the remote-tracking refs that may have been implicitly updated in the background are integrated locally before allowing a forced update.

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

如果您总是使用 --force-with-lease as-is 而不是 --force 并寻找一些快速信息,请开始使用 --force-with-lease --force-with-includes,因为它稍微安全一些。

如果我能让你的注意力再多一点,请继续阅读。 如果你在 git push --force-with-lease 之前 git fetch 你基本上只是在没有安全的情况下强行推动。 添加 --force-if-includes 将除了远程跟踪分支之外,还使用 ​​reflog 来帮助防止此类事情发生,因为执行提取似乎是无害的,甚至可能在后台发生。

来自docs粗体斜体对我的评论,粗体在重点上。

--[no-]force-with-lease
--force-with-lease=<refname>
--force-with-lease=<refname>:<expect>

Usually, "git push" refuses to update a remote ref that is not an ancestor of the local ref used to overwrite it.

This option overrides this restriction if the current value of the remote ref is the expected value. "git push" fails otherwise.

[...]

Alternatively, specifying --force-if-includes as an ancillary option along with --force-with-lease[=<refname>] (this is what we are doing when we do --force-with-lease) (i.e., without saying what exact commit the ref on the remote side must be pointing at, or which refs on the remote side are being protected) at the time of "push" will verify if updates from the remote-tracking refs that may have been implicitly updated in the background are integrated locally before allowing a forced update.

这意味着如果你强制(​​有租约)将你的 blah 分支推到 origin 而你的 origin/blah 分支(远程跟踪分支)不同于服务器的 blah 分支会阻止您。 这意味着有人进行了您不知道的更改。

有更好的使用方式 --force-with-lease 但老实说,我们只是在寻找快速安全的方法。如果您想了解它们,请查看文档。使用 --force-if-includes 除了检查远程跟踪分支外,还会检查您的 reflog,以确保没有遗漏任何更改。