Appearance
Git 子模块
1. 概述
在项目开发时,你有可能经常性地想要去引用一些库文件或其它资源文件。手动的方法就是直接下载那些必要的代码文件,然后拷贝到你的项目中,最后将这些新的文件提交到你的 Git 仓库中去。
虽然这是一种有效的方法,但是这种操作并不是最简单有效的。如果只是任意地将这些库文件提交到你的项目中,将带来一系列的问题:
外部代码和自己开发的代码会被合并保存在一个项目中。其实那些库文件自身就应该是一个项目,并且也应该独立于我们的工作之外。在我们当前项目的版本控制系统中,它们并不需要被保存。
如果库文件发生了变化(可能因为修复错误或是添加新的功能),更新这些库文件的代码对我们来说会是很繁琐的事。我们需要再次下载它的原代码文件,并且替换掉在仓库中已有的文件。
由于这些都是在日常项目开发时非常普遍存在的问题,所以 Git 也提供了一个解决方案:子模块(Submodule)。
2. 什么是子模块
一个 “子模块” 其实就是一个标准的 Git 仓库。不同的是,它被包含在另一个主项目的仓库中。一般情况下,它包含一些库文件和其它资源文件,你可以简单地把这些库文件作为一个子模块添加到你的主项目中。
一个子模块也是一个功能齐全的 Git 仓库,就内部而言它和别的仓库没有什么区别,你可以对它进行修改、提交、抓取、推送等等操作。
让我们来看看在实际操作中子模块是如何工作的吧。
3. 添加一个子模块
在这个简单的项目中,我们建立一个新的 lib 文件目录用来存放一些库文件。
Bash
$ mkdir libBash
$ cd lib使用 git submodule add 命令,我们会从 GitHub 中添加一个小的 Javascript 库:
Bash
$ git submodule add https://github.com/djyde/ToProgress信息
还可以指定子模块的名称、分支、路径:
EBNF
'git submodule add' ['--name' Name] ['-b' Branch] URL [Path]不带用户名的子模块 URL
如果你希望通过不带用户名的 SSH 来连接 Git 服务器。首先确保你已经生成了 SSH 密钥对,并将公钥添加到 Git 服务器上。并配置:
在
~/.ssh/config文件中设置相应的配置项:BashHost your-gitblit HostName your-gitblit-domain.com Port 29418 User username IdentityFile ~/.ssh/id_rsa然后在
.gitmodules中可以使用不带用户名的 URL:Bash[submodule "subrepo"] path = subrepo url = your-gitblit:subrepo.git branch = master
来让我们来看看现在发生了什么:
这个命令将对一个指定的 Git 仓库进行了一个简单地克隆操作:
TextCloning into 'lib/ToProgress'... remote: Counting objects: 180, done. remote: Compressing objects: 100% (89/89), done. remote: Total 180 (delta 51), reused 0 (delta 0), pack-reused 91 Receiving objects: 100% (180/180), 29.99 KiB | 0 bytes/s, done. Resolving deltas: 100% (90/90), done. Checking connectivity... done.1
2
3
4
5
6
7当然这一切也都会反映在我们当前项目的文件结构上。在项目中的
lib目录中包括了一个新的ToProgess文件目录。通过这个文件目录所包含的.git子文件夹我们就能确认,这就是一个标准的 Git 仓库。
信息
子模块的工作文件会放置在指定的目录中,然而,这些文件并不属于主项目的版本控制范围。在本例中,主项目仅会记录子模块的 URL,以及它在主项目中的本地路径和签出的版本。
一个新的
.gitmodules文件会被创建,这个文件就是 Git 用来跟踪我们的子模块并保存它的配置信息的:INI[submodule "lib/ToProgress"] path = lib/ToProgress url = https://github.com/djyde/ToProgress1
2
3你可能会对 Git 的内部工作原理感兴趣。除了
.gitmodules配置文件,Git 也会在你本地的.git/config文件中保存对子模块的记录,最终它也会在它的.git/modules目录中保存每一个子模块的.git仓库。
现在让我们来看看当前的项目状态:
Bash
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD ..." to unstage)
new file: .gitmodules
new file: lib/ToProgress1
2
3
4
5
6
7
2
3
4
5
6
7
像任何其他修改一样,Git 添加了一个子模块,并且要求你提交这个改动到仓库中:
Bash
$ git commit -m "Add 'ToProgress' Javascript library as Submodule"现在,我们已经成功地添加了一个子模块到我们主项目中来了!在了解几个不同的案例之前,让我们先来看看如何克隆一个已经包括了若干子模块的项目。
4. 克隆包含子模块的项目
你已经知道了,一个项目仓库并不包含子模块的文件。主项目仓库仅仅保存子模块的配置信息来作为版本管理的一部分。这就表示,当你要克隆一个带有子模块的项目时,在默认的情况下 git clone 命令仅仅接收这个项目本身。
你有两个选择去设置你保存的子模块:
你可以通过将
--recurse-submodules参数加在git clone上,从而让 Git 知道,当克隆完成的时候要去初始化所有的子模块。如果你仅仅只是简单地使用了
git clone命令,并没有附带任何参数,你就需要在完成之后通过git submodule update --init --recursive命令来初始化那些子模块。
5. 拉取子模块到最新的提交
子模块功能最大优点之一就是你可以很方便地与子模块最新的发行版本同步。让我们来看看子模块是否提供了新的代码版本:
Bash
$ cd lib/ToProgress
$ 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/djyde/ToProgress
83298f7..3e20bc2 master -> origin/master1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
提示
请注意!现在我们切换到了子模块的文件夹,之后的操作就像对待任何一个普通的项目仓库一样(因为它就是一个普通的 Git 仓库)。
现在 git fetch 命令显示,当前的确存在一些新的改动在子模块的远程上。
通常情况下,我们子模块的仓库会处于 detached HEAD 状态。所以我们需要先切换到某个具体的分支:
Bash
$ git switch master然后根据需要,将分支拉取到最新的版本:
Bash
$ git pullgit submodule 有一个非常强大的 foreach 命令,它可以在每个检出的子模块中执行任意的 shell 命令。例如,也可以在主项目中执行以下命令来进行批量处理:
Bash
$ git submodule foreach 'git pull'或者先切换分支再 pull:
Bash
$ git submodule foreach 'git switch master && git pull'信息
git submodule update --remote 的效果与上述命令类似,区别在于,前者会以 .gitmodules 中指定分支(未指定则以 master)的为准拉取到最新状态,并且更新后子模块将处于 detached HEAD 状态。
6. 修改主项目的子模块版本
比方说,我们希望在我们项目中使用 ToProgress 库的某个旧版本。首先,我需要看一下这个库的提交历史记录。我们需要切换到这个子模块的根目录下,然后执行 log 命令:
Bash
$ cd lib/ToProgress/Bash
$ git log --oneline --decorate现在历史记录被打印出来了,假设 0.1.1 版本是我们想要的:
Text
83298f7 (HEAD, master) update .gitignore
a3b6186 remove page
ed693b7 update doc
3557a0e (tag: 0.1.1) change version code
2421796 update readme1
2
3
4
5
2
3
4
5
先检出该提交:
Bash
$ git checkout 0.1.1再让我们来看看父仓库。在主项目的目录中执行下面的命令:
Bash
$ git submodule status
+3557a0e0f7280fb3aba18fb9035d204c7de6344f lib/ToProgress (0.1.1)1
2
2
通过使用 git submodule status,我们可以查看子模块的哪一个版本在当前被签出了。在 hash 之前的 + 符号是非常重要的,它表明该子模块在它父仓库的官方记录中存在一个不同的版本。
如果在父仓库上执行 git status 命令,我们就会发现像任何其他的变化一样,Git 移动了指向这个子模块的指针:
Bash
$ 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/ToProgress (new commits)1
2
3
4
5
6
7
2
3
4
5
6
7
为了使这个改动生效,我们现在需要提交它到仓库中:
Bash
$ git commit -a -m "Moved Submodule pointer to version 1.1.0"7. 同步主项目的子模块版本
如果开发团队的其他成员在项目中修改了子模块的版本呢?当他移动了指向子模块的指针到另一个版本之后,我们就要同步他的改动,首先将主项目拉取到最新版本:
Bash
$ git pull
Updating 43d0c47..3919c52
Fast-forward
lib/ToProgress | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)1
2
3
4
5
2
3
4
5
使用 git submodule status 命令来查看子模块状态信息:
Bash
$ git submodule status
+83298f72c975c29f727c846579c297938492b245 lib/ToProgress (0.1.1-8-g83298f7)1
2
2
还记得那个小的 + 符号吗?这表明子模块发生了变化,我们当前签出的子模块版本不是主项目声明的版本。使用 git submodule update 命令可以帮助我们修正它:
Bash
$ git submodule update lib/ToProgress
Submodule path 'lib/ToProgress': checked out '3557a0e0f7280fb3aba18fb9035d204c7de6344f'1
2
2
信息
在大多数情况下,使用 git submodule 家族的命令是不需要指定一个特定子模块的。但是正如上面的例子一样,如果我们给出一个子模块的路径,这个操作就只会针对那个给定的子模块。
Pull 时一并同步所有子模块
也可以直接在主项目执行 git pull 命令时同时指定 --recurse-submodules 参数,这将一并同步所有子模块。
现在我们签出了相同版本的子模块,也就是之前另一个团队成员提交到项目中的那个。需要注意的是,不论是使用 git pull --recurse-submodules 还是在 git pull 之后再使用 git submodule update 命令,将子模块同步至主项目中声明的版本之后,子模块都将处于 detached HEAD 状态。
如果当前所有的子模块都已经指向了正确的分支,并且希望在将子模块同步至主项目中声明的版本之后,仍然可以让子模块保持指向原分支,则可以在执行 git pull 命令之后,执行以下命令:
Bash
$ git submodule foreach 'git reset --hard $sha1'或者先切换分支再 reset:
Bash
$ git submodule foreach 'git switch master && git reset --hard $sha1'8. 删除子模块
尽管很少会从项目中删除一个子模块,但是如果你确定想要这么做,也请不要手动地删除它,一旦所的有配置文件被打乱,将会不可避免地导致出现一系列问题。
Bash
$ git submodule deinit lib/ToProgress
$ git rm lib/ToPogress
$ git status
...
modified: .gitmodules
deleted: lib/ToProgress1
2
3
4
5
6
2
3
4
5
6
- 使用
git submodule deinit,我们可以确保从配置文件中完全地删除一个子模块; - 使用
git rm,我们可以最终删除这个子模块的文件,包括一些其它废弃的部分;
提交这些改动,这个子模块就会从你的项目中被彻底地删除了。
9. 更新子模块配置
如修改了 .gitmodules 文件中的子模块 URL 后,你需要确保 Git 更新了子模块的配置并重新拉取子模块的内容。具体步骤如下:
修改
.gitmodules配置同步 Git 配置中的子模块 URL
Git 会存储子模块的配置在
.gitmodules文件和.git/config文件中,所以你需要将修改同步到.git/config中。你可以使用以下命令来更新子模块的 URL 配置:Bashgit submodule sync此命令会将
.gitmodules中的更改同步到本地的 Git 配置中(.git/config)。如果有多个子模块,git submodule sync会更新所有子模块的配置。拉取子模块更新
接下来,你可以通过以下命令重新初始化和拉取子模块的内容:
Bashgit submodule update --init --recursive--init会初始化尚未初始化的子模块;--recursive会确保所有嵌套的子模块也会被拉取;
检查子模块状态
你可以用以下命令查看当前子模块的状态,确保它已经正确拉取了新的 URL:
Bashgit submodule status
参考链接