Git應用詳解第十講:Git子庫:submodule與subtree.md
前言
前情提要:Git應用詳解第九講:Git cherry-pick與Git rebase
一箇中大型專案往往會依賴幾個模組,git
提供了子庫的概念。可以將這些子模組存放在不同的倉庫中,通過submodule
或subtree
實現倉庫的巢狀。本講為Git
應用詳解的倒數第二講,勝利離我們不遠了!
一、submodule
submodule
:子模組的意思,表示將一個版本庫作為子庫引入到另一個版本庫中:
1.引入子庫
需要使用如下命令:
git submodule add 子庫地址 儲存目錄
比如:
git submodule add [email protected]:AhuntSun/git_child.git mymodule
執行上述命令會將地址對應的遠端倉庫作為子庫,儲存到當前版本庫的mymodule
目錄下:
隨後檢視當前版本庫的狀態:
可以發現新增了兩個檔案。檢視其中的.gitmodules
檔案:
可以看到當前檔案的路徑和子模組的url
,隨後將這兩個新增檔案新增、提交併推送。在當前倉庫git_parent
對應的遠端倉庫中多出了兩個檔案:
其中mymodule
資料夾上的3bd7f76
對應的是子倉庫git_child
中的最新提交:
點選mymodule
資料夾,會自動跳轉到子倉庫中:
通過上述分析,可以得出結論:兩個倉庫已經關聯起來了,並且倉庫git_child
為倉庫git_parent
的子倉庫;
2.同步子庫變化
當被依賴的子版本庫發生變化時:在子版本庫git_child
中新增檔案world.txt
並提交到遠端倉庫:
這個時候依賴它的父版本庫git_parent
要如何感知這一變化呢?
方法一
這個時候git_parent
只需要進入存放子庫git_child
的目錄mymodule
,執行git pull
就能將子版本庫git_child
的更新拉取到本地:
方法二
當父版本庫git_parent
依賴的多個子版本庫都發生變化時,可以採用如下方法遍歷更新所有子庫:首先回到版本庫主目錄,執行以下指令:
git submodule foreach git pull
該命令會遍歷當前版本庫所依賴的所有子版本庫,並將它們的更新拉取到父版本庫git_parent
拉取完成後,檢視狀態,發現mymodule
目錄下檔案發生了變化,所以需要執行一次新增、提交、推送操作:
3.複製父版本庫
如果將使用了submodule
新增依賴了子庫的父版本庫git_parent
,克隆一份到本地的話。在克隆出來的新版本庫git_parent2
中,原父版本庫存放依賴子庫的目錄雖在,但是內容不在:
進入根據git_parent
複製出來的倉庫git_parent2
,會發現mymodule
目錄為空:
解決方法:可採用多條命令的分步操作,也可以通過引數將多步操作進行合併。
分步操作
這是在執行了clone
操作後的額外操作,還需要做兩件事:
-
手動初始化
submodule
:git submodule init
-
手動拉取依賴的子版本庫;:
git submodule update --recursive
執行完兩步操作後,子版本庫中就有內容了。由此完成了git_parent
的克隆;
合併操作
分步操作相對繁瑣,還可以通過新增引數的方式,將多步操作進行合併。通過以下指令基於git_parent
克隆一份git_parent3
:
git clone [email protected]:AhuntSun/git_parent.git git_parent3 --recursive
--recursive
表示遞迴地克隆git_parent
依賴的所有子版本庫。
4.刪除子版本庫
git
沒有提供直接刪除submodule
子庫的命令,但是我們可以通過其他指令的組合來達到這一目的,分為三步:
-
將
submodule
從版本庫中刪除:git rm --cache mymodule
git rm
的作用為刪除版本庫中的檔案,並將這一操作納入暫存區;
- 將
submodule
從工作區中刪除;
- 最後將
.gitmodules
目錄刪除;
完成三步操作後,再進行新增,提交,推送即可完成刪除子庫的操作:
二、subtree
1.簡介
subtree
與submodule
的作用是一樣的,但是subtree
出現得比submodule
晚,它的出現是為了彌補submodule
存在的問題:
- 第一:
submodule
不能在父版本庫中修改子版本庫的程式碼,只能在子版本庫中修改,是單向的; - 第二:
submodule
沒有直接刪除子版本庫的功能;
而subtree
則可以實現雙向資料修改。官方推薦使用subtree
替代submodule
。
2.建立子庫
首先建立兩個版本庫:git_subtree_parent
和git_subtree_child
然後在git_subtree_parent
中執行git subtree
會列出該指令的一些常見的引數:
3.建立關聯
首先需要給git_subtree_parent
新增一個子庫git_subtree_child
:
第一步:新增子庫的遠端地址:
git remote add subtree-origin [email protected]:AhuntSun/git_subtree_child.git
新增完成後,父版本庫中就有兩個遠端地址了:
這裡的subtree-origin
就代表了遠端倉庫git_subtree_child
的地址。
第二步:建立依賴關係:
git subtree add --prefix=subtree subtree-origin master --squash
//其中的--prefix=subtree可以寫成:--p subtree 或 --prefix subtree
該命令表示將遠端地址為subtree-origin
的,子版本庫上master
分支的,檔案克隆到subtree
目錄下;
注意:是在某一分支(如
master
)上將subtree-origin
代表的遠端倉庫的某一分支(如master
)作為子庫拉取到subtree
資料夾中。可切換到其他分支重複上述操作,也就是說子庫的實質就是子分支。
--squash
是可選引數,它的含義是合併,壓縮的意思。
- 如果不增加這個引數,則會把遠端的子庫中指定的分支(這裡是
master
)中的提交一個一個地拉取到本地再去建立一個合併提交; - 如果增加了這個引數,會將遠端子庫指定分支上的多次提交合並壓縮成一次提交再拉取到本地,這樣拉取到本地的,遠端子庫中的,指定分支上的,歷史提交記錄就沒有了。
拉取完成後,父版本庫中會增添一個subtree
目錄,裡面是子庫的檔案,相當於把依賴的子庫程式碼拉取到了本地:
此時檢視一下父版本庫的提交歷史:
會發現其中沒有子庫李四的提交資訊,這是因為--squash
引數將他的提交壓縮為一次提交,並由父版本庫張三進行合併和提交。所以父版本庫多出了兩次提交。
隨後,我們在父版本庫中進行一次推送:
結果遠端倉庫中多出了一個存放子版本庫檔案的subtree
目錄,並且完全脫離了版本庫git_subtree_child
,僅僅是屬於父版本庫git_subtree_parent
的一個目錄。而不像使用submodule
那樣,是一個點選就會自動跳轉到依賴子庫的指標:
subtree
的遠端父版本庫:
submodule
的遠端父版本庫:
即submodule
與subtree
子庫的區別為:
4.同步子庫變化
在子庫中建立一個新檔案world
並推送到遠端子庫:
在父庫中通過如下指令更新依賴的子庫內容:
git subtree pull --prefix=subtree subtree-origin master --squash
此時檢視一下提交歷史:
發現沒有子庫李四的提交資訊,這都是--squash
的作用。子庫的修改交由父庫來提交。
5.引數--squash
該引數的作用為:防止子庫指定分支上的提交歷史汙染父版本庫。比如在子庫的master
分支上進行了三次提交分別為:a
、b
、c
,並推送到遠端子庫。
首先,複習一下合併分支時遵循的三方合併原則:
當提交4
和6
需要合併的時候,git
會先尋找二者的公共父提交節點,如圖中的2
,然後在提交2
的基礎上進行2
、4
、6
的三方合併,合併後得到提交7
。
父倉庫執行pull
操作時:如果新增引數--squash
,就會把遠端子庫master
分支上的這三次提交合併為一次新的提交abc
;隨後再與父倉庫中子庫的master
分支進行合併,又產生一次提交X
。整個pull
的過程一共產生了五次提交,如下圖所示:
存在的問題:
由於--squash
指令的合併操作,會導致遠端master
分支上的合併提交abc
與本地master
分支上的最新提交2
,找不到公共父節點,從而合併失敗。同時push
操作也會出現額外的問題。
最佳實踐:要麼全部操作都使用--squash
指令,要麼全部操作都不使用該引數,這樣就不會出錯。
錯誤示範:
為了驗證,重新建立兩個倉庫A
和B
,並通過subtree
將B
設定為A
的子庫。這次全程都沒有使用引數--squash
,重複上述操作:
- 首先,修改子庫檔案;
- 然後,通過下列指令,在不使用引數
--squash
的情況下,將遠端子庫A
變化的檔案拉取到本地:
git subtree pull --prefix=subtree subtree-origin master
此時檢視提交歷史:
可以看到子庫兒子
的提交資訊汙染了父版本庫的提交資訊,驗證了上述的結論。
所以要麼都使用該指令,要麼都不使用才能避免錯誤;如果不需要子庫的提交日誌,推薦使用--squash
指令。
補充:
echo 'new line' >> test.txt
:表示在test.txt
檔案末尾追加文字new line
;如果是一個>
表示替換掉test.txt
內的全部內容。
6.修改子庫
subtree
的強大之處在於,它可以在父版本庫中修改依賴的子版本庫。以下為演示:
進入父版本庫存放子庫的subtree
目錄,修改子庫檔案child.txt
,並推送到遠端父倉庫:
此時遠端父版本庫中存放子庫檔案的subtree
目錄發生了變化,但是獨立的遠端子庫git_subtree_child
並沒有發生變化。
-
修改獨立的遠端子庫:
可執行以下命令,同步地修改遠端子版本庫:
git subtree push --prefix=subtree subtree-origin master
如下圖所示,父庫中的子庫檔案
child.txt
新增的child2
內容,同步到了獨立的遠端子庫中: -
修改獨立的本地子庫:
回到本地子庫
git_subtree_child
,將對應的遠端子庫進行的修改拉取到本地進行合併同步:由此無論是遠端的還是本地的子庫都被修改了。
實際上使用
subtree
後,在外部看起來父倉庫和子倉庫是一個整體的倉庫。執行clone
操作時,不會像submodule
那樣需要遍歷子庫來單獨克隆。而是可以將整個父倉庫和它所依賴的子庫當做一個整體進行克隆。
存在的問題
父版本庫拉取遠端子庫進行更新同步會出現的問題:
-
子倉庫第一次修改:
經歷了上述操作,本地子庫與遠端子庫的檔案達到了同步,其中檔案
child.txt
的內容都是child~4
。在此基礎上本地子庫為該檔案新增child5~6
:然後推送到遠端子庫。
-
父倉庫第一次拉取:
隨後父版本庫通過下述指令,拉取遠端子庫,與本地父倉庫
git_subtree_parent
中的子庫進行同步:git subtree pull --p subtree subtree-origin master --squash
結果出現了合併失敗的情況:
我們檢視衝突產生的檔案:
發現父版本庫中的子庫與遠端子庫內容上並無衝突,但是卻發生了衝突,這是為什麼呢?
探究衝突產生的原因之前我們先解決衝突,先刪除多餘的內容:
隨後執行
git add
命令和git commit
命令標識解決了衝突:解決完衝突後將該檔案推送到獨立的遠端子庫,發現檔案並沒有發生更新,也就是說
git
認為我們並沒有解決衝突: -
子倉庫第二次修改與父倉庫第二次拉取:
再次修改本地子庫的檔案並推送到對應的遠端倉庫,父版本庫再次將遠端子庫更新的檔案拉取到本地進行同步:
這次卻成功了!為什麼同樣的操作,有的時候成功有的時候失敗呢?
解決方案
原因出現在--squash
指令中。實際上,--squash
指令把子庫中的提交資訊合併了,導致父倉庫在執行git pull
操作時找不到公共的父節點,從而導致即使檔案沒有衝突的內容,也會出現合併衝突的情況。其實不使用--squash
也會有這種問題,問題的根本原因仍然是三方合併時找不到公共父節點。我們開啟gitk
:
從圖中不難看出,當使用subtree
時,子庫與父庫之間是沒有公共節點的,所以時常會因為找不到公共節點而出現合併衝突的情況,此時只需要解決衝突,手動合併即可。
不使用
subtree
時,普通的版本庫中的各分支總會有一個公共節點:
再次強調:使用--squash
指令時一定要小心,要麼都使用它,要麼都不使用。
7.抽離子庫
git subtree split
當開發過程中出現某些子庫完全可以複用到其他專案中時,我們希望將它獨立出來。
- 方法一:可以手動將檔案拷貝出來。缺點是,這樣會丟失關於該子庫的提交記錄;
- 方法二:使用
git subtree split
指令,該指令會把關於獨立出來的子庫的每次提交都記錄起來。但是,這樣存在弊端:- 比如該獨立子庫為
company.util
,當一次提交同時修改了company.util
和company.server
兩個子庫時。 - 通過上述命令獨立出來的子庫
util
只會記錄對自身修改的提交,而不會記錄對company.server
的修改,這樣在別人看來這次提交就只修改了util
,這是不完整的。
- 比如該獨立子庫為
以上就是本講的全部內容,主要介紹了
git
子庫的基本使用方法。下一講將是Git
應用詳解系列的完結篇:Git
工作流Gitflow
。我們下一講再見!