Git教程 · 版本库之间的依赖
- 1️⃣ 与子模块之间的依赖
- 2️⃣ 与子树之间的依赖
- 🌾 总结
在 Git 中,版本库是发行单位,代表的是一个版本,而分支或标签则只能被创建在版本库这个整体中。如果一个项目中包含了若干个子项目,它们有各自的发布周期和属于自己的版本,那我们就必须要为每个子项目建立对应的版本库了。
对于主项目和子项目之间的关系,我们可以通过Git 中的 submodule
或 subtree
命令来实现。
请注意,subtree
命令是在1.7.11这一版本中首先被正式纳入Git 的。但该命令只是 contrib 目录下的一个可选组件。有些 Git 的安装包会自动包含的subtree
命令,而另一些则需要我们去手动安装。
子模块和子树这两个概念之间的主要区别在于:带子模块的主版本库只能发布模块版本库,而模块版本库的内容中带有子树的话,该模块版本库就被导入了主版本库中。
1️⃣ 与子模块之间的依赖
对于子模块来说,其模块版本库可以被嵌入到主版本库中去。为了实现这一点,模块版本库中的提交会以目录的形式被链接到主版本库中。
下面,我们通过下图来看看其基本结构。该图中有main 和 sub 两个版本库。在主版本库中,sub 目录将会与模块版本库相链接。这样,主版本库工作区的 sub 目录下就有了一个 完整的模块版本库。但事实上主版本库其实只是引用了模块版本库。为了实现这一目标,我们就得有一个名为 .gitmodules
的文件,以便用来定义各模块版本库所在的绝对路径。
[submodule "sub"]
path=sub
url=/project/sub
除了.gitmodules
文件之外,子模块的引用信息还会被被保存在 .git/config
文件中。该文件会在我们调用 submodule init
命令时完成存储,届时该命令会将从 .gitmodules 文件中读取的信息写入到 .git/config文件中。有了这样的间接配置,我们就可以在 git/config 文件对模块版本库的路径进行本地化调整了。
[core]
repositoryformatversion=0
filemode=true
bare=false
logallrefupdates=true
ignorecase=true
[submodule "sub"]
url=/project/sub
凭借上述信息,我们是不可能为主版本库中的每次提交都重现相应模块版本库的版本的。 也正因为如此,模块版本库中的提交才仍会被需要。这些都将会被存储在主版本库的对象树 中。下面我们来看看该对象树。其第三项 sub就是一个子模块,它可以被识别成 commit类型,随后的散列值引用的就是模块版本库中的提交 。
100644 blob le2bld1d51392717a479eaaaa79c82df1c35d442 .gitmodules
100644 tree 19102815663d23f8b75a47e7a01965dcdc96468c src
160000 commit 7fa7elclbd6c920ba71bd791f35969425d28b91b sub
在这里,我们要将一个现有 Git 项目以子模块的形式嵌入到一个不同的项目中。
- 链接目录
如果我们想要纳入某个子模块,就必须调用submodule add
命令,并指定该模块版本库的绝对路径与该模块所在的目录名:
> git submodule add /global-path-to/sub sub
这样一来,模块版本库就会被完整地克隆到指定目录中(并且它也会创建属于它自己 的.git
目录)。此外,主版本库中的.gitmodules
文件也将被同步创建或更新。- 在 config 文件在注册子模块
除此之外,新的子模块还需要被注册到.gitconfig
文件中。我们可以通过submodule init
命令来完成这件事。
> git submodule init
- 选择子模块的版本
该模块版本库的工作空间最初会被设置为默认分支的 HEAD。如果我们想要子模块中 的另一提交,就需要用checkout
命令来选择一下相应的版本。
> cd sub
> git checkout v1.0
- 将该 gitmodules 文件和子目录添加到提交中
当我们添加一个子模块时,主版本库中的.gitmodules 文件就会随之被创建或更新。然
后,我们就必须要将其添加到提交中去。此外,子模块所在的新目录自然也要添加。
> cd
> git add .gitmodules
> git add sub
- 做一次提交
最后,我们需要在主版本库中做一次提交。
> git commit -m "Submodule added"
如果我们克隆了一个带子模块的版本库,就必须调用一下 submodule init
命令。该命令 会将.git/config
文件中各子模块的 URL 传送过来。之后,我们就可以调用 submodule update
命令来克隆模块版本库所在的目录了。
克隆一个带子模块的项目
当我们克隆一个带子模块的版本库时,最初在工作区中创建的只有主版本库。其子模块必须要进行显式的初始化和更新。
- 初始化子模块
首先,我们必须要用submodule init
命令来完成子模块的注册。
> git submodule init
- 更新子模块
待该子模块在完成Git 的初始化配置之后,我们就可以通过submodule update
命令来下载完整的子模块了。
> git submodule update
我们可以用 submodule status
命令查看子模块中被引用提交的散列值。其中如果存在标签的话,也会以括号的形式显示在输出的结尾处。
> git submodule status
091559ec65c0ded42556714c3e6936c3bla90422 sub(v1.0)
在这里,Git 往往引用了模块版本库中的一次提交。而与此同时,该提交对象的散列值也是主版本库中每次提交的一个部分。模块版本库中随后的新提交并不会自动被记录在主版本库中。这种操作必须要显式执行,以便我们在主版本库中恢复某一项目版本时可以获取与之相匹配的、模块版本库中的项目版本。
使用子模块中的新版本
在发现子模块中有新版本可用了,我们要怎么做呢?
- 更新子模块
首先,我们需要将子模块的本地工作区调整到理想的状态。通常情况下,我们应该执行一次fetch
命令,以获取模块版本库中的最新提交。
> cd sub
> git fetch
接下来,我们要用checkout
命令指定自己所需要的提交。
> git checkout v2.0
- 使用新版本
最后,将该新提交预备到模块目录中,并提交它。
> cd ..
> git add sub
> git commit -m "New version of the submodule"
如果我们想在主版本库中使用模块版本库的某一新版本,就必须要对其进行显式修改。如果我们同时在主版本库与模块版本库中工作,就必须要将修改同时提交到两个版本库中。如果你还有一个中央版本库,那么这两个版本库都必须分别执行 push
命令,各自单独完成传送。
与子模块相关的工作
在工作区中,主版本库与模块版本库中的文件都已经被修改了。随后,主版本库应该要指向模块版本库中的新提交。
- 提交并推送模块版本库中的修改
首先,我们要对模块版本库中的修改完成一次提交,并在可能的情况下将其用push
命令传送给中央版本库。
> cd sub
> git add foo.txt
> git commit -m "Changed submodule"
> git push
- 提交并推送主版本库中的修改
接下来,我们要将主版本库中的修改,其中包括对模块版本库的引用提交,并在必要 的情况执行传输。
> cd ..
> git add bar.txt
> git add sub
> git commit -m "New version of submodule"
每次在对包含子模块的工作区执行更新之后后,我们应该随之调用 submodule update
命令来获得各子模块的正确版本。
如果这次是添加了一个全新的子模块,那么在执行submodule update
命令之前,我们还应该先调用一下 submodule init
命令。
另外作为开发者,如果我们在每次更新工作区内容(包括签出、合并、变基、重置、拉取等操作)之后都要执行一次初始化-更新命令序列,就说明事情做得不够好。
更新子模块
如果某子模块的新版本是由别的开发者所记录,那么我们就应该更新自己本地的克隆版本库和工作区。> git submodule init > git submodule update From /project/sub 091559e..4722848 master -> origin/master ★[new tag] v1.0 -> v1.0 ★[new tag] v2.0 -> v2.0 Submodule path 'sub': checked out '472284843ce4c0b0bb503bc4921ab7...le51'
当然,只有在当前工作区中没有相应的模块项的时候, submodule init
命令才会将 .gitmodules
文件中的信息传送给.git/config
文件。这样一来,我们就可以对模块版本库的路径进行本地化调整了。但如果这时有另一个开发者已经修改了.gitmodules
文件中的正式路径, 我们的修改就不会被接受。这就必须要通过 submodule sync
命令来完成此任务了。该命令会更新.git/config
文件中的路径并覆盖掉所有的本地修改。
2️⃣ 与子树之间的依赖
利用子树的概念,我们可以将一些模块版本库嵌入到某一个 Git 版本库中。为了实现这一点,我们必须要将该版本库中的某一目录与模块版本库中的某一提交、标签或分支关联起来。但与子模块不同的是,这回是一个被嵌入的模块版本库,其全部内容是被导入主版本库,而不在仅仅是引用了。这使得主目录中的工作相对更为自给自足了。
下面,我们通过下图来看一下子树处理的基本结构。在该图中,我们有 main 和 sub 两个版本库:我们(通过subtree add
命令) 将主目录中的sub 目录与模块目录链接了起来。
而在主版本库的 sub 目录下,我们看到了来自模块版本库中某一版本的文件。
从技术上来说, subtree add
命令会将模块版本库中所有的提交都导入到主版本库中(即 提交 S1 和 S2) 。然后,主版本库的当前分支就被链接到了模块版本库的特定提交上(即合并提交 G3) 。 在内部, Git 用到了它的子树合并策略(-strategy=subtree
)。这样一来就在特定的目录里出现了一次合并,将模块版本库中的内容载入到了sub 目录下。
嵌入一个子树
如果想要嵌入一个模块版本库,我们就要通过subtree add
命令将它添加到主版本库中(只需要调用一次subtree add
即可)。在这种情况下,你可以通过-prefix
选项来指定目录。
此外,目标模块库及其标签或分支的URL 也必须要指定。
> git subtree add --prefix=sub /global-path-to/sub v2.0
如果模块版本库的历史记录无需与主版本库相关,你也可以用--squash
选项限制其只 获取特定提交的内容。
> git 'subtree add --squash --prefix=sub /global-path-to/sub master
该命令会产生一个新的合并提交,并会以注释的形式添加它的散列值,这可以使得我们在下次更新时获取正确的模块提交。
与子模块不同的是,当某一带子树的版本库被克隆时,我们通常并不会观察到什么特殊情况。 一般情况下, clone
命令都会去捡取整个主版本库以及它所包含的所有模块版本库。
> git clone /path-to/main
使用子树中的新版本
以下操作的前提是被嵌入的子树中已经有别的版本正在使用。我们可以用subtree pull
命令来更新一个已被嵌入的子树。只要是可用于subtree add
的 参数都可用于subtree pull
命令。如果你在使用添加命令时使用了一个标签,必须用一个新的标签来代替。如果已经使用了一个分支,也可以指定是同一分支还是不同分支。如果该 分支上没有任何修改,subtree pull
命令就不会做任何事。
> git subtree pull --prefix=sub /global-path-to/sub v2.1
此外,通过在拉取操作中使用--squash
选项,我们可以跳过模块版本库的历史记录。 在这种情况下,没有中间提交会被涉及到,只有那个被指定的提交。当然,我们也可以用--squash
选项返回到模块版本库的某一个旧版本上,例如,从2.0版回到1.5版。
> git subtree pull --squash --prefix=sub /global-path-to/sub master
另外通过子树,我们才有可能直接在嵌入式模块的目录中做某些修改。在这里,如果我 们并没有什么特别需求的话。只需调用一般性的commit
命令就可以了。当然,我们也可以将主版本库中的相关修改或者某一提交中一个或多个模块目录版本化。
只有在重发各版本库中对模块所所做的修改时,我们才需要采取一些预防性措施。
扩散模块版本库中的修改
在这里,我们要将在模块目录中所做的修改传送相应的模块版本库中去。
- 分离模块目录中的修改
首先,我们要用subtree split
命令将模块目录中所发生的修改从其他修改中分离出来。 该命令会基于目前已知模块版本库的提交来生成一个新的提交,该新提交中将包含各提交中那些被修改的了模块文件。该命令执行完后,我们会得到一个指向这个新提交的本地分支(例如 sub/master)。如果你在调用subtree add
和subtree pull
命令时没有使用--squash
选项,在这里可以使用--rejoin
选项。这可以简化对sqlit
的反复调用。
> git subtree split --rejoin --prefix sub --branch sub/master
- 合并模块版本库中的修改
模块版本库中的本地修改必须要跟远端的修改进行合并。因此,我们先要激活新建的分支,并检索出目标分支中的最新版本。然后,我们就必须要合并这两个分支。
> git checkout sub/master
> git fetch /global-path-to/sub master
> git merge FETCH_HEAD
请注意,上面带URL的那个获取操作会创建一个临时引用 FETCH_HEAD, 该引用会指向其获取分支中的最新提交。如果你此刻正在某个远程分支上工作,理所当然可以使用其远程名称而不是URL。在这之后,目标分支将就可直接使用了,并不非得是 FETCH_HEAD。- 将修改传送到模块版本库中,并删除临时分支
临时分支中的本地修改必须要被推送到远程模块版本库中。在推送完成之后,我们可以切换回主版本库的分支,并删除该临时分支。
> git push /global-path-to/sub HEAD:master
> git checkout master
> git branch -d sub/master
从上述内容,我们可以清楚地看到,大部分子树操作都要比那些相应的子模块简单一些,两者只有在提取修改方面的复杂度是差不多的。
但在多数情况下,我们是不会用到提取操作的,因为我们是在主版本库上工作,而不是 模块目录中。
🌾 总结
- 嵌入子模块:我们可以通过
submodule add
和submodule init
命令来嵌入一个子模块。 - 克隆包含子模块的项目:我们可以在克隆该项目后,对其调用
submodule init
和submodule update
命令。 - 选择子模块中的某个新版本:首先,我们要(通过
checkout
命令来看) 选择在子模块目录中的新提交。然后,在主版本库中对其做一次提交。 - 同时处理模块版本库与主版本库:我们必须要先在模块版本库中执行提交,然后才能在主版本库中执行提交。另外,两个版本库的推送操作也必须要各自执行
push
命令。 - 嵌入子树:我们可以通过
subtree add
命令来嵌入子树。 - 选择子树中的某个新版本:我们可以通过
subtree pull
命令来将模块目录更新到所需的分支或标签上。 - 提取模块目录中的修改:我们可以通过
subtree split
命令创建一个单独的分支,用于 包含模块目录在的修改。然后再使用merge
命令将这些修改与其他修改合并,并用push
命令完成推送操作。
《【Git教程】(九)版本标签 —— 创建、查看标签,标签的散列值,将标签添加到日志输出中,判断标签是否包含特定的提交 ~》
《【Git教程】(十一)一些技巧 —— 引用日志、忽略临时性的本地修改、检查对文本文件的修改、Git 命令别名、为临时指向的提交创建分支、将提交移动到另一分支 ~》