主流管理公共仓库的方案有以下几种:
-
npm包管理(包括版本管理、指定commitid等)
-
Git Submodule
-
Git subtree
-
其他...
npm
npm包管理方案缺点比较明显,代码会被作为第三方包拉取到node_modules下,开发人员难以在主仓库中对其进行开发和管理,主、子仓库的代码管理完全脱钩的,同步是一个很大的问题。比较适合第三方的或已经比较稳定通过版本来进行管理的代码。
Git submodule
Git submodule解决了npm管理的缺点。子仓库的代码包含在主仓库中,可以在主仓库中就对子仓库代码进行管理。但是主、子仓库代码的同步,主仓库管理子仓库代码分支切换、同步都很容易出错。如果子仓库也有多个并行分支在开发,主仓库对应有不同的分支都需要指定不同的子仓库的分支,管理起来成本非常的高。
submodule的一些基本操作命令和特征:
git pull默认不会更新(拉取)子模块,用git pull --recurse-submodules
git submodule update --init 子模块初始化本地配置文件和更新,初始化时有用,后面不会从子仓库远端拉取更新
git config -f .gitmodules submodule.locales.branch dev 指定locales的分支为dev
git submodule update --remote 更新submodule代码(.submodule中指定的dev分支,更新后locales指向dev最新一个commitID,子仓库子仓库留在一个称作"游离的 HEAD"的状态)
本地如果要编辑并保存子仓库代码,需要先切到dev分支,再在子仓库操作(和普通仓库一样)
或者在主仓库操作如
git submodule update --remote --merge 如果本地locales已经切换到dev,主仓库对应的子仓库commit合并到当前子仓库的dev分支
git submodule update --remote --rebase
git push --recurse-submodules=check 代码推送前检查子模块的是否有改动但没有提交
git push --recurse-submodules=on-demand 代码推送前主动推送没有推到远端的子模块?(我用这个命令总是失败)
git checkout --recurse-submodules master 切换分支避免有些分支有子模块,有些没有导致残留的子模块文件的干扰
Git subtree
Subtree和submodule对比 gb.yekai.net/concepts/su...
Subtree相对submodule管理起来简单很多。首先是将子仓库的代码拉取到指定的文件夹(相当于copy了一份)。子仓库的代码就像是主仓库本地的文件夹一样编辑、提交到主仓库。正常情况下完全感觉不到有subtree的存在,只有当subtree管理者觉得有必要将主仓库的代码同步到子仓库再去做同步即可。
subtree相关命令和特征:
git subtree add --prefix=locales gitlab.xxx/locales.git dev 拉取dev代码到locales(命令自动创建)文件夹
git subtree split --prefix=locales --rejoin 在git subtree push之前执行,提高后续git subtree push效率,也避免下面的subtree常见问题(一), split会单独将涉及subtree
目录的提交摘出来了,最终这个分支合并到原分支,产生了一个Split xxxxx的提交记录。
git subtree push --prefix=locales gitlab.xxx/locales.git dev 遍历主项目中所有涉及到locales文件夹改动的commit提交记录提交到locales.git仓库
git subtree pull --prefix=locales gitlab.xxx/locales.git dev
git remote add locales git@gitlab.xxx.git 在主仓库中添加子仓库的remote便于查看git graph
git subtree merge --prefix=
结论
最终选择subtree实现,原因有
-
远程仓库locales.git代码更改后要同步到主应用仓库main.git都需要手动拉取一下代码并新增一条提交记录,subtree更直观(git graph能展示合并记录)
-
subtree操作更方便,本地修改子仓库代码。submodule下更改内容都需要有一条locales.git的提交记录,main.git也需要有一条记录引用了具体哪个submodule commit的提交记录。操作过程中一旦忘了提交submodule,就可能导致主应用代码提交上去不是我们想要的。subtree由于代码已经是本地代码,所以不存在这个问题。只需要在最后一步将subtree中的代码push到locales.git即可(即使不推也不影响)
-
同步子仓库代码。同2,subtree只需要主应用生成一条提交记录,submodule模式要生成两条提交记录(locales.git和main.git分别有一条)
-
submodule子仓库无法解决本地开发、各环境打包引入的多语言子仓库分支要不一样的问题。subtree没有这样的烦恼,代码就在本地,跟随main.git的分支不同而不同。最终将代码同步到locales.git即可
locales.git仓库只有两个分支main(对应线上翻译)、dev(开发分支)
运营人员操作流程(locales.git仓库操作) :
- 热修复线上翻译。直接在locales.git main分支修改后提交。前端开发需要从主应用的main分支切出hotfix分支同步翻译修改代码,提交后上线。然后在主应用main合并到dev后,将subtree部分推到将locales.git dev即可(建议直接操作locales.git仓库合并,更简单和不容易报错)
- 普通迭代翻译,当前端开发已经准备完毕,将最新的翻译文件同步到了locales.git dev分支。直接在locales.git dev分支上修改并提交。【即使主应用有多个并行开发的代码,但上线总有先后,只要分别先后同步到locales.git dev翻译即可】
前端开发人员操作流程:
前端开发无需直接操作locales.git。
-
同步locales.git翻译。理论上只有在开发本地翻译基本处理完毕,需要同步到locales.ts dev分支才需要进行拉取、合并、推到locales.ts
-
上线完毕后,将代码然后在主应用将subtree部分推到将locales.git main即可
subtree常见问题
(一)subtree push或subtree split报Segmentation fault
Segmentation fault
错误的常见原因就是访问的内存超过了系统所给这个程序的内存空间,结合subtree push
和subtree split
做的事情,可以推测是遍历的commit太多了,有以下几种原因。
1.太久没split 上次split的提交到这次提交的数量已经超越了内存范围。
解决办法: (1)先备份一个分支,然后在当前分支删了subtree再subtree add
,重新生成一个subtree add
的提交记录,这时subtree还原到master状态,再从备份的分支把对subtree的操作pick过来,最后subtree push
。 (2)在mac上执行$ ulimit -s unlimited
,把栈大小限制设置为不限制,再执行subtree split
可执行成功,只是会很慢,因为还是遍历了很多次提交,只是没有超过栈大小限制。window设置方法网上也有一些,未做测试。
(3)代码分支树过多,通过git rebase方式减少分支然后重新subtree add
2.存在没有subtree add和subtree split的分支合入当前分支 这个情况在项目刚引入subtree时很容易发生。原因是有类似这样的提交树,test1在某个提交执行了subtree add
,test2在subtree add
之前就checkout
出来了,最后再合入test。
在遍历提交时,遍历到merge提交时,会往两个方向遍历,其中主干这个方向存在一个subtree add
的提交,所以遍历到这里截止,但是test2这个分支是在subtree add
之前就拉出来的,所以他的提交路径没有subtree add
,最终绕过subtree add
,把所有提交都遍历了,产生Segmentation fault
。
项目刚引入subtree时,由于其他同事的需求分支是在引入subtree之前就checkout出来的,最终合入master时就会产生这个情况,导致后续执行subtree push报错。
解决方法: 在引入subtree
后,通知全员把正在开发的分支执行一下git rebase master
。
(二)split没作用
这个问题是无意中发现的,如果有类似这样的提交,split提交的上一次提交,是一个merge提交,那这次split是无效的,分析了脚本代码,感觉应该是git的bug。
回到上面代码阅读时的find_existing_splits
方法,找到了split commit后,会调用try_remove_previous
输出结果,输出为^commitId^
,这个语法的处理优先级为^(commitId^)
,(commitId^)
表示取这个commitId的第一个parent,^commitId
表示非这个commitId,在git rev-list
命令中表示排除掉能到达这个commitId的提交,到达reachable这个词是官方文档写的,能到达它的提交就是这个提交的所有parent(包括parent的parent),不能到达它的提交就是这次提交的后续提交,综合起来就是输出(这个commitId的第一个parent)的后续提交。
bash
bash
复制代码
try_remove_previous () { if rev_exists "$1^" then echo "^$1^" fi}
当subtree push
遍历提交时,不是遍历到一个split commit就截止遍历,而是会从这个split commit message中找到记录的subtree-mainline
和subtree-split
的commitId,然后再分别取这两个提交的第一个parent,作为截止点。
问题出在comitId^
取的是第一个parent,如果这个提交是一个merge提交,那会有两个parent,只取了第一个parent导致另外一个parent的分支绕过了这个split,没有截止点,整条分支被遍历。
最终的遍历顺序如下,找到了split,从split的message里找到了subtree-mainline
,也就是那个merge提交,取他的第一个parent也就是no subtree file commit作为截止点,但是另一边,merge的第二个parent,没有设置截止点,就一直遍历到init了(实际情况是会遍历到上一个split或add提交产生的截止点),最终的结果是这次split几乎等于无效。