通过具体示例,使用 git rebase 覆盖共享历史记录的危险
Dangers of overwriting shared history with git rebase, by concrete example
所以我正在学习更多关于 Git 变基的过程,我刚刚了解到如果不使用 force[= 就无法在初始推送后推送变基分支73=] 选项。含义:
- 我切断了我的分支 develop (
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 的一部分提出
- 我在
feature/mybranch
中解决本地更改
- 再次,我添加并提交 (
git add . && git commit -m "some message"
)
- 再次,我从
origin/develop
- 我尝试再次推送
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 的问题:
- 我切断了我的分支 develop (
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 的一部分提出
- 当我在本地处理这些更改时,另一位开发人员错误地将 PR 合并到
develop
。所以现在 develop
包含其他开发人员对其他 tickets/PRs、 加上 我的更改,这些更改不应该存在。
- 因此,与此同时,我在
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 的 references 或 refs。 (这个数据库目前以一种相当特别的方式实现,使用各种文件的混合,这些文件的路径名包含 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。 main
或 feature/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中根本不存在。)
所以我正在学习更多关于 Git 变基的过程,我刚刚了解到如果不使用 force[= 就无法在初始推送后推送变基分支73=] 选项。含义:
- 我切断了我的分支 develop (
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 的一部分提出
- 我在
feature/mybranch
中解决本地更改
- 再次,我添加并提交 (
git add . && git commit -m "some message"
) - 再次,我从
origin/develop
- 我尝试再次推送
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 的问题:
- 我切断了我的分支 develop (
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 的一部分提出
- 当我在本地处理这些更改时,另一位开发人员错误地将 PR 合并到
develop
。所以现在develop
包含其他开发人员对其他 tickets/PRs、 加上 我的更改,这些更改不应该存在。 - 因此,与此同时,我在
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 的 references 或 refs。 (这个数据库目前以一种相当特别的方式实现,使用各种文件的混合,这些文件的路径名包含 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。 main
或 feature/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中根本不存在。)