Git多项目管理

Posted on Aug 29, 2016

开发中我们经常会遇到这样的情况:项目越来越大,一些通用的模块我们希望将他抽离出来作为单独的项目,以便其他项目也可以使用,或者使用一些第三方库,可能我们并不想将代码直接拷贝进我们的项目里面,而仅仅只是单纯的引用。这时问题来了,你想把他们当做独立的项目,同时又想在项目中使用另一个。

我们举一个例子。假设你正在使用Hexo搭建自己的个人博客,然后使用了某个主题。Hexo中的主题通常以独立项目的形式提供。如果直接将主题项目代码复制到博客项目中,不仅丢弃了主题项目的维护历史,同时你将再也无法自由及时地合并上游的更新。这时你就需要在个人博客项目中引用主题项目。

基于Git有多种方式来解决这个问题:Git SubmoduleGit SubtreeGitSlaveGoogle Repo

Git Submodule

Git 1.5.3中加入了git submodule这个命令。Git子模块允许你将一个Git仓库作为另一个Git仓库的子目录。它能让你将另一个仓库克隆到自己的项目中,同时还保持独立的提交。

添加子模块

将一个已存在的Git仓库添加为正在工作的仓库的子模块,可以使用git submodule add <repository> [<path>]命令。以Hexo博客添加Hacker主题为例:

$ git submodule add /private/tmp/remote/Hacker.git themes/Hacker
Cloning into '/private/tmp/feilongwang.org/themes/Hacker'...
done.

默认情况下,子模块会将子项目放在一个与仓库同名的目录中。我们也可以通过在命令结尾添加一个path来指定放到其他地方。

如果这时运行git status,你会注意到2件事情。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   .gitmodules
	new file:   themes/Hacker

首先应当注意到新的.gitmodules文件。该文件保存了子模块的url与本地目录之间的映射:

[submodule "themes/Hacker"]
	path = themes/Hacker
	url = /private/tmp/remote/Hacker.git

如果有多个子模块,该文件中就会有多条记录。

git status输出中列出的另一个是项目文件夹记录。如果你运行git diff,会看到类似下面的信息:

$ git diff --cached themes/Hacker
diff --git a/themes/Hacker b/themes/Hacker
new file mode 160000
index 0000000..98260cd
--- /dev/null
+++ b/themes/Hacker
@@ -0,0 +1 @@
+Subproject commit 98260cd27f0bb6340757cd05c3fb00d574b42d52

虽然themes/Hacker是工作目录中的一个子目录,但Git将它视作一个子模块。当你不在那个目录中时,Git并不会跟踪他的内容,而是将它看作该仓库中的一个特殊提交。该特殊提交记录着父项目依赖的子模块的版本(子模块当前HEAD所指向的版本)。

注意themes/Hacker记录的160000模式。这是Git中的一种特殊模式,它本质上意味着你是将一次提交记作一项目录记录的,而非将它记录成一个子目录或者一个文件。

除了git status看到的差异外,还有个隐藏的变化在.git/config中:

$ cat .git/config 
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
[remote "origin"]
	url = /private/tmp/remote/feilongwang.org.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
	remote = origin
	merge = refs/heads/master
[submodule "themes/Hacker"]
	url = /private/tmp/remote/Hacker.git

父项目的.git/config文件中也保存了子模块的信息,所以你可以根据自己的需要,通过配置父项目.git/config文件来覆盖.gitmodules中的配置。如通过在本地执行git config submodule.themes/Hacker.url <url>来覆盖.gitmodules中的url

克隆子模块

当你克隆一个含有子模块的项目时,默认会包含该子模块目录,但其中还没有任何文件。

$ git clone /private/tmp/remote/feilongwang.org.git
Cloning into 'feilongwang.org'...
done.
$ cd feilongwang.org/themes/Hacker/
$ ls -al
total 0
drwxr-xr-x 2 wangfeilong wheel  68  9 19 03:01 ./
drwxr-xr-x 4 wangfeilong wheel 136  9 19 03:01 ../

你必须运行两个命令:git submodule init用来初始化本地配置文件,而git submodule update则从该项目中抓取所有数据并检出父项目中列出的合适的提交。

$ git submodule init
Submodule 'themes/Hacker' (/private/tmp/remote/Hacker.git) registered for path 'themes/Hacker'
$ git submodule update
Cloning into '/private/tmp/feilongwang.org/themes/Hacker'...
done.
Submodule path 'themes/Hacker': checked out 'dc4a047cac6f26c47aba7bcd5b36f3ea7d3abf8b'

