由于您已经推送了两个特性分支,因此不应再进行变基。强烈不建议对已发布的分支进行变基,因为它会破坏其他开发人员的存储库。这是因为变基只是提交的一个完全重写。正在变基的提交将被重新创建,内容已更改,并且最重要的是具有不同的哈希。这导致新提交与旧提交不兼容,因此谁拥有旧提交的人最终会与替换它们的新提交产生冲突。
正确的解决方案是简单地合并更改。虽然这可能看起来不太美观,但这是一种非破坏性的操作,在这种操作中没有更改现有的提交。所有发生的事情就是添加提交,这样在推送或拉取时就不会出现任何问题。
话虽如此,您仍然可以进行变基并发布更改的分支。但是,为此,您需要强制推送分支,而拉取这些更改的其他开发人员将需要将其分支重置为新版本。
将我下面的一些评论纳入答案:重要的是要理解,在Git中,分支只是指向提交的指针。整个历史记录——没有分支——是一个大的无环图形,其中提交只指向其父提交。因此,以您在问题中的示例为例,这是没有任何分支指针的历史记录:
A -- B -- C -- D -- E
\ /
F --- G
\
H -- I -- J
每个字母代表一个提交,所有与其相连且在其左侧的提交称为其父提交。例如,
F
的父提交是
A
,
C
是一个合并提交,其父提交是
B
和
G
。
现在,如果我们在此可视化中添加分支,则只需添加指向某些提交的指针即可。这实际上什么都不是(分支就是一个文件,其中包含提交哈希):
master
↓
A -- B -- C -- D -- E
\ /
F --- G ← feature-1
\
H -- I -- J
↑
feature-2
现在,想象一下我们对feature-2
分支进行了提交。我们将该提交添加到树中...
\
H -- I -- J -- K
↑
feature-2
...然后我们将分支指针向前移动一个:
\
H -- I -- J -- K
↑
feature-2
现在,为了理解在推送期间会发生什么,我们需要记住远程分支也只是分支,因此只是另一组指针。因此实际上看起来像这样:
\
H -- I -- J ----------- K
↑ ↑
origin/feature-2 feature-2
我想你现在可以想象在push期间会发生什么:我们告诉远程仓库更新它的分支指针,使其指向K
。但是服务器只有J
,所以我们需要给服务器构建可访问K
的树中缺失的所有内容(也就是在它们之间的所有提交和实际内容)。但当然我们不需要物理地推送J
、H
或甚至A
(虽然它们都是feature-2
分支上的技术上的对象,因为你可以到达它们);Git聪明到足以确定哪些对象实际上是缺失的(你可以在开始推送时看到Git正在计算这个过程)。
因此,一旦我们将缺失的对象传输到远程仓库,我们就会告诉远程仓库更新它的feature-1
指针,使其也指向K
。如果成功了,我们还会更新我们的远程分支(origin/feature-2
),使其也指向K(只是为了保持同步)。
现在,合并的情况也是相同的。想象一下我们把master
合并到feature-2
中(使用git merge master
命令处于feature-2
分支):
master
↓
A
\ / \
F
\ \
H
↑
feature-2
现在,如果我们想要推送
feature-2
,我们需要给远程仓库所有缺少的对象。由于我们现在处于合并提交状态,所以我们需要检查
所有的父级:所以如果服务器没有
K
,我们需要推送
K
; 但是,如果它没有
E
,我们也需要推送
E
。当然,我们还需要再次遵循这些父级,以确保所有对象都存在于远程。一旦完成了这个过程,我们只需再次告诉远程更新分支指针。
因此,总结一下:一个分支包含通过导航其提交的父级在非循环树中以某种方式可访问的所有提交。但是即使这意味着分支通常非常“大”(历史长度),Git也只会将那些它没有的对象传输到远程存储库。因此,尽管合并可以向分支添加更多提交,但如果远程已经从另一个分支知道这些提交,则不必传输这些提交。
最后,关于rebase的一些注意事项:上面我们使用git merge master
将master
分支合并到feature-2
。如果我们改为使用git rebase master
,现在的完整树将如下所示:
master feature-2
↓ ↓
A -- B -- C -- D -- E -- H' -- I' -- J' -- K'
\ /
F --- G ← feature-1
\
H -- I -- J -- K
↑
origin/feature-2
正如您所看到的,有新的提交 H'
, I'
, J'
和 K'
。这些是重写的提交,使它们从 E
开始(在 rebase 时 master
指向的位置)而不是 G
。由于我们进行了 rebase,因此没有合并提交 L
。正如上面明确说明的那样,原始提交仍然存在。只是没有指针指向它们;因此它们是“丢失的”,最终将被垃圾回收。
那么现在推送时的问题是什么?远程分支仍然指向原始的 K
,但我们现在希望它指向 K'
。因此,我们开始像以前一样向远程仓库提供所有所需的对象。但是当我们告诉它更新分支指针时,它会拒绝执行。原因是通过将指针设置为 K'
,它必须“回退历史记录”并忽略提交 H
到 K
的存在。它不知道我们已经对它们进行了 rebase,而且重写的提交和原始的提交之间也没有链接。因此,为防止意外数据丢失,远程服务器将拒绝更新分支指针。
现在,您可以进行 强制推送 分支。这将告诉远程仓库即使这样做会丢弃那些原始提交,也要更新分支指针。因此,您这样做后,情况将如下所示:
origin/feature-2
master feature-2
↓ ↓
A -- B -- C -- D -- E -- H' -- I' -- J' -- K'
\ /
F --- G ← feature-1
现在为止一切都很好:你决定将分支 rebasing,并告诉远程仓库接受它,没有质疑。但是现在想象一下我想要 pull;而我的分支仍然指向 I。所以运行 pull 相当于 push 的反向操作:远程给我所有完成历史所需的对象,然后告诉我在哪里设置分支指针。此时,我的本地 Git 拒绝这样做,原因与远程仓库之前拒绝相同。
在 push 之前,我们知道要替换原始提交;但是对于 pull,我们没有这个信息,因此现在需要调查或询问是否应该仅替换本地分支,或者是否实际上在远程存在某些错误。如果我们现在在本地进行了一些工作需要合并,则情况会变得更糟。
这些问题发生在每个获取那些原始提交的人身上。通常,您希望完全避免这种混乱,因此规则是永远不要 rebase 已经发布的内容。只要没有其他人获取到这些原始提交,就可以进行 rebase,但是一旦不再是这种情况,将会对所有涉及的人造成麻烦。因此,强烈建议使用 merge。