提交嫁接可以实现在本地版本库上将两条完全不同提交线(分支)嫁接(连接)到一起。对于一些项目将版本控制系统迁移到Git上,该技术会非常有帮助。例如Linux本身的源代码控制在转移到Git上时,尚没有任何工具可以将Linux的提交历史从旧的Bitkeeper版本控制系统中导出,后来Linux旧的代码通过bkcvs导入到Git中,如何将新旧两条开发线连接到一起呢?于是发明了提交嫁接,实现新旧两条开发线的合并,这样Linux开发者就可以在一个开发分支中由最新的提交追踪到原来位于Bitkeeper中的提交。(参考:https://git.wiki.kernel.org/index.php/GraftPoint)
提交嫁接是通过在版本库中创建.git/info/grafts文件实现的。该文件每一行的格式为:
<commit sha1> <parent sha1> [<parent sha1>]*
用空格分开各个字段,其中第一个字段是一个提交的SHA1哈希值,而后面用空格分开的其他SHA1哈希值则作为该提交的父提交。当把一个提交线的根节点作为第一个字段,将第二个提交线顶节点作为第二个字段,就实现了两个提交线的嫁接,看起来像是一条提交线了。
在本书第6篇“第35.4节Git版本库整理”中介绍的git filter-branch命令在对版本库整理时,如果发现存在.git/info/grafts则会在物理上完成提交的嫁接,实现嫁接的永久生效。
提交替换是在1.6.5或更新版本的Git提供的功能,和提交嫁接类似,不过提交替换不是用一个提交来伪成装另外一个提交的父提交,而是直接替换另外的提交,实现在不影响其他提交的基础上实现对历史提交的修改。
提交替换是通过在特殊命名空间.git/refs/replace/下定义引用来实现的。引用的名称是要被替换掉的提交SHA1哈希值,而引用文件的内容(引用所指向的提交)就是用于替换的(正确的)提交SHA1哈希值。由于提交替换通过引用进行定义,因此可以在不同的版本库之间传递,而不像提交嫁接只能在本地版本库中使用。
Git提供git replace命令来管理提交替换,用法如下:
用法1: git replace [-f] <object> <replacement>
用法2: git replace -d <object>...
用法3: git replace -l [<pattern>]
其中:
提交替换可以被大部分Git命令理解,除了一些针对被替换的提交使用--no-replace-objects参数的命令。例如:
当提交foo被提交bar替换后,显示未被替换前的foo提交:
$ git --no-replace-objects cat-file commit foo
...foo 的内容...
不使用--no-replace-objects参数,则访问foo会显示替换后的bar提交:
$ git cat-file commit foo
...bar 的内容...
提交替换使用引用进行定义,因此可以通过git fetch和git push在版本库之间传递。但因为默认Git只同步里程碑和分支,因此需要在命令中显式的给出提交替换的引用表达式,如:
$ git fetch origin refs/replace/*
$ git push origin refs/replace/*
提交替换也可以实现两个分支的嫁接。例如要将分支A嫁接到B上,就相当于将分支A的根提交<BRANCH_A_ROOT>的父提交设置为分支B的最新提交<BRANCH_B_CURRENT>。可以先创建一个新提交<BRANCH_A_NEW_ROOT>,其父提交设置为<BRANCH_B_CURRENT>而提交的其他字段完全和<BRANCH_A_ROOT>一致。然后设置提交替换,用<BRANCH_A_NEW_ROOT>替换<BRANCH_A_ROOT>即可。
创建<BRANCH_A_NEW_ROOT>可以使用下面的命令,注意要用实际值替换下面命令中的<BRANCH_A_ROOT>和<BRANCH_B_CURRENT>。
$ git cat-file commit <BRANCH_A_ROOT> |
sed -e "/^tree / a \
parent $(git rev-parse <BRANCH_B_CURRENT>)" |
git hash-object -t commit -w --stdin
其中git cat-file commit命令用于显示提交的原始信息,sed命令用于向原始提交中插入一条parent SHA1...的语句,而命令git hash-object是一个Git底层命令,可以将来自标准输入的内容创建一个新的提交对象。
上面命令的输出即是<BRANCH_A_NEW_ROOT>的值。执行下面的替换命令,完成两个分支的嫁接。
$ git replace <BRANCH_A_ROOT> <BRANCH_A_NEW_ROOT>