起因
这个需求起源于我的一个仓库 DawningW/Microcontroller-Projects, 这个仓库存储着我自高三开始接触嵌入式以来到现在的所有项目. 但前年发现随着我接触的单片机越来越多, 仓库变的越来越大, 不方便他人拉取仓库并贡献代码, 而且其中的某些玩具项目也越做越大, 例如 rgblight, 都放在同一个仓库里也不方便别人 star 和推广. 所以急需一种办法分离子目录到单独的仓库里, 而且还需要保留历史提交记录(要不然直接复制粘贴到新仓库里不就好了= =).
解决方案
我记得那节课是个什么实验课, 我做完实验没事干就开始搜该怎么做, 然后搜到了 git subtree
可以实现这个目的, 具体步骤如下所示:
# 1. 将子目录提取到一个新分支上 git subtree split --prefix=<子目录> -b <新分支名称(我一般就用子目录名作为新分支名)> # 2. 在其他地方新建空仓库并拉取刚才创建的新分支 mkdir ../<新仓库目录> && cd ../<新仓库目录> git init git pull ../<仓库目录> <新分支名称> # 3. 提交新仓库 git remote add origin <xxx.git> git push -u origin master # 4. 清理旧仓库 cd ../<仓库目录> git branch -D <新分支名称> git rm -rf <子目录> git commit -m 'Remove <子目录>' git push
一通操作之后, 子目录里的代码就和历史提交记录一起拆分到新的仓库里了, 就是这么简单. 具体的效果请见 DawningW/Microcontroller-Projects (未拆分) 和 https://git.dawncraft.cc/IoT (已拆分), 这里我并没有选择全部拆分, 而是仅仅把可能会做大的项目拆了出来, 毕竟模板和开发板的 DEMO 实在没必要单独放到一个仓库里.
subtree 的真正用途
事情到这里还没有结束, 你可以注意到我在 DawningW/Microcontroller-Projects 这个项目的描述中写道”我的单片机项目, 仅做备份和存放个人测试项目用, 大部分项目均已迁移至自建git”. 因此我实际上只是想把子目录拆分到我自己的 Git 平台上的仓库, 但是在 GitHub 上的仓库保持原状(这么做是为了先在自己的 Git 平台上进行开发, 待作品完善后再在 GitHub 上新建仓库), 而且二者还需要同步提交.
这实际上是一个管理子仓库的需求, 也就是说我自己 Git 平台上的仓库需要成为 GitHub 上的主仓库的子仓库, 且二者都包含完整的代码及提交记录. Git 本身提供了两种管理子仓库的方式, 分别是 git submodule 和 git subtree, 下面是这两个命令的对比:
submodule:
- 是 Git 的命令
- 需要增加
.gitmodule
元数据文件用于记录子仓库的分支和提交号 - 添加子仓库时只会向
.gitmodule
中添加一条记录, 不会占用太多父仓库空间 - 由于父仓库里只有元数据, 所以 clone 时需要添加
--recurse-submodules
参数或执行submodule init/update
克隆子仓库 - 子仓库更新简单, 只需
git submodule update
, 提交时则比较复杂, 需先提交子仓库, 然后更新子仓库, 最后提交父仓库
subtree:
- 是自 v1.5.2 起被集成进 Git 的第三方脚本, 本质上是对已有命令的简化
- 无需元数据, 对拉取者无感
- 添加子仓库时会把整个子仓库拷贝到父仓库中, 包括历史记录(除非使用
--squash
参数), 会占用大量空间 - 由于父仓库中包含完整的子仓库, clone 时无需其他操作
- 子仓库的更新与推送指令相对复杂(但相比原始命令已经很简化了), 使用
git subtree pull/push
命令, 参数较多且每个子仓库均需执行一遍
一言以蔽之, submodule 是链接, 而 subtree 是拷贝. 在我看来两者没有孰好孰坏之分, 该用谁要按具体需求而定. 这里 GitHub 上的仓库作为备份, 我希望他能包含完整的源代码而无需到我自己 Git 平台上去克隆, 所以我选择了 subtree. 而如果你只是想把第三方库包含进来, 且不会对第三方库进行修改, 那么用 submodule 足矣.
git subtree 的基本用法如下所示:
# 添加子仓库 (如果不想保留子仓库的提交记录, 可以加上 --squash 参数) git subtree add --prefix=<子仓库相对路径> <xxx.git> <要拉取的子仓库分支> # 拉取子仓库代码 (也可以加 --squash 参数将提交记录压为一条) git subtree pull --prefix=<子仓库相对路径> <xxx.git> <要拉取的子仓库分支> # 推送子仓库代码 git subtree push --prefix=<子仓库相对路径> <xxx.git> <要推送的子仓库分支>
你可能已经发现了, 每次操作都需要输入子仓库的源仓库地址, 这好麻烦啊, 不过不要担心, 地址支持从 remote 中获取, 所以我们只要添加一个 remote 就好了. 还是以 DawningW/Microcontroller-Projects/rgblight 和 https://git.dawncraft.cc/IoT/RGBLight 为例, 将我自己的 Git 平台上的 IoT/RGBLight 项目作为子仓库添加到 GitHub 上的 DawningW/Microcontroller-Projects 项目中的命令如下所示:
git remote add -f rgblight https://git.dawncraft.cc/IoT/RGBLight.git
git subtree add -P rgblight rgblight master # --prefix 可以简写为 -P
git subtree pull -P rgblight rgblight master
# 如果修改后想要推送提交只需把 pull 换成 push
现在这个命令已经很简单了, 但是我的子仓库有点多, 如果每次都一个个输效率很低, 所以我又写了两个批处理脚本用来一键拉取/推送子仓库, 见 DawningW/Microcontroller-Projects 根目录下的 autopull.bat
和 autopush.bat
.
进行到这里我的目标基本上完全实现了, 我感觉已经非常完美了…吗?
还有坑?
我在尝试编译 minichatserver 这个子仓库时出现了问题, 因为这个子仓库里用 submodule 引入了一个第三方库, 而 Git 只识别根目录下的 .gitmodules, 导致子仓库引用的子模块没克隆下来. 在网上搜了一圈也没找到太好的解决方案, 最后还是选择在父仓库和子仓库的根目录各放一份 .gitmodules, 提醒大家注意一下这个坑吧.