通过具体示例,使用 git rebase 覆盖共享历史记录的危险

Dangers of overwriting shared history with git rebase, by concrete example

所以我正在学习更多关于 Git 变基的过程,我刚刚了解到如果不使用 force[= 就无法在初始推送后推送变基分支73=] 选项。含义:

  1. 我切断了我的分支 develop (git pull develop && git checkout -b feature/mybranch)
  2. 我在 feature/mybranch
  3. 上工作
  4. 我添加并提交 (git add . && git commit -m "some message")
  5. 我从 origin/develop
  6. 变基
  7. 我推送 git push -u origin feature/mybranch 并创建了一个 PR
  8. 更改请求作为 PR 的一部分提出
  9. 我在 feature/mybranch
  10. 中解决本地更改
  11. 再次,我添加并提交 (git add . && git commit -m "some message")
  12. 再次,我从 origin/develop
  13. 我尝试再次推送 git push 以便将代码审查期间请求的更改推送到远程分支。 Git 不会 让我这样做!必须指定 force 选项。

我想了解为什么。所以我询问了这个,我被告知:

"You can't rebase after pushing to form the pull request, because that rewrites shared history. Shared history is anything you've pushed that someone else might have fetched; you will have to use force to push a rebased version of an already pushed branch, and that is a Bad Smell that should warn you off, because you can damage the relationship of others to the data."

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

试图将该响应梳理成我能理解的东西,这几乎听起来好像这是后续 rebases + pushes create 的问题:

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

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

有几种不同的方法可以解决这个问题。一种是纯粹的Git力学,一种是更高层次的视角

机械

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

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

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

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

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

这一大堆东西最终形成了一个有向无环图:带注释的标签对象引用另一个对象(标签的目标)。提交指的是其他较早的提交和树。树是指子树和斑点。 Blob 保存原始数据(主要是文件数据,但也有 symlinks 的符号 link 目标)。

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

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

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

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

这里 H 代表一些提交哈希 ID。 mainfeature/tall 之类的名称可能指的是提交 H。同时,提交 H 引用更早的提交 G,后者又引用更早的提交 F,依此类推。

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

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

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

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

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

这种简单的 添加 提交到链的末尾的操作 - 保证 所有先前引用的提交仍然是参考。测试是否对名称的此更新保留所有早期提交 易于执行: 我们只是从提议的 new 开始 提交,说 J,然后我们向后工作,一步一步地搜索,看看我们是否到达名称指向的 old 提交-到更早。 (我们可以在这里使用深度优先或广度优先搜索;Git 通常使用一种广度优先搜索,但这种祖先测试无处不在,因此被大量优化。)

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

如果更新是快进操作,即仅添加新提交,则允许​​。 (好吧,这里允许 预接收和更新挂钩有机会因其他原因拒绝它。)如果不是,它会被拒绝,因为没有更努力地工作,Git 无法判断它是否会导致某些现有提交变得 无法访问.

这就是这种推送存在问题的机械原因。

更高级别:Git 缺少 obsolescence

的概念

当我们 运行 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 链有一个新的和改进的链,它依赖于并扩展自提交 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,现在“在野外”,可能会像某种病毒瘟疫一样回来困扰我们。 (我们对当今世界的病毒性瘟疫没有任何经验,是吗?咳咳。)我们必须以某种方式将它们标记为过时的,而实际上 根本不接触它们 因为没有 Git 对象可以被修改。

(Mercurial 的 Evolve 扩展做这种事情,但在 Mercurial 中,提交 可以 被触及。例如,所有提交都有“阶段”位,可以在任何时候。通过推送或 Hg 等同于 Git 的 fetch 发布提交,其中 hg 拼写 pull - 通常将其从 draft阶段到public阶段。这些在Git中根本不存在。)