git submodule的场景和应用

随着git的使用,我们开始习惯性的用git进行项目的管理,但是作为天选牛马,我们往往需要参与不同的项目,这些项目中的某些功能是相同的。这时出于解耦和模块代码独立运维管理的需求,我们一般会针对一些功能实现进行独立的子仓库管理,当然除了自己的,我们可能也会用到一些第三方的仓库。 他们的一个共同特点是有一个独立的仓库进行管理,同时作为一个相对独立的功能实现在我们的仓库中被使用。

丑陋一点的方法把所需模块的源代码都拷贝到主项目里,这样主项目就变成一个完整的项目了。但是显而易见的,如果子仓库有一些独立的优化或性能升级,那么在我们主项目进行同步,或者我们的一些开发需要同步到子项目这些工作都会非常麻烦,甚至随着项目增加而成为灾难。

显然,我们需要一个更好的解决方案。而git submodule 模块就是为了解决这个问题而生的,submodule 让我们可以将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。

配置

首先介绍一些配置可以在后续使用中减少键盘的敲击~~, 因为后续可能是复用比较多的配置代码,所以卸载最前面

1
2
3
4
5
6
7
8
9
10
11
12
13
# 使用git status 直接显示你的子模块的更改摘要
git config status.submodulesummary 1

# git diff 显示子模块的区别,不需要额外输入 --submodule
git config --global diff.submodule log

# 推送主项目更改时,会自动检查所有子仓库的更改是否已经推送,并自动尝试推送未推送的子项目仓库
git config push.recurseSubmodules on-demand
# 配置子模块的仓库分支
git config -f .gitmodules submodule.DbConnector.branch stable

# 让 Git 为每个拥有 --recurse-submodules 选项的命令(除了 git clone) 总是递归地在子模块中执行。
git config submodule.recurse true

使用

添加子模块

我们首先将一个已存在的 Git 仓库添加为正在工作的仓库的子模块。 你可以通过在 git submodule add 命令后面加上想要跟踪的项目的相对或绝对 URL 来添加新的子模块。 在本例中,我们将会添加一个名为 “DbConnector” 的库。

1
2
3
4
5
6
7
$ git submodule add https://github.com/chaconinc/DbConnector
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.

添加子模块并指定目录

默认情况下,子模块会将子项目放到一个与仓库同名的目录中,本例中是 “DbConnector”。 如果你想要放到其他地方,那么可以在命令结尾添加一个不同的路径(eg:scripts/DbConnector)。

1
git submodule add https://github.com/chaconinc/DbConnector  scripts/DbConnector

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

1
2
3
4
5
6
7
8
9
$ 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: DbConnector

.gitmodules

我们通过 git submodule add 拉取子仓库后,会增加了一个 .gitmodules 文件。 该配置文件保存了项目 URL 与已经拉取的本地目录之间的映射:

1
2
3
[submodule "DbConnector"]
path = DbConnector
url = https://github.com/chaconinc/DbConnector

如果有多个子模块,该文件中就会有多条记录。 要重点注意的是,该文件也像 .gitignore 文件一样受到版本控制。 它会和该项目的其他部分一同被拉取推送。 这就是克隆该项目的人知道去哪获得子模块的原因。

变更子仓库的配置

  1. 修改 .gitmodules 文件中对应模块的url属性;
  2. 使用 git submodule sync 命令,将新的URL更新到文件.git/config;
    再使用命令初始化子模块:git submodule init
    最后使用命令更新子模块:git submodule update

子仓库信息

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

1
2
3
4
5
6
7
8
$ git diff --cached DbConnector
diff --git a/DbConnector b/DbConnector
new file mode 160000
index 0000000..c3f01dc
--- /dev/null
+++ b/DbConnector
@@ -0,0 +1 @@
+Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc

虽然 DbConnector 是工作目录中的一个子目录,但 Git 还是会将它视作一个子模块。当你不在那个目录中时,Git 并不会跟踪它的内容, 而是将它看作子模块仓库中的某个具体的提交。

如果你想看到更漂亮的差异输出,可以给 git diff 传递 --submodule 选项,可以在diff的时候,展示更详细的模块信息(远程仓库和版本变动)。

1
2
3
4
5
6
7
8
9
10
11
$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..71fc376
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "DbConnector"]
+ path = DbConnector
+ url = https://github.com/chaconinc/DbConnector
Submodule DbConnector 0000000...c3f01dc (new submodule)

提交

整体而言,提交带子仓库的模块和普通的提交推送所需要的操作是一样的.

1
2
3
4
5
6
7
8
9
# 提交时,会看到类似下面的信息:
$ git commit -am 'added DbConnector module'
[master fb9093c] added DbConnector module
2 files changed, 4 insertions(+)
create mode 100644 .gitmodules
create mode 160000 DbConnector

#最后,推送这些更改:
$ git push origin master

只不过注意看的化会发现 DbConnector 记录的文件类型是 160000 模式。 这是 Git 中的一种特殊模式,它本质上意味着你是将一次提交记作一项目录记录的,而非将它记录成一个子目录或者一个文件。

