一.背景
大きなコードベース(repo)を複数の小さな repo に分割したいというシナリオに頻繁に直面します。例えば:
-
既存のコードベースが膨大で、モジュール管理が混乱しており、他人のものを誤って変更しやすい
-
特定のモジュールを個別にビルドする必要がある。例えば jQuery プロジェクト内の React パイロット、Node プロジェクト内の純粋なフロントエンド部分、Electron プロジェクト内の UI 部分など
-
特定のモジュールがブラックボックスの依存項であり、開発中はビルド後のバージョンのみに依存する。例えばフレームワークライブラリなど
このような状況に対して、一般的に 3 つの解決策があります:
-
npm package:依存項を npm package として分離し、コードベースも独立して分離する
-
monorepo:単一 repo のサイズが膨大でも問題ない。モジュールごとに管理すればよい
-
git submodules:依存項を複数の独立した repo に分割し、主 repo の submodule として管理する
npm package
npm package の利点は、成熟した依存関係の管理メカニズムにあり、規範的で使いやすいことです。欠点は、主プロジェクトがパッケージのバージョン番号を通じて独立モジュールの更新を取得するしかないことで、主プロジェクトが子モジュールと連携してデバッグする必要があるシナリオでは非常に面倒になります:
主プロジェクト:動作しない啊
子モジュール:問題があります、修正します...バージョン番号変更 - ビルド-npm package 公開
主プロジェクト:依存関係を更新、再試行...まだ動作しない啊
子モジュール:まだ問題があります...
頻繁にバージョンをリリースするのは愚かです。ローカルで修正してビルドしてからコピーすることもできますが、それでも面倒です。もちろん、通常は mock インターフェースまたはデータを通じて連携デバッグの依存関係を解消できますが、場合によっては API 全体を mock するコストが高く、また偽物は本物ほど使いやすくありません
monorepo
monorepo は repo を分割せず、単一 repo 内で各モジュールのビルドフロー、バージョン番号などを統一管理することを主張し、他人のコードを変更することも推奨します
これはモジュールの境界が明確で、owner が明確なプロジェクト(React、Babel など)には適していますが、実際のアプリケーションでは、ビジネス repo は明確なモジュール境界と依存関係を維持するのが難しいため、此时 monorepo は理想化されすぎます
git submodules
git submodules は npm package に似た依存関係の管理メカニズムを提供します。依存項の追加、削除、更新などの機能を含みます。違いは、前者が管理する依存関係は子モジュールのソースコードであり、後者が管理するのは子モジュールのビルド成果物という点です。この点において、git submodules は monorepo と一致しています(どちらも子モジュールのソースコードを気にします)
これにより、主プロジェクトが子モジュールと頻繁に連携デバッグする必要がある場合の面倒がなくなります。主プロジェクトが取得する submodules は完全な repo であり、直接修正 - ビルド - 提交できるからです
二.submodules と monorepo
構造から見ると、submodules プロジェクトの主 repo は monorepo に似ています。monorepo 内の各モジュールを独立した repo に抽出したようなもので、主 repo が依存する各モジュールのバージョン番号(commit hash 形式)のみを記録します
具体的には、monorepo は単一 repo 内にすべての子モジュールのソースコード(packages/xxx/src)を格納します。例えば:
react/
packages/
react-dom/
/src
react-reconciler/
/src
...
一方、submodules は主 repo 内にすべての子モジュールの「インデックス」のみを格納します(repo url + branch name + commit hash)。例えば:
# 主 repo の.gitmodules ファイル
[submodule "react-dom"]
path = packages/react-dom
url = https://github.com/facebook/react.git
branch = master
[submodule "react-reconciler"]
path = packages/react-reconciler
url = https://github.com/facebook/react.git
branch = master
...
主 repo 内には対応する空のディレクトリを子モジュールの「穴」として保持し、ソースコードは格納しません:
react/
packages/
react-dom/ # 空ディレクトリ
react-reconciler/ # 空ディレクトリ
すべての submodules 依存関係を取得した後、実際のディレクトリ構造は以下のようになります:
react/
packages/
react-dom/
/src
react-reconciler/
/src
...
主 repo は子モジュールのソースコードを追跡せず、そのバージョン番号のみを記録します(commit hash 形式):
# 空を出力し、子モジュール src を追跡しないことを示す
$ git ls-tree -r master | grep packages/react-dom/src
# 主 repo 内で git に追跡されている子モジュールの穴位置を表示
$ git ls-tree -r master | grep ' commit'
160000 commit 3edf340cee50fd4bc918a0a95b438a30447ae042 packages/react-dom
160000 commit 373f207b09a7bf900fa82c3188aeefdc9ce6146c packages/react-reconciler
...
P.S.git ls-treeの出力形式の意味については、Output Format を参照
三.具体的な使い方
git submoduleコマンドは子モジュールを管理するために使用します:
$ git submodule --help
git-submodule - Initialize, update or inspect submodules
# 初期化
git submodule init
# 追加
git submodule add
# 削除
git submodule deinit
# 変更(バージョン管理)
git submodule update
子モジュール依存関係の追加
$ cd ./react
# 依存関係を追加
$ git submodule add -b master https://github.com/path-to/react-dom.git src/packages/react-dom
主 repo に src/packages/react-dom 空ディ��クトリを作成し、子モジュールの穴位置とします。実際には、add プロセスでは主に 3 つのことが発生します:
-
子モジュール repo を主 repo の git キャッシュディレクトリに clone する。例えば
.git/modules/src/packages/react-dom -
穴位置の空ディレクトリを作成し、子モジュール repo の最新 commit hash と関連付ける
-
主 repo のルートディレクトリに必要に応じて
.gitmodulesファイルを作成し、子モジュール repo アドレス(url)、ブランチ名(branch)、穴位置パス(path)を記録する
その後、これらの子モジュール設定を提交します:
$ git add ./src/packages/react-dom ./.gitmodules
$ git commit -m "build: add react-dom submodule"
$ git push origin master
次に、ローカルで子モジュールを取得して初期化を完了します:
# 子モジュールを初期化
$ git submodule update --init
子モジュール repo を src/packages/react-dom ディレクトリに clone します。実際には 2 つのことが発生します:
-
キャッシュが存在するかチェックし、clone 済みの子モジュール repo が存在するか確認する(例えば clone された主 repo が
addされていない場合、キャッシュは存在しない)。必要に応じて clone -
子モジュール repo のルートディレクトリに
.git/configを作成し、その repo アドレス(url)を記録する
子モジュールの初期化
submodules を含む repo を clone した後、初期化を行う必要があります:
# いくつかのローカル設定を作成
$ git submodule init
# 各子モジュール repo を取得
$ git submodule update --init
または、主 repo を clone する際に --recursive オプションを使用して上記 2 つのステップを完了することもできます:
$ git clone git://gihub.com/path-to/main-repo.git --recursive
子モジュールの更新を取得
すべての子モジュールを更新します:
$ git submodule update --remote
子モジュールの対応するブランチの最新コードを取得します。更新がある場合、プレースホルダーディレクトリの git 状態が変化します:
$ git status
modified: src/packages/react-dom (new commits)
実際には commit hash が変化しました:
$ $ git diff
diff --git a/src/packages/react-dom b/src/packages/react-dom
index 3edf340cee..d056efbc62 160000
--- a/src/packages/react-dom
+++ b/src/packages/react-dom
@@ -1 +1 @@
-Subproject commit 3edf340cee50fd4bc918a0a95b438a30447ae042
+Subproject commit d056efbc62cbf976b4ef83e70d7019fba4506e85
P.S.submodules 内の commit hash は npm package の dependencies バージョン番号に相当します
依存項のバージョンを制御
主 repo が依存する子モジュールのバージョンを更新したい場合、この commit hash の変更を提交します:
$ git add src/packages/react-dom
$ git commit -m "build: update react-dom submodule"
$ git push origin master
そうでない場合、--remote オプションを付けないと現在の依存バージョンに戻る:
$ git submodule update
子モジュールコードの変更
子モジュールは独立した repo なので、通常通り操作します:
$ cd ./packages/react-dom
# ブランチを切り替えることに注意。通常は detached 状態
$ git checkout master
$ git add .
$ git commit -m 'feat: xxx'
$ git push origin master
その後、主 repo は git submodule update --remote を通じて最新版本を取得でき、主 repo が依存する子モジュールのバージョンをアップグレードするかどうかを決定できます
各子モジュールに対して同じ git コマンドを実行
foreach コマンドを提供して、子モジュールに対してバッチ処理を行います。例えば:
# 各子モジュールディレクトリに入り、git stash を実行
$ git submodule foreach 'git stash'
# 統一して feature ブランチを作成
$ git submodule foreach 'git checkout -b featureA'
複数の子モジュール依存関係がある場合、このコマンドは非常に便利です
P.S.submodules のより多くの使い方については、7.11 Git Tools - Submodules を参照
四.よくある問題
子モジュールのブランチが detached 状態にある
git submodule update --remote を実行するたびに、子モジュールは detached 状態になります。例えば:
$ cd ./packages/react-dom
$ git branch
* (HEAD detached at ac4d1fc)
master
設計上如此であり、良い解決策はありません:
It's also important to realize that a submodule reference within the host repository is not a reference to a specific branch of that submodule's project, it points directly to a specific commit (or SHA1 reference), it is not a symbolic reference such as a branch or tag. In technical terms, it's a detached HEAD pointing directly to the latest commit as of the submodule add.
したがって、子プロジェクトのコードを変更する前に、手動で master ブランチに切り替える必要があります:
$ git checkout master
$ git add .
$ git commit -m 'feat: xxx'
$ git push origin master
ローカル子モジュールキャッシュ
子モジュール repo が移行发生时、git submodule add を実行するとローカルキャッシュの問題に遭遇する可能性があります:
$ git submodule add ssh://XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git projets/fdf
A git directory for 'projets/fdf' is found locally with remote(s): origin ssh://git @XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git If you want to reuse this local git directory instead of cloning again from ssh://XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git use the '--force' option. If the local git directory is not the correct repo or you are unsure what this means choose another name with the '--name' option.
元の設定(第 2 第 3 歩)を削除し、次にローカルキャッシュの子モジュール情報(第 1 第 4 歩)を削除する必要があります:
# 1.git キャッシュおよび物理ファイルを削除
$ git rm --cached path_to_submodule
$ rm -rf path_to_submodule
# 2..gitmodules 内の該子モジュールの関連設定を削除
$ vi .gitmodules
[submodule "path_to_submodule"]
path = path_to_submodule
url = https://github.com/path_to_submodule
# 3..git/config 内の該子モジュールの関連設定を削除
$ vi .git/config
[submodule "path_to_submodule"]
url = https://github.com/path_to_submodule
# 4. 子モジュールキャッシュを削除
$ rm -rf .git/modules/path_to_submodule
クリーンアップ完了後、再度 git submodule add を実行します
P.S.第 4 歩で、子モジュールのキャッシュ位置は以下のコマンドで確認できます:
$ cat path_to_submodule/.git
gitdir: ../.git/modules/path_to_submodule
P.S.より多くのよくある問題については、Using Git Submodules を参照
コメントはまだありません