原文:Google Git-Repo (Android)多仓库项目管理
前言
如果不是 Android dev 可以忽略标题的 Android。
项目模块化/组件化
之后各模块也作为独立的 Git
仓库从主项目里剥离了出去,各模块各自管理自己的版本。正常 Android 项目,各剥离出去的子模块仓库则通过 Maven 仓库
来管理,然后和引入第三方库一样依赖到主项目里。这种状态下的项目迭代带来的问题会是:需要频繁发布子模块的 版本并需要把修改的版本提交 Merge Request 到主模块里,尤其像我们一周一版的快速迭代的情况下,问题尤为凸显。此外还有个问题就是 Debug/断点
会变得不太方便,因为可能子模块的源码并未发到 Maven 上。
在上面所说的状态下工作了一段时间后,需要找到个方案来解决这个问题。遂尝试在多仓库管理的方向上尝试。支持 Git 多仓库的管理工具也有好几个:git-repo、git-submodule、gitslave、git-subtree,看了一番,除了 git-repo 外的几个要么 子模块需要手动指定本地仓库路径
、要么 主仓库与子仓库会产生污染
且操作不够简便化。所以最终就采用了 Google 的 Git-repo(Repo)
Git-repo
Repo 是对 Git 构成补充的多(可以巨多的那种)代码库管理工具,简单说就是使用 Python 在 Git 基础上开发的一系列脚本命令。当前整个 Android 项目(AOSP)就是通过 repo 来管理,最新版本的仓库大约 七百多个,可见 Repo 在多仓库的代码管理和版本管理上是可以支撑现有我们的项目的。接下来描述分析下迁移使用 Repo 的过程和一些解释。
环境准备
准备好梯子,把 repo
装到系统里:
mkdir ~/bin
PATH=~/bin:$PATH # 自己加到 ~/.zshrc or ~/.bashrc or /etc/profile 里
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo
# 最后 source 下环境变量
然后就可以操作项目了,如初始化 AOSP 项目:
repo init -u https://android.googlesource.com/platform/manifest -b android-4.0.1_r1
Manifest
Repo 管理的核心就在于 Manifest
,每个采用 repo 管理的复杂多仓库项目都需要一个对应的 manifest 仓库,如 AOSP 的 manifest ,此仓库用来存储所有子仓库的配置信息,repo 也是读取此仓库的配置文件来进行管理操作。里面的配置就是 xml 定义的结构,一般有两个主要的配置:子仓库用到的仓库地址(remote)、子仓库详细配置信息(project)。举例如下:
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remote name="remote1"
alias="origin"
fetch=".."
review="https://android-review.googlesource.com/" />
<remote name="remote2"
alias="origin"
fetch="git@github.com:group2/"
review="https://android-review2.googlesource.com/" />
<default revision="master"
remote="remote1"
sync-j="4" />
<project path="build/make" name="platform/build" groups="pdk" >
<copyfile src="core/root.mk" dest="Makefile" />
<linkfile src="CleanSpec.mk" dest="build/CleanSpec.mk" />
</project>
<project path="build/blueprint" name="platform/build/blueprint" groups="pdk,tradefed" revision="other_branch" remote="remote1"/>
<!-- ... -->
</manifest>
解释下常用的各节点:
remote
远程仓库地址配置,可以多个。
- name: 名字,也用于子仓库的 git remote 名称(.git/config 里的 remote)
- alias: 别名,可省略,建议设为
origin
, 设置了那么子仓库的 git remote 即为此名,方便不同的 name 下可以最终设置生成相同的 remote 名称 - fetch: 仓库地址
前缀
,即 project 的仓库地址为:remote.fetch + project.name
- pushurl: 一般可省略,省略了则直接用 fetch
- review: Gerrit code review 的地址,如果没有用 Gerrit 则不需要配置(也就不能用 repo upload 命令了)
- revision: 使用此 remote 的默认分支
这里的 fetch 遇到个坑,git@github.com:group2/
这样 git scheme 开头的地址会有问题(ssh/https 的正常),最终子仓库在本地的 remote 地址一直是 manifest 的地址拼上 fetch 再加 project 的 name (manifest.git_path + remote.fetch + project.name)。最终发现是 git-repo 里的代码 manifest_xml.py 有问题,需要 fix 下,简单的把 78
行注释掉,然后推到自己的仓库,指定下系统环境变量:
REPO_URL=your_fix_git-repo_git_url
# repo 脚本会使用此变量的地址
project
子项目仓库配置,可以多个。
- path: repo sync 同步时,相对于根目录的子仓库文件夹路径
- name: 子仓库的 git 仓库名称
- group: 分组
- revision: 使用的分支名
- clone-depth: 仓库同步 Git 的 depth
copyfile
project 的子节点属性.
- src: project 下的相对路径
- dest: 整个仓库根路径下的相对路径
linkfile
project 的子节点属性,类似 copyfile,只是把复制文件变为创建链接文件。
default
project 没有设置属性时会使用的默认配置,常用的是 remote 和 revision。
其他
其他现在暂时用的较少,详细完整的 manifest 格式说明请看官方文档。
了解了 Repo 下的 Manifest 配置仓库的概念后就可以根据自己的项目来创建了,Just do it!
local_manifest
local_manifest 简单说就是一个比 repo init 时设置的 manifest 有更高优先级的本地配置,一般用在不改动远程 manifest 配置又想设置到本地的专属配置时用得到。版本高一点的 repo 下,在工作根目录新建配置文件即可: .repo/local_manifests/local_manifest.xml
有一点需要注意的是如果需要覆盖主 manifest 文件已有 project 的配置那么需要先 remove-project
才行,不然会报错:
fatal: duplicate path project_name in .repo/manifest.xml
fix :
<remove-project name="project_name" />
<!-- 再重写 project 配置 -->
<project path="xxx" name="xxx" revision="xxxx" remote="xxxx" />
Repo 命令
repo init -u yout_manifest_git_url
初始化了你的项目 repo 工作区后,repo sync
,你就可以进入正常的特性开发状态了。那么我们以开发一个 feature 为例,顺序说明下需要使用到的常用命令,也是我现在尝试使用的 repo workflow
了。
repo init
初始化。
repo init -u your_project_git_url
其他常用可选参数:
- -b 选取的 manifest 仓库分支,默认 master
- -m 选取的默认配置文件,默认 default.xml
- --depth=1 git clone 的深度,一般如在 Jenkins 上打包时可用,加快代码 clone 速度
- --repo-url=URL 使用自定义的 git-repo 代码,如前面说到的 fix 了 bug 的 git-repo
- --no-repo-verify 不验证 repo 的源码,如果自定义了 repo url 那么这个一般也加上
repo sync
初始化好一个 repo 工作目录后下一步就是把代码同步下来了:
repo sync
同步完成后就会在当前目录下生成 manifest 里配置的各仓库和其对应目录。其他常用可选参数:
- -f 不阻断同步,即一个 project 失败会继续下一个 project 同步
- --force-sync 如果需要,强制覆盖现有的 git 目录指向不同的对象目录。此操作可能会导致数据丢失
- -d 把项目回退到 manifest 里配置的分支
- -m 手动指定当前操作使用哪个 manifest 文件
- -t 使用对应 tag 里的 manifest 文件
同步完后你就可以用你的 IDE 打开项目了。此时,你需要根据当前的子仓库目录配置,在项目里处理下子仓库的依赖问题,比如 Android 项目,在 settting.gradle 里就需要根据此 repo 仓库的项目目录把子仓库 include 进来。
repo start
从 manifest 配置文件中指定的分支里,创建一个新的分支进行开发,如:
repo start your_feature_branch_name --all | [project1 project2]
后续如果发现还改动到了其他模块仓库那么再 start 一个对应 project 的分支
Work in progress
开发过程中需要用到的常用命令:
- repo status: 跟 git status 类似,会把当前 repo 工作区的状态信息列出来
- repo diff: 同理 git diff
- repo forall <PROJECT_LIST> -c <COMMAND>: 在(所有)子仓库下执行命令,比如 repo 没有类似
git stash
的命令,利用 forall 就可以实现:repo forall -c git stash
- repo prune: 删除已经合并分支
- repo stage: 把文件添加到 index 表(暂存区)中
- repo manifest: 显示当前使用的 manifest 信息内容
repo upload
提交代码到 Gerrit code review 中,如果没有使用 Gerrit,则可以跟之前那样手动操作单个仓库 push 或者使用 repo forall -c git push xxx
Other
其他更详细的参考可以查看 Repo 命令参考资料
Workflow
上面一节介绍的 repo 命令的顺序过程可以说就是一个常用的 repo 工作流程,AOSP
给的开发流程可以先看下:
开发
那么根据上一节的介绍,给出下当前尝试使用的 repo-workflow
的图示:
具体到每个仓库来说,主仓库位置原有的 git workflow 不用变,各子仓库新增 dev
、build
分支,那么对应的合版打包与发版打包对应 Manifest 固定的分支配置就行,如果没有项目删减,那么合版与发版时的子模块仓库版本也是不用动的。避免了频繁的合版时,各模块版本频繁/繁琐的修改合并。
Jenkins
使用 Jenkins 的需要使用 repo 插件,也还算挺方便的,稍微由原来的 git 迁移到 repo 的配置就行。
配合 Jenkins 的自动化打包,加一些自动化的打包配置,如远程参数化构建、自定义的 Gradle 打包插件等,如果需要在 Jenkins 上打 Feature 包的,那么在 Manifest 项目里可以不需要再新建 Feature 分支了。所以综合来看,合版时的工作量减轻了不少~
End
参考: