為什麼我不推薦你使用 git submodule
最近,筆者在某些項目的開發中碰到了 git submodule 並結結實實地被它噁心到了,從奇怪的版本管理到與父項目的尷尬關係——總之處處透漏著不舒服,本以為只有我是這麼覺得,結果隨手一搜發現與我有同樣想法的人不在少數,這次就讓我結合@dizlet的一篇博文,來談談為什麼 git submodule 那麼令人生厭?又有什麼別的方式可以替換掉它?
太長不讀版
永遠別用 git submodule,即便你覺得現在碰到的情況很適合用它,也別用。
下面我會詳細解釋一下為啥不要用,以及各種替代方案。
Git Submodule 的問題
它的問題主要體現在兩點:
- 底層設計上就有問題。它破壞了 git 的數據模型,包括但不限於以下幾個方面:
- 你的倉庫中的 git 對象不再一定能解析為有意義的數據。(淺克隆也有類似的問題,但那只是對於歷史記錄來說。而 git submodule 對樹也一樣有問題。)
- 它不符合 git 的一般規定。子模塊中的 URL 和主機名都是由 git 配置文件決定的,而不是通常的 git 倉庫本身。
- 它會導致 git 樹處於奇怪的狀態,而要排除起來則非常痛苦。
- 細節實現上也問題多多。有的是從設計根上帶出來的,但更多的是實現上的問題,即便你壓根沒用
git submodule init
初始化各個子模塊,它還是會影響到你的倉庫,暴露出的問題如下:- 用於切換分支的
git checkout
命令不再可靠。 - 編輯和提交會變得非常痛苦
- 從主分支拉取代碼會變麻煩
git ls-files
的輸出會和git log
和git cat-file
產生衝突.gitmodules
中的 URL 可能包含惡意鏈接,它甚至能被緩存在本地的.git/config
文件里。
總的來說,很多非常常見的 git 操作,諸如git checkout
和git pull
這樣的命令就會導致子模塊處於非常怪異的狀態,你必須要運行某個與子模塊相關的命令才能回到正常的狀態。對絕大部分人來說,他們可能更願意直接把倉庫刪了,然後重新克隆——我也是這麼乾的,除非你是某位 git 絕地大師,否則千萬別試圖排除奇怪的子模塊問題,純屬浪費時間。
- 用於切換分支的
所以,對我們開發者(庫的維護者)而言,就只剩兩個方案了:
- 乾脆別用,跟各位開發者講清楚子模塊的各種問題,並讓他們放棄。
- 捏著鼻子用,並準備好應對它給你帶來的各種麻煩核問題,並浪費大量的時間和精力處理這些問題。
有什麼替代品嗎?
但是,如果真的有類似需求,又不想在子模塊上浪費人生要怎麼辦呢?我推薦下面的這些解決方案,你可以根據自己的需求選擇合適的方案。
Git Subtree
Git Subtree 可以解決很多 git submodule 能解決的問題,同時不會破壞 git 的數據模型。
如果你的項目符合這些特點,可以考慮使用 subtree:
- 你想要在你的代碼樹里跟蹤並使用另一個項目,但又想保存它的獨立性。
- 與你的項目相比,子項目的大小相對合理。
如果你使用 git subtree,大部分開發者都不需要與子樹中的項目交互,他們甚至不會注意到這是一個子樹,可以隨便提交、切換分支,怎麼搞都沒問題。git subtree 可以自動從下游分離出對下游分支的更改,以應用於(或提交給)上游分支。
我之前在自己的項目中使用過 git subtree,並發現它強大、方便,且非常簡單直接,因此我推薦你也嘗試一下!
乾脆就用一個倉庫
如果你想引入的項目本來維護人就是你,那麼選擇用一個倉庫也是一個不錯的選項,因為你可以直接把另一個項目的 git 歷史合併進來。
如果你的項目符合這些特點,可以考慮直接使用單個倉庫:
- 你倉庫中使用的各個項目能夠共享同樣的 git 歷史記錄
- 整個項目的大小在合理的範圍內
- 你項目中長期存在的分支只是為了維護髮版,而不是為了讓某個內部項目維持在不同的版本。(例如 PCRE2 與倉庫中的 sljit)
使用包管理系統和顯式的依賴
與其自己維護依賴,還不如直接用別人打包好的!這個方案的精髓就在於將你的項目與依賴分開,並使用它提供的各種 API 取代之前在樹中代碼的作用,然後用一個包管理器安裝它!(如有必要的話,你可以自己維護一個特殊版本的下游包)
可供選用的包管理系統包括:
- 給發行版用的包管理系統:比如 Debian 的 apt + dpkg + sbuild 的組合。
- 語言專屬的包管理系統,比如 pip 或者 cargo。
如果你的項目滿足以下要求,可以考慮這個方案:
- 你正在使用、或者很熟悉一個合適的包管理器。
- 這個包管理器可以將你需要的 API 以合理的方式暴露出來。
使用多倉庫工具:MR
mr 是一個可以讓你方便地管理多個代碼樹的工具,通常這些樹之間是並列關係。
我雖然沒有親自用過這個工具,但它看起來還不錯,你可以把它跟我下面要介紹的基於..
的依賴解決方案結合起來。如果你的項目里有很多外部項目的話,可以考慮這個方案。
把依賴放在 ../dependency 里
這個解決方案非常輕量,不需要任何工具,只需要把你項目的依賴放到../dependency
里就好了。並讓用戶手動選擇正確版本的依賴。
如果你的項目符合這些條件,可以考慮這個方案:
- 你的項目處於初期起步階段,你不想過多操心依賴和構建的問題。
- 依賴默認是被禁用的,並且幾乎不會被啟用。
每個需要依賴項的程序或人都需要明白如何克隆依賴項並更新,這可能會帶來一些麻煩,如果你正在使用 CI(持續集成),則需要編寫一些自定義的 CI 腳本。但這還是比 git submodule 強,至少對於大部分人來說,究竟發生了什麼、以及如何對依賴項進行更改等都是完全可見的。
提供一個臨時的內部腳本來下載依賴項
作為最後的手段,你可以在頂層軟體包的構建系統中嵌入用於查找依賴項的 URL 和下載指令。這種方法很笨、也很麻煩,但可能讓你驚訝的是,它還是比 git submodule 要強得多。
如果你的項目符合這些特點,可以考慮這個方案:
- 大多數使用/構建你的軟體的人根本不需要依賴項。
- 大多數人不需要編輯依賴項。
任何其他的情況下,都不要考慮這個方案。
通常情況下,下游構建過程應該使用 git clone 命令克隆依賴項,下游代碼樹應該指定所需的準確提交 ID。
盡量避免使用這個方案,這並不是一個優秀的解決方案。但是:
真的,git submodule 還不如 Makefile
臨時的 shell 腳本看起來很不讓人滿意。但是與 git submodule 相比,它還是有一些優點的。比如,與 git submodule 不同,這種方法(就像我提出的大多數其他方法)意味著:
- 所有期望克隆你的存儲庫、進行更改、構建、跟蹤更改等的工具都能正常工作。
- 你可以精確控制下載發生的時間/條件:也就是說,你可以安排在需要依賴項時精確下載它。
- 你可以精確控制依賴項的版本管理和檢查:你的腳本控制著使用的依賴項版本以及是否「固定」或動態更新。
再次重申一遍,我並不喜歡臨時腳本,也不認為它是一個好主意,只是因為 git submodule 實在是太爛了,總之一句話,千萬別用!