repo
是Android为了方便管理多个git库而开发的Python脚本。repo的出现,并非为了取代git,而是为了让Android开发者更为有效的利用git。
Android源码包含数百个git库,仅仅是下载这么多git库就是一项繁重的任务,所以在下载源码时,Android就引入了repo。 Android官方推荐下载repo的方法是通过Linux curl命令,下载完后,为repo脚本添加可执行权限:
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod a+x ~/bin/repo
由于国内Google访问受限,所以上述命令不一定能下载成功。其实,我们现在可以从很多第三方渠道找到repo脚本,只需要取下来,确保repo可以正确执行即可。
repo需要关注当前git库的数量、名称、路径等,有了这些基本信息,才能对这些git库进行操作。通过集中维护所有git库的清单,repo可以方便的从清单中获取git库的信息。 这份清单会随着版本演进升级而产生变化,同时也有一些本地的修改定制需求,所以,repo是通过一个git仓库来管理项目的清单文件的,这个git仓库名字叫manifests
。
当打开repo这个可执行的python脚本后,发现代码量并不大(不超过1000行),难道仅这一个脚本就完成了AOSP数百个git库的管理吗?并非如此。 repo是一系列脚本的集合,这些脚本也是通过git库来维护的,这个git库名字叫repo
。
在客户端使用repo初始化一个项目时,就会从远程把manifests
和repo
这两个git库拷贝到本地,但这对于Android开发人员来说,又是近乎无形的(一般通过文件管理器,是无法看到这两个git库的)。 repo将自动化的管理信息都隐藏根目录的**.repo**子目录中。
AOSP项目清单git库下,只有一个文件default.xml,是一个标准的XML,描述了当前repo管理的所有信息。 AOSP的default.xml的文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remote name="aosp"
fetch=".."
review="https://android-review.googlesource.com/" />
<default revision="master"
remote="aosp"
sync-j="4" />
<project path="build" name="platform/build" groups="pdk,tradefed" >
<copyfile src="core/root.mk" dest="Makefile" />
</project>
<project path="abi/cpp" name="platform/abi/cpp" groups="pdk" />
<project path="art" name="platform/art" groups="pdk" />
...
<project path="tools/studio/translation" name="platform/tools/studio/translation" groups="notdefault,tools" />
<project path="tools/swt" name="platform/tools/swt" groups="notdefault,tools" />
</manifest>
<remote>
:描述了远程仓库的基本信息。
<default>
:default标签的定义的属性,将作为<project>
标签的默认属性,在<project>
标签中,也可以重写这些属性。
<project>
:每一个纳入repo管理的git库,就是对应到一个<project>
标签,
<include>
通过name属性可以引入另外一个manifest文件(路径相对于manifest repository’s root)。
如果需要新增或替换一些git仓库,可以通过修改default.xml来实现,repo会根据配置信息,自动化管理。但直接对default.xml的定制,可能会导致下一次更新项目清单时,与远程default.xml发生冲突。 因此,repo提供了一个种更为灵活的定制方式local_manifests
:所有的定制是遵循default.xml规范的,文件名可以自定义,譬如local_manifest.xml, another_local_manifest.xml等, 将定制的XML放在新建的.repo/local_manifests子目录即可。repo会遍历.repo/local_manifests目录下的所有*.xml文件,最终与default.xml合并成一个总的项目清单文件manifest.xml。
local_manifests的修改示例如下:
$ ls .repo/local_manifests
local_manifest.xml
another_local_manifest.xml
$ cat .repo/local_manifests/local_manifest.xml
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<project path="manifest" name="tools/manifest" />
<project path="platform-manifest" name="platform/manifest" />
</manifest>
repo对git命令进行了封装,提供了一套repo的命令集(包括init, sync等),所有repo管理的自动化实现也都包含在这个git库中。 在第一次初始化的时候,repo会从远程把这个git库下载到本地。
仓库目录保存的是历史信息和修改记录,工作目录保存的是当前版本的信息。一般来说,一个项目的Git仓库目录(默认为.git目录)是位于工作目录下面的,但是Git支持将一个项目的Git仓库目录和工作目录分开来存放。 对于repo管理而言,既有分开存放,也有位于工作目录存放的:
既然.repo目录下保存了项目的所有信息,所有要拷贝一个项目时,只是需要拷贝这个目录就可以了。repo支持从本地已有的.repo中恢复原有的项目。
repo命令的使用格式如下所示:
$ repo <COMMAND> <OPTIONS>
可选的的有:help、init、sync、upload、diff、download、forall、prune、start、status,每一个命令都有实际的使用场景, 下面我们先对这些命令做一个简要的介绍:
$ repo init -u <URL> [<OPTIONS>]
运行该命令后,会在当前目录下新建一个.repo子目录:
.repo
├── manifests # 一个git库,包含default.xml文件,用于描述repo所管理的git库的信息
├── manifests.git # manifest这个git库的实体,manifest/.git目录下的所有文件都会链接到该目录
├── manifest.xml # manifests/default.xml的一个软链接
└── repo # 一个git库,包含repo运行的所有脚本
这些本地的目录是如何生成的呢?执行repo命令时,可以通过--trace
参数,来看实际发生了什么。
$ repo --trace init -u $URL -b $BRANCH -m $MANIFEST
--------------------
mkdir .repo; cd .repo
git clone --bare $URL manifests.git
git clone https://android.googlesource.com/tools/repo
mkdir -p manifests/.git; cd manifests/.git
for i in ../../manifests.git/*; do ln -s $ı .; done
cd ..
git checkout $BRANCH -- .
cd ..
ln -s manifests/$MANIFEST manifest.xml
首先,在当前目录下创建.repo子目录,后续所有的操作都在.repo子目录下完成;
然后,clone了两个git库,其中一个是**-u**参数指定的manifests,本地git库的名称是manifest.git;另一个是默认的repo,后面我们会看到这个URL也可以通过参数来指定;
接着,创建了manifest/.git目录,里面的所有文件都是到manifests.git这个目录的链接,这个是为了方便对manifests目录执行git命令,紧接着,就会将manifest切换到**-b**参数指定的分支;
最后,在.repo目录下,创建了一个软链接,链接到**-m**参数制定的清单文件,默认情况是manifests/default.xml。
这样,就完成了一个多git库的初始化,之后,就可以执行其他的repo命令了。
我们还介绍几个不常用的参数,在国内下载Android源码时,会用到:
$ repo sync [PROJECT_LIST]
下载远程代码,并将本地代码更新到最新,这个过程称为“同步”。如果不使用任何参数,那么会对所有repo管理的进行同步操作;也可以PROJECT_LIST参数,指定若干要同步的PROJECT。 根据本地git库代码不同,同步操作会有不同的行为:
git clone
,会将远程git库直接拷贝到本地git remote update && git rebase origin/<BRANCH>
,就是当前与本地分支所关联的远程分支 代码合并可能会产生冲突,当冲突出现时,只需要解决完冲突,然后执行git rebase --continue
即可。当sync命令正确执行完毕后,本地代码就同远程代码保持一致了。在一些场景下,我们会用到sync命令的一些参数:
$ repo upload [PROJECT_LIST]
从字面意思理解,upload就是要上传,将本地的代码上传到远程服务器。upload命令首先会找出本地分支从上一次同步操作以来发生的改动,然后会将这些改动生成Patch文件,上传至Gerrit服务器。 如果没有指定PROJECT_LIST,那么upload会找出所有git库的改动;如果某个git库有多个分支,upload会提供一个交互界面,提示选择其中若干个分支进行上传操作。
upload并不会直接将改动合并后远程的git库,而是需要先得到Reviewer批准。Reviewer查看改动内容、决定是否批准合入代码的操作,都是通过Gerrit完成。 Gerrit服务器的地址是在manifests中指定的:打开.repo/manifest.xml,这个XML TAG中的review属性值就是Review服务器的URL:
<remote name="aosp"
fetch=".."
review="https://android-review.googlesource.com/" />
Gerrit的实现机制不是本文讨论的内容,但有几个与Gerrit相关的概念,是需要代码提交人员了解的:
git commit --amend
命令修正上一次的提交并上传时, Commit-ID已经发生了变化,但仍可以保持Change-ID不变,这样,在Gerrit原来的Review任务下,就会出现新的Patch-Set。修正多少次,就会出现多少个Patch-Set, 可以理解,只有最后一次修正才是我们想要的结果,所以,在所有的Patch-Set中,只有最新的一个是真正有用的,能够合并的。$ repo download <TARGET> <CHANGE>
upload是把改动内容提交到Gerrit,download是从Gerrit下载改动。与upload一样,download命令也是配合Gerrit使用的。
$ repo forall [PROJECT_LIST] -c <COMMAND>
对指定的git库执行**-c**参数制定的命令序列。在管理多个git库时,这是一条非常实用的命令。PROJECT_LIST是以空格区分的,譬如:
$ repo forall frameworks/base packages/apps/Mms -c "git status"
表示对platform/frameworks/base和platform/packages/apps/Mms同时执行git status
命令。 如果没有指定PROJECT_LIST,那么,会对repo管理的所有git库都同时执行命令。
该命令的还有一些其他参数:
$ repo prune [<PROJECT_LIST>]
删除指定PROJECT中,已经合并的分支。当在开发分支上代码已经合并到主干分支后,使用该命令就可以删除这个开发分支。
随着时间的演进,开发分支会越来越多,在多人开发同一个git库,多开发分支的情况会愈发明显,假设当前git库有如下分支:
* master
dev_feature1_201501 # 已经合并到master
dev_feature2_201502 # 已经合并到master
dev_feature3_201503 # 正在开发中,还有改动记录没有合并到master
那么,针对该git库使用prune命令,会删除dev_feature1_201501和dev_feature2_201502。
定义删除无用的分支,能够提交团队的开发和管理效率。prune就是删除无用分支的”杀手锏“。
$ repo start <BRANCH_NAME> [<PROJECT_LIST>]
在指定的PROJECT的上,切换到<BRANCH_NAME>指定的分支。可以使用**–all**参数对所有的PROJECT都执行分支切换操作。 该命令实际上是对git checkout
命令的封装,<BRANCH_NAME>是自定义的,它将追踪manifest中指定的分支名。
当第一次sync完代码后,可以通过start命令将git库切换到开发分支,避免在匿名分支上工作导致丢失改动内容的情况。
$ repo status [<PROJECT_LIST>]
status用于查看多个git库的状态。实际上,是对git status
命令的封装。
Android推荐的开发流程是:
在实际使用过程中,我们会用到repo的一些什么子命令和参数呢?哪些参数有助于提高开发效率呢?下面我们以一些实际场景为例展开说明。
通过local_manifest
机制,能够避免了直接修改default.xml,不会造成下次同步远程清单文件的冲突。
CyanogenMod(CM)适配了上百款机型,不同机型所涉及到的git库很可能是有差异的。以CM对清单文件的定制为例,通过新增local_manifest.xml,内容如下:
<manifest>
<!-- add github as a remote source -->
<remote name="github" fetch="git://github.com" />
<!-- remove aosp standard projects and replace with cyanogenmod versions -->
<remove-project name="platform/bootable/recovery" />
<remove-project name="platform/external/yaffs2" />
<remove-project name="platform/external/zlib" />
<project path="bootable/recovery" name="CyanogenMod/android_bootable_recovery" remote="github" revision="cm-10.1" />
<project path="external/yaffs2" name="CyanogenMod/android_external_yaffs2" remote="github" revision="cm-10.1" />
<project path="external/zlib" name="CyanogenMod/android_external_zlib" remote="github" revision="cm-10.1" />
<!-- add busybox from the cyanogenmod repository -->
<project path="external/busybox" name="CyanogenMod/android_external_busybox" remote="github" revision="cm-10.1" />
</manifest>
local_manifest.xml会与已有的default.xml融合成一个项目清单文件manifest.xml,实现了对一些git库的替换和新增。 可以通过以下命令导出当前的清单文件,最终snapshot.xml就是融合后的版本:
$ repo manifest -o snapshot.xml -r
在编译之前,保存整个项目的清单,有助于问题的回溯。当项目的git库发生变更,需要回退到上一个版本进行验证的时候,只需要重新基于snapshot.xml初始化上一个版本即可:
$ cp snapshot.xml .repo/manifests/
$ repo init -m snapshot.xml # -m 参数表示自定义manifest
$ repo sync -d # -d 参数表示从当前分支脱离,切换到manifest中定义的分支
在repo init的时候,会从远程下载manifests和repo这两个git库,默认情况下,这两个git库的地址都是写死在repo这个python脚本里面的。对于AOSP而言,这两个git库的地址显然是google提供的。 但由于google访问受限的缘故,会导致init时,无法下载manifests和repo。这时候,可以使用init的**-u和–repo-url参数,自定义这两个库的地址,辅以–no-repo-verify**来绕过代码检查。
$ repo init --repo-url [PATH/TO/REPO] -u [PATH/TO/MANIFEST] -b [BRANCH] --no-repo-verify
$ repo sync
repo默认会同步git库的所有远程分支的代码,但实际开发过程中,用到的分支是有限的。使用sync的**-c参数,可以只下载manifest中设定的分支**,这会节省代码下载时间以及本地的磁盘空间:
$ repo sync -c
如果实际开发过程中,需要用到另外一个分支,而又不想被其他分支干扰,可以在已有的工程根目录下,使用如下命令:
$ repo manifest -o snapshot.xml -r
$ repo init -u [PATH/TO/MANIFEST] -b [ANOTHER_BRANCH]
$ repo sync -c -d
以上命令序列,相当更新了manifest,而且仅仅只下载ANOTHER_BRANCH的代码,这样本地只保存了两个分支的代码。利用保存的snapshot.xml,还能将所有git库方便的切换回原来的分支。
如果本地已经有一份Android源码,假设路径为~/android-exsit,想要下载另一份新的Android源码,通过**–reference**参数,在数分钟以内,就能将代码下载完毕:
$ mkdir ~/android-new && cd ~/android-new
$ repo init --reference=~/android-exsit -u [PATH/TO/MANIFEST] -b [BRANCH]
$ repo sync -c
在sync完代码后,所有git库默认都是在一个匿名分支上(no branch),很容易会由于误操作导致丢失代码修改。可以使用如下命令将所有的git库切换到开发分支:
$ repo start BRANCH --all
一般不这样操作,因为我们的改动可能只涉及了几个git仓库。不过也无所谓,相当于一些不需要修改的仓库我们也建立了对应的分支,只要我们在分支合并后执行repo prune即可将其删除。
开发人员可能同时在多个git库,甚至多个分支上,同时进行修改,针对每个git库单独提交代码是繁琐的。可以使用如下命令,一并提交所有的修改:
$ repo upload
不用担心会漏提交或者误提交,upload会提供一个交互界面,开发人员选择需要提交的git库和分支即可。
如果需要省去Gerrit上填写reviewer的操作,可以使用**–reviewer**参数指定Reviewer的邮箱地址:
$ repo upload --reviewer="R.E.viewer@google.com"
Git鼓励在修复Bug或者开发新的Feature时,都创建一个新的分支。创建Git分支的代价是很小的,而且速度很快,因此,不用担心创建Git分支的成本,而是尽可能多地使用分支。
随着时间的演进,开发分支会越来越多,而一些已经合并到主干的开发分支是没有存在价值的,可以通过prune命令定期删除无用的开发分支:
$ repo prune [PROJECT_LIST]
repo adandon [PROJECT_LIST]相当于git branch -D
对于部分开发人员而言,同时操作多个git库是常态,如果针对每个git库的操作命令都是相同的,那么可以使用如下命令一次性完成所有操作:
$ repo forall -c "git branch | grep tmp | xargs git branch -D; git branch"
参数**-c**指定的命令序列可以很复杂,多条命令只需要用“;”间隔。上述的命令实现删除repo管理的仓库的分支名 包含tmp的所有分支。
-p可以打印出仓库的路径信息。