前言

最近用 Hugo 重新架了新的部落格,部落格本身是一個 git repo,但 Theme 的資料夾的內容是另一個 git repo,導致在不同電腦想要同步部落格內容時,遇到 Git Submodule 的議題,是一個實務開發上較少遇到的情境,所以順手紀錄一下操作記錄.

何謂 Git Submodule?

使用 Git Submodule 的情境是在開發專案時資料夾內有第三方 Library 或你正單獨開發並被多個父專案使用的子專案,此時你想要將兩個專案視為獨立的,但又希望可以在其中一個專案使用到另一個專案的內容.

Git Submodule 其實就是一個巢狀的 Git 檔案結構,他會幫你把專案內某個子資料夾視為 Library 處理,不會受到主專案的 Git 操作影響.

以下是我遇到 Git Submodule 的情境:

我的部落格資料夾內含有 Theme 主題,部落格資料夾是父資料夾,而 Theme 是子資料夾,因為他參照的是另一個第三方的 git remote url,也就是一個第三方維護的靜態主題專案,未來我可能會需要同步他上面最新的更新,或是切換到他專案某個時間的 commit,這時候我就需要將他視為和我部落格是不同的專案,但彼此卻又需要被互相引用,這時候 Git Submodule 就可以很好的幫我解決這個問題.

實戰演練

為了模擬巢狀的 Git 結構,在 GitLab 上分別建立一組父子專案,父專案名稱是 Parent,子專案名稱是 Child.建立好之後,分別 clone 到本地端的 parent 和 child 資料夾.

.
├── child
│   └── README.md
└── parent
    └── README.md

Git Submodule 連結父子專案

cd 進到 parent 資料夾後,用底下指令將 child 資料夾加為 parent 資料夾的 submodule

# git submodule add <remote repository> <local path>
git submodule add https://gitlab.com/DysonMa/child.git child
  • remote repository: 遠端子專案的 git remote url
  • local path: 預計要在 parent 資料夾的哪一個路徑底下存放 child 專案

執行後會發現 parent 資料夾多了 .gitmodules 檔案和 child 資料夾

.
├── child
│   └── README.md
└── parent
    ├── README.md
    ├── .gitmodules  # add
    └── child        # add
        └── README.md

打開 .gitmodules 內容如下:

[submodule "child"]
	path = child
	url = https://gitlab.com/DysonMa/child.git

這個檔案負責記錄子模組的參數,包含剛剛設定的 local path 還有子專案的 git remote url

情境一:修改父專案,觀察子專案變化

該情境模擬的是最基本的情境,當我部落格發布新文章後,推上去遠端 git repo 後,Theme 子資料夾會如何顯示在遠端 GitLab repo 上呢?

首先在 parent 資料夾內加上一個文字檔,並將他推上去父專案的遠端 repo,觀察 child 專案的變化

echo "Hello" >> parent.txt

git add .
git commit -m "Add parent.txt"
git push origin main

打開瀏覽器,看一下 GitLab 上剛剛推上去的 parent 專案

gitlab-parent

跟以往不同的是 child @ fb9ec1ef 並非資料夾的連結,而是連結到 child 專案的 remote url,所以點選後會跳轉到 child 的 repo,而後面的 fb9ec1ef 其實就是目前 child 專案的 commit hash 值.可以去本地端的 child 資料夾 git log 驗證

cd ../child
git log --oneline
fb9ec1e (HEAD -> main, origin/main, origin/HEAD) Initial commit

由此可知,我們可以透過該 commit 值知道父專案目前關聯的子專案是哪一個版本的.

情境二:子專案更新後,父專案內關聯的 submodule 如何同步?

該情境模擬的是哪一天 Theme 的專案有新的 feature,而我希望我的部落格專案也能夠同步享有該主題的新 feature,這時候該怎麼做?

在 child 專案內加上一個文字檔,並推上去遠端 child repo

echo "Hello"  >> child.txt

git add .
git commit -m "Add child.txt"
git push origin main

回到 parent 專案,將 sub module 所關聯的 child 專案更新,預計會看到剛剛新增的文字檔

cd ../parent

# 將sub module所關聯的遠端子專案內容抓取下來並merge
git submodule update --remote --merge
.
├── README.md
├── child
│   ├── README.md
│   └── child.txt  # 成功
└── parent.txt

需注意的是,目前只是 parent 的本地端更新,遠端 repo 並沒有更新,所以需要將這個改動 push 上去

git add .
git commit -m "Update sub module"
git push origin main

打開瀏覽器觀察 parent 遠端 GitLab repo 的 child ,的確如預期的更新 commit hash 值了,跳轉到 child 遠端 repo 也發現該版本確實是增加 child.txt 的版本

gitlab-parent-update

情境三:首次 clone 含有 sub module 的父子專案

該情境是模擬另台電腦首次 clone 部落格專案的情況,希望同步把 sub module 的內容依照 .gitmodules 的設定 clone 下來

首先,再建立一個新資料夾,將 parent 專案 clone 下來

mkdir new_repo
cd new_repo

git clone https://gitlab.com/DysonMa/parent.git

觀察檔案結構發現 child 是空的資料夾,因為 Git submodule 會將他視為不同的專案,也就是視為父專案的 Library,所以不會隨著 git clone 複製下來本地端.

.
└── parent
    ├── README.md
    ├── child       # child是sub module,但是是空資料夾
    └── parent.txt

如果想同步把子模組的 child 專案內容也一併 clone 下來,需要 cd 進到剛剛 clone 下來的 parent 資料夾內,再執行底下指令

cd ./parent

# 首次clone,用recursive方式把所有sub modules都clone下來
git submodule update --init --recursive

Git 會依照父專案內的 .gitmodules 配置,從子模組的 remote url 拉取關聯到的 commit hash 值的子專案版本

重新觀察檔案結構,發現 child 資料夾不再為空,而是剛剛 child 專案的 5c1f8871 這個 commit hash 的版本(有增加 child.txt)

.
├── README.md
├── child
│   ├── README.md   # 成功
│   └── child.txt   # 成功
└── parent.txt

情境四:同步 pull 更新父子專案

該情境是模擬非首次更新含有 sub module 的父子專案,適用於不同電腦已經 clone 過了,但需要再同步到最新的文章內容

模擬父子專案都有更動,所以各自在資料夾內增加新的文字檔,並且 push 到遠端 repo

# child資料夾
echo "new update" >> new_update_child.txt
git add .
git commit -m "Add new file new_update_child.txt"
git push origin main

# parent資料夾
echo "new update" >> new_update_parent.txt
git submodule update --remote --merge  # 同步child的更新
git add .
git commit -m "Add new file new_update_parent.txt and update sub module"
git push origin main

回到 new_repo 的 parent 資料夾,把 parent 和 child 兩個專案的更新都 pull 下來

# 遞迴的同步所有sub modules
git pull --recurse-submodules

觀察 new_repo 的檔案結構,確認剛剛的改動都已經反映到另一個本地端的 repo

.
└── parent
    ├── README.md
    ├── child
    │   ├── README.md
    │   ├── child.txt
    │   └── new_update_child.txt  # 成功
    ├── new_update_parent.txt     # 成功
    └── parent.txt

總結

以上就是 Git Submodule 的學習筆記,透過部落格專案實際遇到的問題來切入,並透過 GitLab 的 sample repo 來 demo,給自己留個紀錄,供未來參考