git submodule
此文已由作者张磊薪授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
前言
submodule 目前对 git 仓库拆分的已有实现之一。环境 git version 2.7.4.windows.1
准备工作
首先创建主仓库 subrepo-master,随意提交一次文本,接着拉取到本地
建立子仓库 subrepo 和 subrepo1,随意提交一次文本
操作
在主仓库下运行如下命令后,可以看到在仓库中多出来文件 subrepo 以及 .gitmodules
git submodule add [subrepo url]
运行 git status
On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: .gitmodules new file: subrepo
接着添加 submodule1,并指定路径
git submodule add [subrepo1 url] ./module/module1
运行 git status 得到
On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: .gitmodules new file: module/module1 new file: subrepo
cat .gitmodules 得到
[submodule "subrepo"] path = subrepo url = [subrepo url] [submodule "module/module1"] path = module/module1 url = [subrepo1 url]
这是一份子模块与路径的映射关系图,这份文件很重要,git 根据这份文件去识别 submodule,所以这份文件应该被加入版本控制
接着运行提交命令,可以看到三个目录都被添加到仓库了,注意子模块下面的文件并没有被添加进去。160000 的含义是这是 Git 中的一种特殊模式,基本上意味着您将提交记录为目录条目而不是子目录或文件。 然后提交到远端,就有了一个 submodule 的仓库 :)
$ git commit -m "add submodule" [master 5c88033] add submodule 3 files changed, 8 insertions(+) create mode 100644 .gitmodules create mode 160000 module/module1 create mode 160000 subrepo
接下来模拟多人协作,首先新建一个文件夹,运行命令后,会发现 subrepo 以及 module/module1 目录并没有文件。
git clone [subrepo-master url]
这时候需要运行命令 git submodule init 去初始化本地配置文件以及 git submodule update 拉取代码。
$ git submodule init Submodule 'module/module1' (https://github.com/xxx/subrepo1.git) registered for path 'module/module1' Submodule 'subrepo' (https://github.com/xxx/subrepo.git) registered for path 'subrepo'
$ git submodule update Cloning into 'module/module1'... remote: Counting objects: 3, done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), done. Checking connectivity... done. Submodule path 'module/module1': checked out '5c47ee69895b8acd3291eb0551f751ba43 488c68' Cloning into 'subrepo'... remote: Counting objects: 3, done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), done. Checking connectivity... done. Submodule path 'subrepo': checked out '1b6f8742270c5affc9fc055ce1bf03907cf9a0f8'
通过 6、7 两步完成还是太过麻烦,好在有简单的命令,在 clone 那一步运行 git clone --recursive 就会自动完成 6、7两步。
如果只更改主仓库的代码,那各种操作大家都会。下面主要是子仓库远端修改、子仓库改分支、子仓库本地修改推送远端、主仓库下的子仓库的引用同时被多人修改等等。
子仓库远端修改
修改子仓库远端的内容,接着同步到本地。一种方式,进入子仓库目录 git fetch && git merge,另一种方式 git submodule update --remote [子仓库目录]。 运行 git diff,可以看到
$ git diff diff --git a/module/module1 b/module/module1index 5c47ee6..e55e7ef 160000--- a/module/module1+++ b/module/module1 @@ -1 +1 @@ -Subproject commit 5c47ee69895b8acd3291eb0551f751ba43488c68 +Subproject commit e55e7efb44f1c07d23e2deede0a13fa8a953f96d diff --git a/subrepo b/subrepoindex 1b6f874..afdc5bf 160000--- a/subrepo+++ b/subrepo @@ -1 +1 @@ -Subproject commit 1b6f8742270c5affc9fc055ce1bf03907cf9a0f8 +Subproject commit afdc5bf559beacb08032e23d22a2beaa65d3ca9c
子仓库改分支
在子仓库远端新建分支 a,然后在主仓库运行命令,再运行 cat .gitmodules 查看,发现 subrepo 多了 branch 指向。运行 git submodule status 可以看到 submodule 的状态。运行 git log -p --submodule 可以查看到子模块的日志修改。同时可以将 branch 指向 tag ,但不支持 commitid。这里有一点,子模块记录的是 commitid 即使指定了 branch,同时它不会主动升级(主动升级会导致每个人的代码不一致)。
git config -f .gitmodules submodule.subrepo.branch a git submodule update --remote
$ cat .gitmodules [submodule "subrepo"] path = subrepo url = https://xx/subrepo.git branch = a [submodule "module/module1"] path = module/module1 url = https://xx/subrepo1.git
$ git submodule status +e55e7efb44f1c07d23e2deede0a13fa8a953f96d module/module1 (remotes/origin/HEAD)+afdc5bf559beacb08032e23d22a2beaa65d3ca9c subrepo (remotes/origin/HEAD)
指向 commitid 的方案是运行如下指令,通过将这个子模块文件夹的 commitid 指定为新的,即可完成指定相应 commitid 的动作。此时观察 .gitmodules,会发现 subrepo 指向的 branch 字段消失。
cd subrepo git checkout fa1317acd .. git add subrepo git commit ...
其他人在各自目录运行,即可获取最新更新
git submodule update
子仓库本地修改推送远端
主仓库下的子仓库的引用同时被多人修改
比如 subrepo 指向的 commitid 是 a,A 成员修改为 b, B 成员修改为 c,这时候就需要去合并代码,让其指向最新的 a1。
如果 b 和 c 是祖先关系,则 git 会直接快进式合并。下面模拟分叉的情况。
检出一个本地仓库, git reset --HARD oldcommitid,然后将子模块的代码进行修改,接着添加到主仓库进行提交。接下来运行 git pull,会发现有冲突了。
$ git pull origin masterFrom https://xx/subrepo-master* branch master -> FETCH_HEADwarning: Failed to merge submodule subrepo (commits don't follow merge-base) Auto-merging subrepo CONFLICT (submodule): Merge conflict in subrepo Automatic merge failed; fix conflicts and then commit the result.
解决此问题的方案是,首先 git diff 查看一下代码
diff --cc subrepo index 4660574,55167f5..0000000--- a/subrepo +++ b/subrepo diff --git a/module/module1 b/module/module1index e55e7ef..5c47ee6 160000--- a/module/module1+++ b/module/module1@@ -1 +1 @@ -Subproject commit e55e7efb44f1c07d23e2deede0a13fa8a953f96d +Subproject commit 5c47ee69895b8acd3291eb0551f751ba43488c68
找到冲突的 commitid(其中 4660574 是公有的, 55167f5 是上游的),进入子模块,然后检出为分支 git branch merge1 55167f5 && git merge merge1,如果有冲突,此时就需要手动解决冲突了,解决完成,提交。再回到主目录,查看是否有冲突,对主仓库进行提交。此时再 git pull 则无冲突,然后就可以用 git push --recurse-submodules=on-demand 提交本次修改。
删除子模块后会发现 .gitmodules 文件内容同时发生了改变。如果需要备份,请提前备份目录
git submodule deinit module/module1rm -rf .git/modules/modulegit rm -f module/module1vi .git/config
子模块本地提交代码
当本地子模块没有指定 branch 的时候,是处于一个称作 “游离的 HEAD”(detached HEAD) 的状态(git 提示的是 commitid 而不是分支名)。这个状态下你可以正常的 git 操作,但是此时是没有分支进行跟踪的,也就没办法推送代码。想摆脱这种状态,在子模块运行 git checkout branch 即可。
子模块拉取服务端代码
然后对远端子模块做一次修改(称为 patchA),并运行 git submodule update --remote --merge,就可以看到 subrepo 发生了改变,此时可以提交这个改变,即将 subrepo 指向的旧的 commitid 换成 patchA 的 commitid。后面的人更新代码的时候会拿到新的 commitid,如果没有人运行 update 命令并提交,则所有人拿到的都是旧的代码。
$ 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: subrepo (new commits) no changes added to commit (use "git add" and/or "git commit -a")
本地提交与远端合并代码
接下来对本地的 subrepo 做一次提交,接着对远端的子模块也做一次提交。然后更新子模块。可能会运行 git submodule update --remote,此命令只会更新成远端的代码,会发现本地的代码丢失,此时不需要慌张,运行 git checkout branch 即可获取本地代码。合并的命令是 git submodule update --remote 后面加上 --merge 或者 --rebase 即可,会遇到冲突的情况,进入目录手工解决即可。运行 git diff -p --submodule,可以查看到子模块的修改,现在修改的内容包含本地提交以及远端合并的代码,接下来就需要进行推送。
本地提交到远端 在主仓库进行一次提交,提交子仓库最新的 commitid。
如果我们在主项目中提交并推送但并不推送子模块上的改动,其他尝试检出我们修改的人会遇到麻烦,因为他们无法得到依赖的子模块改动。那些改动只存在于我们本地的拷贝中。
为了确保这不会发生,你可以让 Git 在推送到主项目前检查所有子模块是否已推送。 git push --recurse-submodules=check (如果子模块没有提交,会直接报错)或者 git push --recurse-submodules=on-demand(如果子模块没有提交,会尝试提交,提交不成功同时会阻止主仓库的推送)
技巧
foreach,可以在每一个子模块中运行任意命令。例如:保存进度, git submodule foreach 'git stash';切换分支 git submodule foreach 'git checkout -b featureA' 等等,这个可以对子模块统一进行操作管理。
git 命令别名,举例:
$ 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'
这样运行更新命令的时候,就简化为 git supdate,其他同理。
git status 之类的获取的均是主仓库的状态,无法查看子模块的状态。建议 git config --global status.submoduleSummary true。git config --global diff.submodule log 可以查看到子模块的日志
缺点
例如在有子模块的项目中切换分支可能会造成麻烦。 如果你创建一个新分支,在其中添加一个子模块,之后切换到没有该子模块的分支上时,你仍然会有一个还未跟踪的子模块目录,这时候如果不小心提交了这个子模块(git commit -am "message"),就会有问题了。
对子模块的更新略显复杂,每次操作都需要所有人手动同步更新,增加了学习成本。
对子模块做了修改,需要先推送子模块再主模块,同时拉取的时候也需要先主模块,再子模块。
对子模块做本地修改需要先检出分支,否则有可能在 “游离的 HEAD” 上做修改。
删除子模块,需要的步骤有点复杂。
如果子模块被高频次更新,会有大量合并代码的工作,参考上面的 10-13
如果你的同事更新了 submodule,然后更新了父项目中依赖的版本号。你需要在 git pull 之后,调用 git submodule update 来更新 submodule 信息。这儿的坑在于,如果你 git pull 之后,忘记了调用 git submodule update,那么你极有可能再次把旧的submodule 依赖信息提交上去(使用 git submit -am "message" 或者 git add . 提交的人会遇到这种事)。
优点
不需要获取子模块整个代码库
主仓库只是获取到了子仓库的引用
参考
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 当我们在谈论multidex65535时,我们在谈论什么