Git 子倉庫深入淺出

前言

在前端日常開發中, 我們經常 git 來當做代碼版本管理工具, 使用中基本都是一個項目一個 Git 倉庫的形式, 那麼當我們的代碼中碰到了業務級別的需要複用的代碼, 我們一般怎麼做呢?

我們大致的考慮一下, 一般有兩種方案:

在塗鴉的小程序業務場景開發中, 兩個程序中有部分頁面是重疊的, 開發過程中重疊部分如果開發兩套代碼會浪費不少的人力, 考量之後決定使用 Git 子模塊的方式進行開發, 父級倉庫依賴兩個公共的子模塊, 子模塊本身和父級倉庫一同進行開發, 避免了版本問題和重複開發的問題。

我們在下面介紹的子倉庫的使用場景基本都是如下的開發方式:

多個父級倉庫都依賴同一個子倉庫, 但是子倉庫自身不單獨進行修改, 而是跟隨父級項目進行更新發布, 其他依賴子倉庫的項目只負責拉取更新即可。

那麼什麼是 Git 的子倉庫呢?

通俗上的理解, 一個 Git 倉庫下面放了多個其他的 Git 倉庫, 其他的 Git 倉庫就是我們父級倉庫的子倉庫。

在正式開始介紹 git 的子倉庫之前, 我們要提前認識到一點, 在剛開始使用 Git 子倉庫的時候, 如果不是很瞭解底層原理, 很可能會導致使用子倉庫出現雲裏霧裏的現象, 搞不清楚是父級倉庫先提交, 還是子倉庫先提交, 所以在本教程中, 我們會先介紹子倉庫的兩種使用方式, 然後攜帶一些子倉庫的 Git 底層的分析, 讓大家對子倉庫有一個更加全面的認識。

Git 兩種子倉庫使用方案
  1. git submodule

  2. git subtree

我們按照順序分別演示這兩種子倉庫的使用方式, 方便大家深入理解兩種子倉庫的使用方式:

git submodule(子模塊)

Git 子模塊允許我們將一個或者多個 Git 倉庫作爲另一個 Git 倉庫的子目錄, 它能讓你將另一個倉庫克隆到自己的項目中, 同時還保持提交的獨立。

我們演示一下 git submodule的使用方法:

爲了方便後續對兩種子倉庫的原理進行講解, 我們會詳細的描述 git 的相關操作步驟

開始使用子模塊

使用 git init--bare在本地創建兩個裸倉庫, 分別表示主倉庫和依賴的子倉庫, 我們將主倉庫命名爲 main, 依賴的子倉庫命名爲 lib, git subtree使用同樣的初始化方法, 下文不再贅述。

# 爲了方便演示,我們使用/path/to/repos代表你當前開發的絕對路徑
# 比如筆者的/path/to/repos代表/Users/userName/Documents/work
git --git-dir=/path/to/repos/main.git init --bare # 初始化主倉庫
git --git-dir=/path/to/repos/lib.git init --bare # 初始化子倉庫
# 本地拉取到這兩個倉庫
git clone /path/to/repos/main.git
git clone /path/to/repos/lib.git
# 我們分別對這兩個倉庫進行一次提交
cd main
echo "console.log('main');"
> index.js
git add .
git commit -m "feat: 父級倉庫創建index.js"
git push
cd ../lib
echo 
"console.log('utils');"> util.js
git add .
git commit -m "feat: 子倉庫創建util.js"
git push

初始化結束兩個子倉庫後, 我們想讓 main主倉庫能夠使用 lib倉庫的代碼進行後續的開發, 使用 git submodule add的命令後面加上想要跟蹤項目 URL 來添加新的子模塊 (本文中的 lib倉庫)。

# 首先進入到main的工作目錄下
cd main
# 添加lib模塊到main倉庫下的lib同名目錄
git submodule add /path/to/repos/lib.git

默認情況下, 子模塊會被添加到項目的子模塊同名的目錄下, 如果想放到其他目錄. 在 add命令的結尾跟上放置目錄的相對路徑即可。

