I. 何为 Git 子模块?
以前端项目为例,通常我们用 npm dependencies 来集成第三方库,或者将自己维护的多个项目中通用的组件抽取出来。
"devDependencies": { "babel-eslint": "^8.2.3", "base64-img": "^1.0.3", "body-parser": "^1.17.2", "colors": "^1.3.0", "eslint": "^4.19.1", "eslint-plugin-babel": "^5.1.0", "express": "^4.15.3", "fs-extra": "^3.0.1", "fs-watch-tree": "^0.2.5", "klaw-sync": "^2.1.0", "less": "^2.7.2", "lodash": "^4.17.4", "node-file-eval": "^1.0.0", "nodemon": "^1.11.0", "postcss": "^6.0.5", "precommit-hook": "^3.0.0", "ws": "^5.1.1" }
这种方式简单方便、支持广泛,适用于大部分情况;但是对于其中某些库来说,也存在一些痛点:
那么,基于以上几点,如果不得不将第三方源码手动拷贝到项目中,又会带来更多的问题:
一个虽然不一定是最好的,但可行的办法是:
submodule
子模块(submodule)允许你将一个 Git 仓库作为另一个 Git 仓库的子目录; 它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立
简单的说,子模块的解决方案更像是上面两种的融合,类似于一种特区模式:代码既存在于主项目的子文件夹中,可以灵活的为我所用;在 Git 层面又是独立提交管理的,和主项目的 commit 时间线保持了完全的独立性。
如果第三方库发生了变化,那么项目中的子模块可以自由自主的选择 合并、变基、切换分支 等各种操作。比如一个通用组件作为子模块分别被公司中不同项目引用,则各个项目组做出的改进,最后都可以汇总到主组件库中,为大家所共享。
在当前项目中,添加已有的第三方库:
git submodule add 3RD_LIB_GIT_PATH
默认情况下,子模块会自动放入一个与其仓库同名的子目录中;在末尾也可以加一个自定义的路径参数。
同时项目中会出现一个新的 .gitmodules
配置文件,保存了一些映射关系:
[submodule "3RD_LIB_NAME"] path = 3RD_LIB_NAME url = 3RD_LIB_GIT_PATH ......
子模块所在的子目录是被 Git 特殊对待的 – 也就是说,当你不在此目录中时,Git 默认并不跟踪其中的内容,而是将其变动当成一种特殊的提交对待。
克隆含有子模块的项目时,对应的子目录其实默认是空的,需要额外的步骤。
默认做法是:
# 克隆主项目 git clone MAIN_PROJECT_GIT # 初始化本地配置文件 git submodule init # 抓取所有数据并检出父项目中列出的合适的提交 git submodule update
更简单一些的做法是在 clone 时加上参数:
git clone --recursive MAIN_PROJECT_GIT
在项目中使用子模块的最简单模式,就是只对其更新并享用最新版本,但并不修改之。
更新子模块的命令为:
git submodule update --remote
Git 默认会尝试更新所有子模块;如果子模块数量众多,也可以在以上命令中传入需要更新的子模块名称。
默认情况下,子模块并没有本地分支,而是会停留在一种特殊的 “detached HEAD” 模式下;要对其修改并被 Git 跟踪的话,就要先手动检出分支:
# 检出一个叫 stable 的分支 git checkout stable
然后从上游拉取新的内容,此时有两种选择:
# 选择A:合并 git submodule update --remote --merge # 选择B:变基 git submodule update --remote --rebase
因为主项目并不会跟踪子模块中的变更,也就是说子目录中更改的具体业务文件不会在 push 时被自动发布;所以需要要求 Git 在推送主项目之前检查所有子模块是否已正确提交:
git push --recurse-submodule=check
根据上述检查结果,可以进入每个子模块并手动提交。
还有更简单的做法是自动完成这项操作:
git push --recurse-submodule=on-demand
此时会先推送子模块再推送主项目,如果前者失败整个流程将停止。
会遇到和其他人先后改动了同一个子模块的情况,也就是一个提交是另一个的直接祖先,那么 Git 会简单地选择之后的提交来合并,这样没什么问题。
不过,当两边同时修改,也就是子模块提交已经分叉的情况下,如果尝试合并,Git 会报 “merge following commits not found” 错误。
解决的方法有些麻烦,罗列如下:
# 得到试图合并的两个分支中记录的提交的 SHA-1 值 $ git diff diff --cc 3RD_LIB_GIT_PATH index eb41d76,c771610..0000000 --- a/3RD_LIB_GIT_PATH +++ b/3RD_LIB_GIT_PATH # 进入子模块目录 $ cd 3RD_LIB_GIT_PATH # 基于 git diff 的第二个 SHA 创建一个分支 $ git branch my-try-merge-branch c771610 (3RD_LIB_GIT_PATH) $ git merge my-try-merge-branch 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 # 返回到主项目目录中 $ cd .. # 再次检查 SHA-1 值 $ git diff # 添加解决后的子模块记录 $ git add 3RD_LIB_GIT_PATH # 提交合并 $ git commit -m "Merge Tom's Changes"
git rm –cached <子模块名称>
git status
,顶多只能知道子模块有变化,但具体是什么还要到子目录中再去运行一次git subtree
命令,从 git v1.8 后可用,官方推荐使用 subtree 代替 submodule,其并不需要保存 .submodule 这样的元信息。
subtree 用法如下:
# 其中-f意思是在添加远程仓库之后,立即执行fetch git remote add -f <子仓库名> <子仓库地址> # --squash意思是把subtree的改动合并成一次commit,这样就不用拉取子项目完整的历史记录。--prefix之后的=等号也可以用空格 git subtree add --prefix=<子目录名> <子仓库名> <分支> --squash
git fetch <远程仓库名> <分支> git subtree pull --prefix=<子目录名> <远程分支> <分支> --squash
# 需要确认有写权限 git subtree push --prefix=<子目录名> <远程分支名> 分支
本文分享自微信公众号 - 云前端(fewelife),作者:lua
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2018-07-09
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句