Git子仓库深入浅出

前言

前端早读课的朋友,2020年元旦快乐。今日早读文章由涂鸦大前端 @孟浩然投稿分享。

正文从这开始~

在前端日常开发中,我们经常git来当做代码版本管理工具,使用中基本都是一个项目一个Git仓库的形式,那么当我们的代码中碰到了业务级别的需要复用的代码,我们一般怎么做呢?

我们大致的考虑一下,一般有两种方案:

  • 抽象成NPM包进行复用;

  • 使用Git的子仓库对代码进行复用;

在涂鸦的小程序业务场景开发中,两个程序中有部分页面是重叠的,开发过程中重叠部分如果开发两套代码会浪费不少的人力,考量之后决定使用Git子模块的方式进行开发,父级仓库依赖两个公共的子模块,子模块本身和父级仓库一同进行开发,避免了版本问题和重复开发的问题。

我们在下面介绍的子仓库的使用场景基本都是如下的开发方式:

多个父级仓库都依赖同一个子仓库,但是子仓库自身不单独进行修改,而是跟随父级项目进行更新发布,其他依赖子仓库的项目只负责拉取更新即可。

那么什么是Git的子仓库呢?

通俗上的理解, 一个Git仓库下面放了多个其他的Git仓库,其他的Git仓库就是我们父级仓库的子仓库。

在正式开始介绍git的子仓库之前,我们要提前认识到一点,在刚开始使用Git子仓库的时候,如果不是很了解底层原理,很可能会导致使用子仓库出现云里雾里的现象,搞不清楚是父级仓库先提交,还是子仓库先提交,所以在本教程中,我们会先介绍子仓库的两种使用方式,然后携带一些子仓库的Git底层的分析,让大家对子仓库有一个更加全面的认识。

Git两种子仓库使用方案
  1. git submodule

  2. git subtree

我们按照顺序分别演示这两种子仓库的使用方式,方便大家深入理解两种子仓库的使用方式:

git submodule(子模块)

Git子模块允许我们将一个或者多个Git仓库作为另一个Git仓库的子目录,它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。

我们演示一下 git submodule的使用方法:

为了方便后续对两种子仓库的原理进行讲解,我们会详细的描述git的相关操作步骤

开始使用子模块

使用 git init--bare在本地创建两个裸仓库,分别表示主仓库和依赖的子仓库,我们将主仓库命名为 main,依赖的子仓库命名为 lib, git subtree使用同样的初始化方法,下文不再赘述。

1.  `# 为了方便演示,我们使用/path/to/repos代表你当前开发的绝对路径`

2.  `# 比如笔者的/path/to/repos代表/Users/userName/Documents/work`

3.  `git --git-dir=/path/to/repos/main.git init --bare # 初始化主仓库`

4.  `git --git-dir=/path/to/repos/lib.git init --bare # 初始化子仓库`

6.  `# 本地拉取到这两个仓库`

7.  `git clone /path/to/repos/main.git`

8.  `git clone /path/to/repos/lib.git`

10.  `# 我们分别对这两个仓库进行一次提交`

11.  `cd main`

12.  `echo "console.log('main');"> index.js`

13.  `git add .`

14.  `git commit -m "feat: 父级仓库创建index.js"`

15.  `git push`

17.  `cd ../lib`

18.  `echo "console.log('utils');"> util.js`

19.  `git add .`

20.  `git commit -m "feat: 子仓库创建util.js"`

21.  `git push`

初始化结束两个子仓库后,我们想让 main主仓库能够使用 lib仓库的代码进行后续的开发,使用 git submodule add的命令后面加上想要跟踪项目URL来添加新的子模块(本文中的 lib仓库)。

1.  `# 首先进入到main的工作目录下`

2.  `cd main`

4.  `# 添加lib模块到main仓库下的lib同名目录`

5.  `git submodule add /path/to/repos/lib.git`

默认情况下,子模块会被添加到项目的子模块同名的目录下,如果想放到其他目录. 在 add命令的结尾跟上放置目录的相对路径即可。

执行完上述命令后,我们查看 main仓库下当前的目录结构:

1.  `tree`

2.  `.`

3.  `├── index.js`

4.  `├── .gitmodules`

5.  `└── lib`

6.  `└── util.js`

