Git應用詳解第十講:Git子庫:submodule與subtree

AhuntSun發表於2020-04-20

前言

前情提要:Git應用詳解第九講:Git cherry-pick與Git rebase

一箇中大型專案往往會依賴幾個模組,git提供了子庫的概念。可以將這些子模組存放在不同的倉庫中,通過submodulesubtree實現倉庫的巢狀。本講為Git應用詳解的倒數第二講,勝利離我們不遠了!

一、submodule

submodule:子模組的意思,表示將一個版本庫作為子庫引入到另一個版本庫中:

image-20200408224205125

1.引入子庫

需要使用如下命令:

git submodule add 子庫地址 儲存目錄

比如:

 git submodule add git@github.com:AhuntSun/git_child.git mymodule

執行上述命令會將地址對應的遠端倉庫作為子庫,儲存到當前版本庫的mymodule目錄下:

隨後檢視當前版本庫的狀態:

image-20200329203048016

可以發現新增了兩個檔案。檢視其中的.gitmodules檔案:

image-20200329203507411

可以看到當前檔案的路徑和子模組的url,隨後將這兩個新增檔案新增提交推送。在當前倉庫git_parent對應的遠端倉庫中多出了兩個檔案:

image-20200329203746236

其中mymodule資料夾上的3bd7f76 對應的是子倉庫git_child中的最新提交

image-20200329203905051

點選mymodule資料夾,會自動跳轉到子倉庫中:

image-20200329203957392

通過上述分析,可以得出結論:兩個倉庫已經關聯起來了,並且倉庫git_child為倉庫git_parent的子倉庫;

2.同步子庫變化

當被依賴的子版本庫發生變化時:在子版本庫git_child中新增檔案world.txt並提交到遠端倉庫:

image-20200329204252524

這個時候依賴它的父版本庫git_parent要如何感知這一變化呢?

方法一

這個時候git_parent只需要進入存放子庫git_child的目錄mymodule,執行git pull就能將子版本庫git_child的更新拉取到本地:

image-20200330102106961

方法二

當父版本庫git_parent依賴的多個子版本庫都發生變化時,可以採用如下方法遍歷更新所有子庫:首先回到版本庫主目錄,執行以下指令:

 git submodule foreach git pull

該命令會遍歷當前版本庫所依賴的所有子版本庫,並將它們的更新拉取到父版本庫git_parent

image-20200330102642607

拉取完成後,檢視狀態,發現mymodule目錄下檔案發生了變化,所以需要執行一次新增、提交、推送操作:

image-20200330102914556

3.複製父版本庫

如果將使用了submodule新增依賴了子庫的父版本庫git_parent,克隆一份到本地的話。在克隆出來的新版本庫git_parent2中,原父版本庫存放依賴子庫的目錄雖在,但是內容不在:

image-20200330103417911

進入根據git_parent複製出來的倉庫git_parent2,會發現mymodule目錄為空:

image-20200330103502848

解決方法:可採用多條命令的分步操作,也可以通過引數將多步操作進行合併。

分步操作

這是在執行了clone操作後的額外操作,還需要做兩件事:

  • 手動初始化submodule

    git submodule init
    
  • 手動拉取依賴的子版本庫;:

    git submodule update --recursive
    

image-20200330103803762

執行完兩步操作後,子版本庫中就有內容了。由此完成了git_parent的克隆;

合併操作

分步操作相對繁瑣,還可以通過新增引數的方式,將多步操作進行合併。通過以下指令基於git_parent克隆一份git_parent3

git clone git@github.com:AhuntSun/git_parent.git git_parent3 --recursive

image-20200330104210732

--recursive表示遞迴地克隆git_parent依賴的所有子版本庫。

4.刪除子版本庫

git沒有提供直接刪除submodule子庫的命令,但是我們可以通過其他指令的組合來達到這一目的,分為三步:

  • submodule從版本庫中刪除:

    git rm --cache mymodule
    

image-20200330105131697

git rm的作用為刪除版本庫中的檔案,並將這一操作納入暫存區;

  • submodule從工作區中刪除;

image-20200330105226923

  • 最後將.gitmodules目錄刪除;

image-20200330105542069

完成三步操作後,再進行新增,提交,推送即可完成刪除子庫的操作:

image-20200330105614793

二、subtree

1.簡介

subtreesubmodule的作用是一樣的,但是subtree出現得比submodule晚,它的出現是為了彌補submodule存在的問題:

  • 第一:submodule不能在父版本庫中修改子版本庫的程式碼,只能在子版本庫中修改,是單向的;
  • 第二:submodule沒有直接刪除子版本庫的功能;

