何时应使用 "git push --force-if-includes"?

53

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

在阅读更新文档后,我仍然不完全清楚在哪些情况下我会使用 --force-if-includes 而不是像我通常那样使用 --force-with-lease


也许应该将这个标志重新命名为“--force-if-local-head-matches-remote-head”,因为那真的是我想要的。我通常运行 git push --force-with-lease master:refs/heads/master。我打算将提交 A 重写为提交 Ax。如果远程提交已不是 A,则我想取消操作。也许整个操作可以进行修改:git push --force --replace-sha=a1s2d3f - Devin Rhode
其实我现在重新阅读@torek的下面的回答,意识到我完全没有理解--force-if-includes的重点。git push --force-with-lease origin main已经实现了我所说的应该做的事情... - Devin Rhode
注意事项涉及到 git fetch - 请阅读下面 torek 的回答。 - Devin Rhode
这个链接解释得很好:https://delicious-insights.com/en/posts/git-push-with-lease/ - hiru007
5个回答

93

TL;DR

Git 2.30中新增的--force-if-includes参数除非同时使用--force-with-lease,否则不会起作用。如果您使用了--force-with-lease,那就足够了。我个人认为--force-if-includes的效用并不明显,因此会使用多步骤的处理方法:

git fetch <remote>         # get commit(s) from remote
git show <remote>/<name>   # inspect their most recent commit
                           # to make sure it's what we think
                           # it is, so that we're sure of what
                           # we're tossing *from* their Git repo

然后,如果以上一切顺利:

git push --force-with-lease <remote> <name>

然而,如果你有自己的偏好,那也没问题:只需记住--force-if-includes表示你的Git在进行强制推送时,会检查你之前使用git fetch至少一次查看过他们最新的提交。这意味着你必须密切关注git fetch获取的内容(这就是为什么我只会进行获取和检查,然后不再烦恼新标志的原因)。
如果你没有使用--force--force-with-lease,你绝对不需要新标志。由于大多数git push操作一开始就不需要--force,所以你可以跳过所有花哨的东西,只需使用常规(非强制性)推送,而无需进行仔细的检查。 < h1>长篇大论 --force-if-includes选项是新功能,如你所述,如果以前从未需要过它,现在也不需要。因此,“何时应该使用此选项”的最短答案是“从不”。推荐的答案是(或将成为)“始终”(一旦证明了?)。(我个人还没有完全相信其中任何一种方法。)
然而,“始终”或“永远不”并不十分有用。让我们看看何时需要使用它。严格来说,它从不是必要的,因为它只是稍微修改了--force-with-lease。因此,如果要使用--force-if-includes,我们已经实际上启用了--force-with-lease1在查看--force-with-includes之前,我们应该了解一下--force-with-lease的工作原理。我们要解决什么问题?我们的“使用案例”或“用户故事”或任何其他最新的流行语可能是什么?
我们在这里遇到的根本问题是“原子性”。Git最终基本上或至少在很大程度上是一个数据库,而任何好的数据库都具有我们使用ACID助记符的四个属性:原子性、一致性、隔离性和持久性。Git并不完全实现所有这些属性:例如,对于持久性属性,它依赖于操作系统(至少部分)。但是其中三个——C、I和D——从一开始就是Git存储库中的本地属性:如果您的计算机崩溃,则您的数据库副本可能会受损,可恢复或其他,这取决于您自己的硬件和操作系统的状态。
然而,Git不仅仅是一个本地数据库。它是一个通过复制分布式的数据库,并且其原子单位——提交——分布在数据库的多个复制中。当我们在本地进行新提交时,我们可以使用git push将其发送到数据库的某些其他副本。那些副本将尝试在它们自己的计算机上提供它们自己的ACID行为。但是在推送本身期间,我们希望保留原子性。
我们可以通过几种方式来实现这一点。一种方法是从每个提交都具有全局唯一标识符(GUID或UUID)的想法开始。2(我将在此处使用UUID形式。)只要我们都同意它获得了我给出的UUID并且您没有该UUID,我可以安全地向您提供我制作的新提交。
但是,虽然Git确实使用这些UUID来查找提交,但Git还需要对提交进行命名——好吧,对于某个链中的最后一个提交进行命名。这保证了使用存储库的人有一种找到提交的方法:名称找到该链中的最后一个提交,从而找到同一链中的所有较早提交。
如果我们两个人使用相同的名称,则会出现问题。假设我们正在使用名称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,而b789abca123456作为其父项,那么让他们更新他们的main以指向b789abc是“安全的”。为了确保这真的是安全的,他们必须原子地替换他们的main,但我们只让他们自己处理。
这种向某个远程Git存储库添加提交的方法很好。不起作用的情况是,我们想要删除他们的a123456。我们发现a123456存在问题或错误。我们不再进行简单的更正,而是使我们的b789abc跳过了错误的提交,这样它就可以添加到分支上。
... <-something <-a123456   <--main