我们发现 lib仓库已经被放到 main仓库下的 lib目录下面了,同时还要注意的是,Git为我们创建了一个 .gitmodules文件,这个配置文件中保存了子仓库项目的URL和在主仓库目录下的映射关系:

1.  `cat .gitsubmodules`

3.  `[submodule "lib"]`

4.  `path = lib`

5.  `url = /path/to/repos/lib.git`

执行 git status发现有了新的文件

1.  `git status`

3.  `On branch master`

4.  `Your branch is up to date with 'origin/master'.`

6.  `Changes to be committed:`

7.  `(use "git reset HEAD ..." to unstage)`

9.  `new file:   .gitmodules`

10.  `new file:   lib`

我们对 main仓库进行一次提交:

1.  `git add .`

2.  `git commit -m "feat: 增加子仓库依赖"`

3.  `git push`

操作结束后,我们的 main仓库就依赖了 lib仓库的代码并且已经上传到了云端的仓库当中,那么别人应该怎么去克隆包含子模块的项目呢?

克隆含有子项目的仓库

当我们正常克隆 main项目的时候,我们会发现, main仓库中虽然包含 lib文件夹,但是里面并不包含任何内容,仿佛就是一个空文件夹:

1.  `git clone /path/to/repos/main.git`

2.  `Cloning into 'main1'...`

3.  `done.`

5.  `cd main`

6.  `tree # 使用tree命令查看当前目录,省略隐藏文件`

8.  `.`

9.  `├── index.js`

10.  `└── lib`

此时你需要运行 git submodule的另外两个命令,不需要担心, submodule的命令不会太多。

首先执行 git submodule init用来初始化本地配置文件,也就是向 .git/config文件中写入了子模块的信息。

git submodule update则是从子仓库中抓取所有的数据找到父级仓库对应的那次子仓库的提交id并且检出到父项目的目录中。

1.  `git submodule init`

3.  `Submodule'lib'(/path/to/repos/lib.git) registered for path 'lib'`

5.  `git submodule update`

7.  `done.`

8.  `Submodule path 'lib': checked out '40f8536319ede421cfd9ca9f9904b5106946e8ec'`

现在我们查看 main仓库下的目录结构,会发现和我们之前的提交的结构保持一致了,我们成功的拉取到了父级仓库和相关依赖子仓库的代码。

1.  `tree`

3.  `.`

4.  `├── index.js`

5.  `└── lib`

6.  `└── util.js`

上述命令着实有些麻烦,有没有简单一些的命令能够直接拉取整个仓库的代码的方式呢? 答案是有的,我们使用 git clone--recursive,Git会自动帮我们递归去拉取我们所有的父仓库和子仓库的相关内容。

1.  `git clone --recursive /path/to/repos/main.git`

3.  `Cloning into 'main'...`

4.  `done.`

5.  `Submodule'lib'(/path/to/repos/lib.git) registered for path 'lib'`

6.  `Cloning into '/path/to/repos/main/lib'...`

7.  `done.`

8.  `Submodule path 'lib': checked out '40f8536319ede421cfd9ca9f9904b5106946e8ec'`

在主仓库上进行协同开发

我们在 main仓库下对 lib文件夹做了一些修改,然后我们想提交父仓库( main)和子仓库( lib)的修改,此时首先我们应该先提交子仓库的修改。

1.  `$ cd lib`

当我们执行完上述命令后发现, lib目录竟然包含了一整个完整的git仓库,甚至包含了 .git目录。

但是我们也发现当前不在 libmaster分支上,而是在一个游离分支上面,这个游离分支的hash正式 lib仓库的 master分支的hash值,这正是 git submodule为我们所做的, Git不关心我们开发的分支,而只是去拉取子仓库对应的 commit提交

所以我们需要先切换到正常分支, 然后正常操作git仓库一样去进行子仓库的提交。

1.  `git add .`

2.  `git commit -m "子仓库进行修改"`

3.  `git push`

子仓库提交结束后,我们回到 main仓库的主目录下,执行 git status:

1.  `git status`

3.  `On branch master`

4.  `Your branch is up to date with 'origin/master'.`

6.  `Changes not staged for commit:`

7.  `(use "git add ..." to update what will be committed)`

8.  `(use "git checkout -- ..." to discard changes in working directory)`

10.  `modified:   lib (new commits)`

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