執行完上述命令後, 我們查看 main倉庫下當前的目錄結構:

tree
.
├── index.js
├── .gitmodules
└── lib
└── util.js

我們發現 lib倉庫已經被放到 main倉庫下的 lib目錄下面了, 同時還要注意的是, Git 爲我們創建了一個 .gitmodules文件, 這個配置文件中保存了子倉庫項目的 URL 和在主倉庫目錄下的映射關係:

cat .gitsubmodules
[submodule "lib"]
    path = lib
    url = /path/to/repos/lib.git

執行 git status發現有了新的文件

git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
        new file:   .gitmodules
        new file:   lib

我們對 main倉庫進行一次提交:

git add .
git commit -m "feat: 增加子倉庫依賴"
git push

操作結束後, 我們的 main倉庫就依賴了 lib倉庫的代碼並且已經上傳到了雲端的倉庫當中, 那麼別人應該怎麼去克隆包含子模塊的項目呢?

克隆含有子項目的倉庫

當我們正常克隆 main項目的時候, 我們會發現, main倉庫中雖然包含 lib文件夾, 但是裏面並不包含任何內容, 彷彿就是一個空文件夾:

git clone /path/to/repos/main.git
Cloning into 'main1'...
done.
cd main
tree # 使用tree命令查看當前目錄,省略隱藏文件
.
├── index.js
└── lib

此時你需要運行 git submodule的另外兩個命令, 不需要擔心, submodule的命令不會太多。

首先執行 git submodule init用來初始化本地配置文件, 也就是向 .git/config文件中寫入了子模塊的信息。

git submodule update則是從子倉庫中抓取所有的數據找到父級倉庫對應的那次子倉庫的提交 id 並且檢出到父項目的目錄中。

git submodule init
Submodule'lib'(/path/to/repos/lib.git) registered for path 'lib'
git submodule update
done.
Submodule path 'lib': checked out '40f8536319ede421cfd9ca9f9904b5106946e8ec'

現在我們查看 main倉庫下的目錄結構, 會發現和我們之前的提交的結構保持一致了, 我們成功的拉取到了父級倉庫和相關依賴子倉庫的代碼。

tree
.
├── index.js
└── lib
└── util.js

上述命令着實有些麻煩, 有沒有簡單一些的命令能夠直接拉取整個倉庫的代碼的方式呢? 答案是有的, 我們使用 git clone--recursive,Git 會自動幫我們遞歸去拉取我們所有的父倉庫和子倉庫的相關內容。

git clone --recursive /path/to/repos/main.git
Cloning into 'main'...
done.
Submodule'lib'(/path/to/repos/lib.git) registered for path 'lib'
Cloning into '/path/to/repos/main/lib'...
done.
Submodule path 'lib': checked out '40f8536319ede421cfd9ca9f9904b5106946e8ec'

在主倉庫上進行協同開發

我們在 main倉庫下對 lib文件夾做了一些修改, 然後我們想提交父倉庫 ( main) 和子倉庫 ( lib) 的修改, 此時首先我們應該先提交子倉庫的修改。

$ cd lib

當我們執行完上述命令後發現, lib目錄竟然包含了一整個完整的 git 倉庫, 甚至包含了 .git目錄。

但是我們也發現當前不在 libmaster分支上, 而是在一個遊離分支上面, 這個遊離分支的 hash 正式 lib倉庫的 master分支的 hash 值, 這正是 git submodule爲我們所做的, Git 不關心我們開發的分支, 而只是去拉取子倉庫對應的 commit提交

所以我們需要先切換到正常分支, 然後正常操作 git 倉庫一樣去進行子倉庫的提交。

git add .
git commit -m "子倉庫進行修改"
git push

子倉庫提交結束後, 我們回到 main倉庫的主目錄下, 執行 git status:

git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
        modified:   lib (new commits)
no changes added to commit (use "git add" and/or "git commit -a")

我們發現本次的 Git 的 status 和以往有些不一樣的地方, Git 並沒有告訴我們當前到底修改了什麼文件, 而是說 lib下有一次新的提交, 我們記住這個點, 正常將主倉庫進行提交併且 push 到雲端倉庫即可。

