熟悉使用Git子模块


成也子模块,败也子模块

经常有这样的事情,当你在一个项目上工作时,你需要在其中使用另外一个项目。也许它是一个第三方开发的库或者是你独立开发和并在多个父项目中使用的。这个场景下一个常见的问题产生了:你想将两个项目单独处理但是又需要在其中一个中使用另外一个。Git 通过子模块处理这个问题。子模块允许你将一个 Git 仓库当作另外一个 Git 仓库的子目录。这允许你克隆另外一个仓库到你的项目中并且保持你的提交相对独立。

熟悉使用Git子模块


1. 现有仓库加入子模块

如何在现有仓库当中加入子模块?

  • [1] 快速使用
# 在现有仓库中增加子模块
$ git submodule add git://github.com/chneukirchen/rack.git rack

# 查看子模块信息
$ git diff --cached
$ git diff --cached --submodule
  • [2] 详细讲解

假设你想把 Rack 库加入到你的项目中,既要保持自己的变更,又要延续上游的变更。首先要把外部的仓库克隆到你的子目录中,然后使用下面命令将外部项目加为子模块。

# 添加子模块
$ git submodule add git://github.com/chneukirchen/rack.git rack
Initialized empty Git repository in /opt/subtest/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Receiving objects: 100% (3181/3181), 675.42 KiB | 422 KiB/s, done.
Resolving deltas: 100% (1951/1951), done.

现在在项目里的 rack 子目录下有了一个 Rack 项目,我们注意到有一个 .gitmodules 文件。这是一个配置文件,保存了项目 URL 和你拉取到的本地子目录。

$ cat .gitmodules
[submodule "rack"]
      path = rack
      url = git://github.com/chneukirchen/rack.git

如果你有多个子模块,这个文件里会有多个条目。很重要的一点是这个文件跟其他文件一样也是处于版本控制之下的,就像你的 .gitignore 文件一样。它跟项目里的其他文件一样可以被推送和拉取。这是其他克隆此项目的人获知子模块项目来源的途径。

尽管 rack 是你工作目录里的子目录,但 Git 把它视作一个子模块,当你不在那个目录里时并不记录它的内容。取而代之的是,Git 将它记录成来自那个仓库的一个特殊的提交。当你在那个子目录里修改并提交时,子项目会通知那里的 HEAD 已经发生变更并记录你当前正在工作的那个提交。

# 查看子模块变化
$ git diff --cached rack
diff --git a/rack b/rack
new file mode 160000
index 0000000..08d709f
--- /dev/null
+++ b/rack
@@ -0,0 +1 @@
+Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433

当你提交时,注意 rack 条目的 160000 模式。这在 Git 中是一个特殊模式,基本意思是你将一个提交记录为一个目录项而不是子目录或者文件。你可以将 rack 目录当作一个独立的项目,保持一个指向子目录的最新提交的指针然后反复地更新上层项目,且所有的 Git 命令都在两个子目录里独立工作。

# 提交子模块的变化
$ git commit -m 'first commit with submodule rack'
[master 0550271] first commit with submodule rack
 2 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 .gitmodules
 create mode 160000 rack

2. 克隆含子模块的仓库

如何使用子模块的仓库?

  • [1] 快速使用
# ---------------------
# 克隆项目并且更新子模块
# ---------------------

【方法一】
    1.克隆项目(子模块目录默认被克隆但是空的)
      $ git clone git://github.com/chneukirchen/rack.git
    2.初始化子模块(初始化本地配置文件)
      $ git submodule init
    3.该项目中抓取所有数据并检出父项目中列出的合适的提交
      $ git submodule update

【方法二】
    1.用--recursive命令,跟方法一样达到效果
      $ git clone --recursive git://github.com/chneukirchen/rack.git
# --------------
# 更新子模块代码
# --------------

【方法一】需要进入子项目目录
    $ cd DbConnector
    $ git fetch
    $ git merge origin/master

【方法二】需要进入子项目目录
    $ git submodule update --remote rack
    # 这里默认更新master分支,如果更新其他分支
    $ git config -f .gitmodules submodule.DbConnector.branch stable
    $ git submodule update --remote
    $ git merge origin/master

【方法三】不进入子项目目录
  $ git submodule update --remote --merge
# -------------------------------
# 子模块状态显示(父模块中操作子模块)
# -------------------------------

# 遍历所有子模块并把子模块存储起来
$ git submodule foreach 'git stash'

# 移动刚刚储藏的子模块到新分支,然后开始新的bug修复等开发
$ git submodule foreach 'git checkout -b featureA'

