背景
在前端日常开发中,我们经常Git来做代码版本管理工具,使用方式基本是one To one。即一个项目对应一个Git仓库的形式,那么当我们的代码中碰到了业务级别的需要复用的代码,怎么做呢?
方案一般有两种:
封装成NPM包进行依赖复用
使用Git子仓库对代码进行依赖复用
在基因宝Genebox小程序业务场景开发中,由于多个小程序中都会涉及支付、地址管理、订单等功能,部分页面是重叠的,开发过程中重叠部分如果开发两套代码会影响开发效率,考量之后决定使用Git子模块的方式进行开发,父级仓库依赖公共的子模块,子模块本身和父级仓库一同进行开发,避免了版本和重复开发的问题。
多个父级仓库都依赖同一个子仓库,但是子仓库自身不单独进行修改,而是跟随父级项目进行更新发布,其他依赖子仓库的项目只负责拉取更新。
什么是Git子仓库?
简单理解, 一个Git仓库下面放了多个其他的Git仓库,其他的Git仓库就是父级仓库的子仓库。
在刚开始使用Git子仓库的时候,很可能会导致使用子仓库出现云里雾里的现象,搞不清楚是父级仓库先提交,还是子仓库先提交,所以我们会先介绍子仓库的两种使用方式,然后携带一些子仓库的Git底层的分析让大家对子仓库有一个更加全面的认识。
Git两种子仓库使用方案
git submodule
git subtree
Git submodule (子模块)
Git子模块允许我们将一个或者多个Git仓库作为另一个Git仓库的子目录,它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
- 使用方式
以GitHub为例,我们分别在远端创建两个仓库: modulemain、modulesub,分别表示主仓库和依赖的子仓库。使用 git clone 将两个仓库克隆到本地。使用 git init --bare 在本地创建两个裸仓库。分别对这两个仓库进行一次提交,例如,在 modulemain 中增加 index.jsx 文件。在 modulesub 中增加 utils.js 文件。
# modulemain
echo "console.log('main');"> index.jsx
git add .
git commit -m "feat: 父仓库创建 index.jsx"
git push
# modulesub
echo "console.log('utils');"> util.js
git add .
git commit -m "feat: 子仓库创建 util.js"
git push
初始化结束两个子仓库后,我们想让 main主仓库能够使用 lib仓库的代码进行后续的开发,使用 git submodule add 命令后面加上要跟踪项目URL来添加新的子模块,例如 modulesub 仓库
git submodule add https://github.com/songxiaoliang/modulesub.git
默认情况下,子模块会被添加到项目的子模块同名的目录下,如果想放到其他目录,在 add命令的结尾跟上放置目录的相对路径即可。
执行完以上命令,会发现 modulesub仓库已经被放到 modulemain仓库下了,同时还要注意的是,Git为我们创建了一个 .gitmodules 文件,这个配置文件中保存了子仓库项目的URL和在主仓库目录下的映射关系:
[submodule "modulesub"]
path = modulesub
url = https://github.com/songxiaoliang/modulesub.git
执行 git status
git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD
new file: .gitmodules
new file: modulesub
我们对 main仓库进行一次提交:
git add .
git commit -m "feat: 增加子仓库依赖"
git push
操作结束后,我们的 main仓库就依赖了lib仓库的代码并且已经上传到了远程仓库,其他开发同学应该怎么去克隆包含子模块的项目呢?
克隆含有子项目的仓库
当我们正常克隆 main项目的时候,会发现main仓库中虽然包含 lib文件夹,但是里面并不包含任何内容,就是一个空文件夹:
git clone https://github.com/songxiaoliang/modulemain.git
别着急,此时你需要运行git submodule的另外两个命令(submodule的命令不多)。
首先执行 git submodule init用来初始化本地配置文件,也就是向 .git/config文件中写入子模块的信息。
git submodule update 则是从子仓库中抓取所有的数据找到父级仓库对应的那次子仓库的提交id并且检出到父项目的目录中。
git submodule init
Submodule 'modulesub' (https://github.com/songxiaoliang/modulesub.git) registered for path 'modulesub'
git submodule update
Cloning into '/Users/songlcy/Desktop/modulemain/modulesub'...
Submodule path 'modulesub': checked out '97f6b31bec231768b357496f9c1134303ce14bd6'
执行完以上命令,就成功拉取到了父级仓库和相关依赖子仓库的代码。上述命令玩起来感觉有些麻烦,Git 为我们提供了操作更为便捷的命令
git clone --recursive
Git 会自动帮我们递归去拉取我们所有的父仓库和子仓库的相关内容。
git clone --recursive https://github.com/songxiaoliang/modulesub.git
Cloning into 'modulesub'...
remote: Enumerating objects: 24, done.
remote: Counting objects: 100% (24/24), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 24 (delta 3), reused 24 (delta 3), pack-reused 0
Unpacking objects: 100% (24/24), done.
在主仓库上进行协同开发
我们在 main仓库下对 lib文件夹做了一些修改,然后我们想提交父仓库( main)和子仓库( lib)的修改,此时首先我们应该先提交子仓库的修改
当我们执行完上述命令后发现,modulesub目录竟然包含了完整的 git仓库,甚至包含了.git目录。此时我们进入到子仓库目录
但是我们也发现当前不在 modulesub 的 master分支上,而是在一个游离分支上面,这个游离分支的 hash 正式 modulesub仓库的 master分支的 hash 值,这正是 git submodule为我们所做的,Git不关心我们开发的分支,而只是去拉取子仓库对应的Commit提交。
所以我们需要先切换到正常分支,然后正常操作git仓库一样去进行子仓库的提交。
git checkout master
git add .
git commit -m "子仓库进行修改"
git push
子仓库提交结束后,我们回到 main仓库的主目录下,执行 git status:
git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add
(use "git checkout --
modified: modulesub (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
我们发现本次的 git status 日志输出和以往有些不一样,Git并没有告诉我们当前修改了哪些文件,而是说 modulesub下有一次新的提交,此时我们正常将主仓库进行提交并且 push 到云端仓库即可。
git submodule 本身就是一个大的Git仓库下包含了多个子的Git仓库,我们修改之后,首先对每个子仓库进行了提交,然后父级仓库就会记录下每个子仓库的提交,然后正常提交父级仓库即可,拉取也是同样的过程,如果是在子仓库的分支上开发,也是先拉取子仓库,随后拉取父级仓库的更新。
如果觉得对每个子仓库进行提交繁琐的话, git sumodule foreach 就可以解决你这个烦恼:
git submodule foreach git pull
对所有子仓库拉取了一次最新的代码,foreach后面使用的就是你要对子模块执行的Git命令。
思考一个问题,在上面操作中我们在修改了子仓库提交后,回到父仓库执行 git status 后为什么只是给出了 modified:mosulesub(newcommits)日志信息呢?
##### git submodule 原理分析
我们知道Git底层大致依赖了四种对象,构成了Git对于文件内容追踪的基础:
> blob: 二进制大文件,可以通俗理解为对文件的修改
> tree: 记录了blob对象和其他tree对象的修改,通俗理解为目录
> commit: 提交对象,记录了本次提交的tree对象和父类的commit对象以及我们的提交信息
> tag: 我们对当前提交记录版本的对象
我们此处需要使用一个 print_all_object.sh 的工具函数,它会将 Git 仓库下的这四种对象按照反向提交历史的排序展现出来:
print_all_object() {
for object in git rev-list --objects --all | cut -d ' ' -f 1
; do
echo 'SHA1: ' object
echo '-------------------------------'
done
}
print_all_object
我们在 main仓库下执行 print_all_object:
bash /your path/print_all_object.sh
SHA1: ef0d247b410b66a41b1a87920e743505b82217dd
tree d16e27143eecc87f7dcffe02fb808d903d5dd11e
parent 49e02652c469d201aea8162b811fc466e483a12f
author Songlcy [email protected] 1602212588 +0800
committer Songlcy [email protected] 1602212588 +0800
feat: 子仓库增加utils1.js
SHA1: 49e02652c469d201aea8162b811fc466e483a12f
tree 67dba4e6069163b0c0a89e406b35f7ea348bf1b1
parent cc7630b45e43a928aaf6d575fcb1fd9313dced63
author Songlcy [email protected] 1602210788 +0800
committer Songlcy [email protected] 1602210788 +0800
feat:增加子仓库modulesub依赖
SHA1: cc7630b45e43a928aaf6d575fcb1fd9313dced63
tree 689dac8ff3f1012efd1b971b149063bffcc24d5a
author Songlcy [email protected] 1602210368 +0800
committer Songlcy [email protected] 1602210368 +0800
feat:主仓库创建index.js
SHA1: d16e27143eecc87f7dcffe02fb808d903d5dd11e
100644 blob 0cb81245e94af0442f57e8802322de9c122a3c30 .gitmodules
100644 blob cb089cd89a7d7686d284d8761201649346b5aa1c HEAD
100644 blob e6da231579bcc39d57002491b84fedf465ccb754 config
100644 blob 498b267a8c7812490d6479839c5577eaaec79d62 description
040000 tree 8a987373f429345a4079bd3f3056a9e42b09c63c hooks
100644 blob c0b933d7b562e0ed088d24de83e1cba6ea31eb10 index.jsx
040000 tree c88b2a15165058d5921a3ffd7cdffe9fd5c73f48 info
160000 commit 97f6b31bec231768b357496f9c1134303ce14bd6 modulesub
SHA1: 0cb81245e94af0442f57e8802322de9c122a3c30
[submodule "modulesub"]
path = modulesub
url = https://github.com/songxiaoliang/modulesub.git
我们查看 feat:增加子仓库依赖此次 commit对象的 tree对象,发现内容如下:
SHA1: 67dba4e6069163b0c0a89e406b35f7ea348bf1b1
100644 blob 0cb81245e94af0442f57e8802322de9c122a3c30 .gitmodules
100644 blob cb089cd89a7d7686d284d8761201649346b5aa1c HEAD
100644 blob e6da231579bcc39d57002491b84fedf465ccb754 config
100644 blob 498b267a8c7812490d6479839c5577eaaec79d62 description
040000 tree 8a987373f429345a4079bd3f3056a9e42b09c63c hooks
100644 blob c0b933d7b562e0ed088d24de83e1cba6ea31eb10 index.jsx
040000 tree c88b2a15165058d5921a3ffd7cdffe9fd5c73f48 info
160000 commit c256812e92b531d55cb27a5194b44baa21ccab81 modulesub
index.jsx文件是 blob对象,对应的 file mode 是100644,但是对于 modulesub 子仓库的确是一个 commit对象, file mode为160000,这是Git中一种特殊的模式,表明我们是将一次提交的 commit记录在Git当中,而非将它记录成一个子目录或者文件。
而这正式 git submodule的核心原理,Git在处理 submodule引用的时候,并不会去扫描子仓库下的文件的变化,而是取子仓库当前的 HEAD指向的 commit的hash值,当我们对子仓库进行了更改后,Git获取到子模块的 commit值发生变化,从而记录了这个Git指针的变化。
在暂存区所以我们才发现了 new commits这种提示语,Git并不关心子模块的文件如何变化,我只需要在当前提交中记录子模块的commit的hash值即可,之后我们从父级仓库拉取子仓库的时候,Git拉取了本次提交记录中的子模块的hash值对应的提交,就还原了我们的整个仓库的代码。
注意
> 1. 当子模块有提交的时候,没有push到远程仓库,父级引用子模块的commit更新,并提交到远程仓库,当别人拉取代码的时候就会报出子模块的commit不存在 fatal:reference isn’t a tree。
> 2. 如果你仅仅引用了别人的子模块的游离分支,然后在主仓库修改了子仓库的代码,之后使用 git submodule update拉取了最新代码,那么你在子仓库游离分支做出的修改会被覆盖掉。
> 3. 我们假设你一开始在主仓库并没有采用子模块的开发方式,而是在另外的开发分支使用了子仓库,那么当你从开发分支切回到没有采用子模块的分支的时候,子模块的目录并不会被Git自动删除,而是需要你手动的删除了。