王大拿
知道的越多,不知道的也就越多! 只要学不死,就往死里学!!!

Git子模块

img

一、使用场景

当项目越来越庞大之后,不可避免的要拆分成多个子模块,我们希望各个子模块有独立的版本管理,并且由专门的人去维护,这时候我们就要用到git的submodule功能和Subtree功能。

首先明确,父项目和子项目没有实际关系,他们就是各自完全独立的两个git仓库而已。只是父项目中需要用到子项目。所以父项目和子项目的管理是分开进行的,即他们的代码拉取和提交都要分别进行;

同时,子项目模块A可能被Project2、Project3等多个项目使用,Parent_Project主项目主项目的模块组都在不同的git仓库中,这是需要使用git的模块功能,目前git提供的子模块功能主要有SubModule和Subtree两种

二、SubModule的命令介绍及使用

$ git submodule --help               #linux环境可以直接使用man命令进行查看;例如:man git submodule
git submodule [--quiet] [--cached]   #查看子模块的缓存
git submodule [--quiet] add [<options>] [--] <repository> [<path>] #添加子模块
git submodule [--quiet] status [--cached] [--recursive] [--] [<path>…] #查看当前子模块的状态
git submodule [--quiet] init [--] [<path>…] #初始化子模块
git submodule [--quiet] deinit [-f|--force] (--all|[--] <path>…) #去除初始化子模块
git submodule [--quiet] update [<options>] [--] [<path>…]#更新子模块
git submodule [--quiet] set-branch [<options>] [--] <path>#子模块设置分支
git submodule [--quiet] set-url [--] <path> <newurl>#子模块设置访问的URL
git submodule [--quiet] summary [<options>] [--] [<path>…]#查看子模块的概要
git submodule [--quiet] foreach [--recursive] <command># 进入到每一个子模块执行指定的命令
git submodule [--quiet] sync [--recursive] [--] [<path>…]#同步子模块
git submodule [--quiet] absorbgitdirs [--] [<path>…]#将子模块的.git目录嵌入到主项目的.git目录内;[添加子模块时,git默认完成]

1、以实际例子进行讲解SubModule

假设我当前的主项目是Parent_Project主项目

第一步,克隆主项目Parent_Project���项目

$ git clone git@192.168.68.23:DevOps/Top/parent_project.git

第二步,进入主项目切换分支

$ cd parent_project/
$ git branch -av
$ git switch Parent_Project_dev_mazk
$ git submodule add -b Child_Project_ModuleA_dev_mazk git@192.168.68.23:DevOps/Top/Sub/Child_Project_ModuleA.git src/Child_Project_ModuleA

第四步,查看子模块及主项目的当前状态,并将子模块添加至主库推送至gitlab

用法示例: git submodule [--quiet] status [--cached] [--recursive] [--] [<path>…]
作用:显示子模块的状态。这将为每个子模块打印当前检出的提交的 SHA-1,以及子模块路径和SHA-1的git describe输出
常用可选参数选项说明:
[--cached]           #此命令将打印超级项目中记录的每个子模块的 SHA-1;
[--recursive]        #此命令将递归到嵌套子模块中,并显示它们的状态;
$ $ git submodule status      #查看子模块的状态
 c1b0968bf36ef1b7d6806f03f49253c1211e85e9 src/Child_Project_ModuleA (heads/Child_Project_ModuleA_dev_mazk)
$ git status                 #查看当前主项目的状态
$ cat .gitmodules                     #查看当前的状态及.gitmodules文件
[submodule "src/Child_Project_ModuleA"]
        path = src/Child_Project_ModuleA
        url = git@192.168.68.23:DevOps/Top/Sub/Child_Project_ModuleA.git
        branch = Child_Project_ModuleA_dev_mazk
$ git commit -m "引入了子项目的模块A"
$ git push

