Git小寄巧之使用subtree将子目录拆分到独立仓库

起因

这个需求起源于我的一个仓库 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 的真正用途

注意: 如果你只想了解如何拆分仓库, 不需要使用 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/rgblighthttps://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.batautopush.bat.

进行到这里我的目标基本上完全实现了, 我感觉已经非常完美了…吗?

还有坑?

我在尝试编译 minichatserver 这个子仓库时出现了问题, 因为这个子仓库里用 submodule 引入了一个第三方库, 而 Git 只识别根目录下的 .gitmodules, 导致子仓库引用的子模块没克隆下来. 在网上搜了一圈也没找到太好的解决方案, 最后还是选择在父仓库和子仓库的根目录各放一份 .gitmodules, 提醒大家注意一下这个坑吧.

标题: Git小寄巧之使用subtree将子目录拆分到独立仓库
作者: QingChenW
链接: https://dawncraft.cc/2023/10/532/
本文遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 许可
禁止商用, 非商业转载请注明作者及来源!
上一篇
下一篇
隐藏