Git:子模块
官方手册:Git-工具-子模块
关于submodule
有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。 也许是第三方库,或者你独立开发的,用于多个父项目的库。 现在问题来了:你想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。
Git 通过子模块来解决这个问题。 子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
可以使用 git submodule --help
查看所有相关命令。
git clone <repository> --recursive //递归的方式克隆整个项目
git submodule add <repository> <path> //添加子模块
git submodule init //初始化子模块
git submodule update //更新子模块
git submodule foreach git pull //拉取所有子模块
开始使用子模块
关联项目子模块:
# 添加子模块
git submodule add https://github.com/test/subb.git
# 添加子模块,并为子模块设置路径(/modules/subb)
git submodule add https://github.com/test/subb.git modules/subb
项目根目录下有一个.gitmodules文件,即子模块关联文件,如:
[submodule "modules/suba"]
path = modules/suba
url = https://github.com/test/suba.git
每添加一个子模块就会新增一条记录,如果是第一次添加Git子模块会自动生成。
克隆含有子模块的项目
当一个 git 项目包含子模块(submodule) 时,克隆这样的项目时,默认会包含该子模块目录,但子模块目录里面是空的。有两种方法解决:
- 如果项目已经克隆到了本地,执行下面的步骤:
- 初始化本地子模块配置文件:
git submodule init
- 更新项目,抓取子模块内容:
git submodule update
- 初始化本地子模块配置文件:
- 对于未克隆项目,使用“--recursive”参数,可以自动初始化并更新每一个子模块。
git clone --recursive 仓库地址 # 或者 git clone --recurse-submodules 仓库地址
Note:
- “--recursive”,用于的嵌套(项目中的子模块,子模块中的子模块)。
git submodule init
和git submodule update
可以合并成一步:git submodule update --init # 当有子模块嵌套时: git submodule update --init --recursive
在包含子模块的项目上工作
同时在主项目和子模块项目上与队员协作开发时:
从子模块的远端拉取上游修改
在项目中使用子模块的最简模型,就是只使用子项目并不时地获取更新,而并不在你的检出中进行任何更改。
- 如果想要在子模块中查看新工作,可以进入到目录中运行 git fetch 与 git merge,合并上游分支来更新本地代码:
# 进入子模块目录 cd modules/suba # 拉取更新 git fetch # 合并分支 git merge origin/master
- 如果不想在子目录中手动抓取与合并,那么可以采取另一种方式进行抓取和更新:
git submodule update --remote <submodule_name>
- 此命令默认更新并检出子模块仓库的 master 分支,如果需要操作其他分支:
- 需要修改分支配置【既可以在 .gitmodules 文件中设置 (这样其他人也可以跟踪它),也可以只在本地的 .git/config 文件中设置(本地有效)】。
# 在.gitmodules 文件中设置,使用DbConnector模块的stable分支 $ git config -f .gitmodules submodule.DbConnector.branch stable # 拉取子模块更新 $ git submodule update --remote
从项目远端拉取上游更改
作为主项目协作开发者来说,一般情况下会使用 git pull 来拉取项目更新,但是:默认情况下,git pull 命令会递归地抓取子模块的更改,然而,它不会更新子模块!这点可通过 git status 命令看到,它会显示子模块“已修改”,且“有新的提交”。
- 为了完成更新,需要运行 git submodule update:
# 拉取项目更新 git pull # 更新子模块代码 git submodule update --init --recursive
- “--init”:是为了防止 提交中有新的子模块,未初始化而不能更新 的情况;
- “--recursive”:是为了 子模块中有嵌套子模块 的情况;
- 如果想要自动化以上过程:
git pull --recurse-submodules
- 这会让 Git 在拉取后运行 git submodule update,将子模块置为正确的状态;
- 如果要让 Git 总是以 --recurse-submodules 拉取,可以将配置选项 submodule.recurse 设置为 true;
- 在为父级项目拉取更新时,还会出现一种特殊的情况:可能 .gitmodules 文件中记录的子模块的 URL 发生了改变(如,子模块项目改变了它的托管平台),此时,若父级项目引用的子模块提交不在仓库中本地配置的子模块远端上,那么执行
git pull --recurse-submodules
或git submodule update
就会失败。- 此时需要:
# 将新的 URL 复制到本地配置中 $ git submodule sync --recursive # 从新 URL 更新子模块 $ git submodule update --init --recursive
在子模块上工作
在开发过程中可能会出现:在子模块中编写代码的同时,还想在主项目上编写代码(或者跨子模块工作)。这就需要我们了解:
- 如何在子模块与主项目中同时做修改;
- 如何同时提交与发布那些修改。
当运行 git submodule update 从子模块仓库中抓取修改时, Git 将会获得这些改动并更新子目录中的文件,但是会将子仓库留在一个称作“'''游离的 HEAD'''”的状态(HEAD未指向任何分支或提交)。 这意味着子模块没有本地工作分支(如 “master” )跟踪改动,也就意味着即便将更改提交到了子模块,这些更改也很可能会在下次运行 git submodule update 时丢失。
所以,我们需要:
- 进入子模块目录然后检出一个分支:
# 进入子模块DbConnector $ cd DbConnector/ # 检出DbConnector的stable分支 $ git checkout stable Switched to branch 'stable'
- 更新上游代码到子模块分支,再进行本地工作;
Note:
- 更新子模块,并合并到子模块当前分支:
$ cd .. $ git submodule update --remote --merge
- 更新子模块,并变基合并到子模块当前分支:
$ cd .. $ git submodule update --remote --rebase
- 若忘记 --rebase 或 --merge,Git 会将子模块更新为服务器上的状态,并且会将项目重置为一个游离的 HEAD 状态。
$ git submodule update --remote
- 只需回到目录中再次检出分支(即包含着本地工作的分支),然后手动地合并或变基(任何需要的远程分支)即可;
- 若没有提交子模块的改动,那么运行一个子模块更新也不会出现问题,此时 Git 会只抓取更改而并不会覆盖子模块目录中未保存的工作;
- 若本地做了一些与上游改动冲突的改动,当运行更新时 Git 会有提示,进入子模块目录中后进行修复冲突即可。
发布子模块改动
现在我们的子模块目录中有一些改动。其中一部分是通过更新从上游引入的,而另一部分是本地生成的,由于还没有进行推送,所以对任何其他人都不可用。
为了确保推送主项目依赖的子模块已推送,可以让 Git 在推送到主项目前检查所有子模块是否已推送: git push 命令接受可以设置为 “check” 或 “on-demand” 的 --recurse-submodules 参数。
如果任何提交的子模块改动没有推送那么 “check” 选项会直接使 push 操作失败:
$ git push --recurse-submodules=check
The following submodule paths contain changes that can
not be found on any remote:
DbConnector
Please try
git push --recurse-submodules=on-demand
or cd to the path and use
git push
to push them to a remote.
如上,同时给出了操作建议,指导接下来该如何做:
- 进入每一个子模块中然后手动推送到远程仓库,之后再次尝试推送主项目。
- 如果要对所有推送都执行检查,那么可以通过设置
git config push.recurseSubmodules check
让它成为默认行为。
- 如果要对所有推送都执行检查,那么可以通过设置
- 使用“on-demand”值:Git 进入到子模块中,并在推送主项目前推送子模块。
- 如果某个子模块因为某些原因推送失败,主项目也会推送失败。
- 也可以通过设置
git config push.recurseSubmodules on-demand
让它成为默认行为
$ git push --recurse-submodules=on-demand Pushing submodule 'DbConnector' Counting objects: 9, done. Delta compression using up to 8 threads. Compressing objects: 100% (8/8), done. Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done. Total 9 (delta 3), reused 0 (delta 0) To https://github.com/chaconinc/DbConnector c75e92a..82d2ad3 stable -> stable Counting objects: 2, done. Delta compression using up to 8 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done. Total 2 (delta 1), reused 0 (delta 0) To https://github.com/chaconinc/MainProject 3d6d338..9a377d1 master -> master
合并子模块改动
如果你和其他人同时改动了同一个子模块,也就是说,如果子模块的历史已经分叉并且在父项目中分别提交到了分叉的分支上,那么你需要做一些工作来修复它:
- 如果一个提交是另一个的直接祖先(一个快进式合并),那么 Git 会简单地选择之后的提交来合并;
- 但如果子模块提交已经分叉且需要合并,Git 就不能通过简单的选择commit来解决问题,如:
$ 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.
合并思路:
找到子模块的当前提交点(的 SHA-1 值),然后:直接通过 SHA-1 或 创建一个分支(推荐)来尝试合并。
合并步骤:
- 运行git diff,就会得到试图合并的两个分支中记录的提交的 SHA-1 值:
$ git diff diff --cc DbConnector index eb41d76,c771610..0000000 --- a/DbConnector +++ b/DbConnector
- 进入子模块目录,基于 git diff 的第二个 SHA-1 创建一个分支然后手动合并:
$ cd DbConnector $ git rev-parse HEAD eb41d764bccf88be77aced643c13a7fa86714135 $ git branch try-merge c771610 (DbConnector) $ git merge try-merge Auto-merging src/main.c CONFLICT (content): Merge conflict in src/main.c Recorded preimage for 'src/main.c' Automatic merge failed; fix conflicts and then commit the result.
- 在这儿得到了一个真正的合并冲突,所以如果想要解决并提交它,那么只需简单地通过结果来更新主项目。
$ vim src/main.c (1) $ git add src/main.c $ git commit -am 'merged our changes' Recorded resolution for 'src/main.c'. [master 9fd905e] merged our changes $ cd .. (2) $ git diff (3) diff --cc DbConnector index eb41d76,c771610..0000000 --- a/DbConnector +++ b/DbConnector @@@ -1,1 -1,1 +1,1 @@@ - Subproject commit eb41d764bccf88be77aced643c13a7fa86714135 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d ++Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a $ git add DbConnector (4) $ git commit -m "Merge Tom's Changes" (5) [master 10d2c60] Merge Tom's Changes
合并总结:
- 首先解决冲突
- 然后返回到主项目目录中
- 再次检查 SHA-1 值
- 解决冲突的子模块记录
- 提交我们的合并
删除项目的子模块
git没有直接删除子模块的命令,所以只能逐步删除相关文件。
- 在版本控制中删除子模块:
git rm -r modules/subb
- 在编辑器中删除如下相关内容,也可以使用命令“vi .gitmodules”在vim中删除:
[submodule "modules/subb"] path = modules/subb url = https://github.com/test/subb.git branch = dev
- 在编辑器中删除如下相关内容,也可以使用命令“vim .git/config”在vim中删除:
[submodule "modules/subb"] path = modules/subb url = https://github.com/test/subb.git active = true
- 删除.git下的缓存模块:
rm -rf .git/modules/subb
- 提交修改:
git commit -am "delete subb" git push origin dev
子模的块技巧
子模块遍历
如果项目中包含了大量子模块,使用foreach子模块命令,能在每一个子模块中运行任意命令。如:
- 保存所有子模块的工作现场:
$ 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 2.13 以前的版本时,在有子模块的项目中切换分支可能会造成麻烦。 如果你创建一个新分支,在其中添加一个子模块,之后切换到没有该子模块的分支上时,你仍然会有一个还未跟踪的子模块目录:
$ git --version
git version 2.12.2
$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...
$ git commit -am 'adding crypto library'
[add-crypto 4445836] adding crypto library
2 files changed, 4 insertions(+)
create mode 160000 CryptoLibrary
$ git checkout master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
(use "git add <file>..." to include in what will be committed)
CryptoLibrary/
nothing added to commit but untracked files present (use "git add" to track)
对此,如果你移除它然后切换回有那个子模块的分支,需要运行 submodule update --init
来重新建立和填充。
$ git clean -fdx
Removing CryptoLibrary/
$ git checkout add-crypto
Switched to branch 'add-crypto'
$ ls CryptoLibrary/
$ git submodule update --init
Submodule path 'CryptoLibrary': checked out 'b8dda6aa182ea4464f3f3264b11e0268545172af'
$ ls CryptoLibrary/
Makefile includes scripts src
而新版的 Git(>= 2.13)通过为 git checkout
命令添加 --recurse-submodules
选项简化了所有这些步骤, 它能为了我们要切换到的分支让子模块处于的正确状态:
$ git --version
git version 2.13.3
$ git checkout -b add-crypto
Switched to a new branch 'add-crypto'
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
...
$ git commit -am 'adding crypto library'
[add-crypto 4445836] adding crypto library
2 files changed, 4 insertions(+)
create mode 160000 CryptoLibrary
$ git checkout --recurse-submodules master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean
当你在父级项目的几个分支上工作时,对 git checkout
使用 --recurse-submodules
选项也很有用, 它能让你的子模块处于不同的提交上。确实,如果你在记录了子模块的不同提交的分支上切换, 那么在执行 git status
后子模块会显示为“已修改”并指出“新的提交”。 这是因为子模块的状态默认不会在切换分支时保留。
这点非常让人困惑,因此当你的项目中拥有子模块时,可以总是使用 git checkout --recurse-submodules。 (对于没有 --recurse-submodules
选项的旧版 Git,在检出之后可使用 git submodule update --init --recursive
来让子模块处于正确的状态)。
幸运的是,你可以通过 git config submodule.recurse true
设置 submodule.recurse
选项, 告诉 Git(>=2.14)总是使用 --recurse-submodules
。 如上所述,这也会让 Git 为每个拥有 --recurse-submodules
选项的命令(除了 git clone) 总是递归地在子模块中执行。
从子目录切换到子模块
另一个主要的告诫是许多人遇到了将子目录转换为子模块的问题。 如果你在项目中已经跟踪了一些文件,然后想要将它们移动到一个子模块中,那么请务必小心,否则 Git 会对你发脾气。 假设项目内有一些文件在子目录中,你想要将其转换为一个子模块。 如果删除子目录然后运行 submodule add
,Git 会出现:
$ rm -Rf CryptoLibrary/
$ git submodule add https://github.com/chaconinc/CryptoLibrary
'CryptoLibrary' already exists in the index
你必须要先取消暂存 CryptoLibrary 目录。 然后才可以添加子模块:
$ git rm -r CryptoLibrary
$ git submodule add https://github.com/chaconinc/CryptoLibrary
Cloning into 'CryptoLibrary'...
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.
现在假设你在一个分支下做了这样的工作。 如果尝试切换回的分支中那些文件还在子目录而非子模块中时——你会得到这个错误:
$ 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
你可以通过 checkout -f
来强制切换,但是要小心,如果其中还有未保存的修改,这个命令会把它们覆盖掉。
$ git checkout -f master
warning: unable to rmdir CryptoLibrary: Directory not empty
Switched to branch 'master'
当你切换回来之后,因为某些原因你得到了一个空的 CryptoLibrary
目录,并且 git submodule update
也无法修复它。 你需要进入到子模块目录中运行 git checkout .
来找回所有的文件。 你也可以通过 submodule foreach
脚本来为多个子模块运行它。
要特别注意的是,近来子模块会将它们的所有 Git 数据保存在顶级项目的 .git 目录中,所以不像旧版本的 Git,摧毁一个子模块目录并不会丢失任何提交或分支。