Git 子模块(gitsubmodules) - 将一个仓库嵌套到另一个仓库中

名称

gitsubmodules - 将一个仓库嵌套到另一个仓库中

概要

  • .gitmodules$GIT_DIR/config

  • git submodule

  • git <command> --recurse-submodules

描述

子模块是嵌套在另一个仓库中的仓库。子模块有自己的历史记录;嵌套它的仓库被称为父项目。

在文件系统上,子模块通常(但不总是 - 参见下文的形式)包括

  1. 位于其父项目的 $GIT_DIR/modules/ 目录下的 Git 目录
  2. 父项目的工作目录内的一个工作目录,以及指向 1 的子模块工作目录根目录下的 .git 文件。

假设子模块在 $GIT_DIR/modules/foo/ 具有 Git 目录,工作目录位于 path/to/bar/,则父项目通过树中的 path/to/bar 处的 gitlink 条目和其 .gitmodules 文件中的条目(参见 gitmodules(5))跟踪子模块,形式为 submodule.foo.path = path/to/bar

gitlink 条目包含父项目期望子模块工作目录位于的提交的对象名称。

.gitmodules 文件中,submodule.foo.* 部分向 Git 的用户界面层提供了附加提示。例如,submodule.foo.url 设置了获取子模块的位置。

子模块可用于至少两种不同的用例:

  1. 在保持独立历史的同时使用另一个项目。子模块允许你在自己的工作树中包含另一个项目的工作树,同时保持两个项目的历史记录分开。而且,由于子模块被固定到任意版本,另一个项目可以独立开发,而不影响父项目,允许父项目仅在需要时固定到新版本。

  2. 将(逻辑上的单一)项目拆分为多个仓库并将它们重新绑定在一起。这可以用于克服 Git 实现的当前限制,以实现更精细的访问控制:

    • Git 仓库的大小:在当前形式下,对于包含未通过树之间的增量计算压缩的大型仓库,Git 的扩展性较差。例如,你可以使用子模块来保存大型二进制资源,而这些仓库可以被浅克隆,以便本地没有大量的历史记录。

    • 传输大小:在当前形式下,Git 需要整个工作树存在。它不允许在提取或克隆中传输部分树。如果你的项目由父项目中的子模块以子模块的形式绑定在一起,你可以避免获取你不感兴趣的仓库的工作树。

    • 访问控制:通过限制用户对子模块的访问,可以实现不同用户的读/写策略。

子模块的配置

子模块操作可以使用以下机制进行配置(从高到低的优先级):

  1. 针对支持将子模块作为其路径规范的命令的命令行。大多数命令都有一个布尔标志 --recurse-submodules,它指定是否递归到子模块中。例如,grepcheckout

  2. 子模块内部的配置。包括子模块中的 $GIT_DIR/config,以及在树中的 .gitattributes.gitignore 文件,它们指定了子模块内部命令的行为。

    • 例如,子模块的 .gitignore 文件的效果将在父项目中运行 git status --ignore-submodules=none 时观察到。这通过在子模块中运行 status 并关注 .gitignore 文件来收集子模块的工作目录中的信息。

    • 当在父项目中运行 git push --recurse-submodules=check 时,将使用子模块的 $GIT_DIR/config 文件,这将检查子模块是否有未发布到任何远程的更改。远程通常在子模块中像在 $GIT_DIR/config 文件中一样配置。

  3. 父项目的配置文件 $GIT_DIR/config。Git 仅递归到活动的子模块(参见下面的“活动子模块”部分)。

    • 如果子模块尚未初始化,则子模块内部的配置不存在,因此在这里配置从哪里获取子模块是必要的。
  4. 父项目中的 .gitmodules 文件。项目通常使用此文件为所需的映射提供上游仓库的默认值,该映射对于子模块的名称和路径之间所需的映射很重要。

    • 该文件主要用作父项目中子模块的名称和路径之间的映射,以便可以定位子模块的 Git 目录。

    • 如果子模块尚未初始化,则只在此处找到子模块配置。它作为指定从何处获取子模块的最后的后备。

形式