becomes:

... <-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之间保持唯一。在这种情况下,我们可以安全地拥有我所谓的“替身”:具有相同哈希ID但不同内容的提交或其他内部对象。尽管如此,最好还是使它们真正独特。

3“开箱即用”的Git并没有此类权限检查,但像GitHub和Bitbucket等托管提供商会添加它,作为他们增值服务中的一部分,以说服我们使用他们的托管系统。

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


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

力推的概念就像它所到达的地方一样好,但这还不够。假设我们有一个托管在某个地方(如GitHub等)接收git push请求的存储库。进一步假设我们不是唯一一个进行推送的人/团队。

我们git push了一些新提交,然后发现它是错误的,并想立即用新的和改进的提交替换它,因此我们需要花费几秒钟或几分钟-无论需要多长时间来制作新的改进提交-并将其放置并运行git push --force。具体而言,让我们假设整个过程需要一分钟或60秒。

在这六十秒内,其他人可能会:

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

因此,此时我们认为托管系统拥有:

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

需要替换的提交H存在问题,需要使用我们新而改进的H'进行替换。但实际上,他们现在有:

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

这里的提交 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他们可能通过将已经提交的代码进行变基来实现此目的。在原本基于G的提交被拒绝后,他们会进行变基操作。他们的变基操作会自动将新的提交复制到我们称之为I的提交中,并且不会出现合并冲突,使他们能够在比我们修复提交H'所需时间更短的时间内运行git push


输入 --force-with-lease

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

