【Git】Git submodule常用指令

文章出自个人博客 https://knightyun.github.io/2021/03/21/git-submodule,总结的很详细,本文只做学习记录,方便自己查阅~

文章目录

  • 一、概念
  • 二、常用指令
    • 1. 新增子模块
    • 2. 查看子模块
    • 3. 拉取子模块
    • 4. 更新子模块
      • 拉取更新
      • 分支切换
      • 分支合并
      • 注意事项!!!
    • 5. 删除子模块
    • 6. 子模块与父项目的联系
    • 7. 其他

一、概念

先引用 git 的官方定义描述:

A submodule is a repository embedded inside another repository. The submodule has its own history; the repository it is embedded in is called a superproject.

子模块(submodule)是一个内嵌在其他 git 仓库(父工程)中的 git 仓库,子模块有自己的 git 记录。

通常,如果一个仓库存在子模块,父工程目录下的 .git/modules/ 目录中会存在一个 git 目录,子模块的仓库目录会存在于父工程的仓库目录中,并且子模块的仓库目录中也会存在一个 .git 目录;

使用场景:

  • 想要在一个工程中使用另一个工程,但是那个工程包含了单独的提交记录,submodule 就可以实现在一个工程中引入另一个工程,同时保留二者的提交记录并且区分开来;目前 submodule 还能实现单独开发子工程,并且不会影响父工程,父工程可以在需要的时候更新子模块的版本;
  • 想要把一个工程拆分成多个仓库并进行集中管理,这可以用来实现 git 当前的限制,实现更细粒度的访问,解决当仓库过于庞大时所出现的传输量大、提交记录冗杂、权限分设等问题;

二、常用指令

1. 新增子模块

向一个项目中添加子模块

git submodule add https://github.com/yyy/xxx.git

之后会 clone 该子模块对应的远程项目文件到本地父项目目录下的同名文件夹中(./xxx/),父项目下也会多一个叫 .gitmodules 的文件,内容大致为:

[submodule "xxx"]
	path = xxx
	url = [email protected]:yyy/xxx.git

如果存在多个子模块,则会继续向该文件中追加与上面相同格式的内容;

同时父项目下的 .git 目录中也会新增 /modules/xxx/ 目录,里面的内容对应子模块仓库中原有的 .git 目录中的文件,此时虽然子模块目录下的 .git 依然存在,但是已经由一个文件夹变成了文件,内容为:

gitdir: ../.git/modules/xxx

即指向了父项目的 .git/modules/xxx 目录;如果运行 git config --list 查看项目的配置,也会发现多了类似下面两行的内容:

submodule.xxx.url=[email protected]:yyy/xxx.git
submodule.xxx.active=true

如果修改 submodule.xxx.url 的值,则会覆盖 .gitmodules 文件中对应的 url 值;

2. 查看子模块

查看当前项目下的子模块:

git submodule

或者

git submodule status

输出:

70c316ecb7c41a5bdf8a37ff93bf866d3b903388 xxx (heads/master)

如果将父项目推送到远程仓库(如 Github),在网页浏览该项目时子模块所在的目录会多一个类似 @70c316e 的后缀,即上面查看子模块命令输出内容的 hash 值的前面部分点击这个目录会跳转到这个子模块对应的仓库地址(另一个 url);

如果执行:

git submodule deinit

删除了子模块,则再次查看时输出会是这样的:

-70c316ecb7c41a5bdf8a37ff93bf866d3b903388 xxx

3. 拉取子模块

如果要 clone 一个项目,并且包含其子模块的文件,则需要给 git clone 命令最后加上 --recurse-submodules 或者 --recursive 参数(否则只会下载一个空的子模块文件):

git clone https://github.com/yyy/xxx.git --recursive

当然,克隆时忘记了加这个参数,后续也有办法去拉取子模块的文件,

  • 首先执行:
git submodule init

这会初始化子模块相关配置,比如自动在 config 中加入下面两行内容:

[email protected]:yyy/xxx.git
submodule.xxx.active=true

  • 然后执行:
git submodule update

就可以拉取到子模块仓库中的文件了,

  • 也可以将这两步命令合并为一步:
git submodule update --init
  • 要拉取所有层层嵌套的子模块,则执行:
git submodule update --init --recursive