我们发现本次的Git的status和以往有些不一样的地方,Git并没有告诉我们当前到底修改了什么文件,而是说 lib下有一次新的提交,我们记住这个点,正常将主仓库进行提交并且push到云端仓库即可。

git submodule使用下来发现, submodule本身就是一个大的Git仓库下包含了多个子的Git仓库,我们修改之后,首先对每个子仓库进行了提交,然后父级仓库就会记录下每个子仓库的提交,然后正常提交父级仓库即可,拉取也是同样的过程,如果是在子仓库的分支上开发,也是先拉取子仓库,随后拉取父级仓库的更新,此处不再赘述。

如果觉得对每个子仓库进行提交繁琐的话, git sumoduleforeach就可以解决你这个烦恼:

1.  `# main目录下`

2.  `git submodule foreach git pull`

我们对所有的子仓库拉取了一次最新的代码, foreach后面使用的就是你要对子模块使用的git命令。

那么还有一个问题,我们在修改了子仓库提交后,回到父级仓库执行 git status后为什么git不像以前一样告诉我们具体的文件更新信息呢,而是给出了 modified:lib(newcommits)这样一串奇怪的信息,而这正式 git submodule的底层实现原理。

git submodule原理分析

我们知道Git底层大致依赖了四种对象,构成了Git对于文件内容追踪的基础:

  • blob: 二进制大文件,可以通俗理解为对文件的修改

  • tree: 记录了blob对象和其他tree对象的修改,通俗理解为目录

  • commit: 提交对象,记录了本次提交的tree对象和父类的commit对象以及我们的提交信息

  • tag: 我们对当前提交记录版本的对象

更加详细的内容请参考深入理解Git,阅读后更容易理解后续知识点哦~

我们此处需要依赖一个 print_all_object的工具函数,它会帮助我们将git仓库下的这四种对象按照反向提交历史的排序展现出来,可以将它放在环境变量下方便全局使用:

1.  `#!/bin/bash`

3.  `print_all_object() {`

4.  `for object in`git rev-list --objects --all | cut -d ' ' -f 1`; do`

5.  `echo 'SHA1: ' $object`

6.  `git cat-file -p $object`

7.  `echo '-------------------------------'`

8.  `done`

9.  `}`

11.  `print_all_object`

我们在 main仓库下执行 print_all_object:

1.  `# 此时处于我们刚对子模块提交的那个时间点`

2.  `# 对部分长的hash进行了截取处理,不影响阅读观感`

3.  `print_all_object`

5.  `SHA1:  a1cfd26e`

6.  `tree c77ba9c2`

7.  `parent ab118b8`

9.  `feat: 增加子仓库依赖`

10.  `-------------------------------`

11.  `SHA1:  ab118b8`

12.  `tree f5771cd`

14.  `feat: 父级仓库创建index.js`

15.  `-------------------------------`

16.  `SHA1:  c77ba9c2`

17.  `100644 blob d8c9fb4    .gitmodules`

18.  `100644 blob ddd81ae    index.js`

19.  `160000 commit 40f8536 lib`

20.  `-------------------------------`

21.  `SHA1:  d8c9fb4`

22.  `[submodule "lib"]`

23.  `path = lib`

24.  `url = /path/to/repos/lib.git`

25.  `-------------------------------`

26.  `SHA1:  ddd81ae`

27.  `console.log('main');-------------------------------`

28.  `SHA1:  f5771cd`

29.  `100644 blob ddd81ae    index.js`

30.  `-------------------------------`

我们查看 feat:增加子仓库依赖此次 commit对象的 tree对象,发现内容如下:

1.  `SHA1:  c77ba9c`

2.  `100644 blob d8c9fb456  .gitmodules`

3.  `100644 blob ddd81aef    index.js`

4.  `160000 commit 40f85363 lib`

index.js文件是 blob对象,对应的file mode是100644,但是对于 lib子仓库的确是一个 commit对象, file mode为160000,这是Git中一种特殊的模式,表明我们是将一次提交的 commit记录在Git当中,而非将它记录成一个子目录或者文件。

而这正式 git submodule的核心原理,Git在处理 submodule引用的时候,并不会去扫描子仓库下的文件的变化,而是取子仓库当前的 HEAD指向的 commit的hash值,当我们对子仓库进行了更改后,Git获取到子模块的 commit值发生变化,从而记录了这个Git指针的变化。

