git - 通过具体示例,使用 git rebase 覆盖共享历史的危险
问题描述
所以我正在学习更多关于 Git Rebasing 的信息,我刚刚了解到,如果不使用force选项,在初始推送之后你不能推送一个 rebase 的分支。意义:
- 我切断了我的分支开发(
git pull develop && git checkout -b feature/mybranch
) - 我做我的工作
feature/mybranch
- 我添加并提交 (
git add . && git commit -m "some message"
) - 我从
origin/develop
- 我推动
git push -u origin feature/mybranch
并创建公关 - 更改请求是 PR 的一部分
- 我在本地解决了这些变化
feature/mybranch
- 再次,我添加并提交 (
git add . && git commit -m "some message"
) - 再次,我从
origin/develop
- 我尝试再次推送,
git push
以便将代码审查期间请求的更改推送到远程分支。Git不允许我这样做!并非没有指定强制选项。
我试图理解为什么。于是我询问了这件事,有人告诉我:
"在推送形成拉取请求后,您不能变基,因为这会重写共享历史记录。共享历史记录是您推送的任何其他人可能已获取的内容;您将不得不使用强制推送已推送的变基版本分支,那是一种恶臭,应该警告你,因为你可能会破坏其他人与数据的关系。 ”
但是,作为一个 git 新手,这个答案似乎有些神秘,对我来说意义不大,没有具体的例子可以盯着和理解。
试图将这种反应分解成我可以理解的东西,听起来好像这是后续变基 + 推送创建的问题:
- 我切断了我的分支开发(
git pull develop && git checkout -b feature/mybranch
) - 我做我的工作
feature/mybranch
- 我添加并提交 (
git add . && git commit -m "some message"
) - 我从
origin/develop
- 我推动
git push -u origin feature/mybranch
并创建公关 - 更改请求是 PR 的一部分
- 当我在本地处理这些更改时,另一位开发人员错误地将 PR 合并到
develop
. 因此,现在develop
包含其他开发人员对其他票证/PR 所做的任何更改,以及我不应该存在的更改。 - 所以,与此同时,我在本地解决了这些变化
feature/mybranch
- 再次,我添加并提交 (
git add . && git commit -m "some message"
) - 再次,我从
origin/develop
- 问题是:就像我在上面第 7 步中提到的那样,
origin/develop
现在包含作为 PR 的一部分推送的我的初始提交。现在,git 正试图重放那些feature/mybranch
已经包含它们的“未授权”提交,这导致提交历史看起来很奇怪。
我在上面描述的这种情况是 git 在您之前已经重新设置和推送之后强制您强制推送的原因吗?还是我错误地解释了该响应?如果我的解释不正确,有人介意给我一个具体的用例(类似于我上面所做的),以便我可以完全理解这里的内在危险吗?
解决方案
有几种不同的方法可以解决这个问题。一种来自纯 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。类似main
或feature/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 中根本不存在。)
推荐阅读
- regex - 如何用引号将所有单元格括起来?
- r - 使用 dplyr 仅过滤唯一值列表中的一个
- angular - 有没有办法让同一组件上的不同指令调用在它们之间进行通信?
- r - 更改ggboxplot中点的SIZE和ALPHA?(右)
- webpack - 使用 webpack 时未加载 scss 文件
- java - 在 VSCode 中使用 jar 文件中的类
- python - Plotly:如何控制双 y 轴前面的轨迹?
- javascript - 如何从对象数组中选择所有特定的键值
- resources - Adobe Acrobat Pro 在 OCR 期间未使用所有资源 - 这正常吗?
- django - Postgres在多个表上操作bulkinsert的序列