Git子模块
一、使用场景
当项目越来越庞大之后,不可避免的要拆分成多个子模块,我们希望各个子模块有独立的版本管理,并且由专门的人去维护,这时候我们就要用到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_ModuleA为parent_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、以实际例子进行讲解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>]
从
(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 也有自己的问题,上面已做描述,这个最好一开始就统一使用一种
将子仓库推送到远端很慢,相当于对主仓库下某个子仓库的目录的提交记录进行分割,以找出子仓库的修改,这个过程有点慢。
子树不像子模块会添加多余的文件,对仓库无污染