我应该在什么时候使用“git push --force-if-includes”

0lvr5msh  于 2022-12-21  发布在  Git
关注(0)|答案(3)|浏览(154)

当我想要强制推送时,我几乎总是使用--force-with-lease。今天我升级到Git 2.30,发现了一个新选项:--force-if-includes.
在阅读了updated documentation之后,我仍然不完全清楚在什么情况下我会像通常那样使用--force-if-includes而不是--force-with-lease

uurv41yg

uurv41yg1#

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,你可以跳过所有花哨的东西,直接使用常规的(非强制的)push,而不需要仔细检查。

The --force-if-includes option is, as you've noted, new. If you've never needed it before, you don't need it now. So the shortest answer to "when should I use this" would be "never". 🙃 The recommended answer is (or will be once it's proven?) always. (I'm not yet convinced one way or the other, myself.)
blanket "always"或"never"并不是很有用。让我们看看你可能想在哪里使用它。严格地说,它从来没有必要,因为它所做的只是稍微修改--force-with-lease。所以我们已经有了--force-with-lease。如果要使用--force-if-includes。1在了解--force-with-includes之前,我们应该先了解一下--force-with-lease的实际工作原理。我们要解决的 * 问题 * 是什么?我们的"使用案例"或"用户案例"是什么或者是什么最新的流行语,当有人读到这篇文章的时候?
(Note:如果您已经熟悉了所有这些内容,您可以搜索下一个force-if-includes字符串以跳过接下来的几节,或者直接跳到底部,然后向上滚动到节标题。)
这里我们遇到的基本问题是 * 原子性 。Git最终主要是--或者至少在很大程度上是--一个数据库,任何好的数据库都有四个属性,我们有助记符ACID:原子性、一致性、隔离性和持久性。Git本身并不能完全实现这些特性:例如,对于Durability属性,它依赖于(至少部分依赖于)操作系统来提供它。但其中三个属性-C、I和D-首先是Git仓库中的本地属性:如果您的计算机崩溃, 您的 * 数据库副本可能是完整的、可恢复的或其他的,这取决于您自己的硬件和操作系统的状态。
然而,Git不仅仅是一个本地数据库,它是一个 * 分布式 * 的数据库,通过复制来分发,它的原子性单元--提交--分布在数据库的多个复制中。当我们在本地进行一个新的提交时,我们可以使用git push将其发送到数据库的其他副本。这些副本将尝试提供自己的ACID行为。在那些计算机上。但是我们希望在推送过程中保持原子性。
我们可以通过几种方式来实现这一点,一种方式是从每个提交都有一个全局(或通用)唯一标识符的想法开始:GUID或UUID。2(我在这里使用UUID形式。)我可以安全地给你一个我做的新提交,只要我们都同意它得到我给它的UUID,而你没有。
但是,虽然Git使用这些UUID来 * 查找 * 提交,但Git还需要为提交--也就是某个链中的 * 最后一个 * 提交--提供一个 * name *,这就保证了无论谁使用仓库都有办法找到提交:name查找某个链中的"最后一个",从中我们可以找到同一个链中所有较早的。
如果我们都使用相同的 * name *,我们就有问题了。假设我们使用名称main来查找提交b789abc,而他们使用它来查找提交a123456
我们在这里使用git fetch的解决方案很简单:我们给他们的Git仓库取一个名字,比如origin。然后,当我们收到新的提交时(s)从他们那里,我们取 * 他们的 * 名字--在某个链中找到这些提交中的最后一个的名字,也就是--并 * 重命名 * 它。如果他们使用名字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,将提交 * 添加 * 到分支,而是做我们的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中却不能是唯一的,这样我们就可以安全地拥有我所说的"二重身":提交或其他内部对象具有相同的哈希ID,但内容不同。尽管如此,最好还是让它们真正唯一。
3Git虽然是"开箱即用",但没有这种权限检查,但像GitHub和Bitbucket这样的托管提供商添加了这种权限检查,作为他们增值的一部分,以说服我们使用他们的托管系统。
4无法找到的提交实际上并不会马上消失,而是会留到稍后的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的。他们的重定基会自动将他们的新提交复制到我们在这里称为I的提交中,没有合并冲突。这使得他们运行git push所需的时间比我们修改提交H'所需的时间要少。

输入--force-with-lease

--force-with-lease选项在Git内部调用了一个"比较和交换",它允许我们向其他Git发送一个提交,然后 * 让他们检查 * 他们的分支名称--不管它是什么--是否包含我们认为它包含的哈希ID。
让我们把origin/*的名字添加到我们自己的仓库中,因为我们之前把commit 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:<hash>部分,check 他们的refs/heads/main,如果不是给定的<hash>,他们拒绝整个事务,保持他们的数据库完整:他们保留了提交IH,并将我们的新提交H'丢弃在地板上。6
整个事务--main的forced-with-lease更新--插入了锁,这样,如果其他人现在试图推送某个提交(可能是I),其他人会被推迟,直到我们完成--force-with-lease操作(失败或成功)。
不过,我们通常不会把这些都拼出来,通常我们只会运行:

git push --force-with-lease origin main

这里,main提供了我们希望发送的最后一个提交的哈希ID H'和我们希望他们更新的ref-name(refs/heads/main,基于main是一个分支名称这一事实)。--force-with-lease没有=部分,所以Git会填充其余部分:引用名称是我们希望它们更新的名称refs/heads/main,并且预期提交是我们对应的 * 远程跟踪名称 * 中的提交,即我们自己的refs/remotes/origin/main中的提交。
结果都一样:我们的origin/main提供了H哈希,我们的main提供了H'哈希和所有其他名称。
6这取决于他们的Git是否有“隔离”功能,但我认为任何有强制-租约的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:<hash-of-H> origin main,推送将会失败--就像它应该失败一样--因为我们明确声明希望源节点的main包含哈希ID H,但是从git fetch中可以看到,它实际上具有哈希ID I

git push --force-with-lease origin main

如果宿主提供者Git最近一次提交的是I,我们会要求他们把main换成H',正如我们所看到的,他们这样做了:我们已经将I提交到我们的存储库中。我们只是 * 忘记将其放入。*
因此,我们的强制与租约有效,我们清除了在origin上提交的I,这一切都是因为我们运行了git fetch并且忘记检查结果。--force-if-includes选项 * 旨在 * 捕捉这些情况。
它的工作原理取决于Git的reflog,它扫描你自己的reflog来查找你的main分支,并选择提交H而不是I,用作--force-with-lease中的散列ID。这与git rebase的分叉点模式类似(尽管那个使用了你的远程跟踪reflog).我自己并不是100%确信--force-if-includes选项在所有情况下都能工作:例如,--fork-point就不能,但它在 * 大多数 * 情况下都能工作,我怀疑--force-if-includes也能。
所以,你可以尝试用它来处理所有的--force-with-lease推送,它所做的只是使用了一个不同的算法--Git的人希望这个算法更加可靠。考虑到人类的方式-为--force-with-lease使用的原子“如果匹配,则交换出分支名称”操作选择散列ID。您可以通过提供--force-with-lease=<refname>:<hash>部分来手动执行此操作,但目标是以比当前自动方式更安全的方式自动执行此操作。

xa9qqrwz

xa9qqrwz2#

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

[alias]

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

可替换地,在“推送”时将--force-if-includes指定为辅助选项沿着--force-with-lease[=<refname>](即,无需说明远程侧上的引用必须指向哪个确切提交,或者远程侧上的哪些引用正被保护)将验证在允许强制更新之前是否本地集成了来自远程跟踪引用的可能已在后台被隐式更新的更新。
参考:https://git-scm.com/docs/git-push

7d7tgy0s

7d7tgy0s3#

如果你总是使用--force-with-lease而不是--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>

通常,"git push"会拒绝更新一个远程引用,如果它不是用来覆盖它的本地引用的祖先。
如果remote ref的当前值是期望值,则此选项将覆盖此限制。否则,"git push"将失败。
[...]
或者,将--force-if-includes指定为--force-with-lease[=<refname>]***的辅助选项(这就是我们在执行--force-with-lease时所做的)*(即,不用说远程侧上的REF必须指向哪个确切提交,或远程侧上的哪些引用正被保护)将验证来自远程侧的更新是否被删除。在允许强制更新之前,可能已经在后台隐式更新的跟踪引用被本地集成。
这意味着如果你强制(使用租约)将你的blah分支推送到origin,而你的origin/blah分支(远程跟踪分支)与服务器的blah分支不同,它会阻止你。这意味着有人做了你不知道的更改。
有更好的方法来使用--force-with-lease,但老实说,我们只是在寻找一些快速和安全的东西。如果你想了解他们去看看文档。使用--force-if-includes检查你的reflog除了远程跟踪分支,以确保没有你错过的变化。

相关问题