子项目获取

直接初始化并拉取子项目

这是一个更为简单的方式, 在执行 git clone 命令时,传递 –recurse-submodules 选项,它就会自动初始化并更新仓库中的每一个子模块, 包括可能存在的嵌套子模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git clone --recurse-submodules https://github.com/chaconinc/MainProject
Cloning into 'MainProject'...
remote: Counting objects: 14, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 14 (delta 1), reused 13 (delta 0)
Unpacking objects: 100% (14/14), done.
Checking connectivity... done.
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

单独拉取子项目

如果准备单独拉取子项目内容,那么我们开始的初始项目克隆和正常项目是一致的,只不过我们拉取下来的项目中,子项目只会是一个空目录,里面没有完整的子项目内容。
我们需要运行两个命令:git submodule init 用来初始化本地配置文件,git submodule update 则从该项目中抓取所有数据并检出父项目中列出的合适的提交。
或者我们也可以使用 git submodule update --init 命令一次性完成初始化并更新操作。
如果还要初始化、抓取并检出任何嵌套的子模块, 请使用简明的git submodule update --init --recursive

1
2
3
4
5
6
7
8
9
10
$ git submodule init
Submodule 'DbConnector' (https://github.com/chaconinc/DbConnector) registered for path 'DbConnector'
$ git submodule update
Cloning into 'DbConnector'...
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 11 (delta 0), reused 11 (delta 0)
Unpacking objects: 100% (11/11), done.
Checking connectivity... done.
Submodule path 'DbConnector': checked out 'c3f01dc8862123d317dd46284b05b6892c7b29bc'

现在 DbConnector 子目录是处在和之前提交时相同的状态了。

子项目同步

我们有一份包含子模块的项目时,我们将会同时在主项目和子模块项目上与队员协作。

从远程仓库中同步子项目

项目中使用子模块的最简模型,就是只使用子项目并不时地获取更新,而并不在你的检出中进行任何更改。 我们来看一个简单的例子。

直接操作子仓库

如果想要在子模块中查看新工作,可以进入到目录中运行 git fetch 与 git merge,合并上游分支来更新本地代码。这和我们在主项目中运行 fetch 与 merge 的方式完全相同。

通过submodule管理

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

1
2
3
4
5
6
7
# 更新指定子模块,默认使用 master 分枝
$ git submodule update --remote DbConnector

# DbConnector 子模块跟踪仓库的 “stable” 分支,
git config submodule.DbConnector.branch stable # 指定远程仓库的分枝,仅为自己配置,不会推送相关分值信息
git config -f .gitmodules submodule.DbConnector.branch stable # 指定远程仓库的分枝,相关分支信息会推送的远程同步给其他用户。
git submodule update --remote # 更新子仓库内容

子仓库冲突处理

你和其他人同时改动了一个子模块引用,那么可能会遇到一些问题。 也就是说,如果子模块的历史已经分叉并且在父项目中分别提交到了分叉的分支上,那么你需要做一些工作来修复它。

1
2
3
4
5
6
7
8
9
10
11
12
$ git pull
remote: Counting objects: 2, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 2 (delta 1), reused 2 (delta 1)
Unpacking objects: 100% (2/2), done.
From https://github.com/chaconinc/MainProject
9a377d1..eb974f8 master -> origin/master
Fetching submodule DbConnector
warning: Failed to merge submodule DbConnector (merge following commits not found)
Auto-merging DbConnector
CONFLICT (submodule): Merge conflict in DbConnector
Automatic merge failed; fix conflicts and then commit the result.

Git 在这里指出了子模块历史中的两个分支记录点,并且不能自动进行合并。这是你可以通过 git diff 查看具体的差异

1
2
3
4
5
$ git diff
diff --cc DbConnector
index eb41d76,c771610..0000000
--- a/DbConnector
+++ b/DbConnector

eb41d76 是我们的子模块中大家共有的提交,而 c771610 是上游子仓库进行的提交。这时候我们需要单独进行子仓库的冲突处理(处理方式和正常仓库的处理一样,只是我们需要进入到子仓库进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
# 进入子仓库的目录,后续相关操作都是针对子仓库的
$ cd DbConnector

# 将子仓库回退到父项目记录的节点,
$ git rev-parse HEAD
eb41d764bccf88be77aced643c13a7fa86714135

# 拉取子项目的远程最新的代码,并创建一个分支 try-merge
$ git branch try-merge c771610

# 进行代码的合并,将远程新代码合并到本地主分支上,
(DbConnector) $ git merge try-merge

当然可能在merge的时候,会存在一些文件冲突,这时候,就是正常的处理冲突文件,提交推送即可。

1
2
3
$ vim $conflict_File
$ git add $conflict_File
$ git commit -am 'merged our changes'

父项目同步

拉取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 默认拉取父项目
$ git pull
From https://github.com/chaconinc/MainProject
fb9093c..0a24cfc master -> origin/master
Fetching submodule DbConnector
From https://github.com/chaconinc/DbConnector
c3f01dc..c87d55d stable -> origin/stable
Updating fb9093c..0a24cfc
Fast-forward
.gitmodules | 2 +-
DbConnector | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)

# 会显示其中子项目的更改,但是不会自动同步子项目中的更改
$ 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: DbConnector (new commits)

Submodules changed but not updated:

* DbConnector c87d55d...c3f01dc (4):
< catch non-null terminated lines
< more robust error handling
< more efficient db routine
< better connection routine

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

默认情况下,git pull 命令会递归地抓取子模块的更改,如上面第一个命令的输出所示。 然而,它不会 更新 子模块。这点可通过 git status 命令看到,它会显示子模块“已修改”,且“有新的提交”。 此外,左边的尖括号(<)指出了新的提交,表示这些提交已在 MainProject 中记录,但尚未在本地的 DbConnector 中检出。 为了完成更新,你需要运行 git submodule update

1
2
3
4
5
6
7
$ git submodule update --init --recursive
Submodule path 'vendor/plugins/demo': checked out '48679c6302815f6c76f1fe30625d795d9e55fc56'

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

推送

如果我们的子模块目录中有一些改动。如果我们针对父项目进行提交并推送但并不推送子模块上的改动,其他人因为他们无法得到依赖的子模块改动,他们在执行项目的时候会遇到麻烦,
为了避免这个问题,可以让 Git 在推送到主项目前检查所有子模块是否已推送。 git push 命令接受可以设置为 check(检查是否推送) 或 on-demand(自动推送未推送的子模块) 的 --recurse-submodules 参数。 如果任何提交的子模块改动没有推送那么 check 选项会直接使 push 操作失败。on-demand会尝试推送子模块

1
2
3
4
# 推送主项目更改时,会自动检查所有子仓库的更改是否已经推送
git push --recurse-submodules=check

git push --recurse-submodules=on-demand

子模块的技巧性操作

遍历

有一个 foreach 子模块命令,它能在每一个子模块中运行任意命令。 如果项目中包含了大量子模块,这会非常有用。
git submodule foreach "$git_command" 可以让我们对任何一个子模块执行某个相同的命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 将所有子模块的更改进行暂存
$ git submodule foreach 'git stash'
Entering 'CryptoLibrary'
No local changes to save
Entering 'DbConnector'
Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable
HEAD is now at 82d2ad3 Merge from origin/stable

# 对所有子模块都创建一个新的分支,并切换过去。
$ git submodule foreach 'git checkout -b featureA'
Entering 'CryptoLibrary'
Switched to a new branch 'featureA'
Entering 'DbConnector'
Switched to a new branch 'featureA'

# 查看所有子项目的改动
git submodule foreach 'git diff'

别名

可以看到很多命令的参数非常长,尤其是在使用子模块后,额外增加了一些参数,但是 git 并不支持将这些选项作为它们的默认选项。
我们可以通过为这些命令设置别名来简化我们的操作命令,这里有一些例子。

1
2
3
$ git config alias.sdiff '!'"git diff && git submodule foreach 'git diff'"
$ git config alias.spush 'push --recurse-submodules=on-demand'
$ git config alias.supdate 'submodule update --remote --merge'

从子目录切换到子模块

有时候我们想把项目中的一部分工作或者功能,独立抽象出来,形成一个单独的模块(改成子仓库单独保存),那么我们需要先取消暂存对应子模块代码所在的目录(否则会由于目录以存在产生冲突)。 然后才可以添加子模块

1
2
$ git rm -r subFunction
$ git submodule add https://github.com/****/subFunction

现在假设你在一个分支下做了这样的工作。 当你切换到其他分支(文件还在子目录而非子模块中)时——你会得到这个错误,所以在切换时,需要进行强制(-f)切换分支 :

1
2
3
4
5
6
7
8
9
10
# 正常切换由于模块目录在目标分支是父项目的一个目录存在冲突会报错
$ git checkout master
error: The following untracked working tree files would be overwritten by checkout:
CryptoLibrary/Makefile
CryptoLibrary/includes/crypto.h
...
Please move or remove them before you can switch branches.
Aborting
# 强制切换分支
git checkout -f master

子模块的注意事项

切换分支

在切换分支使用 git checkout 命令时,最好添加 --recurse-submodules(git ≥2.14),来确保父项目分支切换后,子项目会和分支实际情况保持一致。
通过 git config submodule.recurse true 设置 submodule.recurse 选项, 告诉 Git(>=2.14)总是使用 --recurse-submodules。 如上所述,这也会让 Git 为每个拥有 --recurse-submodules 选项的命令(除了 git clone) 总是递归地在子模块中执行。
如果使用了子模块,直接进行相关的设置吧,旧版本的一些潜在风险笔记多,但是我们似乎在这也不需要再去过度纠结旧版本的问题了,毕竟此刻,刚开始的你和我可以直接使用新版本。

参考

起始就是边看边抄了一遍 git doc:git-submodule

-------------本文结束感谢您的阅读-------------