# 显示子模块的所有改动
$ git submodule foreach 'git diff'
  • [2] 详细讲解

当你接收到这样一个项目,你将得到了包含子项目的目录,但里面没有文件。

  • git submodule init 来初始化你的本地配置文件
  • git submodule update 来从那个项目拉取所有数据并检出你上层项目里所列的合适的提交
# 初始化
$ git submodule init
Submodule 'rack' (git://github.com/chneukirchen/rack.git) registered for path 'rack'

# 拉取更新
$ git submodule update
Initialized empty Git repository in /opt/myproject/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Receiving objects: 100% (3181/3181), 675.42 KiB | 173 KiB/s, done.
Resolving deltas: 100% (1951/1951), done.
Submodule path 'rack': checked out '08d709f78b8c5b0fbeb7821e37fa53e69afcf433'
  • 如果另外一个开发者变更了 rack 的代码并提交,你拉取那个引用然后归并之,将得到稍有点怪异的东西。你会发现你归并来的仅仅上是一个指向你的子模块的指针,但是它并不更新你子模块目录里的代码,所以看起来你的工作目录处于一个临时状态。
# 本地更新
$ git merge origin/master
Updating 0550271..85a3eee
Fast forward
 rack |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

# 查看不同
$ git diff
diff --git a/rack b/rack
index 6c5e70b..08d709f 160000
--- a/rack
+++ b/rack
@@ -1 +1 @@
-Subproject commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
+Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433
  • 事情就是这样,因为你所拥有的指向子模块的指针和子模块目录的真实状态并不匹配。为了修复这一点,你必须再次运行如下命令。
# 更新代码
$ git submodule update
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 2 (delta 0)
Unpacking objects: 100% (3/3), done.
From [email protected]:schacon/rack
   08d709f..6c5e70b  master     -> origin/master
Submodule path 'rack': checked out '6c5e70b984a60b3cecd395edd5b48a7575bf58e0'
  • 一个常见问题是当开发者对子模块做了一个本地的变更但是并没有推送到公共服务器。然后他们提交了一个指向那个非公开状态的指针然后推送上层项目。当其他开发者试图运行 update 更新子模块,那个子模块系统会找不到所引用的提交,因为它只存在于第一个开发者的系统中。如果发生那种情况,你会看到类似这样的错误。
# 报错信息如下
$ git submodule update
fatal: reference isn’t a tree: 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Unable to checkout '6c5e70b984a60b3cecd395edd5ba7575bf58e0' in submodule path 'rack'
  • 你不得不去查看谁最后变更了子模块,然后你给那个家伙发电子邮件说他一通。
$ git log -1 rack
commit 85a3eee996800fcfa91e2119372dd4172bf76678
Author: Scott Chacon <[email protected]>
Date:   Thu Apr 9 09:19:14 2009 -0700
    added a submodule reference I will never make public. hahahahaha!

3. 子模块中存在的问题

介绍使用子模块当中存在的问题以及注意事项!

  • [1] 定义别名
# 显示主项目和子模块的所有改动
$ alias sdiff="git diff && git submodule foreach 'git diff'"

# 推送主仓库,同时检查子模块
# 如果没有推送,结束此命令提示用户推送子模块先
$ alias spush="push --recurse-submodules=on-demand"

# 更新子模块,并且合并代码到本地
$ alias supdate="submodule update --remote --merge"
  • [2] 存在问题

我们使用子模块并非没有缺点,说多了都是眼泪。首先,你在子模块目录中工作时必须相对小心。当我们第一次运行 submodule update 操作的时候,它会检出项目的指定版本,但是不在分支内。这叫做获得一个分离的头——这意味着 HEAD 文件直接指向一次提交,而不是一个符号引用。如果你再执行了一次 submodule update 命令,Git 会毫无提示地覆盖你的变更。技术上讲你不会丢失工作,但是你将失去指向它的分支,因此会很难取到。

为了避免这个问题,当你在子模块目录里工作时应使用 git checkout -b 创建一个 work 分支。当你再次在子模块里更新的时候,它仍然会覆盖你的工作,但是至少你拥有一个可以回溯的指针。切换带有子模块的分支同样也很有技巧。如果你创建一个新的分支,增加了一个子模块,然后切换回不带该子模块的分支,你仍然会拥有一个未被追踪的子模块的目录

# 增加新分支
$ git checkout -b rack
Switched to a new branch "rack"

$ git submodule add [email protected]:schacon/rack.git rack
Initialized empty Git repository in /opt/myproj/rack/.git/

文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !
  目录