让我们将origin/* 名称添加到我们自己的存储库的图示中。由于我们之前向托管提供商发送了提交 H,并且他们已经采纳了它,因此我们实际上在我们的存储库中拥有 这个

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

当我们使用git push --force-with-lease时,我们完全可以控制--force-with-lease的操作。其完整语法如下:{{syntax}}。
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`中当前包含提交哈希ID`H`时,才要求他们强制执行此更新。
这给了我们一个机会来捕捉一些提交`I`已添加到他们的`main`的情况。他们使用`--force-with-lease=refs/heads/main:<hash>`部分检查他们的`refs/heads/main`。如果它不是给定的`<hash>`,他们会拒绝整个事务,保持其数据库完好无损:他们保留提交`I`和`H`,并将我们的新提交`H'`丢弃。6 整个事务——对他们的`main`进行强制更新——已插入锁定,因此如果其他人现在正在尝试推送某些提交(可能是`I`),则该其他人会被拦截,直到我们完成——失败或成功——我们的`--force-with-lease`操作。
虽然通常我们不会详细说明所有这些。通常我们只会运行:
git push --force-with-lease origin main

在这里,main提供了我们想要发送的最后一次提交的哈希ID——H',以及我们想要更新的引用名称(基于我们的main是一个分支名称,为refs/heads/main)。--force-with-lease没有=部分,所以Git填写了剩下的内容:引用名称是我们想要更新的那个——refs/heads/main,期望的提交是我们相应的远程跟踪名称中的那个,即我们自己的refs/remotes/origin/main中的那个。
这一切都是相同的:我们的origin/main提供了H哈希,而我们的main提供了H'哈希和所有其他名称。它更短,但却起到了作用。
这取决于他们的Git是否具有“隔离”功能,但我认为任何拥有force-with-lease的人都具备此功能。 隔离功能已经存在很长一段时间了。 缺乏隔离功能的旧版本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:<hash-of-H> origin main,推送将失败,因为我们明确声明我们期望 origin 的main包含哈希IDH。但是,从我们的git fetch中可以看出,它实际上具有哈希IDI。如果我们使用更简单的方法:

git push --force-with-lease origin main

如果Git托管提供商的最后一次提交是提交I,我们将要求他们使用提交H'替换他们的主分支。正如我们所看到的,他们确实这样做了:我们将提交I放入了我们的存储库中,只是“忘记”把它放进去了。
因此,我们的force-with-lease起作用了,并且我们通过运行git fetch并忘记检查结果来清除了origin上的提交I。--force-if-includes选项旨在捕获这些情况。
它实际上的工作原理是依赖于Git的reflogs。它会扫描您自己的reflog以获取您的主分支,然后选择提交H而不是提交I作为--force-with-lease中使用的哈希ID。这类似于git rebase的fork-point模式(尽管该模式使用您的远程跟踪reflog)。我本人并不100%确信,这个--force-if-includes选项在所有情况下都能正常工作:例如,--fork-point就不能正常工作。但它在大多数情况下确实有效,我认为--force-if-includes也会有效。
因此,您可以尝试将其用于所有--force-with-lease推送。它所做的就是使用不同的算法——Git开发人员希望这种算法对人类更可靠——来选择原子“如果匹配,则交换分支名称”的操作所使用的哈希ID。您可以通过提供--force-with-lease中的“=:”部分来手动完成此操作,但目标是以比当前自动方式更安全的方式自动完成此操作。

2
我还没有深入研究--force-if-includes的实际代码,我需要这样做才能弄清楚如何欺骗它。但这似乎是一个可能性。 - torek
2
这可能是一个权衡。我可能最终会选择使用 --force-with-lease。但我听说有些人在他们的机器上自动定时运行 fetch。对于他们来说,--force-if-includes--force-with-lease 更好。也许正是他们最初推动了这个功能的出现。;) - TTT
1
@DevinRhode:没有更短的语法(在我看来很不幸;-f 应该真正成为更安全选项的缩写,但由于向后兼容性而无法实现)。预推钩子也不能保证原子性:这将取决于另一侧实现了什么。 - torek
1
@DevinRhode:实际上,--force-with-lease 允许您提供所有输入:"我期望 refspec <name> 等于 <hash1>;如果是这样,请使用 <hash2> 强制替换它"。这在功能上等同于机器语言的 "比较并交换" 指令,在 Git 内部被称为 "compare and swap"。 - torek
1
无论如何,我确实尝试添加了一个更短的版本,但它仍然不是非常短。 - torek
显示剩余12条评论

11

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

[alias]

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

作为附加选项之一,与--force-with-lease[=<refname>]同时指定--force-if-includes(即不指定远程端必须指向哪个确切提交或保护哪些引用),在“push”时将验证是否在允许强制更新之前在本地集成了在后台可能隐式更新的远程跟踪引用的更新。保留HTML标记。
参考:https://git-scm.com/docs/git-push

4
虽然--force-if-includes的复杂性非常难以理解,但现在我完全理解了上面@torek的答案,我相当确定这不是强制推送的“终极”或“最安全”的方式。最终/最安全的方法是显式指定所有sha,如下所示:git push --force-with-lease=refs/heads/main:<hash-of-H> origin <hash-of-H'>:refs/heads/main - Devin Rhode

9

如果你总是直接使用 --force-with-lease 而不是 --force,并且想要一些快速信息,那么可以考虑使用 --force-with-lease --force-with-includes 来代替,因为这样会稍微更安全一些。

如果能抽出更多时间听我说的话,请继续往下看。如果在使用 git push --force-with-lease 之前运行 git fetch,则本质上你只是进行了无保护的强制推送。添加 --force-if-includes 可以同时利用 reflog 和远程跟踪分支来提供保护,因为执行 fetch 操作似乎相当无害,甚至可能发生在后台。

根据文档(我的评论加粗斜体)加粗是重点强调。

--[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.

这意味着,如果你正在使用 --force-with-lease 强制推送你的 blah 分支到 origin,但是你的 origin/blah 分支(远程跟踪分支)与服务器上的 blah 分支不同,它会阻止你进行推送。 这意味着有人对代码进行了更改,而你不知道。

还有更好的使用 --force-with-lease 的方法,但说实话,我们只是在寻找一些快速且安全的方法。如果你想学习更多,请查看文档。使用 --force-if-includes 除了检查远程跟踪分支外,还会检查你的引用日志,以确保没有错过的更改。


4

这实际上是强制推送的最安全方式,也是我个人的建议:

git push --force-with-lease=refs/heads/main:<expected-remote-sha> origin main

这使得--force-if-includes变得无关紧要,因为你直接告诉git你期望替换的远程sha是什么。我不知道还有什么方法可以使其更加安全。
当然,一旦你没有指定完整的显式sha,它就变得不那么安全了。 如果在某个庞大的monorepo中每秒都有新提交被推送,那么你基本上正在与其他开发者比赛,你可能不应该强制推送。 感谢@torek提供的精彩解释!

2
有些人可能好奇为什么这个页面上的一些开发人员关心更安全的强制推送。这就是我关心它的原因:我经常对自己的功能分支进行强制推送。这可能会给协作带来问题。如果其他人想要将更改推送到我的分支,我可能会意外地破坏他们的更改。通过使用完全显式的强制推送语法,我可以确保不会破坏他们所做的任何潜在更改。 - Devin Rhode
如果要推送到主分支,我会打开github.com并查看最近的PR、查看主分支上的提交等。如果要推送到我的特性分支,我可能会查看git reflog,使用“Git Graph”vscode扩展(一个reflog gui),或者查看我的github Pull Request页面上的sha。当然,如果我的PR分支上的提交不是来自我,或者是陌生的,我会暂停并重新考虑强制推送我的当前分支。 - Devin Rhode
1
我可能也会使用 git fetch origin && git log -1 origin/<branch> - Devin Rhode
1
好的。我问这个问题是因为我从来没有看到过使用 --force-with-lease 明确指定预期哈希值的好处。我认为,获取并查看 origin/<branch> 可能是常规操作,而 --force-with-lease 可以防止查看但最近没有获取到。如果您总是首先查看 origin/<branch>,那么我认为您不需要明确指定提交 SHA,因为它已经在使用该值了。如果出于某种原因您知道您不会获取,并且您将使用远程仓库 UI 确定提交,则明确提供它可能会有所帮助。 - TTT
2
话虽如此,尽管我可能永远不会明确指定哈希,但这是一个容易的赞成,因为如果你付出额外的努力去做到这一点,那么我完全同意:“这使--force-if-includes变得无关紧要”。 - TTT
显示剩余4条评论

2

使强制推送更安全

您可能仅通过cli使用git,但是某些编辑器(如vscode)将自动在后台每2分钟运行git fetch,并向您显示是否有新的提交要下载。

如果您想要更安全的强制推送,则需要在编辑器中禁用自动提取。

首先,通过gui进行强制推送是一个好主意,因为它使用非常明确的语法:

vscode git gui extension push/pull dropdown menu "force push" option

vscode 的底层运行:

git push --force-with-lease origin feature/foo:feature/foo

您可以通过 git config 选项 push.useForceIfIncludes 来影响这个调用:

[push]
  useForceIfIncludes = true

你可以将此与在 vscode 中禁用自动提取结合使用:

"git.autofetch": false,

这将导致通过 vscode 的 GUI 菜单进行强制推送失败,除非您在此之前明确运行 git fetchgit pullgit pull --rebase

GUI 可以使这更加安全,执行以下操作:
- 显示本地分支和远程分支之间的差异
- 显示日志差异,允许您查看哪些 shas/commits 将被删除,以及它们将被替换为什么
- 告诉您是否需要强制推送
- 实施 *更加安全* 的检查

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