第五步,查看当前的git库,发现主库里面嵌套了子库(知道就行

tree /f

├─.git
│ COMMIT_EDITMSG
│ config
│ description
│ HEAD
│ index
│ packed-refs
├─hooks
│ applypatch-msg.sample
│ commit-msg.sample
│ ........
├─info
│ exclude
├─logs
│ │ HEAD
│ └─refs
│ ├─heads
│ │ master
│ │ Parent_Project_dev_mazk
│ └─remotes
│ └─origin
│ HEAD
│ Parent_Project_dev_mazk
├─modules #嵌入的子模块目录及子模块的仓库
│ └─src
│ └─Child_Project_ModuleA #引入子模块Child_Project_ModuleA的仓库目录
│ │ config
│ │ description
│ │ HEAD
│ │ index
│ │ packed-refs
│ ├─hooks
│ │ applypatch-msg.sample
│ │ commit-msg.sample
│ │ ........
│ ├─info
│ │ exclude
│ ├─logs
│ │ │ HEAD
│ │ └─refs
│ │ ├─heads
│ │ │ Child_Project_ModuleA_dev_mazk
│ │ │ master
│ │ └─remotes
│ │ └─origin
│ │ HEAD
│ ├─objects
│ │ ├─info
│ │ └─pack
│ │ pack-eeb867c25b10ba692e24da2360d9aacb4a420c30.idx
│ │ pack-eeb867c25b10ba692e24da2360d9aacb4a420c30.pack
│ └─refs
│ ├─heads
│ │ Child_Project_ModuleA_dev_mazk
│ │ master
│ ├─remotes
│ │ └─origin
│ │ HEAD
│ └─tags
├─objects
│ ├─2a
│ │ 2930930f1db1cf12fab02be5e77a01701cc06d
│ ├─........
│ ├─info
│ └─pack
│ pack-7fe7a4a2ab8943dcc9b710cf9e104930786d141e.idx
│ pack-7fe7a4a2ab8943dcc9b710cf9e104930786d141e.pack
└─refs
├─heads
│ master
│ Parent_Project_dev_mazk
├─remotes
│ └─origin
│ HEAD
│ Parent_Project_dev_mazk
└─tags

第六步,查看当前的项目的变化,发现子模块项目上多了一个@e70f8969(知道就行

通过上述的现象,可以得出结论:两个仓库已经关联起来了,并且Child_Project_ModuleAparent_project的子仓库

第七步,主项目中更新子模块的代码

(1)、更新子模块方法一:外层更新;

$ git submodule update --remote
①、进入到子模块的项目目录下,查看当前的状态
$ git status

在配置了子模块的变量的值后,使用git submodule update --remote进行更新操作,则会产生游离状态的commit ID;此种情况下不要进行任何的修改,先切换到指定的分支上,执行git pull即可;一旦修改并提交了,则记住commit ID,切换至指定的分支上使用 git cherry-pick的方式将最新提交的补丁合并到切换后的分支上。

②、进入到主项目的目录下,查看当前状态,并添加更新的子模块的代码
$ git status
 git commit -am "更新了远端子模块的代码"
 $ git push
③、查看当前gitlab上项目的状态

发现由原来的@e70f8969变成@7aa1f728

总结

默认的更新,是按配置文件进行更新;如果想按配置文件的配置进行子模块的更新的更新


(2)、更新子模块方法二:外层更新

$ git submodule foreach git pull
$ git submodule status
$ git commit -am "更新了子模块A的最新代码"
$ git push

发现由原来的@7aa1f728变成@30e0facc

(3)、更新子模块方法三:进入子模块的项目目录进行更新

直接进入到子项目的目录,执行git pull 进行子项目的拉取即可,此处不再详细展开

第八步,克隆带有子模块的项目

$ git clone git@192.168.68.23:DevOps/Top/parent_project.git
$ cd parent_project/
$ git branch -av
$ git switch Parent_Project_dev_mazk
$ ls -la
total 10
drwxr-xr-x 1 naura 197121   0 Dec  4 09:54 ./
drwxr-xr-x 1 naura 197121   0 Dec  4 09:53 ../
drwxr-xr-x 1 naura 197121   0 Dec  4 09:54 .git/
-rw-r--r-- 1 naura 197121 187 Dec  4 09:54 .gitmodules    #此时发现引入了子模块的配置文件
-rw-r--r-- 1 naura 197121  96 Dec  4 09:54 README.md
drwxr-xr-x 1 naura 197121   0 Dec  4 09:54 src/
$ tree .git   #  进行目录树结构查看
$ tree src/    #查看子模块的文件内容

进入到子模块的目录进行查看

$ git submodule status
$ cat .gitmodules

发现:src/Child_project_ModuleA项目的目录是空的,并且此时进入到该子模块的项目后并没有显示当前的项目分支信息,任然在主项目的分支上。

此时必须运行两个命令:1)git submodule init用来初始化本地配置文件;2)git submodule updata则从该项目中抓取所有数据并检出到父项目中的暂存区。

