• 什么是 Git 子模块
  • Git 子模块怎么用

1. Git submodule 命令用法

git submodule 命令用于初始化,更新或检查子模块。

1
2
3
4
5
6
7
8
usage: git submodule [--quiet] add [-b <branch>] [-f|--force] [--name <name>] [--reference <repository>] [--] <repository> [<path>]
   or: git submodule [--quiet] status [--cached] [--recursive] [--] [<path>...]
   or: git submodule [--quiet] init [--] [<path>...]
   or: git submodule [--quiet] deinit [-f|--force] [--] <path>...
   or: git submodule [--quiet] update [--init] [--remote] [-N|--no-fetch] [-f|--force] [--checkout|--merge|--rebase] [--reference <repository>] [--recursive] [--] [<path>...]
   or: git submodule [--quiet] summary [--cached|--files] [--summary-limit <n>] [commit] [--] [<path>...]
   or: git submodule [--quiet] foreach [--recursive] <command>
   or: git submodule [--quiet] sync [--recursive] [--] [<path>...]

2. 子模块

git submodule,子模块:解决了多个独立项目之间有引用(包含)关系,同时又期望能够独立维护这些项目的需求。

2.1. Git仓库中包含其他的仓库

git submodule 允许你将一个 git 仓库当作另外一个 git 仓库的子目录,即允许你克隆另外一个或多个仓库到你的项目中并保持你提交的 commits 相对独立

一个 子模块 其实就是一个标准的 Git 仓库。不同的是,他被包含在另一个主项目的仓库中。一般情况下,它包含一些库文件和其他资源文件,你可以简单地把这些库文件作为一个子模块添加到你的主项目中。

一个子模块也是一个功能齐全的 Git 仓库,就内部而言它和别的仓库没有什么区别,你可以对他进行修改,提交,抓取,推送等操作

2.2. 添加子模块

主项目仓库:https://github.com/kuroko/x-change.git
子模块仓库:https://github.com/kuroko/serivce.git

1
2
3
4
5
6
7
8
# 克隆主项目
$ mkdir -p ~/workspace/code && cd ~/workspace/code
$ git clone https://github.com/kuroko/x-change.git
$ cd x-change

# 使用 'git submodule add' 命令添加子模块
$ git submodule add https://github.com/kuroko/service.git lib/service
# Note: lib/service:指定本地的路径

执行完 git submodule add 命令后,将会发生:

  • 对指定的仓库进行简单的克隆 (lib/service)

    Note: 仓库的内容并不会保存在其父项目中。其实,只有它的远程 URL 会被记录在父仓库,以及它在主项目中的本地路径和签出版本

  • 创建一个新的 .gitmodules 文件,用来跟踪子模块并保存其配置信息

    1
    2
    3
    
    [submodule "lib/service"]
        path = lib/service
        url  = https://github.com/kuroko/serivce.git

    现在让我们来看看当前的项目状态:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    # 使用 git status 查看添加子模块后的一些改变
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    
    Changes to be committed:
    (use "git reset HEAD <file>..." to unstage)
    
    	new file:   .gitmodules
    	new file:   lib/service

像任何其他修改一样,Git 添加了一个子模块,并且要求你提交这个改动到仓库中:

1
2
$ git add .
$ git commit -m "Add `lib/service` as Submodule"

添加子模块,OK !!!

2.3. 克隆一个项目和其子模块

repo-and-submodule

一个项目仓库,并 _不_ 包含子模块的任何代码文件,主项目仓库仅仅保存子模块的_配置信息_来作为版本管理的一部分。

这就表示,当你要克隆一个带有子模块的项目时,在默认的情况下 “git clone” 命令仅仅接收这个项目本身。我们的 “lib” 只是一个空目录,里面没有任何文件。

这里有两个选择去设置这个 lib 目录:

  • git clone 添加参数 --recurse-submodules, 从而让 git 知道,当克隆完成的时候需要去初始化所有的子模块

  • 如果你仅仅只是简单地使用了 “git clone” 命令,并没有附带任何参数,你就需要在完成之后通过 “git submodule update –init –recursive” 命令来初始化那些子模块。

