Git Subtree是Git自带的命令,用于公共库的模块化,并共享给多个项目。
1. 前提
假设有两个主项目:B1和B2。分别存在两个独立的Git库中。
其中B1有一个模块C1,将来可以在B2中使用,宜做成公共模块。目前状况,已经实现了B1的全部,和B2不带C1的部分。

2. 目标
公共模块独立成一个单独的库,且B1,B2复用此库,在其中一个地方更新,可以同步到所有项目。

3. 将公共模块从主项目中分离出来
git subtree split
# B1项目包含两个模块:B1文件夹和C1文件夹
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B1 (develop)
$ ls
B1/ C1/
# 将子模块分离出来,并且重新合并到原项目中。分离出来的子模块同时也存在于一个独立的branch中
# --prefix= 也可以用 -P 替代,--branch= 也可以用 -b 替代
# 使用--rejoin 参数,这样分离后的公共库会自动合并到原项目中
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B1 (develop)
$ git subtree split --prefix=C1 --branch=temp_br_for_C1 --rejoin
Merge made by the 'ours' strategy.
Created branch 'temp_br_for_C1'
f7cc8ba379d95faf11529e4abeb1d61f22f231f7
此时B1的状态变成了这样,多了一个分支,原来分支内容没变

观察历史记录,可以看到多了两个commit

4. 把公共模块独立存成一个库
# 随便找个地方建立一个空的库,这个路径无所谓,也可以是在Gitlab网上建
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B1 (develop)
$ cd ..
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos
$ mkdir CommonModule_C1
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos
$ cd CommonModule_C1/
# 建立空的库
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/CommonModule_C1
$ git init
Initialized empty Git repository in D:/tmp/subtree/local_repos/CommonModule_C1/.git/
# 把刚才B1中的另一个分支pull到这个空的库中
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/CommonModule_C1 (master)
$ git pull ../MainProj_B1 temp_br_for_C1
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), 221 bytes | 27.00 KiB/s, done.
From ../MainProj_B1
* branch temp_br_for_C1 -> FETCH_HEAD
这样我们多了一个C1库。有需要的话,也可以把这个库push到远程服务器,这属于Git通用操作,这里就不叙述了。

这步骤操作以后,可以把B1中的这个branch删掉了(后面也不会再用到)
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B1 (develop)
$ git br -d temp_br_for_C1
Deleted branch temp_br_for_C1 (was f7cc8ba).
5. 将B1与C1库关联
其实这个步骤没有意义,B1和C1本身就是关联的,其奥秘就在于B1主分支中多出来的两个commit。
看看C1库的sha号,就是上面B1关联的那个。

当然我们也可以再确认一下
git subtree pull
# 尝试pull一下,结果发现B1果然没有什么变化。
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B1 (develop)
$ git subtree pull --prefix=C1 ../CommonModule_C1/.git master
From ../CommonModule_C1/
* branch master -> FETCH_HEAD
Subtree is already at commit f7cc8ba379d95faf11529e4abeb1d61f22f231f7.
因此这一步,我们什么都没做,其实已经是这样的关系。

6. 在B2中关联C1库
下面我们要在B2中也用这个公共模块。
git subtree add
# 从库C1中的master分支,取公共模块并放到B2项目的C1子目录
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B2 (develop)
$ git subtree add --prefix=C1 ../CommonModule_C1/.git master
git fetch ../CommonModule_C1/.git master
From ../CommonModule_C1/
* branch master -> FETCH_HEAD
Added dir 'C1'
此时B2项目的log,跟B1项目很像。

也就是完成了这样的操作

至此,综合起来,这3个库就形成了关联。用这种方法,可以关联更多的项目,没有数量限制。

7. 在一个项目改动后的push和pull
假设在B2项目,我们改动了公共模块C1并提交了。
# 修改文件内容
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B2/C1 (develop)
$ echo "add by B2" >> CC1.txt
...
# 提交等操作
...
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B2/C1 (develop)
$ git ci -m "modify C1 by B2"
[develop 25fad03] modify C1 by B2
1 file changed, 1 insertion(+)
此时B2的修改记录如下

将改动推送到C1库中
git subtree push
# 推送发现出错了,意思是,推送只能推送到bare类型的库中
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B2 (develop)
$ git subtree push --prefix=C1 ../CommonModule_C1/.git master
git push using: ../CommonModule_C1/.git master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 278 bytes | 278.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: error: refusing to update checked out branch: refs/heads/master
remote: error: By default, updating the current branch in a non-bare repository
...
推送发现出错了,意思是,推送只能推送到bare类型的库中,因此把C1库再加工一下。
注意如果C1是在Gitlab的话,就没这个问题,因为Gitlab上面的库全部都是bare类型的。
# 创建一个空的bare库
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos
$ git init CommonModule_C1_bare.git --bare
Initialized empty Git repository in D:/tmp/subtree/local_repos/CommonModule_C1_bare.git/
# 把C1库推送到这个新的库
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/CommonModule_C1 (master)
$ git push ../CommonModule_C1_bare.git master
Everything up-to-date
这步结束以后,原来的C1库也可以删掉了。当然也可以留着单独修改C1模块用。

此时再次将B2中的修改推送到C1库中
# 再次推送,这次就推送成功了
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B2 (develop)
$ git subtree push --prefix=C1 ../CommonModule_C1_bare.git master
git push using: ../CommonModule_C1_bare.git master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 278 bytes | 278.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To ../CommonModule_C1_bare.git
f7cc8ba..1bfb723 1bfb7238dd929509b40224447f2c871f6d5e445a -> master
此时查看C1库,看到多了一条提交。

注意:这条提交,还没有跟B2库产生关系。还需要Git subtree pull一下
git subtree pull
# 注意这里pull,会出现一个merge
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B2 (develop)$ git subtree pull --prefix=C1 ../CommonModule_C1_bare.git master
From ../CommonModule_C1_bare
* branch master -> FETCH_HEAD
Merge made by the 'recursive' strategy.
注意看这时候的B2的历史记录,可以发现两个现象
最新的提交,确实与C1库最新改动产生了关联(红色)
存在两个一样的提交(绿色)
其中2是正常现象,不必奇怪,因为Git Subtree中,主项目和公共项目本来就是两个独立的库,修改也是独立的。其中一条是主项目的修改,另一条是公共项目的修改。虽然存在两个提交,但是不会冲突,因为本来就是来源相同的。

8. 在其他项目中同步公共库的修改
在B1项目中,将公共库的改动同步下来
git subtree pull
# 执行的命令与刚才完全相同
twu@twu-PC MINGW64 /d/tmp/subtree/local_repos/MainProj_B1 (develop)
$ git subtree pull --prefix=C1 ../CommonModule_C1_bare.git master
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), 258 bytes | 19.00 KiB/s, done.
From ../CommonModule_C1_bare
* branch master -> FETCH_HEAD
Merge made by the 'recursive' strategy.
C1/CC1.txt | 1 +
1 file changed, 1 insertion(+)
可以看到B1已经同步了修改。B1的提交比较正常,不会出现重复的两条提交,因为并不是在B1这个项目中修改的。

9. 其他补充说明
- C1库也可以单独下载下来,直接在单独的库里修改并提交。这个库独立来看,跟普通的Git库毫无差异。
- B1,B2库以后也可以再也不同步C1的改动,它们完全独立存在,即使C1删掉,对它们也没什么影响。
- 这里C1的库我们全部是在master分支进行的修改,实际上也可以分出多个分支来,更好的管理。
10. 思考题
Git subtree这种方式,比我们自己维护一个独立的公共库,然后手动拷贝到各个项目中,有什么区别,有什么优势?