现在themes/Hacker就处在和之前提交时相同的状态。

不过还有更简单一点的方式。如果给git clone命令传递--recursive选项,它就会自动初始化并更新仓库中的每一个子模块。

$ git clone --recursive /private/tmp/remote/feilongwang.org.git
Cloning into 'feilongwang.org'...
done.
Submodule 'themes/Hacker' (/private/tmp/remote/Hacker.git) registered for path 'themes/Hacker'
Cloning into '/private/tmp/feilongwang.org/themes/Hacker'...
done.
Submodule path 'themes/Hacker': checked out 'dc4a047cac6f26c47aba7bcd5b36f3ea7d3abf8b'

更新子模块

假如在当前开发中,父项目只是使用子项目并不时的获取更新。这时你可以进入到子模块目录中运行git fetchgit merge,合并上游分支来更新本地代码。

$ git fetch && git merge origin/master 
Updating dc4a047..76e18c4
Fast-forward
 layout/components/footer.ejs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

如果你不想在子模块目录中手动抓取与合并,那么还有种更容易的方式。运行git submodule update --remote,Git将会进入子模块然后抓取并更新。

$ git submodule update --remote
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 5 (delta 4), reused 0 (delta 0)
Unpacking objects: 100% (5/5), done.
From /private/tmp/remote/Hacker
   dc4a047..76e18c4  master     -> origin/master
Submodule path 'themes/Hacker': checked out '76e18c4e5c915302fe2545f16c1df9350b33439f'

此命令默认会假定你想要更新并检出所有子模块仓库的master分支。不过你也可以指定想要更新的子模块、想要更新的分支以及更新后进行的操作。