2.4. 签出(checkout)一个版本

一个 Git 仓库可以保存无限多个提交版本,但是仅仅只有一个文件版本能保存在你当前的工作副本中。就像任何其他的 Git 仓库一样,你必须自己来决定在子模块上的哪一个版本应该被签出到你的工作副本中。

Note: 和一个 Git 仓库不同的是,子模块永远指向一个特定的提交, 而不是分支。这是因为,一个分支的内容可以在任何时间通过新的提交来改变, 所以指向一个特定的提交版本就能始终保证代码的正确性

比方说,我们希望在我们项目中使用一个旧版本的 “service” 库。首先,我需要看一下这个库的提交历史记录。我们需要切换到这个子模块的根目录下,然后执行 “log” 命令:

1
2
3
4
$ cd lib/service/
$ git log --oneline --decorate

# Note: Git 命令是对上下文环境很敏感的!也就是说,通过命令行来切换到子模块的目录后,我们执行的所有 Git 命令仅仅只会对子模块有效,而不是对它的父仓库。

现在历史记录被打印出来了,我们会发现这个提交被标记成了 “0.1.1”:

1
2
3
4
5
83298f7 (HEAD, master) update .gitignore
a3b6186 remove page
ed693b7 update doc
3557a0e (tag: 0.1.1) change version code
2421796 update readme

这就是我们希望在我们的项目使用的版本。首先我们可以来简单地看看这个提交:

1
$ git checkout 0.1.1

再让我们来看看父仓库。在主项目的目录中执行下面的命令:

1
2
$ git submodule status
+3557a0e0f7280fb3aba18fb9035d204c7de6344f   lib/service (0.1.1)

Note: 通过 git submodule status 命令, 我们可以查看子模块的哪一个版本在当前被签出了。在 hash 之前的 “+” 符号是非常重要的,它表明该子模块在它父仓库的官方记录中存在一个不同的版本。这是合理的,因为我们的确修改并签出了版本标记为 “0.1.1” 的提交。

如果在父仓库上执行 git status 命令,我们会发现像任何其他的变化一样,Git 移动了指向这个子模块的指针。

1
2
3
4
5
6
7
$ git status
On branch master
Changes not staged for commit:
    (use "git add ..." to update what will be committed)
    (use "git checkout -- ..." to discard changes in working directory)

    modified:   lib/service (new commits)

为了使这个改动生效,我们现在需要提交它到仓库中:

1
$ git commit -a -m "Moved Submodule pointer to version 1.1.0"

2.5. 更新一个子模块,当指向它的指针发生了变化之后

当开发团队的其他成员,在项目中改变了子模块的指针,指到另外一个版本之后,我们就要合并他的改动:例如,抓取、合并 或是 rebase。

在主项目目录执行:

1
2
3
4
5
$ git pull
Updating 43d0c47..3919c52
Fast-forward
 lib/service | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

此时,Git 会以一种相当含蓄的方式告诉我们, lib/service 发生了变化。再次使用 git submodule status 来获取更多、更详细的信息:

1
2
$ git submodule status
+83298f72c975c29f727c846579c297938492b245 lib/ToProgress (0.1.1-8-g83298f7)

还记得那个小的 “+” 符号吗? 这表明子模块发生了变化,我们当前签出的子模块版本不是主项目使用的中的 “官方” 版本。

使用 “update” 命令可以帮助我们修正它:

1
$ git submodule update lib/service

Note: 在大多数情况下,使用 “git submodule” 家族的命令是不需要指定一个特定子模块的。但是正如上面的例子一样,如果我们给出一个子模块的路径,这个操作就只会针对那个给定的子模块。

现在我们签出了相同版本的子模块,这就是之前另一个团队成员提交到项目中的那个。

值得注意的是,“update” 命令会为你下载子模块的改动。设想一下,你的队友在你之前已经改变了指向子模块版本的指针。在这种情况下,Git 会为你获取在子模块的相应版本,并且签出这个子模块的版本,非常方便。

2.6. 检查子模块的最新变化

