---
title: GIT 子模块
date: '2017-03-31'
slug: git-submodule
---

我是 2009 年在~~世界上最大的同性交友网站~~ GitHub 上注册的。我转向 GIT 的根本原因是因为这个网站的界面好看又实用，而不是因为版本控制工具 GIT 比 SVN 好或不好，在那之前我一直蹲守在土得掉渣的 R-Forge 上用 SVN。此后四年里，我基本上一直是在 git 菜鸟状态，日常操作用到的 git 命令非常有限，基本上就是一个库克隆到本地（`git clone`），剩下的就是用图形界面操作了；因为当年在 Ubuntu 底下混，所以用 `git gui`，极少用到命令行 `git add / commit / push / pull` 之类的。我觉得在图形界面里点点点也挺好的。直到现在我仍然是以用 RStudio 的 GIT 界面为主，绝不会手工敲 `git add` 或 `git commit`，我觉得那样既麻烦又容易出错。[^1]

![我恨计算机](https://slides.yihui.org/gif/dump-computer.gif)

如果是自己一个人主要操作一个库，那么其实没什么需要学的，但一旦涉及到跟其他人协作，那就得开始了解稍微进阶一些的概念和命令了，比如最有用的就是分支[^2]。绝对菜鸟和普通菜鸟的最大区别就是，绝对菜鸟永远留在 master 次元一百年不变，普通菜鸟会考虑用新的分支去做一些没有绝对把握的事情，比如修正软件缺陷。前些日子看见[边小编](http://statsjoke.me)竟然威武地召唤出了[摘樱桃模式](https://github.com/cosname/cosx.org/pull/142)（`git cherry-pick`），我都震惊了，我用了这么些年 git 好像一共只摘过一次樱桃。显然边小编已经不是菜鸟，而是一头翼龙了。厉害啊。

![神队友](https://slides.yihui.org/gif/duiyou-4.gif)

闲话少说。

关于 git 的诸多概念，今儿个简单说其中一个：子模块（`git submodule`）。实际上我也是最近才第一次[用到它](https://github.com/yihui/yihui.org/tree/master/themes)。子模块的大概意思是一个 GIT 仓库下面某个文件夹的来源可以跟本库的来源不同，这个文件夹连接着别的库，这样你可以让这个文件夹通过别的库来管理，而主库唯一需要知道的信息是这个子模块当前的 git 版本（也就是那一长串 SHA 码），这样主库可以确定一定以及肯定地从别的库把这个子模块更新到特定的版本。

子模块的好处是，那个文件夹可以不需要你来管理，你只需要决定用子模块的哪个版本即可，它的真正管理是在别的库中进行。举个例子，一个网站用了别人的主题，这个网站本身有个主库，而主题（CSS/JS/图片文件等）可以是由别人写的，不需要你来维护，每次别人做了更新之后，你可以决定更新或不更新。记住，GIT 是一台精确的时光机器，你想穿梭到任意一个历史版本都可以：一手交 SHA 码（或其它标记如分支名称、标签名称等），一手交货。

子模块可以只是一个记录（来源及 SHA 码），也可以实体化，多数情况下我们还是会将它在本地实体化，即把子模块下载下来，否则那个文件夹就是个空的。

子模块可以手动添加，也可以在克隆一个主库的时候就直接实体化。对绳命已经离不开 Stack Overflow 人来说，[这个帖子](http://stackoverflow.com/a/4438292/559676)的几行答案基本上概括了所有你需要知道的关于子模块的命令，我简单概括一下：

- 如果克隆库的时候要初始化子模块，请加上 `--recursive` 参数，如：

    ```bash
    git clone --recursive git@github.com:yihui/yihui.org.git
    ```

- 如果已经克隆了主库但没初始化子模块，则用：

    ```bash
    git submodule update --init --recursive
    ```

- 如果已经克隆并初始化子模块，而需要从子模块的源更新这个子模块，则：

    ```bash
    git submodule update --recursive --remote
    ```
    
    更新之后主库的 git 差异中会显示新的 SHA 码，把这个差异选中提交即可。

- 如果要向一个库中添加一个新的子模块，可以用 `git submodule add`，例如把我的 GitHub 账户下的一个库添加到 `themes/hugo-lithium-theme` 文件夹中：

    ```bash
    git submodule add \
      https://github.com/yihui/hugo-lithium-theme.git \
      themes/hugo-lithium-theme
    ```
    
    上面的反斜杠 `\` 表示一个命令没敲完，下一行继续，我只是为了显示代码方便而已（不要出现水平滚动条），把代码强行折断了，实际用的时候换不换行都行（若不换行，去掉反斜杠即可）。

GIT 子模块和函数子模块有类似之处，都是征用一支相对独立的力量来完成任务，但函数调用子模块通常都是主动式的（嘿，子模块，请借我一用），而 GIT 则是被动模式的：除非你有意更新子模块，否则子模块一直会停留在特定版本。这也体现了 GIT 的关键词：**分布式**版本控制。开发工作可以高度并行，你做你的，我做我的，需要的时候再合并各自的工作。

[^1]: 最恨那种看也不看就暴力添加所有文件 `git add *` 的做法了，该提交的不该提交的都被提交了。尤其是在 GIT 历史里看见 `.DS_Store` 文件，我会爆炸。用图形界面能清楚知道自己到底每次提交了哪些文件。

[^2]: 对我而言，分支算比较简单有用的工具，当年我觉得稍微需要动脑筋理解的是 `git rebase`，这个也非常有趣和有用，作为一个期望 git 历史记录干净的强迫症，我经常用 ` git rebase -i master~5` 之类的命令。