第九步,初始化并更新克隆带有子模块的项目

$ git submodule init

此步骤,只是建立了初始化认证,并没有进行任何的任何代码的拉取,版本库及子模块的项目代码并没有被引入和检出;此时需要执行第二个命令;

第十步,更新主项目中关联的子模块

$ git submodule update

$ git submodule status

发现原来克隆步骤时的减号(-)消失了;


$ tree .git

第十一步,进入到子模块的项目,切换至指定的分支上

$ cd src/
$ cd Child_Project_ModuleA/
$ git branch -av
$ git switch Child_Project_ModuleA_dev_mazk
$ git branch -av

第十二步,回退至主项目的目录,进行更新后的提交

$ cd ../..
$ git submodule status
$ git status
$ git commit -am "更新了子模块的最新变化"
$ git submodule status
$ git status
$ git push

第十三步,主项目中修改子模块的代码,并推送至远端子模块的仓库

$ git status
$ touch parent_w_child.txt
$ echo "在主项目里面进入到子模块的目录,进行代码修改和编辑,然后推送至子模块的项目" >> parent_w_child.txt
$ git add parent_w_child.txt
$ git commit -m "主项目中修改了子项目的代码"
$ git push

第十四步,主项目中添加子模块中修改的代码,并推送至远端主项目库

$ cd ../..
$ git status
$ git submodule status
$ git commit -am "添加子模块中的修改至主项目库"
$ git push

第十五步,主项目中删除添加的子模块

(1)、先删除主项目文件夹下的.gitmodules ;

$ rm -fr .gitmodules

(2)、再删除主项目文件夹下的.git下的config中子模块的信息;

$ cd .git/
$ vim config
:set number 回车
:17,19d 回车
:wq 回车

(3)、紧接着删除主项目文件夹下的.git下的modules文件夹;【如果有多个子模块,只删除一个,则进入到modules文件下,删除对应的子模块目录】

$ ls
HEAD  config  description  hooks/  index  info/  logs/  modules/  objects/  packed-refs  refs/
$ rm -fr modules/

(4)、删除子模块所在的文件夹;

$ cd ..
$ rm -fr src/

(5)、最后查看一下当前的状态,确认后进行删除的提交;

$ git status
$ git add .
$ git commit -m "删除了子模块A"
$ git push

第十六步,不设置任何,添加子模块默认的情况

$ git submodule add  git@192.168.68.23:DevOps/Top/Sub/Child_Project_ModuleA.git 
$ ls -la
$ cat .gitmodules
$ git submodule status
$ git status
$ git commit -m "默认配置添加子模块A至主项目"
$ git push

2、git submodule 子模块中的问题梳理

问题1. 在有子模块的项目中切换分支可能会比较难以理解,比如主项目中的Parent_Project_dev_mazk这个分支上有子模块,则当切换至没有子模块的分支上时,发现会有一个未���踪的子模块的目录;