子模块可以采用以下形式:

  1. 描述在描述部分的基本形式,具有 Git 目录、工作目录、gitlink 和 .gitmodules 条目

  2. “旧形式”子模块:带有嵌入的 .git 目录的工作目录,以及父项目中的跟踪 gitlink 和 .gitmodules 条目。这通常在使用旧版本的 Git 生成的仓库中找到。

    • 可以手动构造这些旧形式的仓库。

    • 当取消初始化或删除子模块(见下文),子模块的 Git 目录会自动移动到父项目的 $GIT_DIR/modules/<name>/

  3. 未初始化的子模块:gitlink、.gitmodules 条目,但没有子模块工作目录。子模块的 Git 目录可能仍然存在,因为取消初始化后,Git 目录会保留在那里。应该是工作目录的目录为空。

    • 可以通过运行 git submodule deinit 来取消初始化子模块。除了清空工作目录外,此命令仅修改父项目的 $GIT_DIR/config 文件,因此不会影响父项目的历史记录。可以使用 git submodule init 撤消此操作。
  4. 删除的子模块:可以通过运行 git rm <submodule path> && git commit 来删除子模块。可以使用 git revert 撤消此操作。

    • 删除将删除父项目的跟踪数据,这既是 gitlink 条目,也是 .gitmodules 文件中的条目。子模块的工作目录从文件系统中删除,但 Git 目录保留在那里,以使可以检出过去的提交而不需要从另一个仓库获取。

    • 要完全删除子模块,手动删除 $GIT_DIR/modules/<name>/

活动子模块

如果满足以下条件之一,子模块被视为活动:

  1. 如果 submodule.<name>.active 设置为 true

  2. 如果子模块的路径与 submodule.active 中的 pathspec 匹配

  3. 如果 submodule.<name>.url 设置。

这些条件按此顺序进行评估。

例如:

1
2
3
4
5
6
7
8
[submodule "foo"]
active = false
url = https://example.org/foo
[submodule "bar"]
active = true
url = https://example.org/bar
[submodule "baz"]
url = https://example.org/baz

在上面的配置中,仅子模块 barbaz 是活动的,bar 是因为 (1),而 baz 是因为 (3)。

请注意,(3) 是一个历史工件,如果 (1) 和 (2) 指定子模块不活动,那么将忽略 (3)。换句话说,如果 submodule.<name>.active 设置为 false 或者子模块的路径在 submodule.active 的 pathspec 中被排除,则 url 是否存在都无关紧要。在以下示例中进行了说明。

1
2
3
4
5
6
7
8
9
10
11
12
[submodule "foo"]
active = true
url = https://example.org/foo
[submodule "bar"]
url = https://example.org/bar
[submodule "baz"]
url = https://example.org/baz
[submodule "bob"]
ignore = true
[submodule]
active = b*
active = :(exclude) baz

在此配置中,除了 bazfoobarbob)以外的所有子模块都是活动的。foo 是因为它自己的活动标志,而其他所有子模块都是由于 submodule 活动 pathspec,该 pathspec 指定以 b 开头的任何子模块都是活动的,而不管是否存在 .url 字段。

用于第三方库的工作流程

1
2
3
4
5
6
7
8
9
10
11
12
# 添加一个子模块
git submodule add <url> <path>

# 偶尔将子模块更新到新版本:
git -C <path> checkout <new version>
git add <path>
git commit -m "将子模块更新到新版本"

# 查看父项目中的子模块列表
git submodule status

# 查看从父项目中移除子模块的 FORMS

用于人工拆分仓库的工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 为相关命令启用递归,使得
# 普通命令默认递归到子模块
git config --global submodule.recurse true

# 与下面的其他命令不同,克隆仍然需要
# 自己的递归标志:
git clone --recurse <URL> <directory>
cd <directory>

# 了解代码:
git grep foo
git ls-files

# 获取新代码
git fetch
git pull --rebase

# 更改工作树
git checkout
git reset

实现细节

当克隆或拉取包含子模块的仓库时,默认情况下子模块不会被检出;可以指示克隆递归到子模块中。git submodule 的 init 和 update 子命令将保持子模块在你的工作树中被检出,并且处于适当的修订状态。或者,可以设置 submodule.recurse 使 checkout 递归到子模块。

引用