背景
随着项目日益复杂,开发人员的增多,用单独的git做项目管理会遇到不少问题
- 产生单人维护一个模块的需求,对应一个或者有限的几个git,这样逻辑更加清晰
- 多人提交频繁遇到冲突,需要merge,非常影响开发效率
针对上述问题,主要有下面几个解决办法
- repo:android采用的方式,相当于在manifest中记录了多个git。
- submodule:git 1.5.3引入,将一个git仓库作为另一个git仓库的子目录。
- subtree:git 1.8引入,将子项目的代码全部
merge
进父项目。
- GitSlave:用于管理相关的一个父项目和多个Slave项目。通常,它会将你要执行的Git常规操作顺序在父项目和Slave项目中执行一遍,所以当你执行pull操作,项目中的所有仓库会顺序执行pull操作。
repo
repo是Google为了有效组织Android的源代码而开发的一个基于Git的管理工具,它实际上是由一系列Python脚本组成,这些Python脚本通过调用Git命令来完成自己的功能。repo体系一般包含3类仓库
- repo仓库:组成repo工具的那些Python脚本
- manifest仓库:记录所有子项目的元信息(子项目的远程地址、所需版本和对应的本地路径)
- 子项目仓库:
安装repo
1
2
|
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod a+x ~/bin/repo
|
初始化manifest
1
|
$ repo init -u https://android.googlesource.com/platform/manifest -b android-10.0.1_r1
|
下载代码
提交代码
repo管理系统中的每一个子项目都是单独的git仓库,所以我们在某一个仓库进行开发时,完全可以使用原先单个git仓库的开发方式,可以使用git pull,git rebase,git push等命令。
submodule
添加子模块
将一个已存在的Git仓库添加为正在工作的仓库的子模块,可以使用git submodule add []
命令。默认情况下,子模块会将子项目放在一个与仓库同名的目录中。我们也可以通过在命令结尾添加一个path
来指定放到其他地方。如果这时运行git status
,你会注意到2件事情。
1
2
3
4
5
6
7
8
|
$ 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
与本地目录之间的映射:
1
2
3
|
[submodule "themes/Hacker"]
path = themes/Hacker
url = /private/tmp/remote/Hacker.git
|
在git status
输出中列出的另一个是项目文件夹记录。如果你运行git diff
,会看到类似下面的信息:
1
2
3
4
5
6
7
8
|
$ 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
|
除了git status
看到的差异外,还有个隐藏的变化在.git/config
中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$ 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
中的配置。
克隆子模块
当你克隆一个含有子模块的项目时,默认会包含该子模块目录,但其中还没有任何文件。
1
2
3
4
5
6
7
8
|
$ 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
则从该项目中抓取所有数据并检出父项目中列出的合适的提交。
1
2
3
4
5
6
|
$ 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
选项,它就会自动初始化并更新仓库中的每一个子模块。
1
2
3
4
5
6
7
|
$ 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 pull
并不会自动更新子模块,需要调用git submodule update
来更新子模块信息。如果忘记调用git submodule update
,那么你极有可能再次把旧的子模块依赖信息提交上去。
- 调用
git submodule update
并不会将子模块切换到任何分支,默认情况下子模块处于“游离的 HEAD”的状态。如果此时我们改动子模块而没有检出一个工作分支,那调用git submodule update
时你所做的任何改动都会丢失。
- Git子模块在父项目中维护所有依赖的子模块版本,当包含大量子模块时,父项目的更新将很容发生冲突,并且父项目的维护历史与所有子模块的维护历史相互交织,维护成本也会比较高。
subtree
添加子项目
将一个已存在的Git仓库以Subtree方式添加为子项目可以使用git subtree add --prefix=
命令,其中--prefix
选项指定了子项目对应的子目录,--squash
选项用以压缩Subtree的提交为一个,这样父项目的历史记录里就不会出现子项目完整的历史记录。
更新子项目
一段时间之后,子项目可能有大量新的代码,父项目也想使用这些代码。此时父项目的维护者只需执行:
1
2
3
4
5
6
7
8
9
10
11
|
$ 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(-)
|
就可以将父项目中子项目对应目录里的内容更新为子项目最新的代码了。
提取子项目
当我们开发一个项目若干时间后,希望将某个目录单独出一个项目来开发,同时又保留这部分代码历史提交记录,使用git subtree split
可以很轻松的完成这个操作。以Hexo博客分离Hacker主题为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$ 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
指定将生成的历史提交记录保存到一个新的分支。
提交子项目
如果我们在使用子项目的过程中,对子项目做了一些改动,同时我们又希望子项目的其他使用者也能共享这些改动,此时可以将我们的改动提交到子项目的远程仓库中。
1
2
3
4
5
6
7
8
9
10
|
$ 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项目。如:
1
2
3
4
5
6
7
8
9
|
$ 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项目:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
$ 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仓库的项目,如:
1
2
3
4
5
|
$ gits clone /private/tmp/remote/feilongwang.org.git
Cloning into 'feilongwang.org'...
done.
Cloning into 'themes/Hacker'...
done.
|
我们看到Git的克隆操作顺序在父项目与子项目中被执行。如果一开始你并知道要克隆的项目带有Slave仓库而直接将父项目克隆下来,此时我们可以通过git populate
来进一步克隆Slave仓库,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$ 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
将克隆所有子项目:
1
2
3
|
$ gits populate
Cloning into 'themes/Hacker'...
done.
|
提交项目
我们对父项目和子项目做些修改、提交并将这些改动push到服务端,只需将对应的git
命令换成gits
,它会顺序在所有仓库中执行对应的git
命令,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
$ 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
命令,它会将所有仓库代码都同步为服务端最新状态,如:
1
2
3
4
5
6
7
8
9
|
$ 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
文件中记录相关子项目的信息,使得父项目本身的提交历史与子项目的增删历史相互交织在一起,一旦子项目增多,父项目的提交历史将变得混乱。