$ git switch Parent_Project_dev
$ git status
$ rm -fr src/
$ git status
$ git switch Parent_Project_dev_mazk
$ git submodule status
$ git submodule update  --init
$ git submodule status
$ cd src/Child_project_ModuleA/
$ git branch -av
$ git switch Child_project_ModuleA_dev_mazk

问题1总结

Parent_Project_dev_mazk,这是带有子模块的分支

Parent_Project_dev,这是不带子模块的分支

从带有子模块的分支切换至不带子模块的分支,会将子模块的目录改为未追踪的文件,此时如果此分支不想要子模块的部分,则直接删除此目录rm -fr src/,此时查看git的状态git status为干净的树。但是切换至有子模块的分支是,则会导致之前初始化的子模块会断连,解决措施很简单:只需要重新初始化及更新git submodule update ----init并进入子模块的目录cd src/Child_project_ModuleA/切换至指定的分支git switch Child_project_ModuleA_dev_mazk即可解决删除导致断连的问题。

三、git-subtree的命令介绍及使用

将子树合并在一起并将存储库拆分为子树,用 Git Subtree 在多个 Git 项目间双向同步子项目

将子树合并在一起并将存储库拆分为子树,子树允许将子项目包含在主项目的子目录中,可选择包含子项目的整个历史

  1. 一个项目下如何引入另一个项目
  2. 如果 子项目 有更新,如何同步到 主项目中
  3. 如果开发 主项目的过程中,修改了 子项目代码,如何将修改的代码同步到 子项目中

1、以实际例子进行讲解SubModule

第一步,克隆主项目Parent_Project主项目

$ git clone git@192.168.68.23:DevOps/Top/parent_project.git

第二步,进入主项目切换分支

$ cd parent_project/
$ git branch -av
$ git switch Parent_Project_dev_mazk
$ ls -la

第三步,添加子目录, 建立与git项目的关联

通过从给定的 <local-commit> 或 <repository> 和 <remote-ref> 导入其内容来创建 <prefix> 子树。自动创建一个新提交,将导入项目的历史与您自己的历史结合起来。使用--squash,仅从子项目导入单个提交,而不是其整个历史记录。可以省略--squash标志,但这样做会增加本地存储库中包含的提交数量
[git subtree <options> -P <prefix> add <local-commit>]   #添加本地的提交树至主项目的存储库做为子树;
[git subtree <options> -P <prefix> add <repository> <remote-ref>]#添加远端仓库至主项目的存储库做为子树;
[git subtree <options> -P <prefix> merge <local-commit>]#将本地的提交合并进子树;
[git subtree <options> -P <prefix> split <local-commit>]#将本地的提交拆分;
[git subtree <options> -P <prefix> pull <repository> <remote-ref>]#将远端仓库子树的变化更新下来;
[git subtree <options> -P <prefix> push <repository> <refspec>]#将本地子树的变化推送至远端仓库;
-P:--prefix=<prefix>   #指定存储库中要操作的子树的路径。此选项对于所有命令都是必需的;–prefix之后的=等号也可以用空格。
--squash    #不要合并子树项目的整个历史记录,而是只生成一个包含您要合并的所有差异的提交,然后将该新提交合并到您的项目中,使用此选项有助于减少日志混乱.
-m:--message=<message>
-f:fetch #获取remote的最新变化至本地

假设我当前的主项目是Parent_Project主项目现在想在该项目中引入另一个共用的子项目为Common,操作步骤如下。