这时我们运行git status,Git会显示子模块中有“新提交”:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   themes/Hacker (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

如果在此时提交,那么你会将父项目锁定为子模块master分支最新的代码。

假如你希望在父项目上编写代码的同时又在子模块上编写代码,那又该如何处理呢?

当我们运行git submodule update从子模块仓库中抓取修改时,Git将会获得这些改动并更新子目录中的文件,但是会将子仓库留在一个称作 “游离的 HEAD”的状态。这意味着没有本地工作分支(例如 “master”)跟踪改动。所以你做的任何改动都不会被跟踪。

为了将子模块设置得更容易进入并修改,你应该进入每个必要的子模块并检出topic工作分支:

$ git checkout -b featureA
Switched to a new branch 'featureA'

然后,我们可以对子模块做些改动,使用git fetch或者git pull来更新代码,git merge或者git rebase合并改动,就像独立的项目开发一样。

当然,我们也可以在父项目中使用git submodule update --remote --merge或者git submodule update --remote --rebase来合并代码。如:

$ git submodule update --remote --rebase
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 5 (delta 4), reused 0 (delta 0)
Unpacking objects: 100% (5/5), done.
From /private/tmp/remote/Hacker
   98260cd..5eb6d18  master     -> origin/master
First, rewinding head to replay your work on top of it...
Applying: Update README.md
Submodule path 'themes/Hacker': rebased into '5eb6d185e1efea4cd4bd48d046a6f8fb376b6836'

如果你没有提交子模块的改动,那么运行一个子模块更新也不会出现问题,此时Git会只抓取更改而并不会覆盖子模块目录中未保存的工作。

如果你忘记--rebase--merge,Git会将子模块更新为服务器上的状态。并且会将项目重置为一个游离的HEAD状态。

$ git submodule update --remote
Submodule path 'themes/Hacker': checked out '5eb6d185e1efea4cd4bd48d046a6f8fb376b6836'

即便这真的发生了也不要紧,你只需回到目录中再次检出你的分支(即还包含着你的工作的分支)然后手动地合并或变基对应的分支(或任何一个你想要的远程分支)就行了。

如果你做了一些与上游改动冲突的改动,当运行更新时Git会让你知道,然后你可以进入子模块目录中然后就像平时那样修复冲突。

提交子模块

如果我们在父项目中提交并推送但并不推送子模块上的改动,其他尝试检出我们修改的人会遇到麻烦,因为他们无法得到依赖的子模块改动。那些改动只存在于我们本地的拷贝中。

提交子模块的改动最简单的选项是进入每一个子模块中然后手动推送到远程仓库。然而git push命令接受值为on-demand--recurse-submodules参数,它会尝试为你这样做。

$ git push --recurse-submodules=on-demand 
Pushing submodule 'themes/Hacker'
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 344 bytes | 0 bytes/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To /private/tmp/remote/Hacker.git
   76e18c4..26fa290  master -> master
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 324 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To /private/tmp/remote/feilongwang.org.git
   3c7501e..fb39823  master -> master

遍历子模块

如果父项目中包含大量子模块,那我们一些通用的子模块操作,如更新子模块,将会变成巨大的工作量。幸好,Git提供了foreach子模块命令。

假如,我们想要开始开发一个新的功能或者修复一些错误,并且需要在几个子模块内工作。这时我们可能需要创建一个新的分支,然后将所有子模块都切换过去。

$ git submodule foreach 'git checkout -b featureA'
Entering 'hexo-generator-feed'
Switched to a new branch 'featureA'
Entering 'themes/Hacker'
Switched to a new branch 'featureA'

子模块的问题

然而使用子模块还是有一些小问题:

  • 在父项目中git pull并不会自动更新子模块,需要调用git submodule update来更新子模块信息。如果忘记调用git submodule update,那么你极有可能再次把旧的子模块依赖信息提交上去。
  • 调用git submodule update并不会将子模块切换到任何分支,默认情况下子模块处于“游离的 HEAD”的状态。如果此时我们改动子模块而没有检出一个工作分支,那调用git submodule update时你所做的任何改动都会丢失。
  • Git子模块在父项目中维护所有依赖的子模块版本,当包含大量子模块时,父项目的更新将很容发生冲突,并且父项目的维护历史与所有子模块的维护历史相互交织,维护成本也会比较高。

Git Subtree

Git在1.8.0版本引入了git subtree这个命令,它使用Git的subtree merge策略来得到类似git submodule的结果。但本质上,它是将子项目的代码全部merge进父项目。使用git subtree,你不仅可以将其他项目合并为父项目的一个子目录,而且可以从父项目提取某个子目录的全部历史作为一个单独的项目。

相比Git子模块

  • 管理和更新流程比较方便
  • 不再有.gitmodules文件
  • 克隆仓库不再需要initupdate等操作
  • 删除时不再像git submodule那样费劲

添加子项目

将一个已存在的Git仓库以Subtree方式添加为子项目可以使用git subtree add --prefix=<prefix> <repository> <ref>命令,其中--prefix选项指定了子项目对应的子目录,--squash选项用以压缩Subtree的提交为一个,这样父项目的历史记录里就不会出现子项目完整的历史记录。我们还是以Hexo博客添加Hacker主题为例:

$ git clone /private/tmp/remote/feilongwang.org.git
Cloning into 'feilongwang.org'...
done.
$ cd feilongwang.org/
$ git subtree add --prefix=themes/Hacker /private/tmp/remote/Hacker.git master --squash 
git fetch /private/tmp/remote/Hacker.git master
warning: no common commits
remote: Counting objects: 216, done.
remote: Compressing objects: 100% (143/143), done.
remote: Total 216 (delta 70), reused 216 (delta 70)
Receiving objects: 100% (216/216), 56.22 KiB | 0 bytes/s, done.
Resolving deltas: 100% (70/70), done.
From /private/tmp/remote/Hacker
 * branch            master     -> FETCH_HEAD
Added dir 'themes/Hacker'

更新子项目

一段时间之后,子项目可能有大量新的代码,父项目也想使用这些代码。此时父项目的维护者只需执行:

$ git subtree pull --prefix=themes/Hacker /private/tmp/remote/Hacker.git master --squash 
remote: Counting objects: 8, done.
remote: Compressing objects: 100% (8/8), done.
remote: Total 8 (delta 6), reused 0 (delta 0)
Unpacking objects: 100% (8/8), done.
From /private/tmp/remote/Hacker
 * branch            master     -> FETCH_HEAD
Merge made by the 'recursive' strategy.
 themes/Hacker/README.md                    | 2 +-
 themes/Hacker/layout/components/footer.ejs | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

就可以将父项目中子项目对应目录里的内容更新为子项目最新的代码了。

如果你觉得每次都输入子项目完整的仓库url太麻烦,你也可以将子项目添加为追踪的仓库。

$ git remote add hacker /private/tmp/remote/Hacker.git
$ git subtree add --prefix=themes/Hacker hacker master --squash
git fetch hacker master
From /private/tmp/remote/Hacker
 * branch            master     -> FETCH_HEAD
 * [new branch]      master     -> hacker/master
Added dir 'themes/Hacker'

提取子项目

当我们开发一个项目若干时间后,希望将某个目录单独出一个项目来开发,同时又保留这部分代码历史提交记录,使用git subtree split可以很轻松的完成这个操作。以Hexo博客分离Hacker主题为例:

$ git subtree split --prefix=themes/Hacker --branch hacker
Created branch 'hacker'
843147f3181399b06528251451bc498e01425f34
$ git branch -a
  hacker
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
$ git checkout hacker 
Switched to branch 'hacker'
$ git log
commit 843147f3181399b06528251451bc498e01425f34
Author: Feilong Wang <i@feilongwang.org>
Date:   Mon Sep 19 01:16:02 2016 +0800

    Change theme to Hacker

其中--branch指定将生成的历史提交记录保存到一个新的分支。

提交子项目

如果我们在使用子项目的过程中,对子项目做了一些改动,同时我们又希望子项目的其他使用者也能共享这些改动,此时可以将我们的改动提交到子项目的远程仓库中。

$ git subtree push --prefix=themes/Hacker /private/tmp/remote/Hacker.git master
git push using:  /private/tmp/remote/Hacker.git master
Counting objects: 301, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (211/211), done.
Writing objects: 100% (301/301), 579.91 KiB | 0 bytes/s, done.
Total 301 (delta 67), reused 292 (delta 64)
remote: Resolving deltas: 100% (67/67), completed with 2 local objects.
To /private/tmp/remote/Hacker.git
   96ca04b..5565513  556551375034489fc8710070a29a2f22240a39b3 -> master

GitSlave

GitSlave用于管理相关的一个父项目和多个Slave项目。通常,它会将你要执行的Git常规操作顺序在父项目和Slave项目中执行一遍,所以当你执行pull操作,项目中的所有仓库会顺序执行pull操作。GitSlave是对Git命令的封装,是被设计用于简化多仓库的Git操作,而不是要取代Git。我们还是以Hexo博客项目和Hacker主题项目为例来说明GitSlave的用法。

添加Slave项目

我们可以通过gits prepare初始化父项目,然后通过gits attach命令来添加Slave项目。如:

$ git clone /private/tmp/remote/feilongwang.org.git
Cloning into 'feilongwang.org'...
done.
$ cd feilongwang.org
$ gits prepare
[master 211dc36] gits creating .gitslave
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 .gitslave
$ cat .gitslave

可以看出,当执行gits prepare命令时,它会在父项目的根目录下添加一个空的.gitslave文件。然后我们可以执行gits attach REPOSITORY LOCALPATH来添加Slave项目:

$ gits attach ../Hacker.git themes/Hacker
Cloning into 'themes/Hacker'...
done.
[master 2e059b2] gits adding "../Hacker.git" "themes/Hacker"
$ git show
diff --git a/.gitignore b/.gitignore
index cd1fbcc..401d29a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ db.json
 node_modules/
 public/
 .deploy*/
+/themes/Hacker/
diff --git a/.gitslave b/.gitslave
index e69de29..3b17767 100644
--- a/.gitslave
+++ b/.gitslave
@@ -0,0 +1 @@
+"../Hacker.git" "themes/Hacker"

克隆带有Slave仓库的项目

我们可以使用gits clone来克隆带有Slave仓库的项目,如:

$ gits clone /private/tmp/remote/feilongwang.org.git
Cloning into 'feilongwang.org'...
done.
Cloning into 'themes/Hacker'...
done.

我们看到Git的克隆操作顺序在父项目与子项目中被执行。

如果一开始你并知道要克隆的项目带有Slave仓库而直接将父项目克隆下来,此时我们可以通过git populate来进一步克隆Slave仓库,如:

$ git clone /private/tmp/remote/feilongwang.org.git
Cloning into 'feilongwang.org'...
done.
$ cd feilongwang.org/
$ ls -al
total 20
drwxr-xr-x 10 wangfeilong wheel  340 10 30 16:43 ./
drwxr-xr-x  3 wangfeilong wheel  102 10 30 16:43 ../
drwxr-xr-x 12 wangfeilong wheel  408 10 30 16:43 .git/
-rw-r--r--  1 wangfeilong wheel   82 10 30 16:43 .gitignore
-rw-r--r--  1 wangfeilong wheel   32 10 30 16:43 .gitslave
-rw-r--r--  1 wangfeilong wheel  540 10 30 16:43 README.md
-rw-r--r--  1 wangfeilong wheel 1599 10 30 16:43 _config.yml
-rw-r--r--  1 wangfeilong wheel  630 10 30 16:43 package.json
drwxr-xr-x  5 wangfeilong wheel  170 10 30 16:43 scaffolds/
drwxr-xr-x  6 wangfeilong wheel  204 10 30 16:43 source/

此时我们发现子项目并没有被克隆。执行gits populate将克隆所有子项目:

$ gits populate
Cloning into 'themes/Hacker'...
done.

提交项目

我们对父项目和子项目做些修改、提交并将这些改动push到服务端,只需将对应的git命令换成gits,它会顺序在所有仓库中执行对应的git命令,如:

$ echo “*.iml" >> .gitignore
$ echo “*.iml" >> themes/Hacker/.gitignore
$ $ gits add -A
$ gits commit -m "Ignore *.iml"
On: (feilongwang.org):
  [master a2ff2d7] Ignore *.iml
   1 file changed, 1 insertion(+)
On: themes/Hacker:
  [master 03dfa7b] Ignore *.iml
   1 file changed, 2 insertions(+), 1 deletion(-)
$ gits push
On: (feilongwang.org):
  To /private/tmp/remote/feilongwang.org.git
     2e059b2..a2ff2d7  master -> master
On: themes/Hacker:
  To /private/tmp/remote/Hacker.git
     dc4a047..03dfa7b  master -> master

更新项目

如果你想更新服务端最新代码,可以使用gits pull命令,它会将所有仓库代码都同步为服务端最新状态,如:

$ gits pull
On: (feilongwang.org):
  Already up-to-date.
On: themes/Hacker:
  From /private/tmp/remote/%REPO%
    master     -> origin/master
  Fast-forward
   .gitignore | 1 +
   1 file changed, 1 insertion(+)

从上面的命令可以看出,GitSlave是对Git命令的封装,它可以简化多项目的Git操作。

GitSlave的缺点

GitSlave被设计用于包含多个Slave仓库的中等大小项目的开发,其在父项目的.gitslave文件中记录所需子项目的信息,并在所有仓库中顺序执行相应Git操作的设计原理,注定其使用场景有一定局限性。

  • GitSlave并不会记录所需子项目的版本,所以其永远只是追踪子项目的最新版本,无法满足父项目基于某一特定版本子项目的场景,而此种场景在开发中却是极为常见。
  • GitSlave在父项目的.gitslave文件中记录相关子项目的信息,使得父项目本身的提交历史与子项目的增删历史相互交织在一起,一旦子项目增多,父项目的提交历史将变得混乱。

Google Repo

到此,我们可以总结出,一个优秀的基于Git的多项目管理系统设计需要满足如下2点:

  1. 记录子项目的远程地址、所需版本和对应的本地路径。
  2. 这个记录文件应该单独维护,而不应该污染任何一个仓库,因为它与这些仓库本身毫无关系。

Google Repo正是完美匹配这2个设计要点的Git多项目管理系统。

Repo是Google为了有效组织Android的源代码而开发的一个基于Git的管理工具。Android开放源码项目(AOSP)由几百个子项目组成(最新Android 7.0的源代码包含520多个子项目),每个子项目又有很多的分支版本,为了有效的管理这些子项目,需要一个自动化管理工具,Repo应运而生。

Repo工具实际上是由一系列Python脚本组成,这些Python脚本通过调用Git命令来完成自己的功能。比较有意思的是,组成Repo工具的那些Python脚本本身也是一个Git仓库。这个Git仓库被称为Repo仓库。我们每次执行Repo命令的时候,Repo仓库都会对自己进行一次更新。

从之前的结论我们知道,一个优秀的基于Git的多项目管理系统会有一个单独的清单文件记录所有子项目的元信息(子项目的远程地址、所需版本和对应的本地路径)。在Repo管理系统中,这个清单文件单独存在于一个Git仓库,被称为Menifest仓库。Menifest仓库的版本对应着整个项目不同的版本。

到目前为止,我们知道Repo管理系统中存在着3种类型的Git仓库,分别是Repo仓库、Manifest仓库以及所有子项目仓库。Repo仓库通过Menifest仓库可以获取所有子项目的元信息。有了这些元信息后,就可以通过Repo仓库里面的Python脚本来操作所有的子项目了。那么,Repo仓库和Manifest仓库又是怎么来的呢?答案是通过一个独立的Repo脚本来获取,这个Repo脚本位于AOSP(Android Open Source Project)的官方网站上,我们可以通过HTTP协议来下载。

我们以AOSP项目为例说明Repo的用法。

安装Repo

在你的用户主目录下创建bin目录,并且添加bin目录到的你的PATH中:

$ mkdir ~/bin
$ PATH=~/bin:$PATH

下载Repo脚本到bin目录,并确保它可执行:

$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod a+x ~/bin/repo

查看Repo脚本的内容,我们可以看到Repo仓库的地址和版本。Repo脚本执行init命令时会从该地址下载Repo仓库并切换到指定的分支。以后执行其他Repo命令,Repo脚本都会自动更新Repo仓库。

$ cat ~/bin/repo
#!/usr/bin/env python

# repo default configuration
#
import os
REPO_URL = os.environ.get('REPO_URL', None)
if not REPO_URL:
  REPO_URL = 'https://gerrit.googlesource.com/git-repo'
REPO_REV = 'stable'
...

初始化Repo用户端

创建工作目录:

$ mkdir android
$ cd android

根据实际情况配置Git,该信息会将会出现在Git项目的提交信息中。

$ git config --global user.name "Feilong Wang“
$ git config --global user.email “i@feilongwang.org”

运行repo init -u url [options]初始化Repo用户端。其中url指定Menifest仓库的地址。默认情况下,repo init会下载Repo仓库,切换到Repo脚本中REPO_REV指定的分支,并下载Menifest仓库,切换到master分支。你也通过加上-b参数来指定Manifest的分支或版本。

$ repo init -u https://android.googlesource.com/platform/manifest

$ repo init -u https://android.googlesource.com/platform/manifest -b android-4.0.1_r1

Repo用户端初始化成功后,当前目录下将会有个.repo目录,里面包含Repo仓库和Manifest仓库。

$ ls -al
total 0
drwxr-xr-x 3 wangfeilong staff 102 11 13 21:03 ./
drwxr-xr-x 8 wangfeilong staff 442 10  9 15:28 ../
drwxr-xr-x 3 wangfeilong staff 102 11 13 21:00 .repo/
$ cd .repo/
$ ls -al
total 44
drwxr-xr-x  7 wangfeilong staff   340  9 16 09:33 ./
drwxr-xr-x 29 wangfeilong staff  1224 10 20 10:51 ../
-rw-r--r--  1 wangfeilong staff 28439 10  8 16:58 .repo_fetchtimes.json
lrwxr-xr-x  1 wangfeilong staff    21  4  4  2016 manifest.xml -> manifests/default.xml
drwxr-xr-x  3 wangfeilong staff   170 11  1 22:20 manifests/
drwxr-xr-x  9 wangfeilong staff   510  3 16  2016 manifests.git/
drwxr-xr-x  6 wangfeilong staff   204  3 16  2016 project-objects/
-rw-r--r--  1 wangfeilong staff 11105 10  8 16:58 project.list
drwxr-xr-x 28 wangfeilong staff   952  4  5  2016 projects/
drwxr-xr-x  7 wangfeilong staff  1700 10  8 16:58 repo/

可以看到Manifest清单文件内容如下,remote标签指定了子项目仓库的地址前缀,project标签指定了子项目仓库的相对地址和本地路径,revision属性指定子项目的分支或版本。

$ cat manifest.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" >
    <copyfile src="core/root.mk" dest="Makefile" />
  </project>
  <project path="build/blueprint" name="platform/build/blueprint" groups="pdk,tradefed" />
  <project path="build/kati" name="platform/build/kati" groups="pdk,tradefed" />
  <project path="build/soong" name="platform/build/soong" groups="pdk,tradefed" >
    <linkfile src="root.bp" dest="Android.bp" />
    <linkfile src="bootstrap.bash" dest="bootstrap.bash" />
  </project>
  <project path="art" name="platform/art" groups="pdk" />
  <project path="bionic" name="platform/bionic" groups="pdk" />
  <project path="bootable/recovery" name="platform/bootable/recovery" groups="pdk" />
...

下载所有子项目代码

现在,我们可以通过执行repo sync来下载所有项目的源代码了。以后如果服务端有更新,我们也可以通过此命令来获取更新。

$ repo sync

提交项目

由于Repo管理系统中每一个子项目都是单独的Git仓库,所以我们在某一个仓库进行开发时,完全可以使用原先单个Git仓库的开发方式,可以使用git pullgit rebase等命令。只是在同时操作多个子项目时,我们可以使用Repo分装好的批处理命令,如repo start在所有子项目中创建一个新的topic分支。更多Repo命令请运行repo help查看,在此不再赘述。


参考