有赞移动

有赞电商移动代码 VCS 的演进

我们有幸在一个毫无疑问以 git 作为 VCS 的开发时代,git 为我们提供了太多想象力,解放了我们许多的生产力。
有赞使用自搭 git 服务器的方式为所有的项目提供版本管理,而我们现在的产品——微商城/微小店/有赞买家版,都是基于我们的 git 仓库进行管理的。

开天辟地

随着公司规模和产品的发展,我们的仓库变得越来越大,feature 越来越多,2016 年我们团队在评估现有的代码之后,决定开始模块化的改造。
模块化首当其冲的就是对 VCS 的需求产生变化,一个看上去非常简单的 repo 可能已经不再满足我们的需求,因为我们需要不同的模块能足够产品化,能够进行独立维护,能够为我们的新需求提供强有力的支持,顺便还能帮我们快速定位到问题转到相关负责人的手中,这是我们的愿景。

我们为模块化做的第一件事(以 Android 为例),就是在当前的项目中,为不同的业务模块,新建好不同的文件夹(Project),把不同业务的代码,从一个巨大的代码库中,慢慢剥离开,一点一点的落地到相应的 Sub Project 里面,好让我们对各个业务的边界有明确的认识。

project_structure.png

莱克星顿的第一声枪响

在这个时候,我们花了很大的时间完成这项工作,在第一阶段完成之后,看见清晰的目录结构不禁沾沾自喜,但是,挖坑的弊端接踵而至——首先,Gradle 编译的时间大大的加长了,一次编译完成的时间足够你去喝一杯热气腾腾的咖啡。每一次的产品迭代中,产品一个新需求,我们会使用 Git Flow 的标准新建一个新的feature分支,在feature分支下实现新需求,写完代码,等待编译,安装到手机上,自测,然后改 Bug,再循环。

没错,在没有 TDD,暂没办法进行自动化测试的条件下,我们活的非常艰难,实现业务很像是在浪费生命,我们的时间如此值钱,怎么能荒废在天书…啊呸, compile 里面?于是我们渴望把我们的生产力继续解放,不能为了头脑清楚而捆绑了手脚。

同时,我们的仓库在业务模块分支的开发中,分支表现的混乱不堪,经常需要一堆人聚在一块解决合并的时候模块的冲突的问题,现在这个样子,好像根本没有解决问题嘛。

巨大仓库的改造

我们这时候维护了一个非常巨大的仓库,每一次的分支提交,都有潜在的可能对对方造成影响。你可能在这个分支上“不小心”改了某一行别人分支的代码,这并不是符合预期的(unexpected),结果在合并代码的时候,就得问对方,这块是不是你改过的,我应该选择哪一
块的代码,这样的分支合并,在比较小的项目里,可能问题表现的不明显,因为就算变动,也是有限的几行代码,但是在代码数量级高许多的项目中,它就有可能是一种灾难了。我们这个时候希望把仓库拆出来,这样使用 VCS 去统一管理各个模块,而不是让他们都寄宿在一个大 repo 里。

在这之前,我们已经了解过 git submodules 的优缺点,如果使用 git submodules 进行改造的话,成本的确是非常小的,它的整个模型如下:

app_submodule.png

但是我们知道,git submodules 是在父 repo中维护一个对submodules的一些指针,如果子 module 代码发生了更改,除了父 module 提交代码外,还需要对子 module 的指针进行更新,这种 contains 的操作在我们的场景里,无疑对项目增加了许多的复杂性,经过一些综合的考虑,我们最终放弃了使用submodules的方式,使用repohttps://gerrit.googlesource.com/git-repo/)进行管理。

repo 的使用

首先,非常推荐微店的梁志涛在知乎上关于【为什么android使用repo而不是直接使用git管理工程呢】的回答,感谢他的一些技术指导,给我们对 repo 和 git submoudle 的认知提高了好多个档次。

repo 的一些特性可以让你在没有插件化之前,就能用上它,用上它之后,我们现在的模型是这样的

repo_structure.png

app 壳工程和业务模块工程并行了,更新各个模块的代码再也不会对其他工程有影响。如果把业务模块转换成坐标依赖,我们还可以选择性地把需要的代码拉到本地,不需要的代码不用去动,连 compile 的时间都省了好多,我们的愿望有多么的美好,那么现实就会有多么的残酷。

首先,按照 Android 的目录结构来看,如果我们不进行较大的配置的话,我们许多的文件,必须放到外部工程的根目录下。

大概像这样

source_directory.png

为了图中红色字文件的正确路径,我们一开始把这些文件打入了一个叫wsc的 repo 里,repo 下拉代码的时候,会在根目录下写入wsc这个 repo 里的所有的文件,且在根目录下面,继续加入其它模块,这样我们发现,其它模块又变成了wsc的子模块了,虽然没有git submodule恶心,但是整体的组织结构还是很奇怪,为了保证其它模块不被打入wsc仓库,我们只好在这个 repo 下面的 .gitignore 中,把其他模块的管理都去掉了。

.gitignore 如下:

1
2
3
4
5
6
7
# 省略其他
# 业务模块的 ignore
wsc_login
wsc_customer
wsc_shop
....

我们可以看到,gitignore 居然和业务耦合在了一块,也就是说,我再抽出一个业务,如果我忘了在wsc里面把这个业务文件夹 ignore 掉,就会被 push 到仓库里,这不和 git submodule 一样了么?我们使用 repo 不是就是要解决这样的问题么?
于是我们参考了Android Source提供的方案,看看它是怎么管理这种文件的。

android_repo_manifest.png

我们可以看到,这里面有两个非常有用的指令就是linkfilecopyfile,给我的启发就是,我们可以创建一个包含构建文件的 repo,用这个 repo 去管理Make相关的逻辑,同时,repo 的颗粒度要足够小,让各个 repo 各司其职,这样 repo 之间的互相耦合就更小了,我们把这个 repo 取名为gradle_files,本来想取名叫build,但是可能会和Gradle生成出来的文件夹冲突,所以只好换了一个名字,名字解决后,我们整个文件夹的结构就是这样的:

project_files.png

带箭头的都是软连接。截图里没有把其他业务模块拉下来。如果我们需要更改业务代码,就把 app 下的build.gradle中改成project依赖,否则使用坐标依赖。

使用 repo 的出发点,就是为了我们后续的快速开发,模块代码级的分离,和更标准化的打包流程。

目前痛点

在现在这个阶段,我们暂时解决了模块间的版本管理问题和编译速度的问题,我们秉承一条原则:不要过度设计,不要一开始就想着完美,出发比思考更重要。

因此,正因为不完美,我们的整个解决方案还有许多的改进空间,目前的痛点还有如下:

  1. 打包系统功能暂不完整,离我们的设想还有一段距离。
  2. 模块的发布尚未标准化,我们目前依赖于清除本地 Gradle Cache 的方式,让开发中的代码进行更新。
  3. 打包和 Code Freeze 需要人工去介入。
  4. 对于 TDD 和 BDD,得益于模块的解耦,可以开始探索,但是尚未出成果。

总结

使用 repo 算是一种走出舒适区的尝试,我们热衷于把我们的基础设施建设得更加现代化和标准化,我们所做的一切,都期望我们自己能更专注于业务,把我们从痛苦的打包和版本管理中解放出来。幸好,一切都开始了,并没有踌躇不前,革命尚未成功,同志仍需努力。