首页 > 解决方案 > 通过具体示例,使用 git rebase 覆盖共享历史的危险

问题描述

所以我正在学习更多关于 Git Rebasing 的信息,我刚刚了解到,如果不使用force选项,在初始推送之后你不能推送一个 rebase 的分支。意义:

  1. 我切断了我的分支开发(git pull develop && git checkout -b feature/mybranch
  2. 我做我的工作feature/mybranch
  3. 我添加并提交 ( git add . && git commit -m "some message")
  4. 我从origin/develop
  5. 我推动git push -u origin feature/mybranch并创建公关
  6. 更改请求是 PR 的一部分
  7. 我在本地解决了这些变化feature/mybranch
  8. 再次,我添加并提交 ( git add . && git commit -m "some message")
  9. 再次,我从origin/develop
  10. 我尝试再次推送,git push以便将代码审查期间请求的更改推送到远程分支。Git不允许我这样做!并非没有指定强制选项。

我试图理解为什么。于是我询问了这件事,有人告诉我:

"在推送形成拉取请求后,您不能变基,因为这会重写共享历史记录。共享历史记录是您推送的任何其他人可能已获取的内容;您将不得不使用强制推送已推送的变基版本分支,那是一种恶臭,应该警告你,因为你可能会破坏其他人与数据的关系。

但是,作为一个 git 新手,这个答案似乎有些神秘,对我来说意义不大,没有具体的例子可以盯着和理解。

试图将这种反应分解成我可以理解的东西,听起来好像这是后续变基 + 推送创建的问题:

  1. 我切断了我的分支开发(git pull develop && git checkout -b feature/mybranch
  2. 我做我的工作feature/mybranch
  3. 我添加并提交 ( git add . && git commit -m "some message")
  4. 我从origin/develop
  5. 我推动git push -u origin feature/mybranch并创建公关
  6. 更改请求是 PR 的一部分
  7. 当我在本地处理这些更改时,另一位开发人员错误地将 PR 合并到develop. 因此,现在develop包含其他开发人员对其他票证/PR 所做的任何更改,以及我不应该存在的更改。
  8. 所以,与此同时,我在本地解决了这些变化feature/mybranch
  9. 再次,我添加并提交 ( git add . && git commit -m "some message")
  10. 再次,我从origin/develop
  11. 问题是:就像我在上面第 7 步中提到的那样,origin/develop现在包含作为 PR 的一部分推送的我的初始提交。现在,git 正试图重放那些feature/mybranch已经包含它们的“未授权”提交,这导致提交历史看起来很奇怪。

我在上面描述的这种情况是 git 在您之前已经重新设置和推送之后强制您强制推送的原因吗?还是我错误地解释了该响应?如果我的解释不正确,有人介意给我一个具体的用例(类似于我上面所做的),以便我可以完全理解这里的内在危险吗?

标签: gitversion-controlrebase

解决方案


有几种不同的方法可以解决这个问题。一种来自纯 Git 机制,一种来自更高层次的视角。

机械地

您需要使用git push --force,因为您必须说服其他Git 存储库采取可能会丢失数据的操作。

Git 存储库主要由两个数据库组成:

  • 一个数据库保存 Git 的对象,它们是提交(带有元数据的快照)、树和 blob(实现快照)和带注释的标签(一个独立的实体,通常指的是提交)。

  • 另一个数据库保存 Git 的引用refs。(这个数据库目前是以一种相当特别的方式实现的,它使用了各种文件的混合,这些文件的路径名包含参考名称组件;有一个长期进行的项目来在这里添加一个真实的数据库。)参考只是一个名称,通常是 ASCII尽管 Git 在这里的限制相对较少,而且 UTF-8 也应该可以正常工作(但请参阅“ad-hoc fashion”并注意文件系统会将其搞砸),通常开始refs/并继续将其作为下一个组件,名称-名称所在的空间。所以refs/heads/保存分支名称,refs/tags/保存标签名称,refs/remotes/保存远程跟踪名称,等等。

主数据库中的对象存储在哈希 ID 名称下;哈希 ID 是对对象内容运行加密校验和的结果,因此一旦输入数据库,对象就永远是只读的。(Git 验证数据在提取时再次校验和时是否与用于查找数据的键匹配。)四种对象类型中的三种具有受约束的格式:带注释的标签、提交和树。这些都可以引用其他对象。提交特别是指提交,通过哈希 ID。

这个大球最终形成了一个有向无环图:带注释的标签对象引用另一个对象(标签的目标)。提交是指其他更早的提交和树。树是指子树和blob。Blob 保存原始数据(主要是文件数据,但也包含符号链接的符号链接目标)。

为了进入这个 DAG,我们使用了引用。任何直接从名称引用的对象都是直接引用的。如果该对象引用其他对象,则间接引用那些其他对象。

在某些情况下,Git 会运行git gc. 这将检查主数据库中每个对象的可达性(直接或间接引用状态)。无法到达的对象被丢弃。(这还有很多,但同样,这是一个合理的高级开始。)

由于提交存储父哈希 ID,因此提交形成链(在合并提交时偶尔会有分支操作,它有两个或多个父级,而不仅仅是通常的一个)。因此,指链中的最后一次提交,指的是该链中的所有提交:

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

这里H代表一些提交哈希 ID。类似mainfeature/tall可能指代 commit的名称H。同时, CommitH指回较早的 commit G,后者又指较早的 commit F,依此类推。

如果我们向这个分支添加一个提交,以通常的方式:

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

我们得到(假设我们I用于下一次提交):

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

也就是说,main 用于定位 commit的名称H。现在它找到了 commit I。Commit通过后退一步I 到达commit 。H如果我们一次添加两个提交,而不是一次只添加一个提交,这一切仍然有效:main将指向J,将指向I,将指向H

这种操作——简单地提交添加到链的末尾——保证所有之前引用的提交仍然被引用。测试是否对名称进行此更新以保持所有较早的提交容易执行:我们只需从提议的提交开始,例如J,然后逐跳向后工作,以查看是否到达提交这个名字早先指出的。(我们可以在这里使用深度优先或广度优先搜索;Git 通常使用一种广度优先搜索,但这种祖先测试无处不在,因此得到了高度优化。)

工作方式git push就是做这种事情。 首先,发送 Git 打包接收 Git 可能需要的新提交。接收 Git 将这些存储在对象数据库中——从技术上讲,在现代 Git 的隔离区中,但这里的细节并不重要。然后发送者要求接收者更新一些参考,通常是一些分支名称。

如果更新是快进操作,即只添加新的提交,则允许。(好吧,这里是允许的 pre-receive 和 update 钩子有机会因为其他原因拒绝它。)如果不是,它会被拒绝,因为如果不加倍努力,Git 无法判断它是否会导致一些现有的提交变得遥不可及

所以这就是这种推动是一个问题的机械原因。

更高层次:Git 缺少过时的概念

当我们运行时git rebase,我们让 Git 将一些现在已经过时且糟糕(无论出于何种原因)的现有提交复制到一系列新的和改进的提交中。例如,在您的场景中,我们可能从以下开始:

...--G--H   <-- origin/develop
         \
          I--J--K   <-- feature/mybranch, origin/feature/mybranch

由于一段时间过去了,在origin. 我们得到它们(使用git fetch),现在在本地拥有它:

...--G--H--L   <-- origin/develop
         \
          I--J--K   <-- feature/mybranch, origin/feature/mybranch

我们git rebase origin/develop退房后跑feature/mybranch。我们的 Git用一个新的和改进的链淘汰了整个I-J-K链,该链依赖于并扩展自 commit K

             I'-J'-K'  <-- feature/mybranch
            /
...--G--H--L   <-- origin/develop
         \
          I--J--K   <-- origin/feature/mybranch

如果 Git 有办法将现有提交标记为“这些新改进版本已过时”,我们也许可以运行git push origin feature/mybranch、发送它们I'-J'-K'并让他们检查这三个提交是否应该消失,并被这些新的- 和改进的。

实现这一点的棘手部分是我们不能丢弃I-J-K,因为任何 DVCS 的分布式特性意味着I-J-K现在“在野外”的 DVCS 可能会像某种病毒瘟疫一样再次困扰我们。(我们对当今世界上的病毒瘟疫没有经验,是吗?咳咳。)我们必须以某种方式将它们标记为过时的,而实际上根本不接触它们,因为任何 Git 对象都不能被修改。

(Mercurial 的 Evolve 扩展做了这种事情,但是在 Mercurial 中,提交可以被触及。例如,所有提交都有可以随时更改的“阶段”位。发布提交 - 通过 push 或 Hg 的 Git 等价物fetch,哪个hg拼写pull——通常将它从草稿阶段移到公共阶段。这些在 Git 中根本不存在。)


推荐阅读