在暂存区所以我们才发现了 newcommits这种提示语,Git并不关心子模块的文件如何变化,我只需要在当前提交中记录子模块的commit的hash值即可,之后我们从父级仓库拉取子仓库的时候,Git拉取了本次提交记录中的子模块的hash值对应的提交,就还原了我们的整个仓库的代码。

git submodule注意点

虽然使用 git submodule为我们的开发带来了很多便利,但是随之而来也会导致一些比较容易犯的错误,整理出来,防止大家采坑:

当子模块有提交的时候,没有push到远程仓库, 父级引用子模块的commit更新,并提交到远程仓库, 当别人拉取代码的时候就会报出子模块的commit不存在 fatal:reference isn’t a tree

如果你仅仅引用了别人的子模块的游离分支,然后在主仓库修改了子仓库的代码,之后使用 git submodule update拉取了最新代码,那么你在子仓库游离分支做出的修改会被覆盖掉。

我们假设你一开始在主仓库并没有采用子模块的开发方式,而是在另外的开发分支使用了子仓库,那么当你从开发分支切回到没有采用子模块的分支的时候,子模块的目录并不会被Git自动删除,而是需要你手动的删除了😭。

git subtree(子树合并)

上面介绍的 git submodule是Git自带的原生功能,我们接下来将要介绍的 git subtree则是由第三方开发者贡献的 contrib script,Git本身并不提供 git subtree命令,contrib中包含一些实验性的第三方工具,由各自的作者进行维护。

同时这也让我们认识到 git subtree不是Git原生支持的命令,而是第三方开发者通过Git的底层命令写出的一个高层次脚本,所以它是可以由基础的Git命令来实现的,我们稍后会介绍 git subtree的实现原理,在此之前,还是先来介绍一下 git subtree的基础使用吧!

开始使用子树合并

我们按照子模块时讲的,首先在本地创建 mainlib的子仓库,然后对两个仓库各自进行一次提交,代码此处不再重复展示。

初始化结束两个子仓库后,我们想让 main主仓库能够使用 lib仓库的代码进行后续的开发,使用 git subtree add的命令后面加上想要跟踪项目URL来添加新的子模块(本文中的 lib仓库),这段和之前的步骤基本一致,但是命令使用方式不同。

1.  `cd main`

3.  `git subtree add --prefix=lib /path/to/repos/lib.git master`

5.  `git fetch /path/to/repos/lib.git master`

7.  `warning: no common commits`

8.  `remote: Enumerating objects: 3, done.`

9.  `remote: Counting objects: 100% (3/3), done.`

10.  `remote: Total3(delta 0), reused 0(delta 0)`

11.  `Unpacking objects: 100% (3/3), done.`

12.  `From/path/to/repos/lib`

13.  `* branch            master     -> FETCH_HEAD`

14.  `Added dir 'lib'`

我们首先分析一下加入子仓库的命令的参数部分:

  • --prefix=lib: 指定我们要把子仓库放到哪个文件夹下面

  • /path/to/repos/lib.git: 指定我们子仓库的地址

  • master: 指定我们子仓库需要拉取回代码的分支

执行完上述命令后,我们查看 main仓库下当前的目录结构:

1.  `tree`

3.  `.`

4.  `├── index.js`

5.  `└── lib`

6.  `└── util.js`

我们发现 lib仓库已经被放到 main仓库下的lib目录下面了,除了一个 lib文件夹外, git subtree并没有额外的标记信息去记录我们子仓库的地址,这也正是 git subtree命令繁琐的地方, add命令和其他的命令每次都要指定我们子仓库的远程地址,如果你觉得复杂,可以使用 git remote add lib/path/to/repos/lib.git去给远程服务器取一个别名,以后就可以使用 git subtree add--prefix=lib lib master进行其他类似命令的操作了。

我们进入到 lib文件夹下面,查看文件夹下面的文件,我们发现 git subtree并没有包含 .git文件夹,这也是和 git submodule不一致的地方, lib在主仓库下就像是一堆子仓库的文件,我们并不能从 lib下进行子仓库的提交,而是要使用 git subtree其他的命令进行提交和拉取操作, subtree表现的就像是所有的代码都在我们的主仓库下,并不存在什么其他的子仓库一样。

克隆含有子项目的仓库

对包含 subtree的主仓库进行拉取非常简单,就像我们拉取普通仓库一样支持克隆即可。

