はじめに

プロジェクト開発において、「外部ライブラリを特定のバージョンで固定して管理したい」「複数のプロジェクトで共有するコンポーネントを一元管理したい」「サードパーティのリポジトリを自社プロジェクトに組み込みたい」という要件に直面することがあります。

Gitには、外部リポジトリをプロジェクトに組み込むための2つの主要なアプローチがあります。それがgit submodulegit subtreeです。どちらも外部リポジトリを管理する機能ですが、その仕組みと適した使用シーンは大きく異なります。

本記事では、サブモジュールとサブツリーの違いから、git submodule addgit submodule updategit submodule syncの使い方、git subtreeの基本操作、そして共有ライブラリの管理パターンまでを実践的に解説します。

この記事を読み終えると、以下のことができるようになります。

  • git submoduleとgit subtreeの違いを理解できる
  • 外部リポジトリを組み込む適切な方法を選択できる
  • git submodule addでサブモジュールを追加できる
  • git submodule updateでサブモジュールを更新できる
  • git submodule syncでURL変更に対応できる
  • git subtree addでサブツリーを追加できる
  • 共有ライブラリの管理パターンを実装できる

実行環境と前提条件

本記事の内容は、以下の環境で動作確認を行っています。

項目 要件
Git 2.40以上
OS Windows 10/11、macOS 12以上、Ubuntu 22.04以上
ターミナル コマンドプロンプト、PowerShell、Terminal.app、bash等
エディタ VS Code推奨

前提条件として、以下の知識があることを想定しています。

  • コマンドライン操作の基礎知識(cdls/dirmkdir等)
  • テキストエディタの基本操作
  • Gitの基本コマンド(git initgit addgit commitgit remote)の理解
  • リモートリポジトリの概念(git clonegit pushgit pull)の理解

Gitのバージョンは以下のコマンドで確認できます。

1
git --version

git submoduleとgit subtreeの違い

外部リポジトリ管理の2つのアプローチ

git submoduleとgit subtreeは、どちらも外部リポジトリをプロジェクトに組み込む機能ですが、その仕組みは根本的に異なります。

項目 git submodule git subtree
データの格納方法 参照(ポインタ)のみを保存 ファイル自体をコピーして保存
リポジトリ構成 親と子で分離 単一リポジトリに統合
メタデータ .gitmodulesファイルが必要 追加のメタデータなし
クローン時 明示的な初期化が必要 追加操作不要
履歴の保持 外部リポジトリの履歴を分離 親リポジトリの履歴に統合
学習コスト やや高い 比較的低い
上流への貢献 容易 やや複雑

git submodule(サブモジュール)の特徴

git submoduleは、外部リポジトリへの「参照」を親リポジトリに記録する方式です。サブモジュールとして追加された外部リポジトリは、独立したGitリポジトリとして存在し続けます。

親リポジトリ(main-project)
├── .git/
├── .gitmodules              <- サブモジュールの設定ファイル
├── src/
│   └── main.js
└── libs/
    └── shared-lib/          <- サブモジュール(特定コミットへの参照)
        ├── .git/            <- 独立したGitリポジトリ
        └── index.js

サブモジュールのメリットは以下の通りです。

  • 外部リポジトリのバージョンを厳密に固定できる
  • 外部リポジトリへの変更を上流に簡単にプッシュできる
  • 親リポジトリのサイズが小さく保たれる
  • 外部リポジトリの独立性が保たれる

サブモジュールのデメリットは以下の通りです。

  • クローン後に追加の初期化コマンドが必要
  • ブランチ切り替え時に状態の不整合が発生しやすい
  • チームメンバー全員がsubmoduleの操作を理解する必要がある
  • 更新手順がやや複雑

git subtree(サブツリー)の特徴

git subtreeは、外部リポジトリのファイルを丸ごとコピーして親リポジトリに「取り込む」方式です。取り込んだファイルは親リポジトリの一部となります。

親リポジトリ(main-project)
├── .git/                    <- 全ての履歴を含む
├── src/
│   └── main.js
└── libs/
    └── shared-lib/          <- 外部リポジトリのファイルがコピーされている
        └── index.js         <- 通常のファイルとして管理

サブツリーのメリットは以下の通りです。

  • 追加のメタデータファイルが不要
  • クローン時に特別な操作が不要
  • チームメンバーがsubtreeを知らなくても利用可能
  • ブランチ切り替え時の問題が発生しにくい

