您的图片中不清楚的一件事是存在哪些分支(或其他参考)并且需要移动。您指的是“B、C、D、E、F、G、H、I、J、K 分支”和“O 分支”,但在您绘制的图表中,这些似乎是提交。假设类似
,我会给出指示
A - B - C - F - G - J - K <--(master)
\ \
\ H - I <--(branch_X)
\
D - E <--(branch_Y)
O <--(new_root)
所以这里只是为每个没有孩子的提交有一个分支。
现在有几种方法可以继续,但在我们开始之前,有一件重要的事情需要了解。你不能“移动”一个提交。您可以创建一个新的提交以将旧提交对A 所做的更改应用于O。这称为变基。你得到的是一个带有新 ID 的新提交。
我想如果你有关心提交 ID 值的工具或文档,这可能很重要,但更一般地说,这意味着你最终会“重写”分支的历史,所以如果你之后想要推送到远程您可能必须“强制推送”(push -f),然后这将需要清理远程的任何其他克隆(请参阅git rebase 文档中的“从上游 rebase 恢复”)。
所以你能得到的最接近你描述的东西就是
A <--(old_root)
O - B' - C' - F' - G' - J' - K' <--(master)
\ \
\ H' - I' <--(branch_X)
\
D' - E' <--(branch_Y)
实际上,要真正摆脱原始提交可能需要相当多的额外步骤;默认情况下,您会得到类似的东西
A - B - C - F - G - J - K
\ \
\ H - I
\
D - E
O - B' - C' - F' - G' - J' - K' <--(master)
\ \
\ H' - I' <--(branch_X)
\
D' - E' <--(branch_Y)
A 树中的所有提交都无法访问,因此不会显示在默认输出中,但它们仍然存在(至少有一段时间)。如果这是一个问题 - 例如,如果所有这些的原因是删除 A 中的一些敏感信息或二进制膨胀 - 那么在重写提交和引用之后需要额外的步骤。
但是首先,如何进行重写?基本上有两种选择。
您也许可以使用git filter-branch。如果有很多分支,如果有需要包含在重写历史中的合并,或者如果有标签,那么这可能是更简单的方法。但是A 和O 之间的差异越大,就越难做到这一点。
另一种选择是变基。在某种程度上,这更直接,因为它为每个提交计算一个补丁,然后将该补丁应用到新的基础上;所以A 和O 之间的差异会被自动处理。 (实际上,我应该说“尽可能自动”;根据A 和O 之间的差异,应用每个补丁时可能会出现冲突。)但是您必须重新设置每个分支,并且您必须移动标签手动,rebase 不能很好地处理合并。
如果你决定试试filter-branch
您最需要的是--parent-filter。 git filter-branch 文档有几个关于如何重新提交提交的示例;在这种情况下,您需要将 B 从 A 重新设置为 O。
但是父过滤器是不够的。您还必须转换提交内容 (TREE) 以解决 A 和 O 之间的差异。例如,如果重点是 O 省略了一个文件,您可以使用 index-filter like
git filter-branch --parent-filter \
'test $GIT_COMMIT = <commit-id> && echo "-p <graft-id>" || cat' \
--index-filter \
'git rm --cached --ignore-unmatch path/to/unwanted/file` \
-- --all
(假设其余提交不会在以后引入您想要保留在该路径上的新内容。)
对于更复杂的转换(例如添加文件),使用tree-filter 可能比使用index-filter 更容易,尽管这样会更慢。
如果有标签,你只是想移动它们,你可以使用--tag-name-filter cat
有关详细信息,请参阅git filter-branch 文档。 https://git-scm.com/docs/git-filter-branch
如果你决定试试rebase
最简单的情况是rebaseing 一个无法到达合并提交的分支。例如,在我们的示例中,您可以说
git rebase --onto O A master
将 O 和 A 替换为相应提交的 SHA ID 或解析为 SHA ID 的其他名称或表达式。 (例如,您可以将O 的分支名称称为new_root。您可以标记A 以使其更易于引用,或使用master~6 之类的名称。
那么你有
A - B - C - F - G - J - K
\ \
\ H - I <--(branch_X)
\
D - E <--(branch_Y)
O <--(new_root)
\
A` - B` - C` - F` - G` - J` - K` <--(master)
请注意,J 和 K 无法访问,因此默认情况下不会显示在日志输出等中。您可以将 K 引用为 master@{1}(使用 reflog 查看 master 的位置以前)。
那么你会想要移动branch_Y。因此,您需要获取C' 的标识符,因为它将成为重写分支的新基础。在本例中,可能是 master~4 之类的表达式。
git rebase --onto master~4 master@{1} branch_Y
给我们
A - B - C - F - G - J - K
\ \
\ H - I <--(branch_X)
\
D - E
O <--(new_root)
\
A` - B` - C` - F` - G` - J` - K` <--(master)
\
D' - E' <--(branch_Y)
并为每个额外的分支继续。
如果有标签可以手动移动
git checkout new-commit-to-tag
git tag -f tag-name
如果这个历史记录包含提交,这是一个更大的问题。默认情况下,rebase 将尝试创建线性历史记录,您可以使用 --preserve-merges 覆盖它,但随后 rebase 将假定每个合并都是其父项的自然产物 - 即它将假定没有“邪恶合并”。出于这个原因,如果您已经通过合并重新定位,或者可能想要“分段重新定位”,您应该验证结果。例如,给定
... A -- B -- C
\
M -- G <--(master)
/
... D .. E .. F
您可以在C 和F 创建临时分支;然后 rebase C,rebase F,手动重新创建合并(确保考虑任何“邪恶”更改),最后在合并后 rebase 提交。