首先,请注意:请确保您确实想要这样做,因为git replace
(下面简要提到)可用于以保留 ID 的方式将历史缝合在一起。当然,它也有其自身的缺点;搜索使用过它的人的报告。
是的,你可以这样做git filter-branch
.
不过,您可能想要combine“rebase new commits atop new conversion”步骤与“...然后编辑所有新提交以也包含其旧 ID”步骤,因为 rebase 的工作方式是copying提交,过滤器分支的工作原理是...copying承诺。 :-)
所有执行此类操作的 Git 命令must复制,因为每个提交的哈希 ID 是提交内容的函数。如果新提交与原始提交有任何不同,它将获得一个新的、不同的 ID。
之间的差异git rebase
and git filter-branch
关键在于复制哪些提交以及如何执行复制。
变基,当没有完成时--preserve-merges
,通过选择非合并提交列表,将每个此类提交转换为变更集(通过减法,或多或少:子项减去父项 = 从父项到子项的增量),然后将此增量添加到--onto
指向或指向到目前为止添加的提交。
当你使用--preserve-merges
, 变基still选择非合并提交的列表。然后,在有合并提交的地方,rebase重新表演合并(这就是为什么您必须重新解决合并冲突的原因)。它必须重新合并,因为新的基础可能会导致不同的合并,并且因为合并不能变成单个变更集(“子-父”为您提供一个增量,但至少有两个父级,因此至少有两个delta,一般情况下我们不能同时保留两者)。
Filter-branch 使用完全不同的方法。无论是否合并,都会选择要过滤的提交。 (实际选择是通过运行完成的git rev-list
,这相当于“管道”git log
.) 这个完整的提交 ID 列表被放入一个堆中:一个存储在普通文件中的已排序的拓扑顺序堆,以便父提交始终在其子提交之前得到处理。
然后,对于列表中的每个 ID:
提取原始提交 a lagit checkout
,进入没有底层 Git 存储库的临时树。
应用树过滤器来修改树。 (此修改在保存临时树的临时目录中运行。当很多人尝试访问像../../fixed-version
。相对路径失败,因为临时树根本不在存储库中。)
重建一组新的 Git 树和 blob 对象来表示新树,即新的提交快照。
将提交消息过滤器应用于消息。
将提交环境过滤器应用于剩余的提交元数据(作者和提交者的内容)。
使用新消息和新树进行新提交。或者,如果您提供提交过滤器,请使用它来进行或不进行提交;此时您还可以使用父过滤器修改新提交的父提交。
最后,记录一个配对:“旧提交 成为新提交 。” (如果您使用提交过滤器跳过提交,则旧哈希会映射到其相应的新祖先,即您的父级)didn't跳过。)这个配对是map.
由于提取+树过滤器+重建部分,这个过程非常慢。因此,如果您don't使用树过滤器,git filter-branch
跳过这一部分:无论如何它都会恢复原来的树。为了让您修改新提交的内容,filter-branch 还允许您指定一个索引过滤器(无论如何,提交总是从索引开始工作,因此提取+修改+重建只是更新索引;如果我们可以就地更新,那就快得多)。但是——这里是关键点——出于您的目的,您不需要对每棵树做任何事情。你想要的只是修改血统!这将使您保留原始合并及其源树,无需重新合并。
请注意,--commit-filter
描述谈论了map便利函数(shell 函数)。这个“地图”功能使用了我上面提到的地图。默认情况下是自动映射到新复制的提交的新父级。
最后,在复制所有提交之后,如果您提供--tag-name-filter
,还复制带注释的标签并映射副本(因此,如果您确实有带注释的标签,则do want a --tag-name-filter cat
此处)—filter-branch 命令重写了一些引用,即分支和标记名称。原始引用仍然指向原始提交(和带注释的标签对象),被转储到refs/original/
名称空间。 (该过程开始时必须为空,除非您使用--force
.) 重写的引用指向新的副本。重写使用相同的映射技术,因此如果有跳过的提交,名称现在指向保留的祖先提交。
(“一些”参考文献?等等,which参考?答案就在文档中,但有点神秘:它谈到了正面参考。参数被传递给git rev-list
这样您就可以过滤特定范围的提交,例如,branch~30..branch
or branch ^otherbranch
。 “积极”引用是主动选择提交的引用,而“消极”引用是限制提交的引用,因此对于branch ^otherbranch
我们有一个积极的参考,branch
,和一个负数,非其他分支部分。所以这仅重写refs/heads/branch
并不是refs/heads/otherbranch
.)
这是很多废话,但是……怎么办?
解释以上所有内容的原因是为了指出移植过程是多么简单,当使用git filter-branch
,然后展示如何访问地图。
首先,我们只需要明确地更换一份单亲身份证。具体来说,我们希望根提交 in git-commits
成为现有的提示提交svn-commits
:
$ git rev-parse svn-commits
9999999999999...
(这是理想的新父母),并且:
$ git rev-list --max-parents=0 git-commits
11111111111111...
(这是根提交 - 幸运的话只有一个,否则,现在怎么办?)。
所以,我们想要一个父过滤器也就是说:“如果这是提交 1111111...则 echo 9999999...,否则只需回显参数即可”。默认的父参数位于 stdin 上,作为一系列-p <id>
s,ID 已映射。当然,现有的根有no父母,所以标准输入将没有我们想要在这里更改的一个提交的内容。因此:
--parent-filter 'if [ $GIT_COMMIT = 11111... ]; then
echo -p 999999...; else cat; fi'
这部分的filter-branch
将完成我们的重新养育。请注意,与git rebase
,所有的树木都被完好无损地保留下来。我们在这里从不将快照转换为增量,我们只是按原样使用它。这意味着不需要重新解决合并冲突。
(旁注:您实际上可以使用名称svn-commits
代替硬编码99999...
这里。您可以使用名称来代替硬编码11111...
也是,但我们不have一个名字。此外,每次查找名称都会给过滤增加一点点延迟。对于一个重新养育孩子的人来说svn-commits
,这是一个微小的延迟;不过,为了测试这是否是旧根,这将是一个微小的延迟乘以 3000 次提交。)
(第二个旁注:您还可以通过“移植”或其更现代的版本来进行重新设置,git replace
。如果您跑步时移植物或替代物正在生效filter-branch
,移植或替换变成永恒的,因为 Git 只是按照指示复制提交,并且指示也在替换之后。)
这仍然留下过滤提交的问题messages, 加上:
Original hash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
如上所示,原始哈希在$GIT_COMMIT
,所以我们需要的是:
--msg-filter 'cat; echo; echo "Original hash: $GIT_COMMIT"'
如果我们想要更花哨,我们甚至可以使用它map便利功能:
--msg-filter 'cat; echo; echo "new commit $(map $GIT_COMMIT) \
filtered to reparent original commit $GIT_COMMIT"'
or something silly like that, but there's no good reason to bother ... unless you want to get really fancy, and see if you can detect old hash IDs in the commit message and rewrite them in place. I'm not sure if this is even a good idea, and won't attempt to provide a bit of shell script for it, but note that all1 of these filters are "eval"-ed as shell fragments. You can invoke other shell scripts from these eval-ed fragments, just remember that all the filtering is going on in a temporary directory.
对参考运行过滤git-commits
。过滤完成后,refs/heads/git-commits
将指向最后复制的提交,并且refs/original/refs/heads/git-commits
将指向原始链(植根于11111...
在上面的例子中)。
1Well, almost all. As the documentation says, "with the notable exception of the commit filter, for technical reasons".
Summary
我们需要两个过滤器,--parent-filter
(或有效的移植物或替代物),以及--msg-filter
。父过滤器说“用我们要移植到的位置的尖端替换移植副本的根部”,这完成了我们的 rebase-without-change-snapshots。消息过滤器说“这个新提交替换了我们在过滤时从变量中扩展的提交$GIT_COMMIT
".