Coding Story

在黑暗中寫故事 👻

0%

git submodule 的分支追蹤

前言故事

小唯在一間資訊公司擔任 Android 軟體工程師,負責公司內的主力產品開發,這是一項功能複雜的產品,它累計了數年的程式碼、商務邏輯、出包經驗等的功能與修正。數年後,團隊中正在推動全新 2.0 的產品,以及公司其他服務也即將推出 App。不可避免的,有許多相似功能相繼橫跨各個產品 App,這讓小唯自覺必須有一套更好的方式管理自己團隊的程式碼,改善之後擴充性、維護性,和保護團隊工程師們的肝~

一個從頭打造的產品,功能是不斷的疊加上去,鮮少有團隊能真的從開頭就搞好架構設計,小唯團隊的產品也不例外。除了自身的核心功能外,還使用了圖隊之前開發的功能,以及許多現成 github 的模組,有的已提供 gradle 的安裝與使用,有的卻只是單純的 Java/Kotlin 程式碼。在初期,最直接暴力的方式就是直接抓下來,塞進當前的 App 中使用,累積到今時今日,已有數個相同模組,分別的複製到數個專案上 😱

小唯團隊有幾個選擇:

  1. 維持現狀,每個 App 複製一份程式碼
    • 優點:現有架構、不需更改、各個 App 工程彈性自由,想怎麼改就怎麼改
    • 缺點:維上非常不便,一個修正需要同步到所有 App,之後工程師要記得去手動 merge 更新的程式碼
  1. 將可模組化的,改寫成 gradle
    • 優點:不必要再包一份程式碼到各個 App,之後其他專案要使用會更便利。模組更新,只需要修改一份程式碼,gradle 版號升級便可更新程式
    • 缺點:之後各個 App 要做客製化,會相對不容易
  1. 將可模組化的,改成 submodule
    • 優點:每個模組只需要維護一份程式碼,需要更新時,使用 git 內建方式,即可同步最新的程式。App 也有一定自由度去做產品客製化
    • 缺點:還是仰賴各 App 去 merge 更新,但不一定需要手動(透過 git 同步 submodule 即可)

最後,小唯團隊選擇 submodule 模式去進行,以下是小唯的手稿紀錄:

將專案切分成 submodule

目前小唯團隊負責的產品 git 架構如下,一個巨型 git repo,內容包山包海,含之前團隊開發的功能模組,以及第三方 github 的功能模組等:

重新打造後,新的 git 架構如下,可被多個產品共用的模組,都會拉成獨立的 git repo,由一個主要 git repo 來把其他功能,以 submodule 方式添加進來:

加入 submodule 指令

以下用其中一個功能為範例。將 function1 以 submodule 方式加入主 App:

shell
❯ git submodule add https://github.com/wm4n/function1.gitfunction1
正複製到 '/Users/[path]/app-host/function1'...
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
接收物件中: 100% (4/4), 完成.

以上指令會將 function1 repo 內容抓到 function1 目錄下,如果印出 .gitmodules 檔案(使用 git submodule add 後自動建立)內容,會發現:

shell
❯ cat .gitmodules
[submodule "function1"]
path = function1
url = https://github.com/wm4n/function1.git

上傳添加的 submodule

照平常 git 使用的方式,將添加好的 submodule 設定,上傳至 github

shell
❯ git add .gitmodules

❯ git add function1

❯ git commit -m "add function1 submodule"
[main 26d42dc] add function1 submodule
2 files changed, 4 insertions(+)
create mode 100644 .gitmodules
create mode 160000 function1

❯ git push
枚舉物件: 4, 完成.
物件計數中: 100% (4/4), 完成.
使用 4 個執行緒進行壓縮
壓縮物件中: 100% (3/3), 完成.
寫入物件中: 100% (3/3), 419 位元組 | 419.00 KiB/s, 完成.
總共 3 (差異 0),復用 0 (差異 0),重用包 0
To https://github.com/wm4n/app-host.git
278058e..26d42dc main -> main