git submodule使用下來發現, submodule 本身就是一個大的 Git 倉庫下包含了多個子的 Git 倉庫, 我們修改之後, 首先對每個子倉庫進行了提交, 然後父級倉庫就會記錄下每個子倉庫的提交, 然後正常提交父級倉庫即可, 拉取也是同樣的過程, 如果是在子倉庫的分支上開發, 也是先拉取子倉庫, 隨後拉取父級倉庫的更新, 此處不再贅述。

如果覺得對每個子倉庫進行提交繁瑣的話, git sumoduleforeach就可以解決你這個煩惱:

# main目錄下
git submodule foreach git pull

我們對所有的子倉庫拉取了一次最新的代碼, foreach後面使用的就是你要對子模塊使用的 git 命令。

那麼還有一個問題, 我們在修改了子倉庫提交後, 回到父級倉庫執行 git status後爲什麼 git 不像以前一樣告訴我們具體的文件更新信息呢, 而是給出了 modified:lib(newcommits)這樣一串奇怪的信息, 而這正式 git submodule的底層實現原理。

git submodule原理分析

我們知道 Git 底層大致依賴了四種對象, 構成了 Git 對於文件內容追蹤的基礎:

更加詳細的內容請參考深入理解 Git, 閱讀後更容易理解後續知識點哦~

我們此處需要依賴一個 print_all_object的工具函數, 它會幫助我們將 git 倉庫下的這四種對象按照反向提交歷史的排序展現出來, 可以將它放在環境變量下方便全局使用:

#!/bin/bash
print_all_object() {
for object in`git rev-list --objects --all | cut -d ' ' -f 1`; do
    echo 'SHA1: ' $object
    git cat-file -p $object
    echo '-------------------------------'
done
}
print_all_object

我們在 main倉庫下執行 print_all_object:

# 此時處於我們剛對子模塊提交的那個時間點
# 對部分長的hash進行了截取處理,不影響閱讀觀感
print_all_object
SHA1:  a1cfd26e
tree c77ba9c2
parent ab118b8
feat: 增加子倉庫依賴
-------------------------------
SHA1:  ab118b8
tree f5771cd
feat: 父級倉庫創建index.js
-------------------------------
SHA1:  c77ba9c2
100644 blob d8c9fb4    .gitmodules
100644 blob ddd81ae    index.js
160000 commit 40f8536  lib
-------------------------------
SHA1:  d8c9fb4
[submodule "lib"]
        path = lib
        url = /path/to/repos/lib.git
-------------------------------
SHA1:  ddd81ae
console.log('main');-------------------------------
SHA1:  f5771cd
100644 blob ddd81ae    index.js
-------------------------------

我們查看 feat:增加子倉庫依賴此次 commit對象的 tree對象, 發現內容如下:

SHA1:  c77ba9c
100644 blob d8c9fb456  .gitmodules
100644 blob ddd81aef    index.js
160000 commit 40f85363  lib

index.js文件是 blob對象, 對應的 file mode 是 100644, 但是對於 lib子倉庫的確是一個 commit對象, file mode 爲 160000, 這是 Git 中一種特殊的模式, 表明我們是將一次提交的 commit記錄在 Git 當中, 而非將它記錄成一個子目錄或者文件。

而這正式 git submodule的核心原理, Git 在處理 submodule引用的時候, 並不會去掃描子倉庫下的文件的變化, 而是取子倉庫當前的 HEAD指向的 commit的 hash 值, 當我們對子倉庫進行了更改後, Git 獲取到子模塊的 commit值發生變化, 從而記錄了這個 Git 指針的變化。

在暫存區所以我們才發現了 newcommits這種提示語, Git 並不關心子模塊的文件如何變化, 我只需要在當前提交中記錄子模塊的 commit 的 hash 值即可, 之後我們從父級倉庫拉取子倉庫的時候, Git 拉取了本次提交記錄中的子模塊的 hash 值對應的提交, 就還原了我們的整個倉庫的代碼。

