背景:
由于架构优化、组件下沉、仓库调整等原因,在工作中时常需要将仓库A中的代码迁移到仓库B中,同时希望代码迁移后,能够保留其git历史记录。
目标:
- 解决A仓库全部迁移到B仓库,并保留git历史记录问题
- 解决A仓库中部分子目录文件迁移到B仓库,并保留git历史记录问题。
- 解决2中,可能丢失部分git记录的问题。
原理:
Git设置多个远程仓库,并分别pull下来
git项目初始化后,默认情况下会有一个叫origin的远程仓库。我们也可以用 git remote add repo_A [repo_A的仓库地址]
来新增一个"远程"仓库,然后使用git pull,将多个仓库的代码拉下来并merge。这样便可以实现A仓库全部迁移到B仓库,并保留git历史记录。
注意这里的[repo_A的仓库地址]
也可以是本地仓库路径。比如我这里添加了两个repo,repo-A是位于本地路径的"远程"仓库。
Git filter-branch 重写历史
我们已经知道了如何将A仓库全部迁移到B仓库,如果我们只需要将A仓库中的部分子目录文件迁移,就需要用到git重写历史的功能。git filter-branch
是git提供的重写历史的命令,我们可以用subdirectory-filter
来实现重写git项目子路径的git历史记录。
--subdirectory-filter
重写子路径的git历史记录 Only look at the history which touches the given subdirectory. The result will contain that directory (and only that) as its project root.
subdirectory-filter
可以从git项目中过滤出给定子目录的git历史记录,同时会把给定的子目录作为git项目的根目录。
举例看下效果,原始repo_A的状态如下,有other和ugc俩个子目录,现在我们只需要迁移A仓库的ugc目录,因此需要先把ugc目录的git历史记录过滤出来。
使用 git filter-branch --subdirectory-filter ./ugc -- --all
重写./ugc
路径的git历史。可以看到,仓库A只会保留ugc下的文件和git历史记录,其他的会被移除。再结合前面的原理,便可以实现A仓库中部分子目录文件迁移到B仓库,并保留git历史记录
按上述的方案已经能够实现A仓库中部分子目录文件迁移到B仓库,但是某些情况下会丢失git记录。publishwtt是从publish模块抽离出来的,按上面的方案提取publishwtt路径的git历史记录,只能提取到新建publishwtt目录时间点之后的git记录,我们的情况是publishwtt刚从publish抽离不久,这样很多git历史会丢失,这是我们不愿意接受的。
既然publishwtt从publish抽离来的,那么我们保留publishwtt和publish的父路径的git历史记录,就不会有git记录丢失的问题了。但是直接使用其父路径的历史记录,会把该目录的所有子目录的历史记录都保留下来,造成过多无用记录,我们还应该删除bytecert、profile等路径的git记录。可以用到index-filter
来删除指定文件或文件夹,以及其git记录。
git filter-branch --force --index-filter 'git rm --cached -r --ignore-unmatch 文件路径' --prune-empty --tag-name-filter cat -- --all
该命令会遍历所有commit记录,删除提交记录中包含指定文件的记录,如果删除后是空提交,把空提交记录也删除。subdirectory-filter
和index-filter
配合使用,可以较好的实现A仓库中部分子目录文件迁移到B仓库,并保留git历史记录。
实现过程:
假设是要把repo A的 ugc/publish移动到repo B的 ugc/publish里面去。
第一步:在repo A里面准备要提交的目录。
- 可以先把remote端给remove掉,这样本地的修改一定不会影响到网络上的版本(可选项);
- 使用
git filter-branch
把需要的目录/文件筛选出来,放到对应目录里面去; - 然后将此修改提交
1. git remote rm origin
2. git filter-branch --subdirectory-filter -- --all
3. mkdir
4. mv *
5. git add .
6. git commit
备注:一般的仓库执行会比较快,对于主业务仓库,比如头条ttmain有20多万次commit记录,执行时间会很长,命令跑起来后就可以先去干其他事了。另外,3-6行为修改文件路径,视情况决定是否需要执行。
第二步:将准备好的repo_A合并到repo_B中去。
- 切换到repo B的目录;
- 将本地的修改后的repo A添加为repo B的一个远程仓库,起名为from_repo_A
- 将这个名为from_repo_A的远程仓库 pull进来,并merge;
- 删除远程仓库 from_repo_A。
1. cd
2. git remote add < repo_A_directory>
3. git pull master --allow-unrelated-histories
4. git remote rm
迁移完成后repo-B的sourcetree状态:
迁移子目录可能丢失git历史记录问题
前面已经介绍过迁移子目录可能会丢失git历史记录的场景,这里直接介绍实现过程:
上图已经是使用subdirectory-filter
重写历史记录得到的git项目,再继续使用index-filter
删除bytecert、profile等无关子目录的git历史记录。删除bytecert历史记录:
git filter-branch --force --index-filter 'git rm --cached -r --ignore-unmatch bytecert' --prune-empty --tag-name-filter cat -- --all
该工程commit次数较多,每个删除任务执行时间较久,我们这里要删除的比较多,可以写一个.sh脚本,跑起来后,我们就可以去干其他事了。执行完后,将这个准备好的repo_A合并到repo_B中去即可。
git filter-branch --force --index-filter 'git rm --cached -r --ignore-unmatch profile' --prune-empty --tag-name-filter cat -- --all
git filter-branch --force --index-filter 'git rm --cached -r --ignore-unmatch bytecert' --prune-empty --tag-name-filter cat -- --all
...
总结:
通过git的重写历史和支持设置多个远程仓库的能力,可以实现跨仓库迁移代码,并保留git历史记录。
参考文档:
- git-filter-branch
- removing-sensitive-data-from-a-repository