サブツリーのデメリットは以下の通りです。

  • 親リポジトリのサイズが大きくなる
  • 上流への変更のプッシュがやや複雑
  • 取り込んだコードの出所が分かりにくくなる可能性がある
  • 履歴が親リポジトリに混在する

使い分けの指針

以下の基準で選択することを推奨します。

git submoduleを選ぶべきケース

  • 外部ライブラリの特定バージョンを厳密に固定したい
  • 外部リポジトリに頻繁に変更を加え、上流にプッシュする予定がある
  • チームメンバー全員がGitに習熟している
  • 親リポジトリのサイズを小さく保ちたい
  • 複数のプロジェクトで同じサブモジュールを共有している

git subtreeを選ぶべきケース

  • チームメンバーにsubmoduleの学習コストを負わせたくない
  • 外部リポジトリを取り込んだ後、ほとんど変更しない
  • クローン後の追加操作を最小限にしたい
  • CI/CDパイプラインをシンプルに保ちたい
  • 外部リポジトリのフォークを自社で独自に発展させる予定がある

git submoduleの基本操作

サブモジュールを追加する(git submodule add)

外部リポジトリをサブモジュールとして追加するには、git submodule addコマンドを使用します。

1
2
# 基本構文
git submodule add <リポジトリURL> [配置先パス]

実際に共有ライブラリをサブモジュールとして追加してみましょう。

1
2
3
4
5
# プロジェクトディレクトリに移動
cd main-project

# サブモジュールを追加(libsディレクトリに配置)
git submodule add https://github.com/example/shared-lib.git libs/shared-lib

このコマンドを実行すると、以下の処理が行われます。

1
2
3
4
5
Cloning into 'libs/shared-lib'...
remote: Counting objects: 50, done.
remote: Compressing objects: 100% (30/30), done.
remote: Total 50 (delta 15), reused 50 (delta 15)
Unpacking objects: 100% (50/50), done.

サブモジュールの追加により、2つのファイルがステージングされます。

1
2
3
4
5
6
7
8
git status

# 出力例
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   .gitmodules
	new file:   libs/shared-lib

.gitmodulesファイルには、サブモジュールの設定情報が記録されます。

1
2
3
[submodule "libs/shared-lib"]
	path = libs/shared-lib
	url = https://github.com/example/shared-lib.git

この変更をコミットして、サブモジュールの追加を確定します。

1
git commit -m "Add shared-lib as submodule"

特定のブランチを追跡する

サブモジュールがデフォルトブランチ以外のブランチを追跡するように設定できます。

1
2
3
4
5
# .gitmodulesに追跡ブランチを設定
git config -f .gitmodules submodule.libs/shared-lib.branch develop

# 設定を確認
cat .gitmodules

出力例は以下の通りです。

1
2
3
4
[submodule "libs/shared-lib"]
	path = libs/shared-lib
	url = https://github.com/example/shared-lib.git
	branch = develop

サブモジュールを含むリポジトリをクローンする

サブモジュールを含むリポジトリをクローンする場合、通常のクローンではサブモジュールのディレクトリは空のままです。

1
2
3
4
5
6
7
# 通常のクローン
git clone https://github.com/example/main-project.git
cd main-project

# サブモジュールのディレクトリを確認
ls libs/shared-lib/
# 出力: (空)

サブモジュールを取得するには、以下のいずれかの方法を使用します。

方法1: クローン時に–recurse-submodulesオプションを使用

1
git clone --recurse-submodules https://github.com/example/main-project.git

方法2: クローン後に初期化と更新を実行

1
2
3
4
5
6
7
8
git clone https://github.com/example/main-project.git
cd main-project

# サブモジュールを初期化(ローカル設定ファイルにサブモジュール情報を登録)
git submodule init

# サブモジュールのデータを取得してチェックアウト
git submodule update

方法3: 初期化と更新を1コマンドで実行

1
git submodule update --init

方法4: ネストしたサブモジュールも含めて再帰的に初期化

1
git submodule update --init --recursive

サブモジュールを更新する(git submodule update)

サブモジュールの更新には複数のシナリオがあります。

シナリオ1: 親リポジトリで記録されているコミットにサブモジュールを合わせる

他のチームメンバーがサブモジュールの参照を更新した場合、git pull後にサブモジュールを更新する必要があります。