(1)、主项目添加远端仓库(前提条件必需项

语法:git remote add <子仓库名> <子仓库地址>
$ git remote add Common git@192.168.68.23:DevOps/Top/Sub/Common.git #不会执行git fetch 拉取;
$ git remote add -f  Common git@192.168.68.23:DevOps/Top/Sub/Common.git#会执行git fetch拉取;

(2)、通过从给定的仓库,导入其内容来创建 子树(前提条件必需项

语法:git subtree add –prefix=<子目录名> <子仓库名> <分支> ----squash
$ git subtree add -P src/Common --squash git@192.168.68.23:DevOps/Top/Sub/Common.git common_dev
# -P src/Common等价于–prefix=src/Common
$ git branch -av
$ git log --color --graph

可以看出,本地新增了两个提交,但是看不到子树Common的提交历史,这是--squash起了作用;后期使用中,一旦开始的时候使用了此参数,后续的所有步骤都必须添加此参数,否则会引发冲突。

第四步,主项目未修改, Common 有更新,更新Common的新代码

语法:git subtree pull –prefix=<子目录名> <远程分支> <分支> –squash
git subtree pull -P src/Common --squash git@192.168.68.23:DevOps/Top/Sub/Common.git common_dev
$ git log --color --graph

可以看到多了两次提交:一次是 squash 整理的提交,一次是合并的提交(但是合并的时候有冲突需要解决,因为不是同源合并因此必然冲突)。

第五步,从子目录push到远程仓库

语法:git subtree push –prefix=<子目录名> <子远程仓库地址> <分支名称>

$ git subtree push -P src/Common git@192.168.68.23:DevOps/Top/Sub/Common.git common_dev

第六步,删除子树

语法:git remote remove name

$ git remote remove Common

父项目修改了子项目中的文件,然后子项目做了更新且成功,然后子项目又做了一次提交然后push,父项目进行更新此时就出现了冲突,一个独立的提交,和要待合并的提交没关系)在合并时,都修改了相同的文件,所以造成了两者合并时出现冲突

第七步,高阶功能,拆分子树

重新split出一个新起点(这样,每次提交subtree的时候就不会从头遍历一遍了)

git subtree [<options>] -P <prefix> split [<local-commit>]

子树的历史中提取一个新的合成项目历史,如果没有给出 ,则从 HEAD 的历史中提取。新的历史记录仅包括影响 的提交(包括合并),并且这些提交中的每一个现在都在项目的根目录而不是子目录中具有 的内容

(1)、将本地的某个目录拆分成一个单独的分支

$ git subtree split -P src/lib -b lib

(2)、将本地的新创建的分支,推送至远端仓库

$ git push --set-upstream origin lib

(3)、清理 master 分支上所有跟 src/lib 目录有关的痕迹

$ tree
$ git filter-branch --index-filter "git rm -rf --cached --ignore-unmatch src/lib" --prune-empty Parent_Project_dev_mazk

(4)、初始化一个新项目,并拉取parent_project的Parent_Project_dev_mazk分支到新初始化的项目

(5)、gitlab创建一个空项目parent_project_fresh,并添加空项目的地址,然后将本地新进的历史推送至远端

$ git remote add origin git@192.168.68.23:DevOps/Top/Parent_project_Fresh.git
$ git push -u origin master

用上述(4),(5)的方法再将lib进行新项目的初始化及推送;

最后再按照子模块和子树的方式添加lib项目进行专人维护和开发及嵌套使用。

总结:

subtree的方案适用的场景是:各个项目共用一个库,而这个库正在快速迭代更新的过程中。如果追求稳定,只需要给库拉出一个如v0.1.0这样的版本号命名的稳定分支,subtree只用这个分支即可。

如果想要历史记录干净一点,可以使用 --squash,可用于 add, merge, push, pull 命令。但是加了 --squash 也有自己的问题,上面已做描述,这个最好一开始就统一使用一种

将子仓库推送到远端很慢,相当于对主仓库下某个子仓库的目录的提交记录进行分割,以找出子仓库的修改,这个过程有点慢。

子树不像子模块会添加多余的文件,对仓库无污染

posted on 2021-12-07 18:57  DevOps_SRE  阅读(541)  评论(0编辑  收藏  举报