subtree則可以實現雙向資料修改。官方推薦使用subtree替代submodule

2.建立子庫

首先建立兩個版本庫:git_subtree_parentgit_subtree_child然後在git_subtree_parent中執行git subtree會列出該指令的一些常見的引數:

image-20200330112616987

3.建立關聯

首先需要給git_subtree_parent新增一個子庫git_subtree_child:

第一步:新增子庫的遠端地址:

 git remote add subtree-origin git@github.com:AhuntSun/git_subtree_child.git

新增完成後,父版本庫中就有兩個遠端地址了:

image-20200330113223780

這裡的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)中的提交一個一個地拉取到本地再去建立一個合併提交;
  • 如果增加了這個引數,會將遠端子庫指定分支上的多次提交合並壓縮成一次提交再拉取到本地,這樣拉取到本地的,遠端子庫中的,指定分支上的,歷史提交記錄就沒有了。

image-20200330114203889

拉取完成後,父版本庫中會增添一個subtree目錄,裡面是子庫的檔案,相當於把依賴的子庫程式碼拉取到了本地:

image-20200330114316257

此時檢視一下父版本庫的提交歷史:
image-20200330114500554

會發現其中沒有子庫李四的提交資訊,這是因為--squash引數將他的提交壓縮為一次提交,並由父版本庫張三進行合併和提交。所以父版本庫多出了兩次提交。

隨後,我們在父版本庫中進行一次推送:

image-20200330114730534

結果遠端倉庫中多出了一個存放子版本庫檔案的subtree目錄,並且完全脫離了版本庫git_subtree_child,僅僅是屬於父版本庫git_subtree_parent的一個目錄。而不像使用submodule那樣,是一個點選就會自動跳轉到依賴子庫的指標

  • subtree的遠端父版本庫:

image-20200330115004586

  • submodule的遠端父版本庫:

image-20200329203746236

submodulesubtree子庫的區別為:

image-20200408224805624

4.同步子庫變化

在子庫中建立一個新檔案world並推送到遠端子庫:
image-20200330115440136

在父庫中通過如下指令更新依賴的子庫內容:

git subtree pull --prefix=subtree subtree-origin master --squash

image-20200330115726052

此時檢視一下提交歷史:

image-20200330115755340

發現沒有子庫李四的提交資訊,這都是--squash的作用。子庫的修改交由父庫來提交。

5.引數--squash

該引數的作用為:防止子庫指定分支上的提交歷史汙染父版本庫。比如在子庫的master分支上進行了三次提交分別為:abc,並推送到遠端子庫。

首先,複習一下合併分支時遵循的三方合併原則:

image-20200408003842196

當提交46需要合併的時候,git會先尋找二者的公共父提交節點,如圖中的2,然後在提交2的基礎上進行246的三方合併,合併後得到提交7

父倉庫執行pull操作時:如果新增引數--squash,就會把遠端子庫master分支上的這三次提交合併為一次新的提交abc;隨後再與父倉庫中子庫的master分支進行合併,又產生一次提交X。整個pull的過程一共產生了五次提交,如下圖所示:

image-20200420103912282

存在的問題:

由於--squash指令的合併操作,會導致遠端master分支上的合併提交abc與本地master分支上的最新提交2,找不到公共父節點,從而合併失敗。同時push操作也會出現額外的問題。

最佳實踐:要麼全部操作都使用--squash指令,要麼全部操作都不使用該引數,這樣就不會出錯。

錯誤示範:

為了驗證,重新建立兩個倉庫AB,並通過subtreeB設定為A的子庫。這次全程都沒有使用引數--squash,重複上述操作:

  • 首先,修改子庫檔案;
  • 然後,通過下列指令,在不使用引數--squash的情況下,將遠端子庫A變化的檔案拉取到本地:
git subtree pull --prefix=subtree subtree-origin master

image-20200330141920474

此時檢視提交歷史:

image-20200330142000915

可以看到子庫兒子的提交資訊汙染了父版本庫的提交資訊,驗證了上述的結論。

所以要麼都使用該指令,要麼都不使用才能避免錯誤;如果不需要子庫的提交日誌,推薦使用--squash指令。

補充:echo 'new line' >> test.txt:表示在test.txt檔案末尾追加文字new line;如果是一個>表示替換掉test.txt內的全部內容。

6.修改子庫