1
2
3
4
5
# 親リポジトリの変更を取得
git pull

# サブモジュールを記録されているコミットにチェックアウト
git submodule update

--initオプションを付けると、新しく追加されたサブモジュールも同時に初期化できます。

1
git submodule update --init --recursive

シナリオ2: サブモジュールを上流の最新版に更新する

サブモジュールのリモートリポジトリにある最新コミットを取得するには、--remoteオプションを使用します。

1
2
# サブモジュールをリモートの最新版に更新
git submodule update --remote libs/shared-lib

このコマンドを実行すると、サブモジュールが最新のコミットをチェックアウトし、親リポジトリに変更が記録されます。

1
2
3
4
5
6
git status

# 出力例
On branch main
Changes not staged for commit:
	modified:   libs/shared-lib (new commits)

この変更をコミットして、新しいサブモジュールのバージョンを記録します。

1
2
git add libs/shared-lib
git commit -m "Update shared-lib to latest version"

シナリオ3: 更新時にマージまたはリベースを行う

サブモジュール内で作業している場合、更新時にローカルの変更を保持するためにマージまたはリベースオプションを使用できます。

1
2
3
4
5
# マージして更新
git submodule update --remote --merge

# リベースして更新
git submodule update --remote --rebase

サブモジュールのURL変更に対応する(git submodule sync)

外部リポジトリのホスティング先が変更された場合、.gitmodulesのURLを更新した後、ローカル設定を同期する必要があります。

1
2
3
4
5
6
7
8
# .gitmodulesのURLを更新(直接編集またはgit configを使用)
git config -f .gitmodules submodule.libs/shared-lib.url https://new-host.com/example/shared-lib.git

# ローカル設定に同期
git submodule sync

# サブモジュールを新しいURLから更新
git submodule update --init --recursive

git submodule syncは、.gitmodulesに記載されたURLを、ローカルの.git/configに反映させるコマンドです。

1
2
# 再帰的に全てのサブモジュールのURLを同期
git submodule sync --recursive

サブモジュールを削除する

サブモジュールを完全に削除するには、複数のステップが必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. サブモジュールの登録を解除
git submodule deinit libs/shared-lib

# 2. .gitからサブモジュールのエントリを削除
git rm libs/shared-lib

# 3. .git/modules内のサブモジュールデータを削除(オプション)
rm -rf .git/modules/libs/shared-lib

# 4. 変更をコミット
git commit -m "Remove shared-lib submodule"

サブモジュールで便利な設定

サブモジュール操作を効率化するための便利な設定を紹介します。

1
2
3
4
5
6
7
8
# git pullで自動的にサブモジュールを更新
git config --global submodule.recurse true

# git statusでサブモジュールの変更サマリーを表示
git config --global status.submoduleSummary true

# git diffでサブモジュールの変更を見やすく表示
git config --global diff.submodule log

git submodule foreachで一括操作

複数のサブモジュールに対して同じ操作を実行するには、foreachコマンドが便利です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 全サブモジュールの状態を確認
git submodule foreach 'git status'

# 全サブモジュールでブランチを作成
git submodule foreach 'git checkout -b feature-branch'

# 全サブモジュールの変更をstash
git submodule foreach 'git stash'

# 全サブモジュールで最新を取得
git submodule foreach 'git pull origin main'

git subtreeの基本操作

サブツリーを追加する(git subtree add)

外部リポジトリをサブツリーとして追加するには、git subtree addコマンドを使用します。

1
2
# 基本構文
git subtree add --prefix=<配置先パス> <リポジトリURL> <ブランチ> [オプション]

実際に共有ライブラリをサブツリーとして追加してみましょう。

1
2
3
4
5
# プロジェクトディレクトリに移動
cd main-project

# サブツリーを追加(履歴を1つのコミットにまとめる)
git subtree add --prefix=libs/shared-lib https://github.com/example/shared-lib.git main --squash

--squashオプションを使用すると、外部リポジトリの履歴が1つのコミットにまとめられます。

1
2
3
4
5
6
7
8
# 出力例
git fetch https://github.com/example/shared-lib.git main
remote: Counting objects: 50, done.
remote: Compressing objects: 100% (30/30), done.
Receiving objects: 100% (50/50), done.
From https://github.com/example/shared-lib
 * branch            main       -> FETCH_HEAD