git submodule注意點

雖然使用 git submodule爲我們的開發帶來了很多便利, 但是隨之而來也會導致一些比較容易犯的錯誤, 整理出來, 防止大家採坑:

當子模塊有提交的時候, 沒有 push 到遠程倉庫, 父級引用子模塊的 commit 更新, 並提交到遠程倉庫, 當別人拉取代碼的時候就會報出子模塊的 commit 不存在 fatal:reference isn’t a tree

如果你僅僅引用了別人的子模塊的遊離分支, 然後在主倉庫修改了子倉庫的代碼, 之後使用 git submodule update拉取了最新代碼, 那麼你在子倉庫遊離分支做出的修改會被覆蓋掉。

我們假設你一開始在主倉庫並沒有採用子模塊的開發方式, 而是在另外的開發分支使用了子倉庫, 那麼當你從開發分支切回到沒有采用子模塊的分支的時候, 子模塊的目錄並不會被 Git 自動刪除, 而是需要你手動的刪除了😭。

git subtree(子樹合併)

上面介紹的 git submodule是 Git 自帶的原生功能, 我們接下來將要介紹的 git subtree則是由第三方開發者貢獻的 contrib script,Git 本身並不提供 git subtree命令, contrib 中包含一些實驗性的第三方工具, 由各自的作者進行維護。

同時這也讓我們認識到 git subtree不是 Git 原生支持的命令, 而是第三方開發者通過 Git 的底層命令寫出的一個高層次腳本, 所以它是可以由基礎的 Git 命令來實現的, 我們稍後會介紹 git subtree的實現原理, 在此之前, 還是先來介紹一下 git subtree的基礎使用吧!

開始使用子樹合併

我們按照子模塊時講的, 首先在本地創建 mainlib的子倉庫, 然後對兩個倉庫各自進行一次提交, 代碼此處不再重複展示。

初始化結束兩個子倉庫後, 我們想讓 main主倉庫能夠使用 lib倉庫的代碼進行後續的開發, 使用 git subtree add的命令後面加上想要跟蹤項目 URL 來添加新的子模塊 (本文中的 lib倉庫), 這段和之前的步驟基本一致, 但是命令使用方式不同。

cd main
git subtree add --prefix=lib /path/to/repos/lib.git master
git fetch /path/to/repos/lib.git master
warning: no common commits
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3)done.
remote: Total3(delta 0), reused 0(delta 0)
Unpacking objects: 100% (3/3)done.
From/path/to/repos/lib
* branch            master     -> FETCH_HEAD
Added dir 'lib'

我們首先分析一下加入子倉庫的命令的參數部分:

執行完上述命令後, 我們查看 main倉庫下當前的目錄結構:

tree
.
├── index.js
└── lib
└── util.js

我們發現 lib倉庫已經被放到 main倉庫下的 lib 目錄下面了, 除了一個 lib文件夾外, git subtree並沒有額外的標記信息去記錄我們子倉庫的地址, 這也正是 git subtree命令繁瑣的地方, add命令和其他的命令每次都要指定我們子倉庫的遠程地址, 如果你覺得複雜, 可以使用 git remote add lib/path/to/repos/lib.git去給遠程服務器取一個別名, 以後就可以使用 git subtree add--prefix=lib lib master進行其他類似命令的操作了。

我們進入到 lib文件夾下面, 查看文件夾下面的文件, 我們發現 git subtree並沒有包含 .git文件夾, 這也是和 git submodule不一致的地方, lib在主倉庫下就像是一堆子倉庫的文件, 我們並不能從 lib下進行子倉庫的提交, 而是要使用 git subtree其他的命令進行提交和拉取操作, subtree表現的就像是所有的代碼都在我們的主倉庫下, 並不存在什麼其他的子倉庫一樣。

克隆含有子項目的倉庫

對包含 subtree的主倉庫進行拉取非常簡單, 就像我們拉取普通倉庫一樣支持克隆即可。

git clone /path/to/repos/main.git