subtree的強大之處在於,它可以在父版本庫中修改依賴的子版本庫。以下為演示:

進入父版本庫存放子庫的subtree目錄,修改子庫檔案child.txt,並推送到遠端父倉庫:

image-20200330121429186

此時遠端父版本庫中存放子庫檔案的subtree目錄發生了變化,但是獨立的遠端子庫git_subtree_child並沒有發生變化。

  • 修改獨立的遠端子庫:

    可執行以下命令,同步地修改遠端子版本庫:

    git subtree push --prefix=subtree subtree-origin master
    

    如下圖所示,父庫中的子庫檔案child.txt新增的child2內容,同步到了獨立的遠端子庫中:

    image-20200330125911158

  • 修改獨立的本地子庫:

    回到本地子庫git_subtree_child,將對應的遠端子庫進行的修改拉取到本地進行合併同步:

    image-20200330144044823

    由此無論是遠端的還是本地的子庫都被修改了。

實際上使用subtree後,在外部看起來父倉庫和子倉庫是一個整體的倉庫。執行clone操作時,不會像submodule那樣需要遍歷子庫來單獨克隆。而是可以將整個父倉庫和它所依賴的子庫當做一個整體進行克隆。

存在的問題

父版本庫拉取遠端子庫進行更新同步會出現的問題:

  • 子倉庫第一次修改:

    經歷了上述操作,本地子庫與遠端子庫的檔案達到了同步,其中檔案child.txt的內容都是child~4。在此基礎上本地子庫為該檔案新增child5~6

    image-20200330145702019

    然後推送到遠端子庫。

  • 父倉庫第一次拉取:

    隨後父版本庫通過下述指令,拉取遠端子庫,與本地父倉庫git_subtree_parent中的子庫進行同步:

     git subtree pull --p subtree subtree-origin master --squash
    

    結果出現了合併失敗的情況:

    image-20200330145839093

    我們檢視衝突產生的檔案:

    image-20200330145922152

    發現父版本庫中的子庫與遠端子庫內容上並無衝突,但是卻發生了衝突,這是為什麼呢?

    探究衝突產生的原因之前我們先解決衝突,先刪除多餘的內容:

    image-20200330150141430

    隨後執行git add命令和git commit命令標識解決了衝突:

    image-20200330150312944

    image-20200330150406317

    解決完衝突後將該檔案推送到獨立的遠端子庫,發現檔案並沒有發生更新,也就是說git認為我們並沒有解決衝突:

    image-20200330150747452

  • 子倉庫第二次修改與父倉庫第二次拉取:

    再次修改本地子庫的檔案並推送到對應的遠端倉庫,父版本庫再次將遠端子庫更新的檔案拉取到本地進行同步:

    image-20200330151140092

    這次卻成功了!為什麼同樣的操作,有的時候成功有的時候失敗呢?

解決方案

原因出現在--squash指令中。實際上,--squash指令把子庫中的提交資訊合併了,導致父倉庫在執行git pull操作時找不到公共的父節點,從而導致即使檔案沒有衝突的內容,也會出現合併衝突的情況。其實不使用--squash也會有這種問題,問題的根本原因仍然是三方合併時找不到公共父節點。我們開啟gitk

image-20200330154944300

從圖中不難看出,當使用subtree時,子庫與父庫之間是沒有公共節點的,所以時常會因為找不到公共節點而出現合併衝突的情況,此時只需要解決衝突,手動合併即可。

不使用subtree時,普通的版本庫中的各分支總會有一個公共節點:

image-20200330160206258

再次強調:使用--squash指令時一定要小心,要麼都使用它,要麼都不使用。

7.抽離子庫

git subtree split

當開發過程中出現某些子庫完全可以複用到其他專案中時,我們希望將它獨立出來。

  • 方法一:可以手動將檔案拷貝出來。缺點是,這樣會丟失關於該子庫的提交記錄;
  • 方法二:使用git subtree split指令,該指令會把關於獨立出來的子庫的每次提交都記錄起來。但是,這樣存在弊端:
    • 比如該獨立子庫為company.util,當一次提交同時修改了company.utilcompany.server兩個子庫時。
    • 通過上述命令獨立出來的子庫util只會記錄對自身修改的提交,而不會記錄對company.server的修改,這樣在別人看來這次提交就只修改了util,這是不完整的。

以上就是本講的全部內容,主要介紹了git子庫的基本使用方法。下一講將是Git應用詳解系列的完結篇:Git工作流Gitflow。我們下一講再見!

相關文章