Added dir 'libs/shared-lib'

コミット履歴を確認すると、squashマージコミットが作成されていることがわかります。

1
2
3
4
5
6
git log --oneline -3

# 出力例
a1b2c3d Merge commit 'xxx' as 'libs/shared-lib'
x9y8z7w Squashed 'libs/shared-lib/' content from commit abc1234
...

リモートを登録してコマンドを短縮する

頻繁にサブツリー操作を行う場合は、リモートを登録しておくとコマンドが短縮できます。

1
2
3
4
5
6
7
8
# リモートを追加
git remote add shared-lib https://github.com/example/shared-lib.git

# フェッチ
git fetch shared-lib

# サブツリーを追加(リモート名を使用)
git subtree add --prefix=libs/shared-lib shared-lib main --squash

サブツリーを更新する(git subtree pull)

外部リポジトリの最新の変更を取り込むには、git subtree pullを使用します。

1
2
# サブツリーを最新版に更新
git subtree pull --prefix=libs/shared-lib https://github.com/example/shared-lib.git main --squash

リモートを登録済みの場合は以下のようになります。

1
2
git fetch shared-lib
git subtree pull --prefix=libs/shared-lib shared-lib main --squash

サブツリーの変更を上流にプッシュする(git subtree push)

サブツリー内のファイルに加えた変更を、元の外部リポジトリにプッシュすることもできます。

まず、プッシュ先となるリモートを設定します(フォークした自分のリポジトリなど)。

1
2
# フォークしたリポジトリをリモートに追加
git remote add shared-lib-fork https://github.com/myuser/shared-lib.git

サブツリーの変更をプッシュします。

1
git subtree push --prefix=libs/shared-lib shared-lib-fork feature-branch

このコマンドは、libs/shared-libディレクトリ内の変更を抽出し、指定したリモートのブランチにプッシュします。

サブツリーを分割する(git subtree split)

既存のディレクトリをサブツリーとして切り出し、独立したブランチを作成できます。

1
2
# 指定ディレクトリの履歴を抽出して新しいブランチを作成
git subtree split --prefix=libs/shared-lib -b shared-lib-branch

この機能は、モノリポジトリから一部のコードを別リポジトリに切り出す場合に便利です。

1
2
# 分割したブランチを新しいリポジトリにプッシュ
git push https://github.com/example/new-shared-lib.git shared-lib-branch:main

サブツリーを削除する

サブツリーは通常のディレクトリとして管理されているため、削除は単純にディレクトリを削除するだけです。

1
2
3
4
5
# サブツリーのディレクトリを削除
git rm -rf libs/shared-lib

# 変更をコミット
git commit -m "Remove shared-lib subtree"

共有ライブラリの管理パターン

パターン1: 社内共通ライブラリをsubmoduleで管理

複数のプロジェクトで共有する社内ライブラリを、submoduleで一元管理するパターンです。

company-projects/
├── project-a/
│   ├── src/
│   └── libs/
│       └── common-utils/     <- submodule
├── project-b/
│   ├── src/
│   └── libs/
│       └── common-utils/     <- 同じsubmodule
└── common-utils/             <- 共通ライブラリ本体

セットアップ手順

1
2
3
4
5
6
7
8
9
# Project Aでサブモジュールを追加
cd project-a
git submodule add git@github.com:company/common-utils.git libs/common-utils
git commit -m "Add common-utils submodule"

# Project Bでも同様に追加
cd ../project-b
git submodule add git@github.com:company/common-utils.git libs/common-utils
git commit -m "Add common-utils submodule"

共通ライブラリを更新した場合のワークフロー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 1. 共通ライブラリで変更を加えてプッシュ
cd common-utils
git checkout main
# 変更を加える
git add .
git commit -m "Add new utility function"
git push origin main

# 2. 各プロジェクトでサブモジュールを更新
cd ../project-a
git submodule update --remote libs/common-utils
git add libs/common-utils
git commit -m "Update common-utils to latest"
git push

パターン2: フォークした外部ライブラリをsubtreeで管理

外部のOSSライブラリをフォークして、自社で独自のカスタマイズを加えながら管理するパターンです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 1. 外部ライブラリをsubtreeとして追加
git subtree add --prefix=vendor/awesome-lib https://github.com/original/awesome-lib.git main --squash