並不摻雜什麼額外的操作, 我們進入到倉庫下發現, 主倉庫和子倉庫的代碼都已經包含在當前目錄下面了。

在主倉庫上進行協同開發

當我們在主倉庫對 lib進行修改後, 我們執行 git status查看一下:

git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
        modified:   lib/util.js
no changes added to commit (use "git add" and/or "git commit -a")

我們發現了 subtreesubmodule不一樣的另外一點, 子倉庫的更改是會反映在主倉庫的更改上面的, 我們剛纔也提到 lib就是代碼目錄, 沒辦法像 submodule一樣直接在子倉庫中提交, 所以 subtree提供了另外兩個命令來對代碼進行拉取和推送的操作

如果當前情況下我們只在主倉庫下對子倉庫的代碼進行了修改, 無論如何我們都需要對主倉庫進行一次提交 (針對本文開始講的大前提的條件下), 這就是 subtree的提交模式, 從我們的主倉庫的提交歷史下拆分部分 commit出去給子倉庫進行提交, 現在不理解也沒有關係, 我們等會在講述 subtree原理的時候會提到這個地方。

我們首先提交主倉庫:

# 省略部分輸出信息
git add .
git commit -m "修改了lib的代碼"

然後我們嘗試去推送子包代碼的更新:

# 首先要拉取一下子倉庫是否存在更新
# 如果拉取子倉庫的過程中存在衝突,我們需要在主倉庫解決衝突後重新提交一次commit
git subtree pull --prefix /path/to/repos/lib.git master
From/path/to/repos/lib
* branch            master     -> FETCH_HEAD
Subtree is already at commit a5f21e31a721920ba7007949f3db59df4b543436.
# push代碼到子倉庫,我們不難發現 subtree的命令格式基本都是一致的
git subtree push --prefix /path/to/repos/lib.git master
git push using:  /path/to/repos/lib.git master
Enumerating objects: 8, done.
Counting objects: 100% (8/8)done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2)done.
Writing objects: 100% (4/4), 511 bytes | 511.00KiB/s, done.
Total4(delta 0), reused 0(delta 0)
To/path/to/repos/lib.git
   a5f21e3..67387da67387da30c87c97bb4e0020be18e8da7a720dbab-> master

這樣子我們就在主倉庫完成了對子倉庫的拉取和推送, subtree使用起來我們發現並沒有什麼奇怪的地方, 但是它確實幫我們管理了子倉庫的代碼, 那麼 subtree是怎麼做到的呢?

git subtree原理分析

我們主要對 subtree新增子倉庫和合並子倉庫更新的原理進行講解, push的操作我們稍後會簡單的提一下。

首先 subtree是如何將子倉庫的代碼加入到我們的子倉庫的呢? git subtree我們中文一般翻譯成 "子樹合併", 那麼正如我們理解的, subtree正是利用了 Git 的的底層的 tree對象和一些相關對象完成了增加子倉庫的操作, 我們簡短的總結爲下面這麼一句話:

在主倉庫中我們通過 Git 拉取到子倉庫下的分支代碼到主倉庫的另外一根分支中, 通過類似 merge的操作, 將代碼合併到我們主倉庫的某個目錄下, 此時會生成的一次提交對象, 這個提交對象 parent 引用了我們主倉庫的當前 commit 對象和子倉庫的當前 commit 對象, 就完成了子倉庫的新增, 主倉庫也順便記錄了子倉庫的所有文件。

這句話有點繞? 沒關係, 我們用 Git 的代碼來演示一下這個過程, 當然首先我們還是要初始化 mainlib兩個倉庫, 並且對各自進行一次初始化的提交。

git subtree add原理解析

然後我們對 git subtree add的過程進行拆解, 使用了部分 Git 的底層命令, 我們首先對它們進行一個簡單介紹:

git read-tree: 將某次 commit 的提交讀取到當前的暫存區, 工作區不變;

git write-tree: 將暫存區保存的文件生成一個頂級的 tree 對象, 返回 tree-hash;

git commit-tree: 生成一個 commit 對象, 可以指定引用的 tree對象, parent對象和提交信息;