之前 clone 时加参数不过是自动执行初始化配置并拉取子模块(甚至嵌套的子模块)中的文件罢了;

命令默认拉取主分支master),想要修改这个默认拉取分支可以修改 .gitmodules 文件中子模块对应的 branch 值,或者执行:

git config submodule.xxx.branch dev

或者执行同时将配置写入文件,这样其他人拉取父项目也会获取该配置:

git config -f .gitmodules submodule.xxx.branch dev

4. 更新子模块

拉取更新

获取子模块仓库的最新提交,同步远程分支的变更,可以直接在子模块目录下执行:

git pull

或者在父目录下执行:

git submodule update --remote

这里给 git submodule update 加上 --remote 是为了直接从子模块的当前分支的远程追踪分支获取最新变更,不加则是默认从父项目的 SHA-1 记录中获取变更;
当有多个子模块时,该命令默认拉取所有子模块的变更
指定更新子模块 xxx 需要执行:

git submodule update --remote xxx

如果将修改子模块的相关变更推送到父项目的远程,其他人拉取代码时,只用 git pull 的话只会把子模块的相关修改拉取到父项目,具体变更并不会更新到子模块中,在父项目里执行:

git diff --submodule

Submodule xxx a6e2962…70c316e (rewind):
< add file

注意子模块提交记录中前的 < 符号,表示变更未更新到子模块文件夹里,所以更新子模块变更需要执行:

git submodule update --init --recursive

或者直接在父项目拉取时同时更新子模块需要子模块已经 init,否则仍然拉取不到文件):

git pull --recurse-submodules

分支切换

更新完子模块(git submodule update)后,虽然会将文件变更同步到子模块目录下,但是此时子模块并没有处于任何已有分支下,去子模块目录下检查一下分支就会发现:

git branch -vv
  • (HEAD detached at 16d1b6b) 16d1b6b mod file
    master 16d1b6b [origin/master] mod file

当前分支并不是 master,而是一个 detached 状态的编号分支,官方文档称为“游离的 HEAD”,虽然可以提交,但是并没有本地分支跟踪这些更改,意味着下次更新子模块就会丢失这些更改;

所以在子模块下开始开发前,需要先切换到某个已有分支或者创建新的分支,比如进入主分支:

git checkout master

分支合并

除了默认的分支同步更新操作,也可以执行其他类型的分支更新行为,比如 mergerebase 等;

  • 如将父项目中记录的子模块最新变更(分支是 submodule.xxx.branch 中配置的,默认主分支 master)merge 到子模块的当前分支中,则执行:
git submodule update --remote --merge
  • rebase 到子模块当前分支则执行:
git submodule update --remote --rebase

注意事项!!!

如果其他人修改了子模块的内容并提交了记录,父项目也提交并推送了远程仓库,但是子模块没有推送其对应的远程仓库,
那么其他人拉取父项目代码变更时没有问题,但是更新子模块时就会遇到下面的问题:

fatal: remote error: upload-pack: not our ref 16d1b6b94e3245f3a7fb4f43e5b6f44b14027fbb
Fetched in submodule path 'xxx', but it did not contain 16d1b6b94e3245f3a7fb4f43e5b6f44b14027fbb.
Direct fetching of that commit failed.

即由于其他人没有及时将子模块的提交 push 的子模块的远程仓库,我们本地父项目有了关于子模块最新的变更,但是在子模块的仓库中却找不到,就报错了,让对方在子模块下 push一下这边再重新更新就行了;

为了避免制造这一不必要的麻烦,可以把在父项目中推送远程的命令替换为

git push --recurse-submodules=check

这样如果子模块(与父项目记录的对应分支)存在未 push 的提交,就会报错,并且子模块有推送失败的,父项目也会推送失败;需要在推送父项目时自动推送未推送的子模块,则执行:

git push --recurse-submodules=on-demand

觉得每次手输太麻烦,就直接将其写入配置:

git config push.recurseSubmodules check

如果父项目中子模块的仓库地址submodule.xxx.url)被其他协作者修改了,那么我们再更新子模块时就可能遇到问题,需要执行:

git submodule sync --recursive

同步完 url,然后再重新初始化更新:

git submodule update --int --recursive

5. 删除子模块

在确认移除子模块前,需要先将其取消注册unregister),即删除该子模块相关的配置文件(git config),比如要移除子模块 xxx,则执行:

git submodule deinit xxx

然后子模块的相关配置会被删除(.gitmodules 和 .git/modules/xxx 中的配置会保留),子模块对应的目录也会被清空(子模块目录本身会保留),再运行 git submodule status 查看子模块则会输出:

-70c316ecb7c41a5bdf8a37ff93bf866d3b903388 xxx

前缀 - 表示该子模块已经被取消注册,可理解为暂时移除,想必官方这样做也是给我们提供反悔的余地,因为想要恢复刚才删除的子模块,重新执行 git submodule update --init xxx 就能重新初始子模块并拉取文件;

由于还有一些配置文件仍然被保留,所以想要彻底删除的话,需要继续手动删除这里配置文件,即:

  • 删除子模块对应的目录 xxx
  • 删除 .gitmoduls 中子模块 xxx 对应的区块配置;
  • 删除 .git/modules/ 目录下的子模块目录 xxx
  • 删除子模块的缓存:git rm --cached xxx
    然后再执行 git submodule 就没有任何输出了,清除完毕;

6. 子模块与父项目的联系

父项目和子模块有着分开的 git 仓库,所以可以分别在父项目和子模块的目录下使用 git 命令,操作的也是各自的仓库,比如分别在父项目和子模块中执行 git branch -a 或者 git remote -v 的输出结果是不同的;

虽然二者有个分开的仓库与提交记录,但是又是关联起来的(这正是 submodule 所做的工作),举个例子,在子模块目录 xxx/ 下新增一个文件 test.txt,然后在子模块目录中执行 git satus 会输出:

Untracked files:
  (use "git add ..." to include in what will be committed)
        xxx/test.txt

此时在父项目下执行 git status 输出的是:

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
  (commit or discard the untracked or modified content in submodules)
        modified:   xxx (untracked content)

即提示需要先在子模块下提交修改记录;

然后子模块下提交记录,执行:

git add .
git commit -m "add file"

这时再分别运行 git status,子模块的输出是:

Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

而父项目的输出是:

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
        modified:   xxx (new commits)

提示子模块中有了新的提交new commits);

假如再把子模块下的这个 test.txt 文件删除,则提示子模块中有了新的提交(new commits);

假如再把子模块下的这个 test.txt 文件删除,则父项目的状态依然是上面那样。

将新增文件的记录 git push 到远程(这会推送到子模块自己的远程仓库),此时子模块的工作区状态是清空状态,但是父项目的依旧是:

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
        modified:   xxx (new commits)

所以,父项目与子模块的关联便是,父模块只是单纯的识别子模块的总体变化,而不会在意具体是新增、修改还是删除,甚至修改已经提交推送到子模块所属的远程仓库,只是将这些调整统一识别为 modified 状态,然后需要提交并推送到自己(父项目)所属的远程仓库

在父项目中使用 git diff 可以查看当前的变更,会输出:

diff --git a/xxx b/xxx
index 70c316e..a6e2962 160000
--- a/xxx
+++ b/xxx
@@ -1 +1 @@
-Subproject commit 70c316ecb7c41a5bdf8a37ff93bf866d3b903388
+Subproject commit a6e29629904538e8f70694df607617084d2659ca

如果想要查看具体子模块的变动,可以执行:

git diff --submodule

Submodule xxx 70c316e…a6e2962:
add file

输出会列出当前子模块的所有变动的提交日志;也可以直接日志中关联的子模块提交记录,执行:

git log -p --submodule

commit 909a721e3755affb7620316b44df8fbc1b3488f2 (HEAD -> master)
Author: ******
Date: ******
mod submodule
Submodule xxx 70c316e…a6e2962:
add file

7. 其他

父项目从含有子模块的分支切换到没有子模块的分支时,默认会保留子模块对应的目录,所以这使得切换过去时本地会保留关于子模块的修改记录,显然这不太合理,所以从包含子模块的分支切换到 xxx 时,需要这样执行:

git checkout xxx --recurse-submodules

当父项目存在许多子模块时,有时需要对多个子模块执行相同的操作,这时就可以使用 foreach 功能,比如批量存储

git submodule foreach 'git stash'

或者在每个子模块中新建切换分支:

git submodule foreach 'git checkout -b new'

你可能感兴趣的:(【备忘录】,【python】,【C/C++】,git)