# 2. ローカルでカスタマイズを加える
vim vendor/awesome-lib/config.js
git add vendor/awesome-lib/
git commit -m "Customize awesome-lib for our use case"

# 3. 上流の更新を取り込む
git subtree pull --prefix=vendor/awesome-lib https://github.com/original/awesome-lib.git main --squash

# 4. コンフリクトがあれば解消
# vim vendor/awesome-lib/config.js
git add vendor/awesome-lib/
git commit -m "Merge upstream changes and resolve conflicts"

パターン3: モノリポジトリでのパッケージ分離

大規模なモノリポジトリで、特定のパッケージを将来的に分離する可能性がある場合の管理パターンです。

monorepo/
├── apps/
│   ├── web-app/
│   └── mobile-app/
└── packages/
    ├── ui-components/        <- 将来分離予定
    └── api-client/           <- 将来分離予定

パッケージを分離する手順

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 1. パッケージの履歴を抽出
git subtree split --prefix=packages/ui-components -b ui-components-branch

# 2. 新しいリポジトリを作成してプッシュ
git push git@github.com:company/ui-components.git ui-components-branch:main

# 3. モノリポジトリからディレクトリを削除
git rm -rf packages/ui-components
git commit -m "Extract ui-components to separate repository"

# 4. サブモジュールとして再度追加(必要な場合)
git submodule add git@github.com:company/ui-components.git packages/ui-components
git commit -m "Add ui-components as submodule"

よくあるトラブルと対処法

submoduleがdetached HEAD状態になる

git submodule updateを実行すると、サブモジュールはdetached HEAD状態になります。これは正常な動作ですが、サブモジュール内で作業する場合は注意が必要です。

1
2
3
4
5
cd libs/shared-lib
git status

# 出力例
HEAD detached at abc1234

サブモジュール内で作業する場合は、明示的にブランチをチェックアウトしてください。

1
2
cd libs/shared-lib
git checkout main

submodule updateで「not a git repository」エラー

サブモジュールの初期化が完了していない場合に発生します。

1
git submodule update --init --recursive

subtree pullでコンフリクトが発生する

サブツリー内のファイルをローカルで変更した後、上流の変更を取り込むとコンフリクトが発生することがあります。

1
2
3
4
5
6
# コンフリクトを解消
vim libs/shared-lib/conflicted-file.js

# 解消後にコミット
git add libs/shared-lib/
git commit -m "Resolve conflict in subtree merge"

ブランチ切り替え時にsubmoduleの状態がおかしくなる

Git 2.13以降では、--recurse-submodulesオプションでサブモジュールも同時に切り替えられます。

1
2
# ブランチ切り替え時にサブモジュールも更新
git checkout --recurse-submodules feature-branch

デフォルトでこの動作を有効にするには、以下の設定を行います。

1
git config --global submodule.recurse true

submoduleとsubtreeの選択チェックリスト

プロジェクトに外部リポジトリを組み込む際の選択基準をチェックリストにまとめました。

git submoduleを選択する場合のチェックリスト

  • 外部リポジトリを独立して管理し続けたい
  • 特定のコミットやタグに固定する必要がある
  • 上流に頻繁にコントリビュートする予定がある
  • チームメンバーがGitに習熟している
  • CI/CDでの追加設定が許容できる

git subtreeを選択する場合のチェックリスト

  • 外部リポジトリを取り込んだ後、あまり更新しない
  • チームメンバーへの学習コストを最小限にしたい
  • クローンや操作をシンプルに保ちたい
  • 外部コードをフォークして独自に発展させる予定がある
  • 単一リポジトリで全てを管理したい

まとめ

本記事では、git submoduleとgit subtreeの違いと使い分け、それぞれの基本操作、そして共有ライブラリの管理パターンについて解説しました。

重要なポイントをまとめると以下の通りです。

  • git submoduleは外部リポジトリへの参照を管理し、厳密なバージョン管理と上流への貢献に適している
  • git subtreeは外部リポジトリのファイルを取り込み、シンプルな運用とチームへの学習コスト軽減に適している
  • サブモジュールの基本操作はaddupdatesyncの3つを押さえておく
  • サブツリーの基本操作はaddpullpushsplitを理解しておく
  • チームの習熟度、プロジェクトの要件、運用の複雑さを考慮して選択する

どちらのアプローチも一長一短があります。プロジェクトの特性とチームの状況に応じて、適切な方法を選択してください。

参考リンク