1.  `git clone /path/to/repos/main.git`

并不掺杂什么额外的操作,我们进入到仓库下发现,主仓库和子仓库的代码都已经包含在当前目录下面了。

在主仓库上进行协同开发

当我们在主仓库对 lib进行修改后,我们执行 git status查看一下:

1.  `git status`

3.  `On branch master`

4.  `Your branch is up to date with 'origin/master'.`

6.  `Changes not staged for commit:`

7.  `(use "git add ..." to update what will be committed)`

8.  `(use "git checkout -- ..." to discard changes in working directory)`

10.  `modified:   lib/util.js`

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

我们发现了 subtreesubmodule不一样的另外一点,子仓库的更改是会反映在主仓库的更改上面的,我们刚才也提到 lib就是代码目录,没办法像 submodule一样直接在子仓库中提交,所以 subtree提供了另外两个命令来对代码进行拉取和推送的操作

  • git subtree pull

  • git subtree push

如果当前情况下我们只在主仓库下对子仓库的代码进行了修改,无论如何我们都需要对主仓库进行一次提交(针对本文开始讲的大前提的条件下),这就是 subtree的提交模式,从我们的主仓库的提交历史下拆分部分 commit出去给子仓库进行提交,现在不理解也没有关系,我们等会在讲述 subtree原理的时候会提到这个地方。

我们首先提交主仓库:

1.  `# 省略部分输出信息`

2.  `git add .`

3.  `git commit -m "修改了lib的代码"`

然后我们尝试去推送子包代码的更新:

1.  `# 首先要拉取一下子仓库是否存在更新`

2.  `# 如果拉取子仓库的过程中存在冲突,我们需要在主仓库解决冲突后重新提交一次commit`

3.  `git subtree pull --prefix /path/to/repos/lib.git master`

5.  `From/path/to/repos/lib`

6.  `* branch            master     -> FETCH_HEAD`

7.  `Subtree is already at commit a5f21e31a721920ba7007949f3db59df4b543436.`

9.  `# push代码到子仓库,我们不难发现 subtree的命令格式基本都是一致的`

10.  `git subtree push --prefix /path/to/repos/lib.git master`

12.  `git push using:  /path/to/repos/lib.git master`

13.  `Enumerating objects: 8, done.`

14.  `Counting objects: 100% (8/8), done.`

15.  `Delta compression using up to 4 threads`

16.  `Compressing objects: 100% (2/2), done.`

17.  `Writing objects: 100% (4/4), 511 bytes | 511.00KiB/s, done.`

18.  `Total4(delta 0), reused 0(delta 0)`

19.  `To/path/to/repos/lib.git`

20.  `a5f21e3..67387da67387da30c87c97bb4e0020be18e8da7a720dbab-> master`

这样子我们就在主仓库完成了对子仓库的拉取和推送, subtree使用起来我们发现并没有什么奇怪的地方,但是它确实帮我们管理了子仓库的代码,那么 subtree是怎么做到的呢?

git subtree原理分析

我们主要对 subtree新增子仓库和合并子仓库更新的原理进行讲解, push的操作我们稍后会简单的提一下。

首先 subtree是如何将子仓库的代码加入到我们的子仓库的呢? git subtree我们中文一般翻译成"子树合并",那么正如我们理解的, subtree正是利用了Git的的底层的 tree对象和一些相关对象完成了增加子仓库的操作,我们简短的总结为下面这么一句话:

在主仓库中我们通过Git拉取到子仓库下的分支代码到主仓库的另外一根分支中, 通过类似 merge的操作,将代码合并到我们主仓库的某个目录下,此时会生成的一次提交对象, 这个提交对象parent引用了我们主仓库的当前commit对象和子仓库的当前commit对象, 就完成了子仓库的新增,主仓库也顺便记录了子仓库的所有文件。

这句话有点绕? 没关系,我们用Git的代码来演示一下这个过程,当然首先我们还是要初始化 mainlib两个仓库,并且对各自进行一次初始化的提交。

git subtree add原理解析

然后我们对 git subtree add的过程进行拆解,使用了部分Git的底层命令,我们首先对它们进行一个简单介绍:

git read-tree: 将某次commit的提交读取到当前的暂存区,工作区不变;

git write-tree: 将暂存区保存的文件生成一个顶级的tree对象,返回tree-hash;