git cat-file: 查看 git 底層四種對象信息;

git可以設置不同的 remote 的遠程服務器地址, 我們之前使用 git fetch都是獲取同一個項目的文件, 但是 git fetch也可以獲取其他項目 (比如子倉庫) 的內容, 放到另外一根分支上, 然後通過合併的策略來完成子倉庫的代碼拉取。

main倉庫進行初始化子倉庫的操作

爲了方便之後對遠程倉庫做跟蹤, 我們在 remote 上面對倉庫做一個別名:

cd main
git remote add lib /path/to/repos/lib.git # git添加服務器別名

首先, 我們需要將子倉庫的代碼拉取到主倉庫裏面來:

git fetch lib
warning: no common commits
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3)done.
remote: Total3(delta 0), reused 0(delta 0)
Unpacking objects: 100% (3/3)done.
From/path/to/repos/lib
* [new branch]      master     -> lib/master

查看一下當前的所有分支:

git branch -a
* master
  remotes/lib/master
  remotes/origin/master

將遠程的 lib分支代碼首先合併到 main 倉庫的 lib分支上面來方便後續操作。

# 創建lib的本地分支
git checkout -b lib lib/master
Branch'lib'set up to track remote branch 'master' from 'lib'.
Switched to a new branch 'lib'
# 然後切換回master分支
git checkout master

當前子模塊 ( lib) 的代碼已經到了我們本地的一根分支上面了, 我們和 submodule的使用方式一樣, 也要將 lib文件夾放到 main文件夾下面使用, 所以需要將子倉庫的代碼合併到主倉庫的一根分支上面來:

查看當前 lib分支的最新提交的 commit-id, 然後獲取到相對應的 tree-id:

# 查看lib對應的tree-id
git cat-file -p lib
tree 309cc0d
author userName <email> 1577349863+0800
committer userName <email> 1577349863+0800
feat: 創建lib文件
# 查看當前tree對象下面的文件:
# 當前tree對象下面包含我們在lib創建的index.js文件
git cat-file -p 309cc0d
100644 blob 043a685d4c9b48a74bdde1181ab22b53f8a8e363  index.js

我們回到 main倉庫下, 調用 git read-treelib分支下的代碼讀取到暫存區和工作目錄下:

# -u 指定在更新暫存區文件成功後,將文件一起寫入到工作目錄下
# --prefix 指定需要將文件放到哪個子目錄下
git read-tree -u --prefix=lib lib

執行 git status查看當前倉庫狀態:

git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
        new file:   lib/index.js

我們發現 lib倉庫的代碼以子目錄的形式存在於 main上面了, 現在還不能夠提交, 因爲現在沒有體現出兩個分支的合併關係, 之後主倉庫下修改了子倉庫代碼就會導致不方便拆分子倉庫代碼, 如果現在直接提交, 僅僅只是我們將 lib的代碼類似拷貝的操作放到了主倉庫下, 所以我們還需要調用 git write-treegit commit-tree生成一次新的 commit 對象建立起來分支間的聯繫:

# 因爲read-tree已經將子倉庫的文件添加到了暫存區
# 我們可以調用write-tree對當前的暫存區文件生成一個tree對象
git write-tree
e0551170da5e913efe6a97c1137f4da890688243

之後我們手動創建一次合併提交, 也就是 commit對象, 這個提交要有兩個父對象, 分別是 main的提交對象和 lib的提交對象, 用 git rev-parse首先獲取到兩次提交的 ID:

git rev-parse master
57d55a545a24a10df7b4be4e3eb79dc3c3c195e6
git rev-parse lib
b8bcfe5c2ec04c7c028bbab8dfd3dbea53dcb745

執行 commit-tree命令手動創建提交, 提交需要的 tree對象來自於我們 write-tree生成的 tree對象, 父級的提交用上面兩個 ID, 我們需要輸出一些提交消息到 commit對象中來顯示當前這次提交做了些什麼事情, commit-tree支持從輸入流中獲取提交消息:

echo "初次合併子模塊"| git commit-tree e0551170da5e913efe6a97c1137f4da890688243 \
-p 
57d55a545a24a10df7b4be4e3eb79dc3c3c195e6 \
-p b8bcfe5c2ec04c7c028bbab8dfd3dbea53dcb745
# 此處顯示的是commit對象的ID;
e54dc295322387a460130f33ed571f724c700fe8

然後此時 master分支並沒有指向到我們新生成的這次 commit對象上面, 我們使用 git reset將分支強指到本次提交對象上面來:

git reset e54dc295322387a460130f33ed571f724c700fe8

查看一下當前的提交歷史, 可以看到我們將父倉庫和子倉庫的提交合併到了一起:

git log --graph --pretty=oneline
*   e54dc295322387a460130f33ed571f724c700fe8 (HEAD -> master) 初次合併子模塊
|\
| * b8bcfe5c2ec04c7c028bbab8dfd3dbea53dcb745 (lib/master, lib) feat: 創建lib文件
* 57d55a545a24a10df7b4be4e3eb79dc3c3c195e6(origin/master) feat: main創建

這樣我們就完成了對子樹的初次合併的操作, 整個過程還是比較繁瑣的, 需要對 Git 的 Plumbing(底層命令)有較爲清楚的認知, 不過我們可以使用 git subtree add就可以完成上述的操作;

git subtree pull原理解析

如果 lib的上游代碼提交了新的代碼, 我們就需要將新代碼合併到 main倉庫下面來, 之前講解了 git subtree pull的用法, 我們現在簡單分析下 pull這個行爲的原理:

首先我們在 main倉庫下切換到 lib分支, 將最新代碼通過 git pull拉取回來, case 比較簡單, 此處不再進行贅述, 切換回到 master分支, 我們利用 git merge子樹合併策略將代碼合併到 lib文件夾下, 如果直接 merge 的話, 代碼就不會合併到子文件夾下面。

# -X指定merge的合併策略爲subtree
git merge -Xsubtree=lib lib

這樣子我們就將子倉庫的上游更新合併到了主倉庫下, 此處涉及到的合併策略我們不再進行詳細分析, 衝突解決都是提升到主倉庫級別進行處理的和 submodule的形式不太一致, 有興趣的讀者可以對 git subtree的源碼進行擴展閱讀。

git subtree push原理解析

push的操作在 subtree的底層涉及到了 split的方式對代碼進行拆分, 我們講解一下 git subtree split的使用。

# 在main倉庫下的master分支進行操作
git subtree split --prefix=lib -b lib-split
Created branch 'lib-split'
8e8086648ff4956062a7239beebef7494d47c137

split操作指定 main下面需要分割的目錄, 按照上次的子倉庫的提交 ID 進行拆分到指定的分支上面去, 我們的 git subtree push底層也依賴了這個操作。

我們假設 main倉庫下我們新增了三次提交, 其中一次提交包含了對 lib文件夾的修改, split會找到上次包含 lib的 commit 的 ID 然後進行查找當前這三次提交有沒有針對子倉庫的修改。

此時 split會找出三次修改中哪次 commit對象修改了子倉庫, 然後將它的 tree對象找出, 然後將此次 commit對象的 commit-message 合併創建一次新的針對 libcommit對象, 重新設置本次提交的父類對象。

之後這些修改拆分後拷貝到另外一根分支上, 這根分支僅包含 lib子倉庫的修改, 然後使用 git push指定提交的遠程倉庫提交到 lib下面。

subtree提交的原理我們只進行了簡單的分析, 有興趣的讀者可以對 git subtree的源碼進行擴展閱讀。

總結

我們對 submodulesubtree的用法以及原理都進行了比較詳細的闡述, 因爲涉及內容比較多, 而且原理部分大多是 Git 的底層機制, 閱讀起來可能有些難度, 我們大多用了 Git 的演示來對這些內容進行了分析, 但因爲筆者能力一般水平有限, 可能存在部分不正確的地方, 大家可以對內容進行糾正 。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ivz2GUEajAhJmXZyW6CEIw