让我们来看看子模块是否提供了新的代码版本:

1
2
3
4
5
6
7
8
$ cd lib/service
$ git fetch
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0),pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/kuroko/service
    83298f7..3e20bc2  master     -> origin/master

Note: 现在我们切换到了子模块的文件夹,之后的操作就像对待任何一个普通的项目仓库一样(因为它就是一个普通的 Git 仓库)。 现在 “git fetch” 命令显示,当前的确存在一些新的改动在子模块的远程上。

在我们准备整合这些改动之前,我想再次重申一下:当检查这个子模块的状态时,我们会发现我们正处在一个 detached HEAD 上:

1
2
3
$ git status
HEAD detached at 3557a0e
nothing to commit, working directory clean

一般情况下,在 Git 中,你总是会签出某个分支,然而你也可以签出一个特定的 提交(而不是分支)。这是一种比较罕见的情况,在 Git 中通常应该避免。 但是,在子模块上工作时,签出某个提交的情况是非常正确的。你要确保在你的项目中,签出一个确切的静态的提交(不是一个分支), 并转移到一个较新的提交上

现在让我们通过拉取操作来整合那些新的改动到你的本地子模块仓库中吧。请注意!你不能使用那个简写的 “git pull” 命令语法,而是需要指定特定的远程和分支。

这是因为我们正处于 “detached HEAD” 状态。因为在这个时刻你不是在本地分支上,你需要告诉 Git,你想要把拉取出来的改动整合到哪一个分支上去。

1
$ git pull origin master

如果你现在已经执行过一遍 “git status” 命令了,你会发现我们的状态仍然处于 detached HEAD,并且在同一个提交上,当前被签出的内容并没有发生改变。如果我们在项目中想要使用这个升级后的子模块的代码,我们必须明确地将 HEAD 指针移动到分支上:

1
$ git checkout master

我们已经完成了在子模块上的工作,现在让我们切换回我们主项目吧:

1
2
3
$ cd ../..
$ git submodule status
+3e20bc25457aa56bdb243c0e5c77549ea0a6a927 lib/service (0.1.1-9-g3e20bc2)

由于我们刚刚移动了子模块的指针到了一个不同的版本,我们需要将这个改动提交到父仓库中去,从而让它成为主项目当前正式引用的 “官方” 版本。

2.7. 在子模块上工作

有些时候,你可能会想要在子模块中作一些自己的改动。你已经知道了在子模块中工作就和在一个普通的 Git 仓库中工作一样,你在子模块目录中执行的所有的 Git 命令只会对这个子模块仓库有效。

比方说,你想对子模块进行一个小小的改动,你编辑了相关的文件,把它们添加到暂存区,并且提交它。

现在你可能会踩到第一块香蕉皮。因为如果当前你正处于一个 detached HEAD 状态,你的提交会迷失方向,它并没有关联到任何一个分支。一旦你签出了其他的东西,它的内容就会丢失。所以你应该在提交之前确保,你当前已经在子模块中签出了一个分支。

除此之外,你已经学到的其它一切 Git 操作都仍然适用。在主项目中 “git submodule status” 会告诉你指向该子模块的指针被移动了,你必须提交这个改动。

顺便提一下,如果你的子模块中还有_未提交_的改动,Git 也会在主项目中提醒你:

1
2
$ git status
modified:   lib/service (modified content)

Note: 请务必始终保持子模块有一个干净的状态。

2.8. 删除一个子模块

尽管很少会从项目中删除一个子模块,但是如果你确定想要这么做,也请不要手动地删除它,一旦所的有配置文件被打乱,将会不可避免地导致出现一系列问题。

1
2
3
4
5
$ git submodule deinit lib/service
$    lib/service
$ git status
modified:   .gitmodules
deleted:    lib/service

使用 git submodule deinit, 我们可以确保从配置文件中完全的删除一个子模块,使用 git rm, 我们可以最终删除这个子模块的文件,包括一些其他废弃的部分。

最后,提交这些改动,这个子模块就会从你的项目中被彻底的删除掉。

3. See Also

Thanks to the authors 🙂