git commit-tree: 生成一个commit对象,可以指定引用的 tree对象, parent对象和提交信息;

git cat-file: 查看git底层四种对象信息;

git可以设置不同的remote的远程服务器地址,我们之前使用 git fetch都是获取同一个项目的文件,但是 git fetch也可以获取其他项目(比如子仓库)的内容,放到另外一根分支上,然后通过合并的策略来完成子仓库的代码拉取。

main仓库进行初始化子仓库的操作

为了方便之后对远程仓库做跟踪,我们在remote上面对仓库做一个别名:

1.  `cd main`

2.  `git remote add lib /path/to/repos/lib.git # git添加服务器别名`

首先, 我们需要将子仓库的代码拉取到主仓库里面来:

1.  `git fetch lib`

3.  `warning: no common commits`

4.  `remote: Enumerating objects: 3, done.`

5.  `remote: Counting objects: 100% (3/3), done.`

6.  `remote: Total3(delta 0), reused 0(delta 0)`

7.  `Unpacking objects: 100% (3/3), done.`

8.  `From/path/to/repos/lib`

9.  `* [new branch]      master     -> lib/master`

查看一下当前的所有分支:

1.  `git branch -a`

3.  `* master`

4.  `remotes/lib/master`

5.  `remotes/origin/master`

将远程的 lib分支代码首先合并到main仓库的 lib分支上面来方便后续操作。

1.  `# 创建lib的本地分支`

2.  `git checkout -b lib lib/master`

4.  `Branch'lib'set up to track remote branch 'master' from 'lib'.`

5.  `Switched to a new branch 'lib'`

7.  `# 然后切换回master分支`

8.  `git checkout master`

当前子模块( lib)的代码已经到了我们本地的一根分支上面了,我们和 submodule的使用方式一样,也要将 lib文件夹放到 main文件夹下面使用,所以需要将子仓库的代码合并到主仓库的一根分支上面来:

查看当前 lib分支的最新提交的 commit-id,然后获取到相对应的 tree-id:

1.  `# 查看lib对应的tree-id`

2.  `git cat-file -p lib`

4.  `tree 309cc0d`

5.  `author userName  1577349863+0800`

6.  `committer userName  1577349863+0800`

8.  `feat: 创建lib文件`

10.  `# 查看当前tree对象下面的文件:`

11.  `# 当前tree对象下面包含我们在lib创建的index.js文件`

12.  `git cat-file -p 309cc0d`

14.  `100644 blob 043a685d4c9b48a74bdde1181ab22b53f8a8e363 index.js`

我们回到 main仓库下,调用 git read-treelib分支下的代码读取到暂存区和工作目录下:

1.  `# -u 指定在更新暂存区文件成功后,将文件一起写入到工作目录下`

2.  `# --prefix 指定需要将文件放到哪个子目录下`

3.  `git read-tree -u --prefix=lib lib`

执行 git status查看当前仓库状态:

1.  `git status`

3.  `On branch master`

4.  `Your branch is up to date with 'origin/master'.`

6.  `Changes to be committed:`

7.  `(use "git reset HEAD ..." to unstage)`

9.  `new file:   lib/index.js`

我们发现 lib仓库的代码以子目录的形式存在于 main上面了,现在还不能够提交,因为现在没有体现出两个分支的合并关系,之后主仓库下修改了子仓库代码就会导致不方便拆分子仓库代码,如果现在直接提交,仅仅只是我们将 lib的代码类似拷贝的操作放到了主仓库下,所以我们还需要调用 git write-treegit commit-tree生成一次新的commit对象建立起来分支间的联系:

1.  `# 因为read-tree已经将子仓库的文件添加到了暂存区`

2.  `# 我们可以调用write-tree对当前的暂存区文件生成一个tree对象`

3.  `git write-tree`

4.  `e0551170da5e913efe6a97c1137f4da890688243`

之后我们手动创建一次合并提交,也就是 commit对象,这个提交要有两个父对象,分别是 main的提交对象和 lib的提交对象,用 git rev-parse首先获取到两次提交的ID:

1.  `git rev-parse master`

2.  `57d55a545a24a10df7b4be4e3eb79dc3c3c195e6`

4.  `git rev-parse lib`

5.  `b8bcfe5c2ec04c7c028bbab8dfd3dbea53dcb745`