更新 repo 內容

當要同步線上程式碼時,一般會用 git pull 來抓取最新內容,但改用 submodule 方式後,會發現 function1 不會同步。如果要更新,則必須進入到 function1 目錄下再次執行 git pull

shell
cd function1

❯ git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
展開物件中: 100% (3/3), 691 位元組 | 138.00 KiB/s, 完成.
來自 https://github.com/wm4n/function1
5f9ae22..e6843b4 main -> origin/main
更新 5f9ae22..e6843b4
Fast-forward
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

當 submodule 數量成長後

小唯團隊負責的產品,如今被拆解成 5 個 submodule 後,發現今後如果要更新,就要進入每個 submodule 的目錄,分別執行 git pull 指令,這表示一個專案如果有 X 個 submodule,每次更新最多就會有 X + 1 個 git pull。這不僅讓小唯受不了,團隊也常為了忘記要確實執行,反而造成許多問題。

為了改善這問題,小唯在模組設定檔中,指定讓模組 submodule 追蹤某個特定的遠端分支

shell
❯ cat .gitmodules
[submodule "function1"]
path = function1
url = https://wm4n@github.com/wm4n/function1.git
branch = main

branch 代表這個 submodule 將追蹤指定的遠端分支。之後,團隊只要使用 git submodule update --remote 指令一次,所有的 submodule 都會依照指定的分支去抓最新的內容,如下:

shell
❯ git submodule update --remote
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
展開物件中: 100% (3/3), 689 位元組 | 344.00 KiB/s, 完成.
來自 https://github.com/wm4n/function1
e6843b4..c7df259 main -> origin/main
子模組路徑 'function1':檢出 'c7df259c57f4c6143c4e29488399b13b9fb188c6'

令人分心的 git status

一切設定好,分支追蹤功能滿足了團隊的需求,從此之後,可重複使用的功能順利地抽出成獨立 git repo,不同 App 依照各自需求,添加自己需要的 submodule,然後一鍵更新所有模組也不再是夢想 🥳。一切是如此的美好,直到團隊發現… 經常 git status,就會出現:

shell
❯ git status
位於分支 main
您的分支與上游分支 'origin/main' 一致。

尚未暫存以備提交的變更:
(使用 "git add <檔案>..." 更新要提交的內容)
(使用 "git restore <檔案>..." 捨棄工作區的改動)
修改: function1 (新提交)

修改尚未加入提交(使用 "git add" 和/或 "git commit -a"

在使用分支追蹤之前,小唯的團隊總是會在跟新模組後,緊接著跟新主 App 的模組 commit ID。使用分支追蹤後,雖然不必要再跟新模組 commit ID(因為 git submodule update --remote 指令會自動同步分支最新的 commit ID),但 git status 指令還是會提醒有更新尚未提交。久而久之,團隊都覺得這訊息實在惱人啊~ 😖

小唯試了數個方式後,包括把相關檔案都加到 .gitignore(此方法無效),最後是在模組設定中,加上 ignore = all,就搞定了:

shell
❯ cat .gitmodules
[submodule "function1"]
path = function1
url = https://wm4n@github.com/wm4n/function1.git
branch = main
ignore = all

對 github 上的第三方專案做客製化

沒多久後,小唯團隊發現,他們想對 github 上的第三方專案做客製化,以符合規格需求,但要如何追蹤 github 專案分支的同時,又做客製化呢?畢竟無法對第三方的專案做修改(除非經由 PR 流程,但一般來說不會接受客製化…)

小唯團隊利用 github fork 專案的方式,讓自己的專案轉追蹤 fork 的拷貝,利用 fork 拷貝來做客製化,同時又能不斷從 upstream 專案中更新


從此之後,git status 也不再困擾小唯團隊了 🥳

相關連結: Demo 專案

------------- 本文结束 git 就是這麼的難搞!-------------