执行 commit-tree命令手动创建提交,提交需要的 tree对象来自于我们 write-tree生成的 tree对象,父级的提交用上面两个ID, 我们需要输出一些提交消息到 commit对象中来显示当前这次提交做了些什么事情, commit-tree支持从输入流中获取提交消息:

1.  `echo "初次合并子模块"| git commit-tree e0551170da5e913efe6a97c1137f4da890688243 \`

2.  `-p 57d55a545a24a10df7b4be4e3eb79dc3c3c195e6 \`

3.  `-p b8bcfe5c2ec04c7c028bbab8dfd3dbea53dcb745`

5.  `# 此处显示的是commit对象的ID;`

6.  `e54dc295322387a460130f33ed571f724c700fe8`

然后此时 master分支并没有指向到我们新生成的这次 commit对象上面,我们使用 git reset将分支强指到本次提交对象上面来:

1.  `git reset e54dc295322387a460130f33ed571f724c700fe8`

查看一下当前的提交历史,可以看到我们将父仓库和子仓库的提交合并到了一起:

1.  `git log --graph --pretty=oneline`

3.  `*   e54dc295322387a460130f33ed571f724c700fe8 (HEAD -> master) 初次合并子模块`

4.  `|\`

5.  `| * b8bcfe5c2ec04c7c028bbab8dfd3dbea53dcb745 (lib/master, lib) feat: 创建lib文件`

6.  `* 57d55a545a24a10df7b4be4e3eb79dc3c3c195e6(origin/master) feat: main创建`

这样我们就完成了对子树的初次合并的操作,整个过程还是比较繁琐的,需要对Git的 Plumbing(底层命令)有较为清楚的认知,不过我们可以使用 git subtree add就可以完成上述的操作;

git subtree pull原理解析

如果 lib的上游代码提交了新的代码,我们就需要将新代码合并到 main仓库下面来,之前讲解了 git subtree pull的用法,我们现在简单分析下 pull这个行为的原理:

首先我们在 main仓库下切换到 lib分支,将最新代码通过 git pull拉取回来,case比较简单, 此处不再进行赘述,切换回到 master分支,我们利用 git merge子树合并策略将代码合并到 lib文件夹下,如果直接merge的话,代码就不会合并到子文件夹下面。

1.  `# -X指定merge的合并策略为subtree`

2.  `git merge -Xsubtree=lib lib`

这样子我们就将子仓库的上游更新合并到了主仓库下,此处涉及到的合并策略我们不再进行详细分析,冲突解决都是提升到主仓库级别进行处理的和 submodule的形式不太一致,有兴趣的读者可以对 git subtree的源码进行扩展阅读。

git subtree push原理解析

push的操作在 subtree的底层涉及到了 split的方式对代码进行拆分,我们讲解一下 git subtree split的使用。

1.  `# 在main仓库下的master分支进行操作`

2.  `git subtree split --prefix=lib -b lib-split`

4.  `Created branch 'lib-split'`

5.  `8e8086648ff4956062a7239beebef7494d47c137`

split操作指定 main下面需要分割的目录, 按照上次的子仓库的提交ID进行拆分到指定的分支上面去, 我们的 git subtree push底层也依赖了这个操作。

我们假设 main仓库下我们新增了三次提交,其中一次提交包含了对 lib文件夹的修改, split会找到上次包含 lib的commit的ID然后进行查找当前这三次提交有没有针对子仓库的修改。

此时 split会找出三次修改中哪次 commit对象修改了子仓库,然后将它的 tree对象找出,然后将此次 commit对象的commit-message合并创建一次新的针对 libcommit对象, 重新设置本次提交的父类对象。

之后这些修改拆分后拷贝到另外一根分支上,这根分支仅包含 lib子仓库的修改,然后使用 git push指定提交的远程仓库提交到 lib下面。

subtree提交的原理我们只进行了简单的分析,有兴趣的读者可以对 git subtree的源码进行扩展阅读。

总结

我们对 submodulesubtree的用法以及原理都进行了比较详细的阐述,因为涉及内容比较多,而且原理部分大多是Git的底层机制,阅读起来可能有些难度,我们大多用了Git的演示来对这些内容进行了分析,但因为笔者能力一般水平有限,可能存在部分不正确的地方,大家可以对内容进行纠正 。

服务推荐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值